Skip to content

fix: bundle lambda deps + {proxy+} routing (fixes 502/CORS outage)#279

Merged
nourshoreibah merged 3 commits into
mainfrom
fix/lambda-bundle-and-proxy-routing
Jul 1, 2026
Merged

fix: bundle lambda deps + {proxy+} routing (fixes 502/CORS outage)#279
nourshoreibah merged 3 commits into
mainfrom
fix/lambda-bundle-and-proxy-routing

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

What was broken

Every lambda returned 502 InternalServerErrorException on every request — including the OPTIONS preflight — which the browser reported as an opaque "CORS error" on all routes, in both prod and preview.

Root cause

The package script was tsc && zip dist. tsc only transpiles; it does not bundle dependencies, so the deploy zip had the compiled handler but no node_modules. At cold start require('kysely') / require('pg') / etc. threw Runtime.ImportModuleError and the function crashed at init — before the handler's OPTIONS short-circuit could run. A 502 preflight fails the browser CORS check regardless of the CORS headers added in #277, so the fix there couldn't help.

Diagnosis was against live prod: curl showed HTTP/2 502 + x-amzn-errortype: InternalServerErrorException on OPTIONS and GET for every service resource.

Fixes (both in one PR)

Bundling

  • package now bundles with esbuild into a single self-contained handler.js (deps inlined). Verified it loads and runs with no node_modules present.
  • reports keeps its Roboto TTFs: esbuild can't inline the font files pdfmake reads at render time, so the package step copies them to dist/fonts/Roboto and report-service resolves FONT_DIR from __dirname (falls back to node_modules in local dev). Verified PDF generation works from the bundle with only the shipped fonts.
  • Moved jest from dependenciesdevDependencies (was wrongly a runtime dep); added esbuild as a devDependency; lockfiles regenerated so npm ci stays in sync.

Routing

  • API Gateway only had single-level resources (/auth, /donors, …), so sub-paths like /auth/login and /projects/{id} matched no method → 403, never reaching the lambda. Added a {proxy+} greedy child per service with an ANY method (covers OPTIONS too), in prod + preview, and bumped the deployment redeploy trigger.
  • Handlers now strip their own /service mount prefix from rawPath so the existing route table (written for bare paths like /login, /{id}) matches. No-op in local dev where paths are already bare.
  • Added the missing OPTIONS short-circuit to the reports and users handlers (the other four already had it) so preflight returns 200.

Verification

  • All 6 bundles build; sizes 112K–1.2M.
  • OPTIONS=200 on every service (was 404 for reports/users).
  • POST /auth/login (empty body) → 400 (reaches the login handler), not 404 — proves prefix strip end-to-end.
  • Bundled pdfmake generates a PDF using only the shipped TTFs.
  • auth unit tests pass (18/18; e2e failures are ECONNREFUSED, need a live server).
  • terraform validate passes for both infrastructure/aws and infrastructure/preview.

🤖 Generated with Claude Code

Every lambda returned 502 InternalServerErrorException on every request
(including OPTIONS preflight), which the browser surfaced as an opaque
"CORS error" on all routes in both prod and preview.

Root cause: the `package` script was `tsc && zip dist` — tsc only
transpiles, it does not bundle dependencies, so the deploy zip contained
the compiled handler but no node_modules. At cold start `require('kysely')`
/ `require('pg')` / etc. failed with Runtime.ImportModuleError, so the
function crashed at init — before the handler's OPTIONS short-circuit ran.
A 502 preflight fails the browser's CORS check regardless of the CORS
headers added in #277.

Fixes, in one PR:

Bundling
- package script now bundles with esbuild into a single self-contained
  handler.js (deps inlined). Verified the bundle loads and runs with no
  node_modules present.
- reports keeps its Roboto TTFs: esbuild can't inline the font files
  pdfmake reads at render time, so the package step copies them to
  dist/fonts/Roboto and report-service resolves FONT_DIR from __dirname
  (falls back to node_modules in local dev). Verified PDF generation works
  from the bundle with only the shipped fonts.
- moved `jest` from dependencies to devDependencies (it was wrongly a
  runtime dep) and added `esbuild` as a devDependency; lockfiles regenerated
  so `npm ci` stays in sync.

Routing
- API Gateway only had single-level resources (/auth, /donors, ...), so
  sub-paths like /auth/login and /projects/{id} matched no method and
  returned 403 — never reaching the lambda. Added a {proxy+} greedy child
  per service with an ANY method (covers OPTIONS preflight too), in both
  prod and preview, and bumped the deployment redeploy trigger.
