From 30efc6dad24866eef489234f82665b7814d5fa5f Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:19:27 +0000 Subject: [PATCH 01/13] docs: Lambda@Edge Cognito enforcement design (v2) Extends the WebUI auth design with the Lambda@Edge enforcement layer: - Architecture (viewer-request Lambda@Edge, cognito-at-edge library) - Lambda@Edge constraints (us-east-1, no env vars, Node 18, 50MB limit) - Build & deploy flow (npm install, placeholder substitution, S3 upload) - Signing key handling (Secrets Manager, deterministic ARN, IAM scoped) - Handler code skeleton - Cost estimate (~$0.40-0.50/mo per deployment) - Rollback path - Deferred: logout endpoint, non-Cognito localhost access Implementation lands in follow-up commits on this branch. --- docs/design/kirocrew-webui-auth.md | 154 +++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/docs/design/kirocrew-webui-auth.md b/docs/design/kirocrew-webui-auth.md index c1049b8..92d7b05 100644 --- a/docs/design/kirocrew-webui-auth.md +++ b/docs/design/kirocrew-webui-auth.md @@ -339,3 +339,157 @@ Uninstall flow: - [ ] KiroCrew gateway: implement JWT middleware (separate PR on KiroCrew repo) - [ ] Update uninstaller to handle Cognito cleanup - [ ] Add telemetry events: `install.webui_auth_configured` + +--- + +# Lambda@Edge Enforcement (v2) + +**Status:** Design phase. Ships in follow-up PR after PR #85. + +## Problem With v1 + +The v1 design (PR #82, #83) creates the Cognito pool, client, domain, and initial user, but **nothing actually enforces authentication on the CloudFront request path**. Requests flow CloudFront → ALB → EC2 → KiroCrew dashboard, which uses its own legacy `?token=` query-param scheme. The Cognito resources exist but are unused. + +## Solution: Lambda@Edge on the CloudFront Distribution + +Lambda@Edge (viewer-request trigger) intercepts every request to the CloudFront distribution, validates a Cognito session cookie, and redirects unauthenticated users to Cognito hosted UI. Uses the AWS-published `cognito-at-edge` library. + +## Architecture + +``` +Browser + │ + ▼ +CloudFront (KiroCrewDistribution) + │ ┌─────────────────────────────┐ + ├──│ Viewer-Request Lambda@Edge │ ← every request goes through here + │ │ (cognito-at-edge) │ + │ └─────────────────────────────┘ + │ │ + │ ├─ Has valid session cookie? ──► forward to ALB origin + │ │ + │ ├─ Path is /auth/callback? ──► exchange code for tokens, set cookie, redirect to / + │ │ + │ └─ No session? ──► 302 → Cognito hosted UI login + │ + ▼ +ALB → EC2 → KiroCrew dashboard +``` + +## Constraints (Lambda@Edge Specifics) + +- **Runtime**: Node.js 18.x (has AWS SDK v3 pre-installed, saves package size). +- **Region**: Function MUST live in `us-east-1` (CloudFront requirement). Executes replicated at every edge location. +- **No environment variables**. Configuration must be baked into code at build time OR fetched at cold start from Secrets Manager / SSM Parameter Store. +- **No VPC access.** Fine — Cognito is public API. +- **50 MB unzipped package limit.** `cognito-at-edge` + deps ≈ 2 MB — well under. +- **Cold-start budget**: <500 ms for viewer-request. Fetching Secrets Manager on cold start adds ~100-200 ms per edge region — acceptable. +- **Update propagation**: Publishing a new Lambda version replicates globally over several minutes. + +## Build & Deploy Flow + +``` +1. Installer (install.sh): + a. cd packs/kirocrew/webui-auth-edge/ + b. npm install --production + c. Substitute placeholders in index.js: + - __POOL_ID__ → resolved via CFN param + - __CLIENT_ID__ → resolved via CFN param + - __COGNITO_DOMAIN__→ resolved via CFN param + - __SECRET_ARN__ → deterministic name pattern + - __REGION__ → us-east-1 + d. zip -r edge-lambda-.zip . + e. aws s3 cp edge-lambda-.zip s3:///edge/edge-lambda-.zip + f. Pass S3 bucket + key as CFN parameters: + - EdgeLambdaS3Bucket + - EdgeLambdaS3Key + +2. CloudFormation stack create: + - WebUIEdgeSigningKeySecret (Secrets Manager, GenerateSecretString, 64-char hex) + - WebUIEdgeLambdaRole (trust: lambda.amazonaws.com + edgelambda.amazonaws.com) + - WebUIEdgeLambdaFunction (Code.S3Bucket/S3Key, Runtime nodejs18.x, us-east-1) + - WebUIEdgeLambdaVersion (needed for association) + - Update KiroCrewDistribution: add LambdaFunctionAssociations[viewer-request] + - Update WebUIUserPoolClient: callback URLs already correct (from v1) + +3. Cold start on first request: + - Lambda fetches signing key from Secrets Manager via IAM role + - Caches in module scope for subsequent invocations at same edge +``` + +## Signing Key Handling + +- Stored in `AWS::SecretsManager::Secret` with `GenerateSecretString` (64-char hex, alphanumeric). +- Secret ARN is deterministic: `arn:aws:secretsmanager:us-east-1::secret:/lowkey//webui-edge-signing-key`. Baked into Lambda code at build time. +- IAM policy on Lambda role grants `secretsmanager:GetSecretValue` on that specific ARN. +- Rotating the secret invalidates all sessions (all users get kicked out) — acceptable, forces re-auth. + +## Handler Code (skeleton) + +```javascript +// packs/kirocrew/webui-auth-edge/index.js +import { Authenticator } from 'cognito-at-edge'; +import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; + +const REGION = 'us-east-1'; +const POOL_ID = '__POOL_ID__'; +const CLIENT_ID = '__CLIENT_ID__'; +const COGNITO_DOMAIN = '__COGNITO_DOMAIN__'; +const SECRET_ARN = '__SECRET_ARN__'; + +let authenticatorPromise = null; + +async function getAuthenticator() { + if (authenticatorPromise) return authenticatorPromise; + authenticatorPromise = (async () => { + const sm = new SecretsManagerClient({ region: REGION }); + const resp = await sm.send(new GetSecretValueCommand({ SecretId: SECRET_ARN })); + const signingKey = JSON.parse(resp.SecretString).key; + return new Authenticator({ + region: REGION, + userPoolId: POOL_ID, + userPoolAppId: CLIENT_ID, + userPoolDomain: COGNITO_DOMAIN, + cookieExpirationDays: 1, + logLevel: 'warn', + // cognito-at-edge signs its nonce/state with a key derived from client-secret; + // since we use no-secret public clients, we pass a shared signing seed here: + cookieSettings: { idToken: null, accessToken: null, refreshToken: null }, + }); + })(); + return authenticatorPromise; +} + +export const handler = async (event) => { + const auth = await getAuthenticator(); + return auth.handle(event); +}; +``` + +## Cost Impact + +- Lambda@Edge: $0.60 / 1M requests + $0.00005001 / GB-sec. Typical dashboard use = <10k req/mo → <$0.01/mo. +- Secrets Manager: $0.40 / secret / mo + $0.05 / 10k API calls. One secret + <100 cold starts/mo → $0.40/mo. +- CloudWatch Logs (edge): pennies. +- **Total: ~$0.40-0.50/mo per deployment.** + +## Rollback + +If Lambda@Edge causes issues, remove the `LambdaFunctionAssociations` block from the CloudFront distribution — takes ~15 min to fully propagate. CloudFront falls back to unauthenticated forwarding (same as v1 today). + +## Deferred / Out-of-Scope + +- **Logout**: cognito-at-edge doesn't handle logout natively. User must clear cookies OR visit `https:///logout?client_id=X&logout_uri=Y`. Follow-up work: add `/logout` path handler in Lambda@Edge. +- **Refresh token flow**: cognito-at-edge auto-refreshes ID token using refresh token. Works out of the box. +- **CSRF**: cognito-at-edge handles state param + PKCE. Verified against upstream code. +- **Localhost/SSM port-forward access**: Lambda@Edge does not intercept direct EC2 access. If users port-forward `localhost:5476`, they hit the dashboard's own `?token` auth. Acceptable — SSM is already authenticated. + +## Implementation Checklist + +- [ ] `packs/kirocrew/webui-auth-edge/` directory with `package.json`, `index.js`, `build.sh` +- [ ] `install.sh`: npm install + zip + S3 upload before CFN deploy +- [ ] `install.sh`: two new CFN params `EdgeLambdaS3Bucket`, `EdgeLambdaS3Key` in `PARAM_CFN_NAMES` + `PARAM_VALUES` +- [ ] CFN: `WebUIEdgeSigningKeySecret`, `WebUIEdgeLambdaRole`, `WebUIEdgeLambdaFunction`, `WebUIEdgeLambdaVersion` (all `Condition: EnableWebUI`) +- [ ] CFN: `KiroCrewDistribution.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations` = viewer-request → new function version +- [ ] CFN outputs: `WebUIEdgeFunctionArn`, `WebUIEdgeSigningKeySecretArn` +- [ ] Validate template, `bash -n install.sh`, deploy test stack From 28a3f4e06333da39459bdcb43aca60b35cddc042 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:33:02 +0000 Subject: [PATCH 02/13] feat(edge): Lambda@Edge Cognito enforcement on CloudFront MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements v2 of the WebUI auth design (see docs/design/kirocrew-webui-auth.md). Adds a viewer-request Lambda@Edge on the KiroCrew CloudFront distribution that validates a Cognito session cookie via cognito-at-edge. Unauthenticated requests are redirected to the Cognito hosted UI; /auth/callback exchanges the code for tokens and sets the session cookie. New pack directory: packs/kirocrew/webui-auth-edge/ - package.json — cognito-at-edge dependency - index.js — Lambda@Edge handler (fetches all config from Secrets Manager at cold start; only SECRET_NAME is baked in at build time) - build.sh — substitutes placeholder, runs npm install --production, zips (~1.4 MB), emits zip path on stdout CFN changes (deploy/cloudformation/template.yaml): - Parameters: EdgeLambdaS3Bucket, EdgeLambdaS3Key - Resources (all Condition: EnableWebUI): - WebUIEdgeSigningKeySecret (Secrets Manager, GenerateSecretString) - WebUIEdgeLambdaRole (trust: lambda + edgelambda) - WebUIEdgeLambdaFunction (Node 18.x, us-east-1, code from S3) - WebUIEdgeLambdaVersion (required for CloudFront association) - KiroCrewDistribution: LambdaFunctionAssociations[viewer-request] - WebUIUserCreationFunction: now writes BOTH admin creds and edge auth config (poolId + clientId + cognitoDomain + signingKey) to Secrets Manager - WebUIUserCreationRole: added Get+PutSecretValue on the edge secret - Outputs: WebUIEdgeFunctionArn, WebUIEdgeSigningKeySecretArn Installer changes: - New function build_and_upload_edge_lambda() runs after prepare_repo, before deploy_cfn_stack. Builds the zip with SECRET_NAME baked in (deterministic: /lowkey/${ENV_NAME}/webui-edge-signing-key), creates the CFN templates bucket if needed, uploads zip, exports EDGE_LAMBDA_S3_BUCKET/EDGE_LAMBDA_S3_KEY for build_deploy_params. - PARAM_CFN_NAMES/PARAM_VALUES extended (28 entries each). Validated: - bash -n install.sh: OK - aws cloudformation validate-template: OK (40 params, CAPABILITY_NAMED_IAM) - Build script tested end-to-end: produces valid zip (42 npm packages) - Python ast.parse on Custom Resource Lambda: OK --- deploy/cloudformation/template.yaml | 171 ++++++++++++++++++-- install.sh | 66 +++++++- packs/kirocrew/webui-auth-edge/build.sh | 68 ++++++++ packs/kirocrew/webui-auth-edge/index.js | 87 ++++++++++ packs/kirocrew/webui-auth-edge/package.json | 13 ++ 5 files changed, 387 insertions(+), 18 deletions(-) create mode 100755 packs/kirocrew/webui-auth-edge/build.sh create mode 100644 packs/kirocrew/webui-auth-edge/index.js create mode 100644 packs/kirocrew/webui-auth-edge/package.json diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 1b3f166..67d5293 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -372,6 +372,16 @@ Parameters: Description: "Email for the initial WebUI admin user. Required when EnableWebUIAuth is true." AllowedPattern: '^([^@]+@[^@]+\.[^@]+)?$' + EdgeLambdaS3Bucket: + Type: String + Default: '' + Description: "S3 bucket holding the Lambda@Edge deployment zip. Required when EnableWebUIAuth is true. Installer uploads and sets this." + + EdgeLambdaS3Key: + Type: String + Default: '' + Description: "S3 key of the Lambda@Edge deployment zip. Required when EnableWebUIAuth is true. Installer uploads and sets this." + # ============================================================================ # RULES # ============================================================================ @@ -672,6 +682,12 @@ Resources: - DELETE CachePolicyId: '4135ea2d-6df8-44a3-9df3-4b5a84be39ad' # CachingDisabled OriginRequestPolicyId: '216adef6-5c7f-47e4-b989-5492eafa07d3' # AllViewer + LambdaFunctionAssociations: !If + - EnableWebUI + - - EventType: viewer-request + LambdaFunctionARN: !Ref WebUIEdgeLambdaVersion + IncludeBody: false + - !Ref 'AWS::NoValue' Origins: - Id: kirocrew-alb-origin DomainName: !GetAtt KiroCrewALB.DNSName @@ -1526,7 +1542,13 @@ Resources: Action: - secretsmanager:PutSecretValue - secretsmanager:UpdateSecret - Resource: !Ref WebUIAdminSecret + Resource: + - !Ref WebUIAdminSecret + - !Ref WebUIEdgeSigningKeySecret + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref WebUIEdgeSigningKeySecret WebUIUserCreationFunction: Type: AWS::Lambda::Function @@ -1572,12 +1594,23 @@ Resources: secrets.SystemRandom().shuffle(pwd) return ''.join(pwd) - def write_secret(sm, secret_arn, email, password): + def write_admin_secret(sm, secret_arn, email, password): sm.put_secret_value( SecretId=secret_arn, SecretString=json.dumps({'email': email, 'password': password}) ) + def write_edge_config(sm, secret_arn, pool_id, client_id, cognito_domain, signing_key): + sm.put_secret_value( + SecretId=secret_arn, + SecretString=json.dumps({ + 'poolId': pool_id, + 'clientId': client_id, + 'cognitoDomain': cognito_domain, + 'signingKey': signing_key, + }) + ) + def create_or_reset_user(cognito, pool_id, email, password): try: cognito.admin_create_user( @@ -1590,8 +1623,8 @@ Resources: MessageAction='SUPPRESS' ) except cognito.exceptions.UsernameExistsException: - # User exists (e.g. stack update or re-run); reset the password - # so the credentials in Secrets Manager remain valid. + # User exists (stack update or re-run); reset password so the credentials + # in Secrets Manager remain authoritative. pass cognito.admin_set_user_password( UserPoolId=pool_id, @@ -1611,60 +1644,154 @@ Resources: print(f'[WARN] Could not delete old user {email}: {e}') def handler(event, context): - # Log event WITHOUT ResourceProperties (which may contain email) + # Never log ResourceProperties (contains email + secret ARNs) print(f"[INFO] RequestType={event.get('RequestType')} LogicalId={event.get('LogicalResourceId')}") try: if event['RequestType'] == 'Delete': - # Secret and user pool are deleted by CFN — no manual cleanup needed send_response(event, context, 'SUCCESS', 'Delete is a no-op') return props = event.get('ResourceProperties', {}) old_props = event.get('OldResourceProperties', {}) pool_id = props.get('UserPoolId', '') + client_id = props.get('ClientId', '') + cognito_domain = props.get('CognitoDomain', '') email = props.get('AdminEmail', '') - secret_arn = os.environ.get('SECRET_ARN', '') + admin_secret_arn = props.get('AdminSecretArn', '') + edge_secret_arn = props.get('EdgeSecretArn', '') + signing_key_secret_arn = props.get('SigningKeySecretArn', '') region = props.get('Region', os.environ.get('AWS_REGION', 'us-east-1')) - if not pool_id or not email or not secret_arn: - send_response(event, context, 'FAILED', - 'Missing UserPoolId, AdminEmail, or SECRET_ARN env') + missing = [k for k, v in { + 'UserPoolId': pool_id, + 'ClientId': client_id, + 'CognitoDomain': cognito_domain, + 'AdminEmail': email, + 'AdminSecretArn': admin_secret_arn, + 'EdgeSecretArn': edge_secret_arn, + 'SigningKeySecretArn': signing_key_secret_arn, + }.items() if not v] + if missing: + send_response(event, context, 'FAILED', f'Missing props: {missing}') return cognito = boto3.client('cognito-idp', region_name=region) sm = boto3.client('secretsmanager', region_name=region) - # On Update, if email changed, delete the old user first + # On Update, delete the old admin user if email changed if event['RequestType'] == 'Update': old_email = old_props.get('AdminEmail', '') if old_email and old_email != email: delete_user_safely(cognito, pool_id, old_email) + # 1) Provision the admin user in Cognito password = generate_password() create_or_reset_user(cognito, pool_id, email, password) - write_secret(sm, secret_arn, email, password) + write_admin_secret(sm, admin_secret_arn, email, password) + + # 2) Read the CFN-generated signing key and write the merged edge config + sk_resp = sm.get_secret_value(SecretId=signing_key_secret_arn) + signing_key = json.loads(sk_resp['SecretString'])['key'] + write_edge_config(sm, edge_secret_arn, pool_id, client_id, cognito_domain, signing_key) - # Return only non-sensitive data; password is in Secrets Manager - send_response(event, context, 'SUCCESS', 'Admin user provisioned', - {'Email': email, 'SecretArn': secret_arn}) + send_response(event, context, 'SUCCESS', 'Admin user + edge config provisioned', + {'Email': email}) except Exception as e: - # Never log the password; only the exception class + short message err = f'{type(e).__name__}: {str(e)[:180]}' print(f'[ERROR] {err}') send_response(event, context, 'FAILED', err) - WebUIUserCreationResource: Type: Custom::WebUIUserCreation Condition: EnableWebUI DependsOn: - WebUIUserPool - WebUIUserPoolClient + - WebUIEdgeSigningKeySecret Properties: ServiceToken: !GetAtt WebUIUserCreationFunction.Arn UserPoolId: !Ref WebUIUserPool + ClientId: !Ref WebUIUserPoolClient + CognitoDomain: !Sub '${WebUIUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com' AdminEmail: !Ref WebUIAdminEmail + AdminSecretArn: !Ref WebUIAdminSecret + EdgeSecretArn: !Ref WebUIEdgeSigningKeySecret + SigningKeySecretArn: !Ref WebUIEdgeSigningKeySecret Region: !Ref 'AWS::Region' + # -------------------------------------------------------------------------- + # WebUI Lambda@Edge (Cognito enforcement on CloudFront) + # -------------------------------------------------------------------------- + WebUIEdgeSigningKeySecret: + Type: AWS::SecretsManager::Secret + Condition: EnableWebUI + Properties: + Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-signing-key' + Description: !Sub 'Nonce/state HMAC signing key for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' + GenerateSecretString: + SecretStringTemplate: '{}' + GenerateStringKey: 'key' + PasswordLength: 64 + ExcludePunctuation: true + Tags: + - Key: loki:managed + Value: 'true' + - Key: loki:pack + Value: !Ref PackName + - Key: loki:env + Value: !Ref EnvironmentName + + WebUIEdgeLambdaRole: + Type: AWS::IAM::Role + Condition: EnableWebUI + Properties: + RoleName: !Sub '${EnvironmentName}-webui-edge-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + - edgelambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: FetchSigningKey + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref WebUIEdgeSigningKeySecret + + WebUIEdgeLambdaFunction: + Type: AWS::Lambda::Function + Condition: EnableWebUI + DependsOn: + - WebUIEdgeSigningKeySecret + - WebUIUserPoolClient + Properties: + FunctionName: !Sub '${EnvironmentName}-webui-edge-auth' + Runtime: nodejs18.x + Handler: index.handler + MemorySize: 128 + Timeout: 5 + Role: !GetAtt WebUIEdgeLambdaRole.Arn + Code: + S3Bucket: !Ref EdgeLambdaS3Bucket + S3Key: !Ref EdgeLambdaS3Key + + # Version is required for LambdaFunctionAssociations; publishing a new version + # is how updates roll out to CloudFront edges. + WebUIEdgeLambdaVersion: + Type: AWS::Lambda::Version + Condition: EnableWebUI + Properties: + FunctionName: !Ref WebUIEdgeLambdaFunction + Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName})' + # SSM Session Manager Preferences (auto-login as ec2-user with welcome) # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- @@ -1883,3 +2010,13 @@ Outputs: Description: ARN of Secrets Manager secret holding initial WebUI admin credentials (email + password). Rotate/delete after first use. Value: !Ref WebUIAdminSecret + WebUIEdgeFunctionArn: + Condition: EnableWebUI + Description: Lambda@Edge function version ARN attached to the CloudFront distribution (viewer-request) + Value: !Ref WebUIEdgeLambdaVersion + + WebUIEdgeSigningKeySecretArn: + Condition: EnableWebUI + Description: ARN of Secrets Manager secret holding the Lambda@Edge nonce/state signing key + Value: !Ref WebUIEdgeSigningKeySecret + diff --git a/install.sh b/install.sh index ddc56fc..73ad8a1 100755 --- a/install.sh +++ b/install.sh @@ -2155,6 +2155,66 @@ configure_webui_auth() { info "Initial password will be shown after stack deploys." } +# ============================================================================ +# WebUI Lambda@Edge zip build + S3 upload (KiroCrew, when auth enabled) +# ============================================================================ +# Builds the Cognito-at-Edge zip, uploads it to the CFN templates bucket, and +# exports EDGE_LAMBDA_S3_BUCKET/EDGE_LAMBDA_S3_KEY so build_deploy_params can +# feed them to the stack as CFN parameters. +build_and_upload_edge_lambda() { + [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" ]] || return 0 + [[ "$PACK_NAME" == "kirocrew" ]] || return 0 + + local build_script="${CLONE_DIR}/packs/kirocrew/webui-auth-edge/build.sh" + # In debug-in-repo mode CLONE_DIR points into the working repo; otherwise it + # points at the freshly-cloned copy. In either case the script must exist + # AFTER prepare_repo has run. This function is called from that point. + if [[ ! -x "$build_script" ]]; then + fail "Edge Lambda build script missing: $build_script" + fi + + step "WebUI Lambda@Edge" + info "Building Cognito Lambda@Edge zip..." + + # Deterministic secret NAME (not ARN — Secrets Manager appends a random suffix + # to the ARN we can't know at build time, but names are stable). + local secret_name="/lowkey/${ENV_NAME}/webui-edge-signing-key" + + local zip_path + zip_path=$(SECRET_ARN="$secret_name" "$build_script" 2>&1 | tail -1) \ + || fail "Edge Lambda build failed: $zip_path" + if [[ ! -f "$zip_path" ]]; then + fail "Edge Lambda build script did not produce a zip: $zip_path" + fi + + local bucket="${ENV_NAME}-cfn-templates-${ACCOUNT_ID}" + local key="edge/$(basename "$zip_path")" + + # The CFN templates bucket was created by deploy_cfn_stack in earlier flows, + # but Lambda@Edge zip must be uploaded BEFORE the stack that references it. + # Ensure the bucket exists (idempotent). + if ! aws s3api head-bucket --bucket "$bucket" --region "$DEPLOY_REGION" 2>/dev/null; then + info "Creating edge Lambda bucket: $bucket" + aws s3api create-bucket --bucket "$bucket" --region "$DEPLOY_REGION" \ + $(if [[ "$DEPLOY_REGION" != "us-east-1" ]]; then echo "--create-bucket-configuration LocationConstraint=$DEPLOY_REGION"; fi) \ + >/dev/null 2>&1 || fail "Failed to create bucket $bucket" + aws s3api put-bucket-versioning --bucket "$bucket" \ + --versioning-configuration Status=Enabled --region "$DEPLOY_REGION" >/dev/null 2>&1 || true + aws s3api put-public-access-block --bucket "$bucket" --region "$DEPLOY_REGION" \ + --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true \ + >/dev/null 2>&1 || true + fi + + info "Uploading edge Lambda zip: s3://${bucket}/${key} ($(wc -c < "$zip_path") bytes)" + aws s3 cp "$zip_path" "s3://${bucket}/${key}" --region "$DEPLOY_REGION" >/dev/null \ + || fail "Failed to upload edge Lambda zip" + + export EDGE_LAMBDA_S3_BUCKET="$bucket" + export EDGE_LAMBDA_S3_KEY="$key" + ok "Edge Lambda uploaded" +} + + collect_config() { step "Configuration" @@ -2288,7 +2348,7 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console and CFN CLI # ============================================================================ # ⚠ KEEP THESE TWO ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaS3Bucket EdgeLambdaS3Key) PARAM_VALUES=() # populated by build_deploy_params() # Per-pack default model (passed to CFN DefaultModel / bootstrap.sh --model). @@ -2350,6 +2410,8 @@ build_deploy_params() { "${TROIKA_CODEX_MODEL:-openai.gpt-5.5}" "${WEBUI_AUTH_ENABLED:-false}" "${WEBUI_ADMIN_EMAIL:-}" + "${EDGE_LAMBDA_S3_BUCKET:-}" + "${EDGE_LAMBDA_S3_KEY:-}" ) # Validate parallel arrays are in sync [[ ${#PARAM_CFN_NAMES[@]} -eq ${#PARAM_VALUES[@]} ]] \ @@ -3470,6 +3532,8 @@ main() { prepare_repo echo "" + build_and_upload_edge_lambda + case "$DEPLOY_METHOD" in "$DEPLOY_CFN_CLI") info "Deploying with CloudFormation..." deploy_cfn_stack "deploy/cloudformation/template.yaml" "CAPABILITY_NAMED_IAM" ;; diff --git a/packs/kirocrew/webui-auth-edge/build.sh b/packs/kirocrew/webui-auth-edge/build.sh new file mode 100755 index 0000000..a575eda --- /dev/null +++ b/packs/kirocrew/webui-auth-edge/build.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Build the KiroCrew WebUI Cognito Lambda@Edge zip. +# +# Substitutes SECRET_ARN placeholder in index.js, runs npm install --production, +# and produces edge-lambda-.zip in $OUT_DIR (default: script dir). +# +# The Lambda code fetches all other config (pool ID, client ID, domain, +# signing key) from Secrets Manager at cold start, using the ARN baked in +# here. This means we can build the zip BEFORE CFN creates the Cognito pool. +# +# Required env vars: +# SECRET_ARN — Secrets Manager ARN for the edge auth config secret +# +# Optional: +# OUT_DIR — Where to place the zip (default: script dir) +# NODE_BIN — Node binary path (default: node from PATH) +# +# Exit codes: 0 success, non-zero failure with message on stderr. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_DIR="${OUT_DIR:-$SCRIPT_DIR}" +NODE_BIN="${NODE_BIN:-node}" + +if [[ -z "${SECRET_ARN:-}" ]]; then + echo "build.sh: missing required env var: SECRET_ARN" >&2 + exit 2 +fi + +if ! command -v "$NODE_BIN" >/dev/null 2>&1; then + echo "build.sh: node binary not found ($NODE_BIN)" >&2 + exit 4 +fi + +NODE_MAJOR=$("$NODE_BIN" -e 'console.log(process.versions.node.split(".")[0])') +if [[ "$NODE_MAJOR" -lt 18 ]]; then + echo "build.sh: node 18+ required, got $NODE_MAJOR" >&2 + exit 5 +fi + +BUILD_DIR=$(mktemp -d) +trap 'rm -rf "$BUILD_DIR"' EXIT + +cp "$SCRIPT_DIR/index.js" "$BUILD_DIR/index.js" +cp "$SCRIPT_DIR/package.json" "$BUILD_DIR/package.json" + +# Substitute only SECRET_ARN placeholder +sed -i -e "s|__SECRET_NAME__|${SECRET_ARN}|g" "$BUILD_DIR/index.js" + +if grep -q '__[A-Z_]*__' "$BUILD_DIR/index.js"; then + echo "build.sh: unresolved placeholders in index.js:" >&2 + grep '__[A-Z_]*__' "$BUILD_DIR/index.js" >&2 + exit 6 +fi + +cd "$BUILD_DIR" +npm install --production --no-audit --no-fund --loglevel=error >&2 + +# Hash based on the secret ARN so the zip name is deterministic per stack +SHA=$(printf '%s' "$SECRET_ARN" | sha256sum | cut -c1-12) +ZIP_NAME="edge-lambda-${SHA}.zip" +ZIP_PATH="${OUT_DIR}/${ZIP_NAME}" + +rm -f "$ZIP_PATH" +zip -r -q "$ZIP_PATH" index.js package.json node_modules >&2 + +echo "$ZIP_PATH" diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js new file mode 100644 index 0000000..c360e5a --- /dev/null +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -0,0 +1,87 @@ +/** + * KiroCrew WebUI Cognito Auth — Lambda@Edge (viewer-request trigger). + * + * Validates a Cognito session cookie set by cognito-at-edge. Unauthenticated + * requests are redirected to Cognito hosted UI. Requests to /auth/callback + * exchange the auth code for tokens and set the session cookie. + * + * Lambda@Edge constraints: + * - No environment variables (config baked in below OR fetched from Secrets Manager) + * - Node.js 18.x runtime + * - Must be deployed in us-east-1 + * + * Only SECRET_ARN is substituted at build time. All other config (pool ID, + * client ID, domain, signing key) is fetched from Secrets Manager on cold + * start, cached in module scope for warm invocations. + * + * This lets us zip the Lambda code BEFORE CFN creates the Cognito pool — + * the Lambda only needs the secret's ARN, which is a deterministic function + * of the stack's environment name. + */ + +const { Authenticator } = require('cognito-at-edge'); +const { + SecretsManagerClient, + GetSecretValueCommand, +} = require('@aws-sdk/client-secrets-manager'); + +// Only placeholder replaced at build time. All other config comes from the secret. +const SECRET_ARN = '__SECRET_NAME__'; +const REGION = 'us-east-1'; + +let authenticatorPromise = null; + +async function loadConfig() { + const sm = new SecretsManagerClient({ region: REGION }); + const resp = await sm.send(new GetSecretValueCommand({ SecretId: SECRET_ARN })); + const parsed = JSON.parse(resp.SecretString); + const required = ['poolId', 'clientId', 'cognitoDomain', 'signingKey']; + for (const k of required) { + if (!parsed[k] || typeof parsed[k] !== 'string') { + throw new Error(`Edge auth secret is missing required field: ${k}`); + } + } + return parsed; +} + +async function getAuthenticator() { + if (authenticatorPromise) return authenticatorPromise; + authenticatorPromise = (async () => { + const cfg = await loadConfig(); + return new Authenticator({ + region: REGION, + userPoolId: cfg.poolId, + userPoolAppId: cfg.clientId, + userPoolDomain: cfg.cognitoDomain, + cookieExpirationDays: 1, + disableCookieDomain: true, + logLevel: 'warn', + cookieCompatibility: 'amplify', + nonceSigningSecret: cfg.signingKey, + }); + })().catch((err) => { + // Reset so the next cold-start attempt can retry + authenticatorPromise = null; + throw err; + }); + return authenticatorPromise; +} + +exports.handler = async (event) => { + try { + const auth = await getAuthenticator(); + return auth.handle(event); + } catch (err) { + console.error('[edge-auth] handler error:', err && err.message); + // Fail closed — do NOT forward an unauthenticated request + return { + status: '503', + statusDescription: 'Service Unavailable', + headers: { + 'content-type': [{ key: 'Content-Type', value: 'text/plain' }], + 'cache-control': [{ key: 'Cache-Control', value: 'no-store' }], + }, + body: 'Auth service temporarily unavailable. Retry in a moment.', + }; + } +}; diff --git a/packs/kirocrew/webui-auth-edge/package.json b/packs/kirocrew/webui-auth-edge/package.json new file mode 100644 index 0000000..98fa161 --- /dev/null +++ b/packs/kirocrew/webui-auth-edge/package.json @@ -0,0 +1,13 @@ +{ + "name": "kirocrew-webui-auth-edge", + "version": "1.0.0", + "description": "Lambda@Edge Cognito enforcement for KiroCrew CloudFront distribution", + "main": "index.js", + "type": "commonjs", + "dependencies": { + "cognito-at-edge": "^1.5.2" + }, + "engines": { + "node": ">=18" + } +} From b139e7dfb2a96a3b2f982ebc1548ca447943fbef Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:44:26 +0000 Subject: [PATCH 03/13] fix(edge): address P0/P1 findings from first review round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 fixes: - CFN Rule WebUIEdgeRequiresUsEast1 enforces stack in us-east-1 when EnableWebUIAuth=true (Lambda@Edge is a CloudFront-only resource that must live in us-east-1). Also asserts EdgeLambdaS3Bucket/Key are set so console-mode deploys can't silently miss the edge Lambda. - Split the single WebUIEdgeSigningKeySecret into two secrets to fix the update-time KeyError: * WebUIEdgeSigningKeySecret — raw HMAC key, CFN GenerateSecretString only, never rewritten. Read by Custom Resource on first Create. * WebUIEdgeConfigSecret — merged {poolId, clientId, cognitoDomain, signingKey} for the Lambda@Edge to consume. Written by Custom Resource; safe to rewrite on Update. - Custom Resource IAM policy updated: read on signing-key, write on admin-secret + edge-config-secret. - Custom Resource property renamed EdgeSecretArn -> EdgeConfigSecretArn to make the intent explicit. - Lambda@Edge index.js: placeholder renamed __SECRET_NAME__ -> __CONFIG_SECRET_NAME__; reads from webui-edge-config secret, rejects 'pending' placeholder values loudly. P1 fixes: - Content-addressed zip name: SHA now hashes the actual substituted index.js + package.json, so any code change or config change produces a new S3 key. This forces CFN to see a diff on Code.S3Key and publish a new AWS::Lambda::Version, propagating updates to CloudFront edges. - Console-mode deploy now rejects EnableWebUIAuth=true with a clear error pointing at CLI mode (console can't build+upload the edge zip locally). Installer: - build_and_upload_edge_lambda uses CONFIG_SECRET_NAME env var (matches new build.sh contract). - Secret name updated to /lowkey/${ENV_NAME}/webui-edge-config. Validated: - bash -n install.sh: OK - aws cloudformation validate-template: OK (40 params) - Python ast.parse on Custom Resource Lambda: OK - build.sh test still produces valid zip --- deploy/cloudformation/template.yaml | 48 +++++++++++++++++++++---- install.sh | 18 +++++----- packs/kirocrew/webui-auth-edge/build.sh | 31 ++++++++-------- packs/kirocrew/webui-auth-edge/index.js | 24 ++++++------- 4 files changed, 80 insertions(+), 41 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 67d5293..54d3aed 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -412,6 +412,16 @@ Rules: - Assert: !Not [!Equals [!Ref WebUIAdminEmail, '']] AssertDescription: "WebUIAdminEmail is required when EnableWebUIAuth is true." + WebUIEdgeRequiresUsEast1: + RuleCondition: !Equals [!Ref EnableWebUIAuth, 'true'] + Assertions: + - Assert: !Equals [!Ref 'AWS::Region', 'us-east-1'] + AssertDescription: "EnableWebUIAuth requires deploying in us-east-1 (Lambda@Edge is a CloudFront-only resource that must live in us-east-1)." + - Assert: !Not [!Equals [!Ref EdgeLambdaS3Bucket, '']] + AssertDescription: "EdgeLambdaS3Bucket is required when EnableWebUIAuth is true (installer uploads the Lambda@Edge zip and sets this)." + - Assert: !Not [!Equals [!Ref EdgeLambdaS3Key, '']] + AssertDescription: "EdgeLambdaS3Key is required when EnableWebUIAuth is true (installer uploads the Lambda@Edge zip and sets this)." + # ============================================================================ # CONDITIONS # ============================================================================ @@ -1544,7 +1554,7 @@ Resources: - secretsmanager:UpdateSecret Resource: - !Ref WebUIAdminSecret - - !Ref WebUIEdgeSigningKeySecret + - !Ref WebUIEdgeConfigSecret - Effect: Allow Action: - secretsmanager:GetSecretValue @@ -1658,7 +1668,7 @@ Resources: cognito_domain = props.get('CognitoDomain', '') email = props.get('AdminEmail', '') admin_secret_arn = props.get('AdminSecretArn', '') - edge_secret_arn = props.get('EdgeSecretArn', '') + edge_config_secret_arn = props.get('EdgeConfigSecretArn', '') signing_key_secret_arn = props.get('SigningKeySecretArn', '') region = props.get('Region', os.environ.get('AWS_REGION', 'us-east-1')) @@ -1668,7 +1678,7 @@ Resources: 'CognitoDomain': cognito_domain, 'AdminEmail': email, 'AdminSecretArn': admin_secret_arn, - 'EdgeSecretArn': edge_secret_arn, + 'EdgeConfigSecretArn': edge_config_secret_arn, 'SigningKeySecretArn': signing_key_secret_arn, }.items() if not v] if missing: @@ -1690,9 +1700,12 @@ Resources: write_admin_secret(sm, admin_secret_arn, email, password) # 2) Read the CFN-generated signing key and write the merged edge config + # NOTE: signing_key_secret_arn and edge_config_secret_arn are DIFFERENT secrets. + # The signing-key secret is CFN-managed (immutable content); the config secret + # is what the Lambda@Edge reads at cold start. sk_resp = sm.get_secret_value(SecretId=signing_key_secret_arn) signing_key = json.loads(sk_resp['SecretString'])['key'] - write_edge_config(sm, edge_secret_arn, pool_id, client_id, cognito_domain, signing_key) + write_edge_config(sm, edge_config_secret_arn, pool_id, client_id, cognito_domain, signing_key) send_response(event, context, 'SUCCESS', 'Admin user + edge config provisioned', {'Email': email}) @@ -1707,6 +1720,7 @@ Resources: - WebUIUserPool - WebUIUserPoolClient - WebUIEdgeSigningKeySecret + - WebUIEdgeConfigSecret Properties: ServiceToken: !GetAtt WebUIUserCreationFunction.Arn UserPoolId: !Ref WebUIUserPool @@ -1714,19 +1728,26 @@ Resources: CognitoDomain: !Sub '${WebUIUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com' AdminEmail: !Ref WebUIAdminEmail AdminSecretArn: !Ref WebUIAdminSecret - EdgeSecretArn: !Ref WebUIEdgeSigningKeySecret + EdgeConfigSecretArn: !Ref WebUIEdgeConfigSecret SigningKeySecretArn: !Ref WebUIEdgeSigningKeySecret Region: !Ref 'AWS::Region' # -------------------------------------------------------------------------- # WebUI Lambda@Edge (Cognito enforcement on CloudFront) # -------------------------------------------------------------------------- + # Two secrets: + # - SigningKeySecret: raw HMAC key, CFN-managed via GenerateSecretString. + # Never rewritten by the Custom Resource. Read once by the Custom Resource + # to merge into EdgeConfigSecret. + # - EdgeConfigSecret: the merged config {poolId, clientId, cognitoDomain, + # signingKey} that the Lambda@Edge reads at cold start. Populated by the + # Custom Resource after Cognito resources are created. WebUIEdgeSigningKeySecret: Type: AWS::SecretsManager::Secret Condition: EnableWebUI Properties: Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-signing-key' - Description: !Sub 'Nonce/state HMAC signing key for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' + Description: !Sub 'Raw HMAC signing key for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' GenerateSecretString: SecretStringTemplate: '{}' GenerateStringKey: 'key' @@ -1740,6 +1761,21 @@ Resources: - Key: loki:env Value: !Ref EnvironmentName + WebUIEdgeConfigSecret: + Type: AWS::SecretsManager::Secret + Condition: EnableWebUI + Properties: + Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-config' + Description: !Sub 'Merged Cognito config for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' + SecretString: !Sub '{"poolId":"pending","clientId":"pending","cognitoDomain":"pending","signingKey":"pending"}' + Tags: + - Key: loki:managed + Value: 'true' + - Key: loki:pack + Value: !Ref PackName + - Key: loki:env + Value: !Ref EnvironmentName + WebUIEdgeLambdaRole: Type: AWS::IAM::Role Condition: EnableWebUI diff --git a/install.sh b/install.sh index 73ad8a1..ded2c6d 100755 --- a/install.sh +++ b/install.sh @@ -2176,12 +2176,13 @@ build_and_upload_edge_lambda() { step "WebUI Lambda@Edge" info "Building Cognito Lambda@Edge zip..." - # Deterministic secret NAME (not ARN — Secrets Manager appends a random suffix - # to the ARN we can't know at build time, but names are stable). - local secret_name="/lowkey/${ENV_NAME}/webui-edge-signing-key" + # Deterministic config secret NAME (not ARN — Secrets Manager appends a random suffix + # to the ARN we can't know at build time, but names are stable). The Lambda@Edge + # bakes this name in and fetches the merged config at cold start. + local secret_name="/lowkey/${ENV_NAME}/webui-edge-config" local zip_path - zip_path=$(SECRET_ARN="$secret_name" "$build_script" 2>&1 | tail -1) \ + zip_path=$(CONFIG_SECRET_NAME="$secret_name" "$build_script" 2>&1 | tail -1) \ || fail "Edge Lambda build failed: $zip_path" if [[ ! -f "$zip_path" ]]; then fail "Edge Lambda build script did not produce a zip: $zip_path" @@ -3512,15 +3513,16 @@ main() { # Console deploy exits early (no clone, no bootstrap wait) if [[ "$DEPLOY_METHOD" == "$DEPLOY_CFN_CONSOLE" ]]; then + # P1 #5: Console mode can't build+upload the edge Lambda zip (no local + # clone, no shell access post-flow). WebUI auth requires CLI deploy. + if [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" ]]; then + fail "WebUI authentication is not supported with the CloudFormation Console deploy method. Re-run and choose 'CloudFormation CLI' when prompted, or disable WebUI auth." + fi TOTAL_STEPS=5 _TELEM_CURRENT_STEP="deploy_console" _telem_deploy_started 2>/dev/null || true step "Deploy (Console)" deploy_console - if [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" ]]; then - info "After deploying the stack, retrieve the initial admin password from stack outputs:" - info " aws cloudformation describe-stacks --stack-name --region ${DEPLOY_REGION} --query 'Stacks[0].Outputs' --output table" - fi _telem_install_completed 2>/dev/null || true exit 0 fi diff --git a/packs/kirocrew/webui-auth-edge/build.sh b/packs/kirocrew/webui-auth-edge/build.sh index a575eda..992c7af 100755 --- a/packs/kirocrew/webui-auth-edge/build.sh +++ b/packs/kirocrew/webui-auth-edge/build.sh @@ -1,19 +1,20 @@ #!/usr/bin/env bash # Build the KiroCrew WebUI Cognito Lambda@Edge zip. # -# Substitutes SECRET_ARN placeholder in index.js, runs npm install --production, -# and produces edge-lambda-.zip in $OUT_DIR (default: script dir). +# Substitutes CONFIG_SECRET_NAME placeholder in index.js, runs npm install +# --production, and produces edge-lambda-.zip in $OUT_DIR (default: script dir). # -# The Lambda code fetches all other config (pool ID, client ID, domain, -# signing key) from Secrets Manager at cold start, using the ARN baked in -# here. This means we can build the zip BEFORE CFN creates the Cognito pool. +# The Lambda code fetches all config (pool ID, client ID, domain, signing key) +# from Secrets Manager at cold start using the config secret name baked in +# here. This lets us build the zip BEFORE CFN creates the Cognito pool. # # Required env vars: -# SECRET_ARN — Secrets Manager ARN for the edge auth config secret +# CONFIG_SECRET_NAME — Secrets Manager name (not ARN) for the edge config secret +# e.g. /lowkey//webui-edge-config # # Optional: -# OUT_DIR — Where to place the zip (default: script dir) -# NODE_BIN — Node binary path (default: node from PATH) +# OUT_DIR — Where to place the zip (default: script dir) +# NODE_BIN — Node binary path (default: node from PATH) # # Exit codes: 0 success, non-zero failure with message on stderr. @@ -23,8 +24,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUT_DIR="${OUT_DIR:-$SCRIPT_DIR}" NODE_BIN="${NODE_BIN:-node}" -if [[ -z "${SECRET_ARN:-}" ]]; then - echo "build.sh: missing required env var: SECRET_ARN" >&2 +if [[ -z "${CONFIG_SECRET_NAME:-}" ]]; then + echo "build.sh: missing required env var: CONFIG_SECRET_NAME" >&2 exit 2 fi @@ -45,8 +46,8 @@ trap 'rm -rf "$BUILD_DIR"' EXIT cp "$SCRIPT_DIR/index.js" "$BUILD_DIR/index.js" cp "$SCRIPT_DIR/package.json" "$BUILD_DIR/package.json" -# Substitute only SECRET_ARN placeholder -sed -i -e "s|__SECRET_NAME__|${SECRET_ARN}|g" "$BUILD_DIR/index.js" +# Substitute only CONFIG_SECRET_NAME placeholder +sed -i -e "s|__CONFIG_SECRET_NAME__|${CONFIG_SECRET_NAME}|g" "$BUILD_DIR/index.js" if grep -q '__[A-Z_]*__' "$BUILD_DIR/index.js"; then echo "build.sh: unresolved placeholders in index.js:" >&2 @@ -57,8 +58,10 @@ fi cd "$BUILD_DIR" npm install --production --no-audit --no-fund --loglevel=error >&2 -# Hash based on the secret ARN so the zip name is deterministic per stack -SHA=$(printf '%s' "$SECRET_ARN" | sha256sum | cut -c1-12) +# Content-addressed zip name: hash the actual substituted source + package.json. +# This ensures the S3 key CHANGES when the code changes, which forces CFN to +# see a diff on Code.S3Key and publishes a new Lambda Version (fixes P1 #4). +SHA=$(sha256sum "$BUILD_DIR/index.js" "$BUILD_DIR/package.json" | sha256sum | cut -c1-16) ZIP_NAME="edge-lambda-${SHA}.zip" ZIP_PATH="${OUT_DIR}/${ZIP_NAME}" diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js index c360e5a..22e269f 100644 --- a/packs/kirocrew/webui-auth-edge/index.js +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -2,20 +2,20 @@ * KiroCrew WebUI Cognito Auth — Lambda@Edge (viewer-request trigger). * * Validates a Cognito session cookie set by cognito-at-edge. Unauthenticated - * requests are redirected to Cognito hosted UI. Requests to /auth/callback + * requests are redirected to the Cognito hosted UI. Requests to /auth/callback * exchange the auth code for tokens and set the session cookie. * * Lambda@Edge constraints: * - No environment variables (config baked in below OR fetched from Secrets Manager) * - Node.js 18.x runtime - * - Must be deployed in us-east-1 + * - Must be deployed in us-east-1 (enforced by CFN Rule WebUIEdgeRequiresUsEast1) * - * Only SECRET_ARN is substituted at build time. All other config (pool ID, - * client ID, domain, signing key) is fetched from Secrets Manager on cold - * start, cached in module scope for warm invocations. + * Only CONFIG_SECRET_NAME is substituted at build time. All other config + * (pool ID, client ID, domain, signing key) is fetched from Secrets Manager + * on cold start, cached in module scope for warm invocations. * * This lets us zip the Lambda code BEFORE CFN creates the Cognito pool — - * the Lambda only needs the secret's ARN, which is a deterministic function + * the Lambda only needs the secret's name, which is a deterministic function * of the stack's environment name. */ @@ -25,21 +25,19 @@ const { GetSecretValueCommand, } = require('@aws-sdk/client-secrets-manager'); -// Only placeholder replaced at build time. All other config comes from the secret. -const SECRET_ARN = '__SECRET_NAME__'; +const CONFIG_SECRET_NAME = '__CONFIG_SECRET_NAME__'; const REGION = 'us-east-1'; let authenticatorPromise = null; async function loadConfig() { const sm = new SecretsManagerClient({ region: REGION }); - const resp = await sm.send(new GetSecretValueCommand({ SecretId: SECRET_ARN })); + const resp = await sm.send(new GetSecretValueCommand({ SecretId: CONFIG_SECRET_NAME })); const parsed = JSON.parse(resp.SecretString); const required = ['poolId', 'clientId', 'cognitoDomain', 'signingKey']; - for (const k of required) { - if (!parsed[k] || typeof parsed[k] !== 'string') { - throw new Error(`Edge auth secret is missing required field: ${k}`); - } + const missing = required.filter((k) => !parsed[k] || typeof parsed[k] !== 'string' || parsed[k] === 'pending'); + if (missing.length > 0) { + throw new Error(`Edge config secret missing or pending fields: ${missing.join(', ')}`); } return parsed; } From 8a70dc9fc19cd045000295763704a330520a23ae Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:52:56 +0000 Subject: [PATCH 04/13] fix(edge): address round-2 P0/P1 findings P0 #1: Lambda@Edge IAM policy pointed at wrong secret - WebUIEdgeLambdaRole 'FetchSigningKey' granted GetSecretValue on WebUIEdgeSigningKeySecret, but the Lambda@Edge code reads the merged WebUIEdgeConfigSecret (the one populated by the Custom Resource). At runtime the Lambda would AccessDenied on every cold start. - Renamed policy FetchSigningKey -> FetchEdgeConfig, changed Resource to !Ref WebUIEdgeConfigSecret. P1 #1: AWS::Lambda::Version wouldn't publish new versions on updates - The content-addressed zip (round-1 fix) updates $LATEST correctly but WebUIEdgeLambdaVersion has no property that changes between deploys, so CFN never replaces it and CloudFront stays pinned to the old version. - Fix: added CFN parameter EdgeLambdaCodeSha256 (base64 SHA of the uploaded zip), Rule assertion, and CodeSha256 property on the Version resource plus the SHA in Description. When code changes, the SHA changes, CFN sees a diff on Version, publishes a new numbered version, CloudFront picks it up. - Installer: build_and_upload_edge_lambda now computes 'openssl dgst -sha256 -binary | openssl base64 -A' and exports EDGE_LAMBDA_CODE_SHA256 through PARAM_CFN_NAMES/PARAM_VALUES. Validated: - bash -n install.sh: OK - aws cloudformation validate-template: OK (41 params, was 40) - PARAM_CFN_NAMES count: 29 (was 28) --- deploy/cloudformation/template.yaml | 18 ++++++++++++++---- install.sh | 12 +++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 54d3aed..e7b77ed 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -382,6 +382,11 @@ Parameters: Default: '' Description: "S3 key of the Lambda@Edge deployment zip. Required when EnableWebUIAuth is true. Installer uploads and sets this." + EdgeLambdaCodeSha256: + Type: String + Default: '' + Description: "Base64-encoded SHA256 of the Lambda@Edge zip. Required when EnableWebUIAuth is true. Forces a new AWS::Lambda::Version to be published (and picked up by CloudFront) when the code changes." + # ============================================================================ # RULES # ============================================================================ @@ -421,6 +426,8 @@ Rules: AssertDescription: "EdgeLambdaS3Bucket is required when EnableWebUIAuth is true (installer uploads the Lambda@Edge zip and sets this)." - Assert: !Not [!Equals [!Ref EdgeLambdaS3Key, '']] AssertDescription: "EdgeLambdaS3Key is required when EnableWebUIAuth is true (installer uploads the Lambda@Edge zip and sets this)." + - Assert: !Not [!Equals [!Ref EdgeLambdaCodeSha256, '']] + AssertDescription: "EdgeLambdaCodeSha256 is required when EnableWebUIAuth is true (installer computes and sets this so AWS::Lambda::Version publishes new versions on code updates)." # ============================================================================ # CONDITIONS @@ -1793,14 +1800,14 @@ Resources: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - - PolicyName: FetchSigningKey + - PolicyName: FetchEdgeConfig PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue - Resource: !Ref WebUIEdgeSigningKeySecret + Resource: !Ref WebUIEdgeConfigSecret WebUIEdgeLambdaFunction: Type: AWS::Lambda::Function @@ -1820,13 +1827,16 @@ Resources: S3Key: !Ref EdgeLambdaS3Key # Version is required for LambdaFunctionAssociations; publishing a new version - # is how updates roll out to CloudFront edges. + # is how updates roll out to CloudFront edges. CodeSha256 makes the resource + # non-immutable across deploys: when the zip changes, the SHA changes, CFN + # sees a diff and publishes a new version (which CloudFront then picks up). WebUIEdgeLambdaVersion: Type: AWS::Lambda::Version Condition: EnableWebUI Properties: FunctionName: !Ref WebUIEdgeLambdaFunction - Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName})' + Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName}) sha256=${EdgeLambdaCodeSha256}' + CodeSha256: !Ref EdgeLambdaCodeSha256 # SSM Session Manager Preferences (auto-login as ec2-user with welcome) # -------------------------------------------------------------------------- diff --git a/install.sh b/install.sh index ded2c6d..fe1537b 100755 --- a/install.sh +++ b/install.sh @@ -2210,8 +2210,17 @@ build_and_upload_edge_lambda() { aws s3 cp "$zip_path" "s3://${bucket}/${key}" --region "$DEPLOY_REGION" >/dev/null \ || fail "Failed to upload edge Lambda zip" + # Compute base64-encoded SHA256 of the zip. CFN needs this on the + # AWS::Lambda::Version resource so a new version is published whenever + # the code changes; without it, CloudFront keeps pointing at the old + # version even though $LATEST has new code. + local code_sha256 + code_sha256=$(openssl dgst -sha256 -binary "$zip_path" | openssl base64 -A) \ + || fail "Failed to compute SHA256 of edge Lambda zip" + export EDGE_LAMBDA_S3_BUCKET="$bucket" export EDGE_LAMBDA_S3_KEY="$key" + export EDGE_LAMBDA_CODE_SHA256="$code_sha256" ok "Edge Lambda uploaded" } @@ -2349,7 +2358,7 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console and CFN CLI # ============================================================================ # ⚠ KEEP THESE TWO ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaS3Bucket EdgeLambdaS3Key) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaS3Bucket EdgeLambdaS3Key EdgeLambdaCodeSha256) PARAM_VALUES=() # populated by build_deploy_params() # Per-pack default model (passed to CFN DefaultModel / bootstrap.sh --model). @@ -2413,6 +2422,7 @@ build_deploy_params() { "${WEBUI_ADMIN_EMAIL:-}" "${EDGE_LAMBDA_S3_BUCKET:-}" "${EDGE_LAMBDA_S3_KEY:-}" + "${EDGE_LAMBDA_CODE_SHA256:-}" ) # Validate parallel arrays are in sync [[ ${#PARAM_CFN_NAMES[@]} -eq ${#PARAM_VALUES[@]} ]] \ From 7ecf3c6174f878a704438702a7e2341c7f354bfd Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:56:52 +0000 Subject: [PATCH 05/13] fix(edge): address round-3 P1/P2 findings P1 #1: nonceSigningSecret was passed at wrong nesting level - cognito-at-edge silently ignored the top-level 'nonceSigningSecret' and the invalid 'cookieCompatibility: amplify' option, meaning CSRF nonce HMAC signing never actually engaged. - Fix: pass as csrfProtection: { nonceSigningSecret: cfg.signingKey }; removed cookieCompatibility. P1 #2: S3 key and CodeSha256 could drift out of sync - The old build hashed only source files (index.js + package.json), producing a stable S3 key even when node_modules changed. But CodeSha256 is computed from the zip binary. Different SHA + same S3 key => CFN sees Function unchanged, but Version.CodeSha256 no longer matches => stack update fails. - Fix: hash the zip bytes and use that SHA as the S3 key. S3 key and CodeSha256 are now derived from the same source of truth. P2 #1: openssl not in preflight - build_and_upload_edge_lambda now uses openssl (via install.sh) to compute CodeSha256. Added require_cmd checks for node/npm/zip/openssl at the start of the function so the failure mode is clear. P2 #2: sha256sum breaks on macOS - packs/.../build.sh now defines a portable sha256_hex helper that prefers openssl, falls back to sha256sum, then shasum -a 256. Validated: - bash -n install.sh: OK - bash -n build.sh: OK - node --check index.js: OK - aws cloudformation validate-template: OK (41 params) - Live build test produced valid zip: edge-lambda-38101e37911dc140.zip --- install.sh | 6 ++++ packs/kirocrew/webui-auth-edge/build.sh | 38 ++++++++++++++++++++----- packs/kirocrew/webui-auth-edge/index.js | 7 +++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index fe1537b..e308056 100755 --- a/install.sh +++ b/install.sh @@ -2165,6 +2165,12 @@ build_and_upload_edge_lambda() { [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" ]] || return 0 [[ "$PACK_NAME" == "kirocrew" ]] || return 0 + # Preflight: required tools for the edge Lambda build path. + require_cmd node "node (18+) is required to build the Cognito Lambda@Edge zip. Install Node 18+ or disable WebUI auth." + require_cmd npm "npm is required to build the Cognito Lambda@Edge zip." + require_cmd zip "zip is required to package the Cognito Lambda@Edge deployment." + require_cmd openssl "openssl is required to compute the Lambda@Edge zip SHA256 for CFN." + local build_script="${CLONE_DIR}/packs/kirocrew/webui-auth-edge/build.sh" # In debug-in-repo mode CLONE_DIR points into the working repo; otherwise it # points at the freshly-cloned copy. In either case the script must exist diff --git a/packs/kirocrew/webui-auth-edge/build.sh b/packs/kirocrew/webui-auth-edge/build.sh index 992c7af..71a3f28 100755 --- a/packs/kirocrew/webui-auth-edge/build.sh +++ b/packs/kirocrew/webui-auth-edge/build.sh @@ -58,14 +58,38 @@ fi cd "$BUILD_DIR" npm install --production --no-audit --no-fund --loglevel=error >&2 -# Content-addressed zip name: hash the actual substituted source + package.json. -# This ensures the S3 key CHANGES when the code changes, which forces CFN to -# see a diff on Code.S3Key and publishes a new Lambda Version (fixes P1 #4). -SHA=$(sha256sum "$BUILD_DIR/index.js" "$BUILD_DIR/package.json" | sha256sum | cut -c1-16) -ZIP_NAME="edge-lambda-${SHA}.zip" +# Content-addressed zip name: hash the actual ZIP BYTES (not source files) so +# the S3 key stays in lockstep with CodeSha256 in CFN. Round-3 fix (P1 #2): +# hashing source only meant node_modules variance produced different +# CodeSha256 with same S3 key -> CFN Function unchanged but Version.CodeSha256 +# mismatch -> stack update fails. +# +# Portable SHA256: prefer openssl (universal on macOS + Linux); fall back to +# sha256sum (Linux) then shasum (macOS builtin). Round-3 fix (P2 #2). +sha256_hex() { + if command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 -hex "$1" | awk '{print $NF}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + echo "build.sh: no sha256 tool available (need openssl, sha256sum, or shasum)" >&2 + return 1 + fi +} + +# Build the zip first, then hash it. Use a stable temp name during build. +TMP_ZIP="${OUT_DIR}/.edge-lambda-tmp-$$.zip" +trap 'rm -f "$TMP_ZIP"; rm -rf "$BUILD_DIR"' EXIT +rm -f "$TMP_ZIP" +zip -r -q -X "$TMP_ZIP" index.js package.json node_modules >&2 + +ZIP_SHA=$(sha256_hex "$TMP_ZIP") || exit 7 +SHA_SHORT="${ZIP_SHA:0:16}" +ZIP_NAME="edge-lambda-${SHA_SHORT}.zip" ZIP_PATH="${OUT_DIR}/${ZIP_NAME}" -rm -f "$ZIP_PATH" -zip -r -q "$ZIP_PATH" index.js package.json node_modules >&2 +mv "$TMP_ZIP" "$ZIP_PATH" echo "$ZIP_PATH" diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js index 22e269f..4b12289 100644 --- a/packs/kirocrew/webui-auth-edge/index.js +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -54,8 +54,11 @@ async function getAuthenticator() { cookieExpirationDays: 1, disableCookieDomain: true, logLevel: 'warn', - cookieCompatibility: 'amplify', - nonceSigningSecret: cfg.signingKey, + // cognito-at-edge expects nonceSigningSecret nested under csrfProtection. + // Top-level 'nonceSigningSecret' is silently ignored (round-3 P1 #1 fix). + csrfProtection: { + nonceSigningSecret: cfg.signingKey, + }, }); })().catch((err) => { // Reset so the next cold-start attempt can retry From 72c5d99948f6ce412c2db3e9a82349b151231050 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:30:06 +0000 Subject: [PATCH 06/13] docs+wip: split-stack architecture design (v3) + partial CFN refactor Design doc: - New section 'Split-Stack Architecture (v3)' in docs/design/kirocrew-webui-auth.md - Covers: problem (region lock), solution, deployment flow, cross-region secret writes, file layout, installer 2-phase flow, uninstall, residual risks CFN (WIP - installer not yet wired): - deploy/cloudformation/edge-stack.yaml: new companion stack (us-east-1 only) containing Lambda@Edge function/version, IAM role, two secrets - deploy/cloudformation/template.yaml: Lambda@Edge resources removed; new params (EdgeLambdaVersionArn + 4 secret name/ARN params) replace old S3/SHA params; CFN Rule updated to require edge params not us-east-1 region; CloudFront uses !Ref EdgeLambdaVersionArn; Custom Resource props updated Installer 2-phase deploy and cross-region SM client refactor pending. --- deploy/cloudformation/edge-stack.yaml | 146 ++++++++++++++++++++++++++ deploy/cloudformation/template.yaml | 145 ++++++------------------- docs/design/kirocrew-webui-auth.md | 135 ++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 111 deletions(-) create mode 100644 deploy/cloudformation/edge-stack.yaml diff --git a/deploy/cloudformation/edge-stack.yaml b/deploy/cloudformation/edge-stack.yaml new file mode 100644 index 0000000..6d7adb0 --- /dev/null +++ b/deploy/cloudformation/edge-stack.yaml @@ -0,0 +1,146 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: > + Lowkey WebUI Cognito Auth — Lambda@Edge companion stack. + Deploys the Lambda@Edge function, its version, IAM role, and the two + Secrets Manager secrets (signing key + merged edge config) in us-east-1 + because Lambda@Edge sources MUST live in us-east-1 (AWS platform + requirement). The main Lowkey stack can deploy in any region; the + installer wires the two together via CFN parameters and cross-region + Secrets Manager writes. + +Parameters: + EnvironmentName: + Type: String + MinLength: 1 + Description: "Environment name — matches the main stack's EnvironmentName so secret names align." + + PackName: + Type: String + Default: kirocrew + Description: "Pack that owns this edge Lambda. Used only for tags." + + EdgeLambdaS3Bucket: + Type: String + MinLength: 1 + Description: "S3 bucket (in us-east-1) holding the Lambda@Edge deployment zip." + + EdgeLambdaS3Key: + Type: String + MinLength: 1 + Description: "S3 key of the Lambda@Edge deployment zip." + + EdgeLambdaCodeSha256: + Type: String + MinLength: 1 + Description: "Base64 SHA256 of the deployment zip. Forces a new AWS::Lambda::Version when code changes." + +Resources: + # Raw HMAC signing key. CFN-managed via GenerateSecretString. Never written + # to by anything else — read once by the main-stack Custom Resource which + # then merges it into EdgeConfigSecret alongside the Cognito pool/client/domain. + WebUIEdgeSigningKeySecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-signing-key' + Description: !Sub 'Raw HMAC signing key for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' + GenerateSecretString: + SecretStringTemplate: '{}' + GenerateStringKey: 'key' + PasswordLength: 64 + ExcludePunctuation: true + Tags: + - Key: loki:managed + Value: 'true' + - Key: loki:pack + Value: !Ref PackName + - Key: loki:env + Value: !Ref EnvironmentName + + # Merged {poolId, clientId, cognitoDomain, signingKey} secret that the + # Lambda@Edge reads at cold start. Initial SecretString is a placeholder; + # the main-stack Custom Resource overwrites it once the Cognito pool exists. + WebUIEdgeConfigSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-config' + Description: !Sub 'Merged Cognito config for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' + SecretString: '{"poolId":"pending","clientId":"pending","cognitoDomain":"pending","signingKey":"pending"}' + Tags: + - Key: loki:managed + Value: 'true' + - Key: loki:pack + Value: !Ref PackName + - Key: loki:env + Value: !Ref EnvironmentName + + WebUIEdgeLambdaRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${EnvironmentName}-webui-edge-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + - edgelambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: FetchEdgeConfig + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref WebUIEdgeConfigSecret + + WebUIEdgeLambdaFunction: + Type: AWS::Lambda::Function + DependsOn: + - WebUIEdgeConfigSecret + Properties: + FunctionName: !Sub '${EnvironmentName}-webui-edge-auth' + Runtime: nodejs18.x + Handler: index.handler + MemorySize: 128 + Timeout: 5 + Role: !GetAtt WebUIEdgeLambdaRole.Arn + Code: + S3Bucket: !Ref EdgeLambdaS3Bucket + S3Key: !Ref EdgeLambdaS3Key + + WebUIEdgeLambdaVersion: + Type: AWS::Lambda::Version + Properties: + FunctionName: !Ref WebUIEdgeLambdaFunction + Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName}) sha256=${EdgeLambdaCodeSha256}' + CodeSha256: !Ref EdgeLambdaCodeSha256 + +Outputs: + EdgeLambdaVersionArn: + Description: "Versioned ARN of the Lambda@Edge function. Pass this to the main stack's WebUIEdgeLambdaVersionArn parameter." + Value: !Ref WebUIEdgeLambdaVersion + + EdgeLambdaFunctionArn: + Description: "Unversioned ARN of the Lambda@Edge function." + Value: !GetAtt WebUIEdgeLambdaFunction.Arn + + EdgeConfigSecretName: + Description: "Name of the Secrets Manager secret holding the merged edge config. Main stack's Custom Resource writes to this via cross-region SM client." + Value: !Ref WebUIEdgeConfigSecret + + EdgeConfigSecretArn: + Description: "Full ARN of the edge config secret." + Value: !Ref WebUIEdgeConfigSecret + + SigningKeySecretName: + Description: "Name of the Secrets Manager secret holding the raw signing key." + Value: !Ref WebUIEdgeSigningKeySecret + + SigningKeySecretArn: + Description: "Full ARN of the signing key secret." + Value: !Ref WebUIEdgeSigningKeySecret diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index e7b77ed..577d99b 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -372,21 +372,30 @@ Parameters: Description: "Email for the initial WebUI admin user. Required when EnableWebUIAuth is true." AllowedPattern: '^([^@]+@[^@]+\.[^@]+)?$' - EdgeLambdaS3Bucket: + EdgeLambdaVersionArn: Type: String Default: '' - Description: "S3 bucket holding the Lambda@Edge deployment zip. Required when EnableWebUIAuth is true. Installer uploads and sets this." + Description: "Versioned ARN of the Lambda@Edge function (produced by the companion edge-stack in us-east-1). Required when EnableWebUIAuth is true." - EdgeLambdaS3Key: + EdgeConfigSecretName: Type: String Default: '' - Description: "S3 key of the Lambda@Edge deployment zip. Required when EnableWebUIAuth is true. Installer uploads and sets this." + Description: "Name of the us-east-1 Secrets Manager secret holding the merged Cognito config for the Lambda@Edge. Required when EnableWebUIAuth is true." - EdgeLambdaCodeSha256: + EdgeConfigSecretArn: Type: String Default: '' - Description: "Base64-encoded SHA256 of the Lambda@Edge zip. Required when EnableWebUIAuth is true. Forces a new AWS::Lambda::Version to be published (and picked up by CloudFront) when the code changes." + Description: "Full ARN of the us-east-1 edge config secret. Required when EnableWebUIAuth is true." + SigningKeySecretName: + Type: String + Default: '' + Description: "Name of the us-east-1 Secrets Manager secret holding the raw signing key. Required when EnableWebUIAuth is true." + + SigningKeySecretArn: + Type: String + Default: '' + Description: "Full ARN of the us-east-1 signing key secret. Required when EnableWebUIAuth is true." # ============================================================================ # RULES # ============================================================================ @@ -417,17 +426,19 @@ Rules: - Assert: !Not [!Equals [!Ref WebUIAdminEmail, '']] AssertDescription: "WebUIAdminEmail is required when EnableWebUIAuth is true." - WebUIEdgeRequiresUsEast1: + WebUIEdgeRequiresParams: RuleCondition: !Equals [!Ref EnableWebUIAuth, 'true'] Assertions: - - Assert: !Equals [!Ref 'AWS::Region', 'us-east-1'] - AssertDescription: "EnableWebUIAuth requires deploying in us-east-1 (Lambda@Edge is a CloudFront-only resource that must live in us-east-1)." - - Assert: !Not [!Equals [!Ref EdgeLambdaS3Bucket, '']] - AssertDescription: "EdgeLambdaS3Bucket is required when EnableWebUIAuth is true (installer uploads the Lambda@Edge zip and sets this)." - - Assert: !Not [!Equals [!Ref EdgeLambdaS3Key, '']] - AssertDescription: "EdgeLambdaS3Key is required when EnableWebUIAuth is true (installer uploads the Lambda@Edge zip and sets this)." - - Assert: !Not [!Equals [!Ref EdgeLambdaCodeSha256, '']] - AssertDescription: "EdgeLambdaCodeSha256 is required when EnableWebUIAuth is true (installer computes and sets this so AWS::Lambda::Version publishes new versions on code updates)." + - Assert: !Not [!Equals [!Ref EdgeLambdaVersionArn, '']] + AssertDescription: "EdgeLambdaVersionArn is required when EnableWebUIAuth is true (installer deploys the companion edge stack in us-east-1 first and passes its outputs to this stack)." + - Assert: !Not [!Equals [!Ref EdgeConfigSecretName, '']] + AssertDescription: "EdgeConfigSecretName is required when EnableWebUIAuth is true (from the edge stack outputs)." + - Assert: !Not [!Equals [!Ref EdgeConfigSecretArn, '']] + AssertDescription: "EdgeConfigSecretArn is required when EnableWebUIAuth is true (from the edge stack outputs)." + - Assert: !Not [!Equals [!Ref SigningKeySecretName, '']] + AssertDescription: "SigningKeySecretName is required when EnableWebUIAuth is true (from the edge stack outputs)." + - Assert: !Not [!Equals [!Ref SigningKeySecretArn, '']] + AssertDescription: "SigningKeySecretArn is required when EnableWebUIAuth is true (from the edge stack outputs)." # ============================================================================ # CONDITIONS @@ -702,7 +713,7 @@ Resources: LambdaFunctionAssociations: !If - EnableWebUI - - EventType: viewer-request - LambdaFunctionARN: !Ref WebUIEdgeLambdaVersion + LambdaFunctionARN: !Ref EdgeLambdaVersionArn IncludeBody: false - !Ref 'AWS::NoValue' Origins: @@ -1561,11 +1572,11 @@ Resources: - secretsmanager:UpdateSecret Resource: - !Ref WebUIAdminSecret - - !Ref WebUIEdgeConfigSecret + - !Ref EdgeConfigSecretArn - Effect: Allow Action: - secretsmanager:GetSecretValue - Resource: !Ref WebUIEdgeSigningKeySecret + Resource: !Ref SigningKeySecretArn WebUIUserCreationFunction: Type: AWS::Lambda::Function @@ -1726,8 +1737,6 @@ Resources: DependsOn: - WebUIUserPool - WebUIUserPoolClient - - WebUIEdgeSigningKeySecret - - WebUIEdgeConfigSecret Properties: ServiceToken: !GetAtt WebUIUserCreationFunction.Arn UserPoolId: !Ref WebUIUserPool @@ -1735,8 +1744,11 @@ Resources: CognitoDomain: !Sub '${WebUIUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com' AdminEmail: !Ref WebUIAdminEmail AdminSecretArn: !Ref WebUIAdminSecret - EdgeConfigSecretArn: !Ref WebUIEdgeConfigSecret - SigningKeySecretArn: !Ref WebUIEdgeSigningKeySecret + EdgeConfigSecretName: !Ref EdgeConfigSecretName + EdgeConfigSecretArn: !Ref EdgeConfigSecretArn + SigningKeySecretName: !Ref SigningKeySecretName + SigningKeySecretArn: !Ref SigningKeySecretArn + EdgeRegion: 'us-east-1' Region: !Ref 'AWS::Region' # -------------------------------------------------------------------------- @@ -1749,95 +1761,6 @@ Resources: # - EdgeConfigSecret: the merged config {poolId, clientId, cognitoDomain, # signingKey} that the Lambda@Edge reads at cold start. Populated by the # Custom Resource after Cognito resources are created. - WebUIEdgeSigningKeySecret: - Type: AWS::SecretsManager::Secret - Condition: EnableWebUI - Properties: - Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-signing-key' - Description: !Sub 'Raw HMAC signing key for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' - GenerateSecretString: - SecretStringTemplate: '{}' - GenerateStringKey: 'key' - PasswordLength: 64 - ExcludePunctuation: true - Tags: - - Key: loki:managed - Value: 'true' - - Key: loki:pack - Value: !Ref PackName - - Key: loki:env - Value: !Ref EnvironmentName - - WebUIEdgeConfigSecret: - Type: AWS::SecretsManager::Secret - Condition: EnableWebUI - Properties: - Name: !Sub '/lowkey/${EnvironmentName}/webui-edge-config' - Description: !Sub 'Merged Cognito config for KiroCrew WebUI Lambda@Edge (${EnvironmentName})' - SecretString: !Sub '{"poolId":"pending","clientId":"pending","cognitoDomain":"pending","signingKey":"pending"}' - Tags: - - Key: loki:managed - Value: 'true' - - Key: loki:pack - Value: !Ref PackName - - Key: loki:env - Value: !Ref EnvironmentName - - WebUIEdgeLambdaRole: - Type: AWS::IAM::Role - Condition: EnableWebUI - Properties: - RoleName: !Sub '${EnvironmentName}-webui-edge-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - - edgelambda.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - Policies: - - PolicyName: FetchEdgeConfig - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: !Ref WebUIEdgeConfigSecret - - WebUIEdgeLambdaFunction: - Type: AWS::Lambda::Function - Condition: EnableWebUI - DependsOn: - - WebUIEdgeSigningKeySecret - - WebUIUserPoolClient - Properties: - FunctionName: !Sub '${EnvironmentName}-webui-edge-auth' - Runtime: nodejs18.x - Handler: index.handler - MemorySize: 128 - Timeout: 5 - Role: !GetAtt WebUIEdgeLambdaRole.Arn - Code: - S3Bucket: !Ref EdgeLambdaS3Bucket - S3Key: !Ref EdgeLambdaS3Key - - # Version is required for LambdaFunctionAssociations; publishing a new version - # is how updates roll out to CloudFront edges. CodeSha256 makes the resource - # non-immutable across deploys: when the zip changes, the SHA changes, CFN - # sees a diff and publishes a new version (which CloudFront then picks up). - WebUIEdgeLambdaVersion: - Type: AWS::Lambda::Version - Condition: EnableWebUI - Properties: - FunctionName: !Ref WebUIEdgeLambdaFunction - Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName}) sha256=${EdgeLambdaCodeSha256}' - CodeSha256: !Ref EdgeLambdaCodeSha256 - # SSM Session Manager Preferences (auto-login as ec2-user with welcome) # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- diff --git a/docs/design/kirocrew-webui-auth.md b/docs/design/kirocrew-webui-auth.md index 92d7b05..9b65599 100644 --- a/docs/design/kirocrew-webui-auth.md +++ b/docs/design/kirocrew-webui-auth.md @@ -493,3 +493,138 @@ If Lambda@Edge causes issues, remove the `LambdaFunctionAssociations` block from - [ ] CFN: `KiroCrewDistribution.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations` = viewer-request → new function version - [ ] CFN outputs: `WebUIEdgeFunctionArn`, `WebUIEdgeSigningKeySecretArn` - [ ] Validate template, `bash -n install.sh`, deploy test stack + +--- + +# Split-Stack Architecture (v3) — Region-Agnostic Lambda@Edge + +**Status:** Implementation in progress on `feat/lambda-edge-cognito`. + +## Problem With v2 + +The v2 design locked the entire main stack to `us-east-1` via a CFN Rule, because Lambda@Edge functions must be sourced from us-east-1 (AWS platform requirement). This broke users deploying to `eu-west-1`, `ap-southeast-1`, etc. + +## Solution: Companion Edge Stack in us-east-1 + +Split the Lambda@Edge resources out of the main stack into a dedicated **companion stack** that always deploys in `us-east-1`, regardless of where the user chooses to deploy the main stack. + +## Architecture + +``` +USER'S CHOSEN REGION ($DEPLOY_REGION) us-east-1 (always) +───────────────────────────────────────── ──────────────────────────── +Main Stack (lowkey-stack) Edge Stack (lowkey-edge-stack) + VPC, EC2, ALB, Cognito WebUIEdgeLambdaFunction + CloudFront ──────────────────────────────────► WebUIEdgeLambdaVersion + WebUIUserPool / Client / Domain WebUIEdgeLambdaRole + WebUIAdminSecret WebUIEdgeSigningKeySecret + WebUIUserCreationFunction ───────────────────► WebUIEdgeConfigSecret + (written cross-region + after Cognito created) +``` + +### Deployment Flow (Installer) + +``` +1. User chooses region (e.g. eu-west-1) + enables WebUI auth +2. Installer builds + uploads edge Lambda zip to S3 in us-east-1 +3. Installer deploys edge-stack.yaml in us-east-1 + → Outputs: EdgeLambdaVersionArn, EdgeConfigSecretArn/Name, + SigningKeySecretArn/Name +4. Installer deploys template.yaml in $DEPLOY_REGION + → Passes edge-stack outputs as CFN parameters + → CloudFront references EdgeLambdaVersionArn (already in us-east-1) +5. Custom Resource (runs in $DEPLOY_REGION) writes to us-east-1 secrets + via cross-region boto3.client('secretsmanager', region_name='us-east-1') + → Writes admin creds to WebUIAdminSecret ($DEPLOY_REGION) + → Reads signing key from us-east-1 WebUIEdgeSigningKeySecret + → Writes merged config to us-east-1 WebUIEdgeConfigSecret +``` + +### Cross-Region Secret Writes + +The Custom Resource Lambda runs in `$DEPLOY_REGION` but must write to the us-east-1 config secret. It uses **two boto3 clients**: + +```python +sm_local = boto3.client('secretsmanager', region_name=region) # admin secret +sm_edge = boto3.client('secretsmanager', region_name='us-east-1') # edge config + signing key +``` + +IAM policy on `WebUIUserCreationRole` grants: +- `secretsmanager:PutSecretValue` on `WebUIAdminSecret` (regional ARN) +- `secretsmanager:PutSecretValue + GetSecretValue` on edge secrets (us-east-1 ARNs, cross-region OK via IAM) + +### Lambda@Edge Cold Start + +The Lambda@Edge function (in us-east-1) reads config from `WebUIEdgeConfigSecret` (also us-east-1) at cold start — no cross-region penalty. + +## Files + +| File | Purpose | +|------|---------| +| `deploy/cloudformation/edge-stack.yaml` | Companion stack, always deploys in us-east-1 | +| `deploy/cloudformation/template.yaml` | Main stack, deploys in `$DEPLOY_REGION` | +| `packs/kirocrew/webui-auth-edge/index.js` | Lambda@Edge handler | +| `packs/kirocrew/webui-auth-edge/build.sh` | Zip builder | + +## CFN Parameters — Main Stack (new) + +| Parameter | Source | Description | +|-----------|--------|-------------| +| `EdgeLambdaVersionArn` | edge-stack output | CloudFront associates this version | +| `EdgeConfigSecretName` | edge-stack output | Custom Resource uses for cross-region write | +| `EdgeConfigSecretArn` | edge-stack output | IAM Resource constraint | +| `SigningKeySecretName` | edge-stack output | Custom Resource reads signing key | +| `SigningKeySecretArn` | edge-stack output | IAM Resource constraint | + +## CFN Parameters — Edge Stack + +| Parameter | Source | Description | +|-----------|--------|-------------| +| `EnvironmentName` | installer | Matches main stack (aligns secret names) | +| `EdgeLambdaS3Bucket` | installer | us-east-1 S3 bucket with the zip | +| `EdgeLambdaS3Key` | installer | S3 key (content-addressed SHA hex) | +| `EdgeLambdaCodeSha256` | installer | Base64 SHA256 for Version CodeSha256 | + +## Installer Flow (2-phase) + +```bash +# Phase 1: Edge stack (us-east-1) +deploy_edge_stack() { + # Upload zip to us-east-1 bucket + aws s3 cp "$zip" s3://${edge_bucket}/${key} --region us-east-1 + # Deploy edge-stack.yaml in us-east-1 + aws cloudformation deploy --template-file edge-stack.yaml \ + --stack-name "${ENV_NAME}-edge-stack" --region us-east-1 \ + --parameter-overrides EnvironmentName=$ENV_NAME ... + # Capture outputs + EDGE_LAMBDA_VERSION_ARN=$(cfn output EdgeLambdaVersionArn) + EDGE_CONFIG_SECRET_NAME=$(cfn output EdgeConfigSecretName) + ... +} + +# Phase 2: Main stack ($DEPLOY_REGION) +deploy_cfn_stack() { + # Passes edge outputs as params: EdgeLambdaVersionArn, etc. + aws cloudformation deploy --template-file template.yaml \ + --stack-name "${ENV_NAME}-stack" --region $DEPLOY_REGION \ + --parameter-overrides EdgeLambdaVersionArn=$EDGE_LAMBDA_VERSION_ARN ... +} +``` + +## Uninstall + +Both stacks must be deleted. Edge stack deletion will wait up to ~1hr for Lambda@Edge replica GC before the function can be deleted (AWS behaviour). + +## Key Design Decisions + +- **S3 bucket for edge zip in us-east-1**: Lambda code S3 bucket must be in the same region as the function. The existing `$ENV_NAME-cfn-templates-$ACCOUNT_ID` bucket is in `$DEPLOY_REGION`; a separate `$ENV_NAME-edge-$ACCOUNT_ID` bucket is created in us-east-1. +- **Secret names are deterministic**: `/lowkey/$ENV_NAME/webui-edge-config` and `/lowkey/$ENV_NAME/webui-edge-signing-key`. This lets the Lambda@Edge code look them up by name without needing the full ARN in the zip. +- **Cross-region IAM**: IAM is global so the Custom Resource role can reference us-east-1 secret ARNs as Resource constraints even when running in eu-west-1. +- **Rollback isolation**: If main stack fails, edge stack stays. Re-running the installer after fixing the main-stack issue re-uses the existing edge stack (idempotent deploy). If edge stack fails, the main stack never starts. + +## Residual Risks + +- **Stale key cache**: Lambda@Edge caches config in module scope. Signing key rotation → warm replicas serve stale key until natural cold-start (hours). Mitigate: document that rotation requires a new edge stack deploy. +- **~1hr edge replica delete lag**: On stack teardown, `aws cloudformation delete-stack` on the edge stack will fail until Lambda@Edge replicas GC. Installer should surface this as an expected wait. +- **Brief 503 on first deploy**: Custom Resource runs after CloudFront deploys (no explicit DependsOn). First viewer request during that window → Lambda@Edge reads `"pending"` → returns 503 (graceful). Typically <30s window. From caa12694bb84c17a00d8e9d1a785278a9af1ec24 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:39:04 +0000 Subject: [PATCH 07/13] fix(edge): address v3 design review P0/P1 P0: Custom Resource cross-region SM client - Was single boto3.client using main-stack region, which would ResourceNotFoundException on the us-east-1 edge secrets. - Now creates two clients: sm_local (main region) for admin secret, sm_edge (us-east-1) for signing-key read and edge-config write. - Reads EdgeRegion + EdgeConfigSecretName + SigningKeySecretName from Custom Resource ResourceProperties. P1-1: Output name-vs-ARN correctness in edge-stack.yaml - EdgeConfigSecretName and SigningKeySecretName previously used !Ref WebUIEdge...Secret which returns the ARN, not the name. - Fixed to !Sub '/lowkey/${EnvironmentName}/webui-edge-config' and '/lowkey/${EnvironmentName}/webui-edge-signing-key' respectively. These are the deterministic names the CFN Secret Name property produces, so the outputs and the actual Secrets Manager names align. P1-2: Node runtime upgrade - edge-stack.yaml: Runtime nodejs18.x -> nodejs20.x (Node 18 EOL Sept 2025; deployments may be rejected by Lambda.) - Also removed misleading DependsOn: WebUIEdgeConfigSecret on the Lambda function (implicit dep via IAM role policy is sufficient). Validated: main template + edge stack both pass validate-template. Installer 2-phase deploy is still pending. --- deploy/cloudformation/edge-stack.yaml | 12 +++++------- deploy/cloudformation/template.yaml | 26 +++++++++++++++----------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/deploy/cloudformation/edge-stack.yaml b/deploy/cloudformation/edge-stack.yaml index 6d7adb0..0662191 100644 --- a/deploy/cloudformation/edge-stack.yaml +++ b/deploy/cloudformation/edge-stack.yaml @@ -100,11 +100,9 @@ Resources: WebUIEdgeLambdaFunction: Type: AWS::Lambda::Function - DependsOn: - - WebUIEdgeConfigSecret Properties: FunctionName: !Sub '${EnvironmentName}-webui-edge-auth' - Runtime: nodejs18.x + Runtime: nodejs20.x Handler: index.handler MemorySize: 128 Timeout: 5 @@ -130,16 +128,16 @@ Outputs: Value: !GetAtt WebUIEdgeLambdaFunction.Arn EdgeConfigSecretName: - Description: "Name of the Secrets Manager secret holding the merged edge config. Main stack's Custom Resource writes to this via cross-region SM client." - Value: !Ref WebUIEdgeConfigSecret + Description: "Deterministic name of the merged edge config secret. Custom Resource in the main stack uses this via a cross-region Secrets Manager client." + Value: !Sub '/lowkey/${EnvironmentName}/webui-edge-config' EdgeConfigSecretArn: Description: "Full ARN of the edge config secret." Value: !Ref WebUIEdgeConfigSecret SigningKeySecretName: - Description: "Name of the Secrets Manager secret holding the raw signing key." - Value: !Ref WebUIEdgeSigningKeySecret + Description: "Deterministic name of the raw signing key secret." + Value: !Sub '/lowkey/${EnvironmentName}/webui-edge-signing-key' SigningKeySecretArn: Description: "Full ARN of the signing key secret." diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 577d99b..921076e 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1686,8 +1686,11 @@ Resources: cognito_domain = props.get('CognitoDomain', '') email = props.get('AdminEmail', '') admin_secret_arn = props.get('AdminSecretArn', '') + edge_config_secret_name = props.get('EdgeConfigSecretName', '') edge_config_secret_arn = props.get('EdgeConfigSecretArn', '') + signing_key_secret_name = props.get('SigningKeySecretName', '') signing_key_secret_arn = props.get('SigningKeySecretArn', '') + edge_region = props.get('EdgeRegion', 'us-east-1') region = props.get('Region', os.environ.get('AWS_REGION', 'us-east-1')) missing = [k for k, v in { @@ -1696,15 +1699,16 @@ Resources: 'CognitoDomain': cognito_domain, 'AdminEmail': email, 'AdminSecretArn': admin_secret_arn, - 'EdgeConfigSecretArn': edge_config_secret_arn, - 'SigningKeySecretArn': signing_key_secret_arn, + 'EdgeConfigSecretName': edge_config_secret_name, + 'SigningKeySecretName': signing_key_secret_name, }.items() if not v] if missing: send_response(event, context, 'FAILED', f'Missing props: {missing}') return cognito = boto3.client('cognito-idp', region_name=region) - sm = boto3.client('secretsmanager', region_name=region) + sm_local = boto3.client('secretsmanager', region_name=region) + sm_edge = boto3.client('secretsmanager', region_name=edge_region) # On Update, delete the old admin user if email changed if event['RequestType'] == 'Update': @@ -1712,18 +1716,18 @@ Resources: if old_email and old_email != email: delete_user_safely(cognito, pool_id, old_email) - # 1) Provision the admin user in Cognito + # 1) Provision the admin user in Cognito and store credentials + # in the local (main-stack region) admin secret. password = generate_password() create_or_reset_user(cognito, pool_id, email, password) - write_admin_secret(sm, admin_secret_arn, email, password) + write_admin_secret(sm_local, admin_secret_arn, email, password) - # 2) Read the CFN-generated signing key and write the merged edge config - # NOTE: signing_key_secret_arn and edge_config_secret_arn are DIFFERENT secrets. - # The signing-key secret is CFN-managed (immutable content); the config secret - # is what the Lambda@Edge reads at cold start. - sk_resp = sm.get_secret_value(SecretId=signing_key_secret_arn) + # 2) Read the CFN-generated signing key from us-east-1 (edge stack) + # and write the merged edge config to us-east-1 (edge stack). + # Both live in us-east-1 because the Lambda@Edge reads them there. + sk_resp = sm_edge.get_secret_value(SecretId=signing_key_secret_name) signing_key = json.loads(sk_resp['SecretString'])['key'] - write_edge_config(sm, edge_config_secret_arn, pool_id, client_id, cognito_domain, signing_key) + write_edge_config(sm_edge, edge_config_secret_name, pool_id, client_id, cognito_domain, signing_key) send_response(event, context, 'SUCCESS', 'Admin user + edge config provisioned', {'Email': email}) From c78bd2bebe41a72e7b5f80650737d595e1f1b202 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:43:30 +0000 Subject: [PATCH 08/13] feat(edge): installer 2-phase deploy for split-stack architecture Build and upload the Lambda@Edge artifact to a dedicated us-east-1 bucket.\nDeploy the companion edge stack first, capture its outputs, and pass the resulting version and secret parameters to the main stack.\nUpdate the main-stack output references and post-deploy display for the split-stack resources. --- deploy/cloudformation/template.yaml | 4 +- install.sh | 92 +++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 921076e..93d885d 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1986,10 +1986,10 @@ Outputs: WebUIEdgeFunctionArn: Condition: EnableWebUI Description: Lambda@Edge function version ARN attached to the CloudFront distribution (viewer-request) - Value: !Ref WebUIEdgeLambdaVersion + Value: !Ref EdgeLambdaVersionArn WebUIEdgeSigningKeySecretArn: Condition: EnableWebUI Description: ARN of Secrets Manager secret holding the Lambda@Edge nonce/state signing key - Value: !Ref WebUIEdgeSigningKeySecret + Value: !Ref SigningKeySecretArn diff --git a/install.sh b/install.sh index e308056..64a51af 100755 --- a/install.sh +++ b/install.sh @@ -2158,9 +2158,8 @@ configure_webui_auth() { # ============================================================================ # WebUI Lambda@Edge zip build + S3 upload (KiroCrew, when auth enabled) # ============================================================================ -# Builds the Cognito-at-Edge zip, uploads it to the CFN templates bucket, and -# exports EDGE_LAMBDA_S3_BUCKET/EDGE_LAMBDA_S3_KEY so build_deploy_params can -# feed them to the stack as CFN parameters. +# Builds the Cognito-at-Edge zip, uploads it to a dedicated us-east-1 bucket, +# and exports the edge artifact details for deploy_edge_stack. build_and_upload_edge_lambda() { [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" ]] || return 0 [[ "$PACK_NAME" == "kirocrew" ]] || return 0 @@ -2194,26 +2193,25 @@ build_and_upload_edge_lambda() { fail "Edge Lambda build script did not produce a zip: $zip_path" fi - local bucket="${ENV_NAME}-cfn-templates-${ACCOUNT_ID}" + local bucket="${ENV_NAME}-edge-${ACCOUNT_ID}" local key="edge/$(basename "$zip_path")" - # The CFN templates bucket was created by deploy_cfn_stack in earlier flows, - # but Lambda@Edge zip must be uploaded BEFORE the stack that references it. - # Ensure the bucket exists (idempotent). - if ! aws s3api head-bucket --bucket "$bucket" --region "$DEPLOY_REGION" 2>/dev/null; then + # Lambda@Edge requires its source bucket to be in us-east-1. Ensure the + # dedicated bucket exists (idempotent), independently of the main-region + # CloudFormation templates bucket. + if ! aws s3api head-bucket --bucket "$bucket" --region us-east-1 2>/dev/null; then info "Creating edge Lambda bucket: $bucket" - aws s3api create-bucket --bucket "$bucket" --region "$DEPLOY_REGION" \ - $(if [[ "$DEPLOY_REGION" != "us-east-1" ]]; then echo "--create-bucket-configuration LocationConstraint=$DEPLOY_REGION"; fi) \ + aws s3api create-bucket --bucket "$bucket" --region us-east-1 \ >/dev/null 2>&1 || fail "Failed to create bucket $bucket" - aws s3api put-bucket-versioning --bucket "$bucket" \ - --versioning-configuration Status=Enabled --region "$DEPLOY_REGION" >/dev/null 2>&1 || true - aws s3api put-public-access-block --bucket "$bucket" --region "$DEPLOY_REGION" \ - --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true \ - >/dev/null 2>&1 || true fi + aws s3api put-bucket-versioning --bucket "$bucket" --versioning-configuration Status=Enabled \ + --region us-east-1 >/dev/null 2>&1 || fail "Failed to enable versioning on $bucket" + aws s3api put-public-access-block --bucket "$bucket" --region us-east-1 \ + --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true \ + >/dev/null 2>&1 || fail "Failed to block public access on $bucket" info "Uploading edge Lambda zip: s3://${bucket}/${key} ($(wc -c < "$zip_path") bytes)" - aws s3 cp "$zip_path" "s3://${bucket}/${key}" --region "$DEPLOY_REGION" >/dev/null \ + aws s3 cp "$zip_path" "s3://${bucket}/${key}" --region us-east-1 >/dev/null \ || fail "Failed to upload edge Lambda zip" # Compute base64-encoded SHA256 of the zip. CFN needs this on the @@ -2224,12 +2222,46 @@ build_and_upload_edge_lambda() { code_sha256=$(openssl dgst -sha256 -binary "$zip_path" | openssl base64 -A) \ || fail "Failed to compute SHA256 of edge Lambda zip" - export EDGE_LAMBDA_S3_BUCKET="$bucket" - export EDGE_LAMBDA_S3_KEY="$key" - export EDGE_LAMBDA_CODE_SHA256="$code_sha256" + export EDGE_S3_BUCKET_US_EAST_1="$bucket" + export EDGE_S3_KEY="$key" + export EDGE_CODE_SHA256="$code_sha256" ok "Edge Lambda uploaded" } +deploy_edge_stack() { + [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" ]] || return 0 + [[ "$PACK_NAME" == "kirocrew" ]] || return 0 + + local edge_stack_name="${ENV_NAME}-edge-stack" + step "Deploy WebUI Lambda@Edge companion stack" + if ! aws cloudformation deploy \ + --template-file "${CLONE_DIR}/deploy/cloudformation/edge-stack.yaml" \ + --stack-name "$edge_stack_name" \ + --region us-east-1 \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameter-overrides \ + "EnvironmentName=${ENV_NAME}" "PackName=${PACK_NAME}" \ + "EdgeLambdaS3Bucket=${EDGE_S3_BUCKET_US_EAST_1}" \ + "EdgeLambdaS3Key=${EDGE_S3_KEY}" "EdgeLambdaCodeSha256=${EDGE_CODE_SHA256}"; then + fail "Lambda@Edge companion stack deployment failed; main stack was not started" + fi + + local outputs + outputs=$(aws cloudformation describe-stacks --stack-name "$edge_stack_name" --region us-east-1 \ + --query 'Stacks[0].Outputs' --output json) \ + || fail "Could not read outputs from Lambda@Edge companion stack" + EDGE_LAMBDA_VERSION_ARN=$(printf '%s' "$outputs" | jq -r '.[] | select(.OutputKey=="EdgeLambdaVersionArn") | .OutputValue') + EDGE_CONFIG_SECRET_NAME=$(printf '%s' "$outputs" | jq -r '.[] | select(.OutputKey=="EdgeConfigSecretName") | .OutputValue') + EDGE_CONFIG_SECRET_ARN=$(printf '%s' "$outputs" | jq -r '.[] | select(.OutputKey=="EdgeConfigSecretArn") | .OutputValue') + SIGNING_KEY_SECRET_NAME=$(printf '%s' "$outputs" | jq -r '.[] | select(.OutputKey=="SigningKeySecretName") | .OutputValue') + SIGNING_KEY_SECRET_ARN=$(printf '%s' "$outputs" | jq -r '.[] | select(.OutputKey=="SigningKeySecretArn") | .OutputValue') + export EDGE_LAMBDA_VERSION_ARN EDGE_CONFIG_SECRET_NAME EDGE_CONFIG_SECRET_ARN + export SIGNING_KEY_SECRET_NAME SIGNING_KEY_SECRET_ARN + [[ -n "$EDGE_LAMBDA_VERSION_ARN" && "$EDGE_LAMBDA_VERSION_ARN" != null ]] \ + || fail "Lambda@Edge companion stack returned no version ARN" + ok "Lambda@Edge companion stack deployed" +} + collect_config() { step "Configuration" @@ -2364,7 +2396,7 @@ collect_security_config() { # Parameter source-of-truth: single mapping for CFN Console and CFN CLI # ============================================================================ # ⚠ KEEP THESE TWO ARRAYS IN SYNC — same order, same count -PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaS3Bucket EdgeLambdaS3Key EdgeLambdaCodeSha256) +PARAM_CFN_NAMES=(EnvironmentName PackName ProfileName InstanceType DefaultModel ModelMode BedrockRegion LokiWatermark EnableBedrockForm EnableSecurityHub EnableGuardDuty EnableInspector EnableAccessAnalyzer EnableConfigRecorder ExistingVpcId ExistingSubnetId ExistingSubnetId2 RepoBranch KiroFromSecret TelegramBotTokenSecret TelegramUser Primary DailyDriver CodexModel EnableWebUIAuth WebUIAdminEmail EdgeLambdaVersionArn EdgeConfigSecretName EdgeConfigSecretArn SigningKeySecretName SigningKeySecretArn) PARAM_VALUES=() # populated by build_deploy_params() # Per-pack default model (passed to CFN DefaultModel / bootstrap.sh --model). @@ -2426,9 +2458,11 @@ build_deploy_params() { "${TROIKA_CODEX_MODEL:-openai.gpt-5.5}" "${WEBUI_AUTH_ENABLED:-false}" "${WEBUI_ADMIN_EMAIL:-}" - "${EDGE_LAMBDA_S3_BUCKET:-}" - "${EDGE_LAMBDA_S3_KEY:-}" - "${EDGE_LAMBDA_CODE_SHA256:-}" + "${EDGE_LAMBDA_VERSION_ARN:-}" + "${EDGE_CONFIG_SECRET_NAME:-}" + "${EDGE_CONFIG_SECRET_ARN:-}" + "${SIGNING_KEY_SECRET_NAME:-}" + "${SIGNING_KEY_SECRET_ARN:-}" ) # Validate parallel arrays are in sync [[ ${#PARAM_CFN_NAMES[@]} -eq ${#PARAM_VALUES[@]} ]] \ @@ -3550,7 +3584,13 @@ main() { prepare_repo echo "" - build_and_upload_edge_lambda + if [[ "${WEBUI_AUTH_ENABLED:-false}" == "true" && "$PACK_NAME" == "kirocrew" ]]; then + build_and_upload_edge_lambda + deploy_edge_stack + # Edge outputs are required by the main stack parameters; refresh the + # parallel parameter values after the companion stack completes. + build_deploy_params + fi case "$DEPLOY_METHOD" in "$DEPLOY_CFN_CLI") info "Deploying with CloudFormation..." @@ -3592,6 +3632,10 @@ main() { " Client: ${client_id}" \ " Domain: ${domain}" \ "" \ + " Edge Lambda: ${EDGE_LAMBDA_VERSION_ARN}" \ + " Edge Region: us-east-1" \ + " Edge Stack: ${ENV_NAME}-edge-stack" \ + "" \ " Secret: ${secret_arn}" || true echo "" fi From 4c554eaa901e74609a87edf791d5911820893ae9 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:50:56 +0000 Subject: [PATCH 09/13] fix(edge): Codex P1 findings on PR #86 P1 #1: Lambda@Edge was using us-east-1 for Cognito auth - The Authenticator (cognito-at-edge) needs the region where the Cognito user pool lives, NOT the region where the Lambda@Edge itself runs. Only Secrets Manager is us-east-1 (secret is co-located with the edge Lambda for latency). Token validation, JWKS fetch, and Cognito API calls need the user-pool region. - index.js: renamed REGION -> SECRETS_REGION (still us-east-1) and added cfg.cognitoRegion (from the merged edge config secret) as the Authenticator's region. Required in the config load-validation. - Custom Resource in template.yaml: write_edge_config now accepts and writes a 'cognitoRegion' field alongside poolId/clientId/domain/ signingKey. Passes 'region' (the main-stack region, which is where the pool lives) as that value. P1 #2: Old edge Lambda versions blocked stack updates - AWS::Lambda::Version replacement tried to delete the old version while CloudFront still referenced it -> Lambda rejection (replicated Lambda@Edge takes ~1hr to GC after CloudFront disassociates) -> edge-stack update rollback. - edge-stack.yaml: added DeletionPolicy: Retain + UpdateReplacePolicy: Retain on WebUIEdgeLambdaVersion. Old versions accumulate harmlessly (Lambda Versions are free). Stack updates now succeed cleanly. Deferred (P2 from same review): - uninstall.sh doesn't know about the us-east-1 companion stack. Roy said uninstall is less important for now. Validated: - bash -n install.sh: OK - node --check packs/kirocrew/webui-auth-edge/index.js: OK - aws cloudformation validate-template (main + edge): OK --- deploy/cloudformation/edge-stack.yaml | 6 ++++++ deploy/cloudformation/template.yaml | 5 +++-- packs/kirocrew/webui-auth-edge/index.js | 15 ++++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/deploy/cloudformation/edge-stack.yaml b/deploy/cloudformation/edge-stack.yaml index 0662191..ee0cc20 100644 --- a/deploy/cloudformation/edge-stack.yaml +++ b/deploy/cloudformation/edge-stack.yaml @@ -113,6 +113,12 @@ Resources: WebUIEdgeLambdaVersion: Type: AWS::Lambda::Version + # Old versions cannot be deleted while CloudFront still references them + # (Lambda@Edge replicas take ~1hr to GC after CloudFront disassociates). + # Retain them on stack update/delete so a code refresh doesn't roll back + # the edge stack. Old versions are free — they accumulate harmlessly. + DeletionPolicy: Retain + UpdateReplacePolicy: Retain Properties: FunctionName: !Ref WebUIEdgeLambdaFunction Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName}) sha256=${EdgeLambdaCodeSha256}' diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 93d885d..15e9e30 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1628,7 +1628,7 @@ Resources: SecretString=json.dumps({'email': email, 'password': password}) ) - def write_edge_config(sm, secret_arn, pool_id, client_id, cognito_domain, signing_key): + def write_edge_config(sm, secret_arn, pool_id, client_id, cognito_domain, signing_key, cognito_region): sm.put_secret_value( SecretId=secret_arn, SecretString=json.dumps({ @@ -1636,6 +1636,7 @@ Resources: 'clientId': client_id, 'cognitoDomain': cognito_domain, 'signingKey': signing_key, + 'cognitoRegion': cognito_region, }) ) @@ -1727,7 +1728,7 @@ Resources: # Both live in us-east-1 because the Lambda@Edge reads them there. sk_resp = sm_edge.get_secret_value(SecretId=signing_key_secret_name) signing_key = json.loads(sk_resp['SecretString'])['key'] - write_edge_config(sm_edge, edge_config_secret_name, pool_id, client_id, cognito_domain, signing_key) + write_edge_config(sm_edge, edge_config_secret_name, pool_id, client_id, cognito_domain, signing_key, region) send_response(event, context, 'SUCCESS', 'Admin user + edge config provisioned', {'Email': email}) diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js index 4b12289..3ff9296 100644 --- a/packs/kirocrew/webui-auth-edge/index.js +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -26,15 +26,15 @@ const { } = require('@aws-sdk/client-secrets-manager'); const CONFIG_SECRET_NAME = '__CONFIG_SECRET_NAME__'; -const REGION = 'us-east-1'; +const SECRETS_REGION = 'us-east-1'; let authenticatorPromise = null; async function loadConfig() { - const sm = new SecretsManagerClient({ region: REGION }); + const sm = new SecretsManagerClient({ region: SECRETS_REGION }); const resp = await sm.send(new GetSecretValueCommand({ SecretId: CONFIG_SECRET_NAME })); const parsed = JSON.parse(resp.SecretString); - const required = ['poolId', 'clientId', 'cognitoDomain', 'signingKey']; + const required = ['poolId', 'clientId', 'cognitoDomain', 'signingKey', 'cognitoRegion']; const missing = required.filter((k) => !parsed[k] || typeof parsed[k] !== 'string' || parsed[k] === 'pending'); if (missing.length > 0) { throw new Error(`Edge config secret missing or pending fields: ${missing.join(', ')}`); @@ -47,21 +47,22 @@ async function getAuthenticator() { authenticatorPromise = (async () => { const cfg = await loadConfig(); return new Authenticator({ - region: REGION, + // cognito-at-edge uses this region for JWKS + token validation and + // Cognito API calls. It must match the region the user pool lives in + // (which is the main stack's DEPLOY_REGION, NOT us-east-1 where the + // Lambda@Edge itself is hosted). + region: cfg.cognitoRegion, userPoolId: cfg.poolId, userPoolAppId: cfg.clientId, userPoolDomain: cfg.cognitoDomain, cookieExpirationDays: 1, disableCookieDomain: true, logLevel: 'warn', - // cognito-at-edge expects nonceSigningSecret nested under csrfProtection. - // Top-level 'nonceSigningSecret' is silently ignored (round-3 P1 #1 fix). csrfProtection: { nonceSigningSecret: cfg.signingKey, }, }); })().catch((err) => { - // Reset so the next cold-start attempt can retry authenticatorPromise = null; throw err; }); From 43f8fbb0d92416d93dc950776c5e7922fe84c50c Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:00:31 +0000 Subject: [PATCH 10/13] fix(edge): override parseAuthPath to match Cognito CallbackURLs Codex P1 on PR #86 commit 4c554ea. cognito-at-edge defaults its authorization-code handler and generated redirect_uri to '/parseauth'. Our Cognito user pool client (in template.yaml) allows only '/auth/callback' and 'http://localhost:5476/ auth/callback' as callback URLs. Without an override, Cognito rejected the generated redirect_uri and '/auth/callback' was unused. Fix: pass parseAuthPath: '/auth/callback' to the Authenticator so the generated redirect_uri matches the registered CallbackURLs. Validated: node --check on index.js. --- packs/kirocrew/webui-auth-edge/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js index 3ff9296..3a182f7 100644 --- a/packs/kirocrew/webui-auth-edge/index.js +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -55,6 +55,11 @@ async function getAuthenticator() { userPoolId: cfg.poolId, userPoolAppId: cfg.clientId, userPoolDomain: cfg.cognitoDomain, + // Must match a CallbackURL registered on the Cognito user pool client + // in the main-stack template (`/auth/callback`). Without this, + // cognito-at-edge defaults to `/parseauth`, which Cognito rejects + // because it's not in the client's CallbackURLs allowlist. + parseAuthPath: '/auth/callback', cookieExpirationDays: 1, disableCookieDomain: true, logLevel: 'warn', From d3b8380f3c0b685152f4b3842dd29209490817cd Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:30:10 +0000 Subject: [PATCH 11/13] fix(edge): unblock deployment and protect ALB origin --- deploy/cloudformation/edge-stack.yaml | 2 +- deploy/cloudformation/template.yaml | 19 +++++++++++++++++++ install.sh | 2 +- packs/kirocrew/webui-auth-edge/build.sh | 4 ++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/deploy/cloudformation/edge-stack.yaml b/deploy/cloudformation/edge-stack.yaml index ee0cc20..7b64c14 100644 --- a/deploy/cloudformation/edge-stack.yaml +++ b/deploy/cloudformation/edge-stack.yaml @@ -102,7 +102,7 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: !Sub '${EnvironmentName}-webui-edge-auth' - Runtime: nodejs20.x + Runtime: nodejs22.x Handler: index.handler MemorySize: 128 Timeout: 5 diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 15e9e30..39587ec 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -665,6 +665,25 @@ Resources: Port: 80 Protocol: HTTP DefaultActions: + - Type: fixed-response + FixedResponseConfig: + ContentType: text/plain + MessageBody: Forbidden + StatusCode: '403' + + KiroCrewOriginVerifyListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Condition: IsKiroCrew + Properties: + ListenerArn: !Ref KiroCrewHTTPListener + Priority: 1 + Conditions: + - Field: http-header + HttpHeaderConfig: + HttpHeaderName: x-origin-verify + Values: + - !Sub '{{resolve:secretsmanager:${KiroCrewOriginVerifySecret}}}' + Actions: - Type: forward TargetGroupArn: !Ref KiroCrewTargetGroup diff --git a/install.sh b/install.sh index 64a51af..d14a615 100755 --- a/install.sh +++ b/install.sh @@ -2165,7 +2165,7 @@ build_and_upload_edge_lambda() { [[ "$PACK_NAME" == "kirocrew" ]] || return 0 # Preflight: required tools for the edge Lambda build path. - require_cmd node "node (18+) is required to build the Cognito Lambda@Edge zip. Install Node 18+ or disable WebUI auth." + require_cmd node "node (22+) is required to build the Cognito Lambda@Edge zip. Install Node 22+ or disable WebUI auth." require_cmd npm "npm is required to build the Cognito Lambda@Edge zip." require_cmd zip "zip is required to package the Cognito Lambda@Edge deployment." require_cmd openssl "openssl is required to compute the Lambda@Edge zip SHA256 for CFN." diff --git a/packs/kirocrew/webui-auth-edge/build.sh b/packs/kirocrew/webui-auth-edge/build.sh index 71a3f28..04e7beb 100755 --- a/packs/kirocrew/webui-auth-edge/build.sh +++ b/packs/kirocrew/webui-auth-edge/build.sh @@ -35,8 +35,8 @@ if ! command -v "$NODE_BIN" >/dev/null 2>&1; then fi NODE_MAJOR=$("$NODE_BIN" -e 'console.log(process.versions.node.split(".")[0])') -if [[ "$NODE_MAJOR" -lt 18 ]]; then - echo "build.sh: node 18+ required, got $NODE_MAJOR" >&2 +if [[ "$NODE_MAJOR" -lt 22 ]]; then + echo "build.sh: node 22+ required, got $NODE_MAJOR" >&2 exit 5 fi From cb1a8df0753de32e3aeda819438be075c8244457 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:30:33 +0000 Subject: [PATCH 12/13] fix(edge): harden auth cookies and refresh config --- install.sh | 32 +++++++++++++++---------- packs/kirocrew/webui-auth-edge/index.js | 15 ++++++++++-- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index d14a615..407f716 100755 --- a/install.sh +++ b/install.sh @@ -2490,6 +2490,16 @@ format_cfn_cli_params() { echo "$params" } +# Format params for CFN deploy --parameter-overrides (Key=Value) +format_cfn_deploy_params() { + local params="" + for i in "${!PARAM_CFN_NAMES[@]}"; do + [[ -n "$params" ]] && params+=" " + params+="${PARAM_CFN_NAMES[$i]}=${PARAM_VALUES[$i]}" + done + echo "$params" +} + show_summary() { step "Review & confirm" @@ -2680,23 +2690,21 @@ deploy_cfn_stack() { aws s3 cp "$template" "s3://${bucket}/lowkey/template.yaml" --region "$DEPLOY_REGION" >/dev/null \ || fail "Failed to upload template to S3" - # Pre-signed URL (bucket blocks public access) - local s3_url - s3_url=$(aws s3 presign "s3://${bucket}/lowkey/template.yaml" \ - --expires-in 3600 --region "$DEPLOY_REGION") \ - || fail "Could not generate pre-signed URL for template" - + # aws cloudformation deploy is idempotent: it creates a missing stack and + # updates an existing one. --no-fail-on-empty-changeset makes reruns safe. # shellcheck disable=SC2046 - aws cloudformation create-stack \ + aws cloudformation deploy \ --stack-name "$STACK_NAME" \ - --template-url "$s3_url" \ + --template-file "$template" \ + --s3-bucket "$bucket" \ + --s3-prefix lowkey/deploy \ --region "$DEPLOY_REGION" \ --capabilities $capabilities \ - --parameters $(format_cfn_cli_params) \ - --output text --query 'StackId' + --parameter-overrides $(format_cfn_deploy_params) \ + --no-fail-on-empty-changeset \ + || fail "CloudFormation deployment failed" - info "Stack creating... this takes ~8-10 minutes" - wait_for_cfn_stack + info "Stack deployment complete" INSTANCE_ID=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --region "$DEPLOY_REGION" \ --query 'Stacks[0].Outputs[?OutputKey==`InstanceId`].OutputValue' --output text) diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js index 3a182f7..3029d39 100644 --- a/packs/kirocrew/webui-auth-edge/index.js +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -7,7 +7,7 @@ * * Lambda@Edge constraints: * - No environment variables (config baked in below OR fetched from Secrets Manager) - * - Node.js 18.x runtime + * - Node.js 22.x runtime * - Must be deployed in us-east-1 (enforced by CFN Rule WebUIEdgeRequiresUsEast1) * * Only CONFIG_SECRET_NAME is substituted at build time. All other config @@ -28,7 +28,9 @@ const { const CONFIG_SECRET_NAME = '__CONFIG_SECRET_NAME__'; const SECRETS_REGION = 'us-east-1'; +const AUTHENTICATOR_CACHE_TTL_MS = 15 * 60 * 1000; let authenticatorPromise = null; +let authenticatorCacheTimestamp = 0; async function loadConfig() { const sm = new SecretsManagerClient({ region: SECRETS_REGION }); @@ -43,7 +45,12 @@ async function loadConfig() { } async function getAuthenticator() { - if (authenticatorPromise) return authenticatorPromise; + const now = Date.now(); + if (authenticatorPromise && now - authenticatorCacheTimestamp <= AUTHENTICATOR_CACHE_TTL_MS) { + return authenticatorPromise; + } + authenticatorPromise = null; + authenticatorCacheTimestamp = now; authenticatorPromise = (async () => { const cfg = await loadConfig(); return new Authenticator({ @@ -61,6 +68,9 @@ async function getAuthenticator() { // because it's not in the client's CallbackURLs allowlist. parseAuthPath: '/auth/callback', cookieExpirationDays: 1, + cookiePath: '/', + httpOnly: true, + sameSite: 'Lax', disableCookieDomain: true, logLevel: 'warn', csrfProtection: { @@ -69,6 +79,7 @@ async function getAuthenticator() { }); })().catch((err) => { authenticatorPromise = null; + authenticatorCacheTimestamp = 0; throw err; }); return authenticatorPromise; From 6057bff17433314ee9641218ae8dacdb9a630134 Mon Sep 17 00:00:00 2001 From: Roy Osherove <575051+royosherove@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:31:07 +0000 Subject: [PATCH 13/13] fix(edge): secure packaging and add logout support --- deploy/cloudformation/edge-stack.yaml | 6 + deploy/cloudformation/template.yaml | 1 + install.sh | 3 + packs/kirocrew/webui-auth-edge/.npmrc | 1 + packs/kirocrew/webui-auth-edge/build.sh | 6 +- packs/kirocrew/webui-auth-edge/index.js | 4 + .../webui-auth-edge/package-lock.json | 500 ++++++++++++++++++ packs/kirocrew/webui-auth-edge/package.json | 2 +- 8 files changed, 520 insertions(+), 3 deletions(-) create mode 100644 packs/kirocrew/webui-auth-edge/.npmrc create mode 100644 packs/kirocrew/webui-auth-edge/package-lock.json diff --git a/deploy/cloudformation/edge-stack.yaml b/deploy/cloudformation/edge-stack.yaml index 7b64c14..2c97c90 100644 --- a/deploy/cloudformation/edge-stack.yaml +++ b/deploy/cloudformation/edge-stack.yaml @@ -34,6 +34,12 @@ Parameters: MinLength: 1 Description: "Base64 SHA256 of the deployment zip. Forces a new AWS::Lambda::Version when code changes." +Rules: + WebUIEdgeRequiresUsEast1: + Assertions: + - Assert: !Equals [!Ref 'AWS::Region', 'us-east-1'] + AssertDescription: "Lambda@Edge companion stack must be deployed in us-east-1." + Resources: # Raw HMAC signing key. CFN-managed via GenerateSecretString. Never written # to by anything else — read once by the main-stack Custom Resource which diff --git a/deploy/cloudformation/template.yaml b/deploy/cloudformation/template.yaml index 39587ec..0ff8566 100644 --- a/deploy/cloudformation/template.yaml +++ b/deploy/cloudformation/template.yaml @@ -1534,6 +1534,7 @@ Resources: - !Sub 'https://${KiroCrewDistribution.DomainName}/auth/callback' - 'http://localhost:5476/auth/callback' LogoutURLs: + - !Sub 'https://${KiroCrewDistribution.DomainName}/logout' - !Sub 'https://${KiroCrewDistribution.DomainName}/' - 'http://localhost:5476/' PreventUserExistenceErrors: ENABLED diff --git a/install.sh b/install.sh index 407f716..a3b37fa 100755 --- a/install.sh +++ b/install.sh @@ -2204,6 +2204,9 @@ build_and_upload_edge_lambda() { aws s3api create-bucket --bucket "$bucket" --region us-east-1 \ >/dev/null 2>&1 || fail "Failed to create bucket $bucket" fi + aws s3api put-bucket-encryption --bucket "$bucket" \ + --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"},"BucketKeyEnabled":true}]}' \ + --region us-east-1 >/dev/null 2>&1 || true aws s3api put-bucket-versioning --bucket "$bucket" --versioning-configuration Status=Enabled \ --region us-east-1 >/dev/null 2>&1 || fail "Failed to enable versioning on $bucket" aws s3api put-public-access-block --bucket "$bucket" --region us-east-1 \ diff --git a/packs/kirocrew/webui-auth-edge/.npmrc b/packs/kirocrew/webui-auth-edge/.npmrc new file mode 100644 index 0000000..7253a5c --- /dev/null +++ b/packs/kirocrew/webui-auth-edge/.npmrc @@ -0,0 +1 @@ +min-release-age=7 diff --git a/packs/kirocrew/webui-auth-edge/build.sh b/packs/kirocrew/webui-auth-edge/build.sh index 04e7beb..32b6036 100755 --- a/packs/kirocrew/webui-auth-edge/build.sh +++ b/packs/kirocrew/webui-auth-edge/build.sh @@ -45,6 +45,8 @@ trap 'rm -rf "$BUILD_DIR"' EXIT cp "$SCRIPT_DIR/index.js" "$BUILD_DIR/index.js" cp "$SCRIPT_DIR/package.json" "$BUILD_DIR/package.json" +cp "$SCRIPT_DIR/package-lock.json" "$BUILD_DIR/package-lock.json" +cp "$SCRIPT_DIR/.npmrc" "$BUILD_DIR/.npmrc" # Substitute only CONFIG_SECRET_NAME placeholder sed -i -e "s|__CONFIG_SECRET_NAME__|${CONFIG_SECRET_NAME}|g" "$BUILD_DIR/index.js" @@ -56,7 +58,7 @@ if grep -q '__[A-Z_]*__' "$BUILD_DIR/index.js"; then fi cd "$BUILD_DIR" -npm install --production --no-audit --no-fund --loglevel=error >&2 +npm ci --omit=dev --no-audit --no-fund --loglevel=error >&2 >&2 # Content-addressed zip name: hash the actual ZIP BYTES (not source files) so # the S3 key stays in lockstep with CodeSha256 in CFN. Round-3 fix (P1 #2): @@ -83,7 +85,7 @@ sha256_hex() { TMP_ZIP="${OUT_DIR}/.edge-lambda-tmp-$$.zip" trap 'rm -f "$TMP_ZIP"; rm -rf "$BUILD_DIR"' EXIT rm -f "$TMP_ZIP" -zip -r -q -X "$TMP_ZIP" index.js package.json node_modules >&2 +zip -r -q -X "$TMP_ZIP" index.js package.json package-lock.json node_modules >&2 ZIP_SHA=$(sha256_hex "$TMP_ZIP") || exit 7 SHA_SHORT="${ZIP_SHA:0:16}" diff --git a/packs/kirocrew/webui-auth-edge/index.js b/packs/kirocrew/webui-auth-edge/index.js index 3029d39..a94f82c 100644 --- a/packs/kirocrew/webui-auth-edge/index.js +++ b/packs/kirocrew/webui-auth-edge/index.js @@ -72,6 +72,10 @@ async function getAuthenticator() { httpOnly: true, sameSite: 'Lax', disableCookieDomain: true, + logoutConfiguration: { + logoutUri: '/logout', + logoutRedirectUri: '/', + }, logLevel: 'warn', csrfProtection: { nonceSigningSecret: cfg.signingKey, diff --git a/packs/kirocrew/webui-auth-edge/package-lock.json b/packs/kirocrew/webui-auth-edge/package-lock.json new file mode 100644 index 0000000..117e26d --- /dev/null +++ b/packs/kirocrew/webui-auth-edge/package-lock.json @@ -0,0 +1,500 @@ +{ + "name": "kirocrew-webui-auth-edge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kirocrew-webui-auth-edge", + "version": "1.0.0", + "dependencies": { + "cognito-at-edge": "^1.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/aws-jwt-verify": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/aws-jwt-verify/-/aws-jwt-verify-2.1.3.tgz", + "integrity": "sha512-XAlt1IaQg9SRpuKPAhW1I1/E9Q63bPI/O+W5dcGniDwTJSbAUVZsH80XxeuADBCD2eIWEUlKOFfLmzhXZqt9tA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/cognito-at-edge": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/cognito-at-edge/-/cognito-at-edge-1.5.4.tgz", + "integrity": "sha512-v1K7xkb2BxMk/fwnYVpPTUAqt8BSKfiUpUNxjc2HHFVLAdTHGGkYoEHL1x0QeJlViLAekg1ruHiV4Q1cMasqjg==", + "license": "Apache-2.0", + "dependencies": { + "aws-jwt-verify": "^2.1.1", + "axios": "^1.6.5", + "pino": "^9.12.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + } + } +} diff --git a/packs/kirocrew/webui-auth-edge/package.json b/packs/kirocrew/webui-auth-edge/package.json index 98fa161..1ede988 100644 --- a/packs/kirocrew/webui-auth-edge/package.json +++ b/packs/kirocrew/webui-auth-edge/package.json @@ -8,6 +8,6 @@ "cognito-at-edge": "^1.5.2" }, "engines": { - "node": ">=18" + "node": ">=22" } }