- handlers strip their own /service mount prefix from rawPath so the
  existing route table (written for bare paths like /login, /{id}) matches;
  no-op for local dev where paths are already bare.
- added the missing OPTIONS short-circuit to the reports and users handlers
  (the other four already had it) so preflight returns 200.

Verified: all 6 bundles build; OPTIONS=200 on every service; POST
/auth/login reaches the login handler (400, not 404); auth unit tests pass;
terraform validate passes for both workspaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Jul 1, 2026
github-actions Bot added 2 commits July 1, 2026 23:23
  - Auto-formatted .tf files with terraform fmt
  - Updated README.md with terraform-docs

  Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
data.aws_caller_identity.current: Reading...
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_preview_assume: Reading...
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.infisical_secrets.github_folder: Read complete after 0s
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket_policy.reports_bucket_policy: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=3rllg0]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
+/- create replacement and then destroy

Terraform will perform the following actions:

  # aws_api_gateway_deployment.branch_deployment must be replaced
+/- resource "aws_api_gateway_deployment" "branch_deployment" {
      ~ created_date = "2026-07-01T06:15:09Z" -> (known after apply)
      ~ id           = "3rllg0" -> (known after apply)
      ~ triggers     = { # forces replacement
          ~ "redeploy" = "f158e536679f5c024208ec92e4bf9af90524d98c" -> "e11e8e0a5b37de832e2339de95aabd69deaa7756"
        }
        # (3 unchanged attributes hidden)
    }

  # aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
  ~ resource "aws_api_gateway_gateway_response" "cors" {
        id                  = "aggr-2apxzxb0r8-DEFAULT_4XX"
      ~ response_templates  = {
          - "application/json" = "{\"message\":$context.error.messageString}" -> null
        }
        # (5 unchanged attributes hidden)
    }

  # aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
  ~ resource "aws_api_gateway_gateway_response" "cors" {
        id                  = "aggr-2apxzxb0r8-DEFAULT_5XX"
      ~ response_templates  = {
          - "application/json" = "{\"message\":$context.error.messageString}" -> null
        }
        # (5 unchanged attributes hidden)
    }

  # aws_api_gateway_integration.lambda_proxy_integrations["auth"] will be created
  + resource "aws_api_gateway_integration" "lambda_proxy_integrations" {
      + cache_namespace         = (known after apply)
      + connection_type         = "INTERNET"
      + http_method             = "ANY"
      + id                      = (known after apply)
      + integration_http_method = "POST"
      + passthrough_behavior    = (known after apply)
      + region                  = "us-east-2"
      + resource_id             = (known after apply)
      + rest_api_id             = "2apxzxb0r8"
      + timeout_milliseconds    = 29000
      + type                    = "AWS_PROXY"
      + uri                     = "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:489881683177:function:branch-auth/invocations"
    }

  # aws_api_gateway_integration.lambda_proxy_integrations["donors"] will be created
  + resource "aws_api_gateway_integration" "lambda_proxy_integrations" {
      + cache_namespace         = (known after apply)
      + connection_type         = "INTERNET"
      + http_method             = "ANY"
      + id                      = (known after apply)
      + integration_http_method = "POST"
      + passthrough_behavior    = (known after apply)
      + region                  = "us-east-2"
      + resource_id             = (known after apply)
      + rest_api_id             = "2apxzxb0r8"
      + timeout_milliseconds    = 29000
      + type                    = "AWS_PROXY"
      + uri                     = "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:489881683177:function:branch-donors/invocations"
    }

  # aws_api_gateway_integration.lambda_proxy_integrations["expenditures"] will be created
  + resource "aws_api_gateway_integration" "lambda_proxy_integrations" {
      + cache_namespace         = (known after apply)
      + connection_type         = "INTERNET"
      + http_method             = "ANY"
      + id                      = (known after apply)
      + integration_http_method = "POST"
      + passthrough_behavior    = (known after apply)
      + region                  = "us-east-2"
      + resource_id             = (known after apply)
      + rest_api_id             = "2apxzxb0r8"
      + timeout_milliseconds    = 29000
      + type                    = "AWS_PROXY"
      + uri                     = "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:489881683177:function:branch-expenditures/invocations"
    }

  # aws_api_gateway_integration.lambda_proxy_integrations["projects"] will be created
  + resource "aws_api_gateway_integration" "lambda_proxy_integrations" {
      + cache_namespace         = (known after apply)
      + connection_type         = "INTERNET"
      + http_method             = "ANY"
      + id                      = (known after apply)
      + integration_http_method = "POST"
      + passthrough_behavior    = (known after apply)
      + region                  = "us-east-2"
      + resource_id             = (known after apply)
      + rest_api_id             = "2apxzxb0r8"
      + timeout_milliseconds    = 29000
      + type                    = "AWS_PROXY"
      + uri                     = "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:489881683177:function:branch-projects/invocations"
    }

  # aws_api_gateway_integration.lambda_proxy_integrations["reports"] will be created
  + resource "aws_api_gateway_integration" "lambda_proxy_integrations" {
      + cache_namespace         = (known after apply)
      + connection_type         = "INTERNET"
      + http_method             = "ANY"
      + id                      = (known after apply)
      + integration_http_method = "POST"
      + passthrough_behavior    = (known after apply)
      + region                  = "us-east-2"
      + resource_id             = (known after apply)
      + rest_api_id             = "2apxzxb0r8"
      + timeout_milliseconds    = 29000
      + type                    = "AWS_PROXY"
      + uri                     = "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:489881683177:function:branch-reports/invocations"
    }

  # aws_api_gateway_integration.lambda_proxy_integrations["users"] will be created
  + resource "aws_api_gateway_integration" "lambda_proxy_integrations" {
      + cache_namespace         = (known after apply)
      + connection_type         = "INTERNET"
      + http_method             = "ANY"
      + id                      = (known after apply)
      + integration_http_method = "POST"
      + passthrough_behavior    = (known after apply)
      + region                  = "us-east-2"
      + resource_id             = (known after apply)
      + rest_api_id             = "2apxzxb0r8"
      + timeout_milliseconds    = 29000
      + type                    = "AWS_PROXY"
      + uri                     = "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:489881683177:function:branch-users/invocations"
    }

  # aws_api_gateway_method.lambda_proxy_any["auth"] will be created
  + resource "aws_api_gateway_method" "lambda_proxy_any" {
      + api_key_required = false
      + authorization    = "NONE"
      + http_method      = "ANY"
      + id               = (known after apply)
      + region           = "us-east-2"
      + resource_id      = (known after apply)
      + rest_api_id      = "2apxzxb0r8"
    }

  # aws_api_gateway_method.lambda_proxy_any["donors"] will be created
  + resource "aws_api_gateway_method" "lambda_proxy_any" {
      + api_key_required = false
      + authorization    = "NONE"
      + http_method      = "ANY"
      + id               = (known after apply)
      + region           = "us-east-2"
      + resource_id      = (known after apply)
      + rest_api_id      = "2apxzxb0r8"
    }

  # aws_api_gateway_method.lambda_proxy_any["expenditures"] will be created
  + resource "aws_api_gateway_method" "lambda_proxy_any" {
      + api_key_required = false
      + authorization    = "NONE"
      + http_method      = "ANY"
      + id               = (known after apply)
      + region           = "us-east-2"
      + resource_id      = (known after apply)
      + rest_api_id      = "2apxzxb0r8"
    }

  # aws_api_gateway_method.lambda_proxy_any["projects"] will be created
  + resource "aws_api_gateway_method" "lambda_proxy_any" {
      + api_key_required = false
      + authorization    = "NONE"
      + http_method      = "ANY"
      + id               = (known after apply)
      + region           = "us-east-2"
      + resource_id      = (known after apply)
      + rest_api_id      = "2apxzxb0r8"
    }

  # aws_api_gateway_method.lambda_proxy_any["reports"] will be created
  + resource "aws_api_gateway_method" "lambda_proxy_any" {
      + api_key_required = false
      + authorization    = "NONE"
      + http_method      = "ANY"
      + id               = (known after apply)
      + region           = "us-east-2"
      + resource_id      = (known after apply)
      + rest_api_id      = "2apxzxb0r8"
    }

  # aws_api_gateway_method.lambda_proxy_any["users"] will be created
  + resource "aws_api_gateway_method" "lambda_proxy_any" {
      + api_key_required = false
      + authorization    = "NONE"
      + http_method      = "ANY"
      + id               = (known after apply)
      + region           = "us-east-2"
      + resource_id      = (known after apply)
      + rest_api_id      = "2apxzxb0r8"
    }

  # aws_api_gateway_resource.lambda_proxy["auth"] will be created
  + resource "aws_api_gateway_resource" "lambda_proxy" {
      + id          = (known after apply)
      + parent_id   = "u8unad"
      + path        = (known after apply)
      + path_part   = "{proxy+}"
      + region      = "us-east-2"
      + rest_api_id = "2apxzxb0r8"
    }

  # aws_api_gateway_resource.lambda_proxy["donors"] will be created
  + resource "aws_api_gateway_resource" "lambda_proxy" {
      + id          = (known after apply)
      + parent_id   = "hybur2"
      + path        = (known after apply)
      + path_part   = "{proxy+}"
      + region      = "us-east-2"
      + rest_api_id = "2apxzxb0r8"
    }

  # aws_api_gateway_resource.lambda_proxy["expenditures"] will be created
  + resource "aws_api_gateway_resource" "lambda_proxy" {
      + id          = (known after apply)
      + parent_id   = "6sdj3w"
      + path        = (known after apply)
      + path_part   = "{proxy+}"
      + region      = "us-east-2"
      + rest_api_id = "2apxzxb0r8"
    }

  # aws_api_gateway_resource.lambda_proxy["projects"] will be created
  + resource "aws_api_gateway_resource" "lambda_proxy" {
      + id          = (known after apply)
      + parent_id   = "chhy2i"
      + path        = (known after apply)
      + path_part   = "{proxy+}"
      + region      = "us-east-2"
      + rest_api_id = "2apxzxb0r8"
    }

  # aws_api_gateway_resource.lambda_proxy["reports"] will be created
  + resource "aws_api_gateway_resource" "lambda_proxy" {
      + id          = (known after apply)
      + parent_id   = "wsnfk2"
      + path        = (known after apply)
      + path_part   = "{proxy+}"
      + region      = "us-east-2"
      + rest_api_id = "2apxzxb0r8"
    }

  # aws_api_gateway_resource.lambda_proxy["users"] will be created
  + resource "aws_api_gateway_resource" "lambda_proxy" {
      + id          = (known after apply)
      + parent_id   = "0dkbds"
      + path        = (known after apply)
      + path_part   = "{proxy+}"
      + region      = "us-east-2"
      + rest_api_id = "2apxzxb0r8"
    }

  # aws_api_gateway_stage.branch_stage will be updated in-place
  ~ resource "aws_api_gateway_stage" "branch_stage" {
      ~ deployment_id         = "3rllg0" -> (known after apply)
        id                    = "ags-2apxzxb0r8-prod"
        tags                  = {}
        # (15 unchanged attributes hidden)
    }

Plan: 19 to add, 3 to change, 1 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Pushed by: @nourshoreibah, Action: pull_request

@nourshoreibah nourshoreibah merged commit b2d7d3b into main Jul 1, 2026
15 checks passed
@nourshoreibah nourshoreibah deleted the fix/lambda-bundle-and-proxy-routing branch July 1, 2026 23:25
github-actions Bot added a commit that referenced this pull request Jul 1, 2026
nourshoreibah added a commit that referenced this pull request Jul 7, 2026
Reconcile the shared-dispatcher routing refactor with main's CORS fixes
(#277/#279), S3 report uploads, and static-export frontend.

Resolution:
- Handlers keep the @branch/lambda-http dispatch() architecture. Re-applied
  main's functional deltas on top: projects PUT uses ProjectValidationUtils
  .buildUpdateValues (+ JSON guard, reject empty); reports gains
  POST /reports/generate, GET /reports/upload-url (S3 presign), and
  POST /reports (record uploaded file), and saveReportRecord now passes title.
- api_gateway.tf keeps the branch ANY-on-root + {proxy+} model; ported main's
  aws_api_gateway_gateway_response.cors so APIGW 4XX/5XX errors carry CORS
  headers. Dropped main's duplicated proxy resources left by the auto-merge.
- next.config.ts takes main's static-export config (dev rewrites obsolete;
  apiFetch targets service ports / NEXT_PUBLIC_API_BASE_URL directly).
- Lambda build scripts keep cjs + --external:@aws-sdk/*; reports retains
  main's pdfmake font-copy step.
- Regenerated lambda package-lock.json files.

Verified: all 6 lambdas + frontend typecheck clean; reports unit 44/44,
projects unit + dashboard unit pass; terraform fmt + validate succeed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant