Skip to content

Declare Substratal-Version in openapi.yaml; 405 for wrong methods; release dev - #67

Merged
Adron merged 7 commits into
mainfrom
feature/header-versioning-spec
Oct 22, 2026
Merged

Adron merged 7 commits into
mainfrom
feature/header-versioning-spec

Conversation

@Adron

@Adron Adron commented Oct 22, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to header versioning (#37), plus the pending dev release.

Versioning in the spec

  • openapi.yaml declares the version header on every versioned operation: a shared ApiVersion parameter (Substratal-Version, enum "1") on all 119 of them, plus a documented 400 on the 105 that lacked one. GET /health and POST /auth/oauth/token stay exempt, matching the middleware. Generated clients now learn the header from the spec.
  • Codegen: an OpenAPI Overlay (internal/api/gen/overlay.yaml) removes the parameter before oapi-codegen runs, because the middleware already enforces it ahead of every route. Handler signatures are unchanged. gen.go's route registrations are re-sorted by the overlay; routing is by pattern, so behavior is identical. MCP tools unchanged.

405 for a wrong method

  • A known path with an unsupported method (e.g. PUT /health) returns 405 method_not_allowed with Allow and details.allowed, instead of the catch-all 404.

/v1 sweep

Releases dev

Test plan

  • go test ./... (new 405 cases with Allow), go vet, gofmt, golangci-lint (0 issues)
  • make gen-check; terraform fmt -check
  • Spec: 121 operations, 119 declare ApiVersion; the two exempt ones don't
  • Jekyll build + internal link/anchor check: 0 broken
  • CI green; merging deploys

🤖 Generated with Claude Code

Adron and others added 6 commits October 21, 2026 16:29
remaining_manual_steps.md is now issues #18-#28, one per step, titled
'[bespoke] N. …' in the order they need doing, each with the full
instructions and a link to the step before it. The file is deleted, and
README, infra/README.md, and plan-remaining.md link to the issues instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manual go-live steps moved to [bespoke] issues #18–#28
The verify-email, reset-password, and other dashboard pages are being
built in the management app (web, macOS, Windows, iOS, Android, and
Linux), outside this repository, and tracked there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Header versioning (#37) is enforced by middleware, but the spec only
mentioned it in prose, so generated clients didn't know to send it.

- openapi.yaml: a shared ApiVersion header parameter (Substratal-Version,
  enum "1") on all 119 versioned operations, and a documented 400 on the
  105 that lacked one. GET /health and POST /auth/oauth/token stay exempt.
- Codegen: an OpenAPI Overlay (internal/api/gen/overlay.yaml) drops the
  parameter before oapi-codegen runs, since the middleware enforces it
  ahead of every route; handler signatures are unchanged. Route
  registrations in gen.go are re-sorted by the overlay's re-serialization;
  routing is by pattern, so behavior is identical. MCP tools unchanged.
- A known path called with an unsupported method returns 405
  method_not_allowed with Allow and details.allowed, instead of the
  catch-all 404.
- Docs: Conventions (versioning, errors), OpenAPI overview, spec
  description, Changelog; plan-remaining's Phase 0 line no longer cites
  the old /v1/health.
- Tests: 405 with Allow for /health and /users/me.

Brings dev up to date with main (SCIM, alarm email, header versioning).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

terraform plan (production)

This is exactly what merging will change in AWS.

Plan
random_password.mcp_session: Refreshing state... [id=none]
module.stripe_webhook.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-stripe-webhook]
module.mcp.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-mcp]
aws_sesv2_email_identity.mail: Refreshing state... [id=mail.substratalapps.com]
aws_sns_topic.alarms: Refreshing state... [id=arn:aws:sns:us-east-1:196936048600:substratal-prod-alarms]
data.aws_route53_zone.main: Reading...
module.stripe_webhook.aws_iam_role.this: Refreshing state... [id=substratal-prod-stripe-webhook]
module.mcp.aws_iam_role.this: Refreshing state... [id=substratal-prod-mcp]
module.api.aws_iam_role.this: Refreshing state... [id=substratal-prod-api]
aws_kms_key.data: Refreshing state... [id=b0cda5a8-651e-419f-b8ab-61772bce79da]
module.webhook_worker.aws_iam_role.this: Refreshing state... [id=substratal-prod-webhook-worker]
module.jobs.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-jobs]
aws_cloudwatch_log_group.api_access: Refreshing state... [id=/aws/apigateway/substratal-prod]
aws_apigatewayv2_api.main: Refreshing state... [id=rv9n7e3ww4]
aws_secretsmanager_secret.app: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:196936048600:secret:substratal-prod/app-1hXTGg]
data.aws_caller_identity.me: Reading...
data.aws_route53_zone.main: Read complete after 0s [id=Z0783627X8ON1GH6I0XB]
data.aws_caller_identity.me: Read complete after 0s [id=196936048600]
data.aws_servicequotas_service_quota.lambda_concurrency: Reading...
aws_kms_key.signing: Refreshing state... [id=0d2770cd-bbc8-4904-a432-2e9c9304a3ca]
aws_rds_cluster.main: Refreshing state... [id=substratal-prod-db]
module.webhook_worker.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-webhook-worker]
aws_sqs_queue.webhooks_dlq: Refreshing state... [id=https://sqs.us-east-1.amazonaws.com/196936048600/substratal-prod-webhooks-dlq]
aws_acm_certificate.api: Refreshing state... [id=arn:aws:acm:us-east-1:196936048600:certificate/a8e2136f-64d3-456b-904a-05ea74eba9b3]
aws_iam_role.scheduler: Refreshing state... [id=substratal-prod-scheduler]
module.api.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-api]
module.jobs.aws_iam_role.this: Refreshing state... [id=substratal-prod-jobs]
aws_cloudwatch_metric_alarm.security["refresh-reuse"]: Refreshing state... [id=substratal-prod-security-refresh-reuse]
aws_sns_topic_subscription.alarms[0]: Refreshing state... [id=arn:aws:sns:us-east-1:196936048600:substratal-prod-alarms:fffb7317-8153-4e66-9212-eded800c9c05]
aws_cloudwatch_metric_alarm.security["destructive-blocked"]: Refreshing state... [id=substratal-prod-security-destructive-blocked]
aws_cloudwatch_metric_alarm.security["failed-logins"]: Refreshing state... [id=substratal-prod-security-failed-logins]
aws_kms_alias.data: Refreshing state... [id=alias/substratal-prod-data]
data.aws_servicequotas_service_quota.lambda_concurrency: Read complete after 1s [id=arn:aws:servicequotas:us-east-1::lambda/L-B99A9384]
aws_route53_record.dmarc: Refreshing state... [id=Z0783627X8ON1GH6I0XB__dmarc.mail.substratalapps.com_TXT]
aws_route53_record.dkim[1]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_fhl63kaowzjfd2splohuyaifnnansx5d._domainkey.mail.substratalapps.com_CNAME]
aws_route53_record.dkim[0]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_fmy2g7whqti56v5vn56es6izxo4yzayc._domainkey.mail.substratalapps.com_CNAME]
aws_route53_record.dkim[2]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_rp4dt5rjjkpnhdyxud6l2spetqo5nom4._domainkey.mail.substratalapps.com_CNAME]
aws_s3_bucket.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
module.mcp.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-mcp:logs]
module.stripe_webhook.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-stripe-webhook:logs]
terraform_data.app_secret_init: Refreshing state... [id=5474e6c3-4843-79d8-9346-9fc3d3f8fa87]
aws_apigatewayv2_stage.default: Refreshing state... [id=$default]
aws_cloudwatch_metric_alarm.api_5xx: Refreshing state... [id=substratal-prod-api-5xx]
module.webhook_worker.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-webhook-worker:logs]
aws_kms_alias.signing: Refreshing state... [id=alias/substratal-prod-jwt-signing]
module.api.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-api:logs]
aws_cloudwatch_metric_alarm.webhook_dlq: Refreshing state... [id=substratal-prod-webhooks-dlq]
aws_sqs_queue.webhooks: Refreshing state... [id=https://sqs.us-east-1.amazonaws.com/196936048600/substratal-prod-webhooks]
aws_cloudwatch_metric_alarm.aurora_near_max: Refreshing state... [id=substratal-prod-aurora-acu-near-max]
aws_route53_record.cert_validation["api.substratalapps.com"]: Refreshing state... [id=Z0783627X8ON1GH6I0XB__45a2e898c02bdc2297dde6c5e5a798a8.api.substratalapps.com._CNAME]
aws_rds_cluster_instance.main: Refreshing state... [id=substratal-prod-db-1]
module.jobs.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-jobs:logs]
module.mcp.aws_lambda_function.this: Refreshing state... [id=substratal-prod-mcp]
aws_cloudwatch_log_metric_filter.security["destructive-blocked"]: Refreshing state... [id=substratal-prod-destructive-blocked]
aws_cloudwatch_log_metric_filter.security["failed-logins"]: Refreshing state... [id=substratal-prod-failed-logins]
aws_cloudwatch_log_metric_filter.security["refresh-reuse"]: Refreshing state... [id=substratal-prod-refresh-reuse]
module.stripe_webhook.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-stripe-webhook:function]
module.webhook_worker.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-webhook-worker:function]
module.api.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-api:function]
module.stripe_webhook.aws_lambda_function.this: Refreshing state... [id=substratal-prod-stripe-webhook]
module.api.aws_lambda_function.this: Refreshing state... [id=substratal-prod-api]
module.webhook_worker.aws_lambda_function.this: Refreshing state... [id=substratal-prod-webhook-worker]
aws_acm_certificate_validation.api: Refreshing state... [id=2026-10-20 20:44:42.323 +0000 UTC]
aws_apigatewayv2_domain_name.api: Refreshing state... [id=api.substratalapps.com]
aws_apigatewayv2_api_mapping.api: Refreshing state... [id=zdmjg2]
aws_route53_record.api: Refreshing state... [id=Z0783627X8ON1GH6I0XB_api.substratalapps.com_A]
module.api.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:live]
module.webhook_worker.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:live]
module.mcp.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:live]
module.stripe_webhook.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:live]
aws_lambda_event_source_mapping.webhooks: Refreshing state... [id=bed6416f-6a72-474a-83e5-559c995cf8f3]
aws_cloudwatch_metric_alarm.lambda_throttles["worker"]: Refreshing state... [id=substratal-prod-worker-throttles]
aws_cloudwatch_metric_alarm.lambda_throttles["api"]: Refreshing state... [id=substratal-prod-api-throttles]
aws_lambda_permission.apigw["api"]: Refreshing state... [id=apigateway-api]
aws_lambda_permission.apigw["stripe"]: Refreshing state... [id=apigateway-stripe]
aws_apigatewayv2_integration.fn["mcp"]: Refreshing state... [id=8xk20ia]
aws_lambda_permission.apigw["mcp"]: Refreshing state... [id=apigateway-mcp]
aws_apigatewayv2_integration.fn["api"]: Refreshing state... [id=3h3bk4c]
aws_apigatewayv2_integration.fn["stripe"]: Refreshing state... [id=34l24l9]
aws_apigatewayv2_route.r["POST /internal/stripe/webhook"]: Refreshing state... [id=bczysio]
aws_apigatewayv2_route.r["DELETE /mcp"]: Refreshing state... [id=peme2oj]
aws_apigatewayv2_route.r["POST /mcp"]: Refreshing state... [id=a197cl8]
aws_apigatewayv2_route.r["GET /.well-known/{proxy+}"]: Refreshing state... [id=kb35wrh]
aws_apigatewayv2_route.r["$default"]: Refreshing state... [id=cr82zzs]
aws_apigatewayv2_route.r["GET /mcp"]: Refreshing state... [id=s6ez97n]
aws_s3_bucket_public_access_block.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_s3_bucket_versioning.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_s3_bucket_server_side_encryption_configuration.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
module.jobs.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-jobs:function]
module.jobs.aws_lambda_function.this: Refreshing state... [id=substratal-prod-jobs]
module.jobs.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:live]
aws_iam_role_policy.scheduler: Refreshing state... [id=substratal-prod-scheduler:terraform-339ca2ff2a164d299b046f72e0]
aws_scheduler_schedule.job["seat-sync"]: Refreshing state... [id=default/substratal-prod-seat-sync]
aws_scheduler_schedule.job["cleanup"]: Refreshing state... [id=default/substratal-prod-cleanup]
aws_scheduler_schedule.job["audit-archive"]: Refreshing state... [id=default/substratal-prod-audit-archive]
aws_scheduler_schedule.job["subscription-access"]: Refreshing state... [id=default/substratal-prod-subscription-access]
aws_scheduler_schedule.job["stripe-events"]: Refreshing state... [id=default/substratal-prod-stripe-events]
aws_cloudwatch_metric_alarm.lambda_errors["worker"]: Refreshing state... [id=substratal-prod-worker-errors]
aws_scheduler_schedule.job["settings-projections"]: Refreshing state... [id=default/substratal-prod-settings-projections]
aws_scheduler_schedule.job["webhook-health"]: Refreshing state... [id=default/substratal-prod-webhook-health]
aws_cloudwatch_metric_alarm.lambda_errors["api"]: Refreshing state... [id=substratal-prod-api-errors]
aws_cloudwatch_metric_alarm.lambda_errors["jobs"]: Refreshing state... [id=substratal-prod-jobs-errors]
aws_cloudwatch_metric_alarm.lambda_errors["mcp"]: Refreshing state... [id=substratal-prod-mcp-errors]
aws_cloudwatch_metric_alarm.lambda_errors["stripe"]: Refreshing state... [id=substratal-prod-stripe-errors]
aws_scheduler_schedule.job["push-maintenance"]: Refreshing state... [id=default/substratal-prod-push-maintenance]
aws_scheduler_schedule.job["entitlement-sweep"]: Refreshing state... [id=default/substratal-prod-entitlement-sweep]
aws_scheduler_schedule.job["webhook-dispatch"]: Refreshing state... [id=default/substratal-prod-webhook-dispatch]
aws_scheduler_schedule.job["erasure-cascade"]: Refreshing state... [id=default/substratal-prod-erasure-cascade]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.api.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "9" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.api.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-api"
      ~ last_modified                  = "2026-10-22T06:34:56.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:9" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:9/invocations" -> (known after apply)
      ~ s3_key                         = "8d1a3f6ed856747a33e07b8fb48c66ce607337d9/api.zip" -> "58fc9e0afc720ae3e9b33f1b758354791a234a39/api.zip"
        tags                           = {}
      ~ version                        = "9" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.jobs.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "9" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.jobs.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-jobs"
      ~ last_modified                  = "2026-10-22T06:34:56.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:9" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:9/invocations" -> (known after apply)
      ~ s3_key                         = "8d1a3f6ed856747a33e07b8fb48c66ce607337d9/jobs.zip" -> "58fc9e0afc720ae3e9b33f1b758354791a234a39/jobs.zip"
        tags                           = {}
      ~ version                        = "9" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.mcp.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "10" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.mcp.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-mcp"
      ~ last_modified                  = "2026-10-22T06:34:56.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:10" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:10/invocations" -> (known after apply)
      ~ s3_key                         = "8d1a3f6ed856747a33e07b8fb48c66ce607337d9/mcp.zip" -> "58fc9e0afc720ae3e9b33f1b758354791a234a39/mcp.zip"
        tags                           = {}
      ~ version                        = "10" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.stripe_webhook.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "9" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.stripe_webhook.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-stripe-webhook"
      ~ last_modified                  = "2026-10-22T06:34:56.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:9" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:9/invocations" -> (known after apply)
      ~ s3_key                         = "8d1a3f6ed856747a33e07b8fb48c66ce607337d9/stripe-webhook.zip" -> "58fc9e0afc720ae3e9b33f1b758354791a234a39/stripe-webhook.zip"
        tags                           = {}
      ~ version                        = "9" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.webhook_worker.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "9" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.webhook_worker.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-webhook-worker"
      ~ last_modified                  = "2026-10-22T06:34:56.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:9" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:9/invocations" -> (known after apply)
      ~ s3_key                         = "8d1a3f6ed856747a33e07b8fb48c66ce607337d9/webhook-worker.zip" -> "58fc9e0afc720ae3e9b33f1b758354791a234a39/webhook-worker.zip"
        tags                           = {}
      ~ version                        = "9" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 10 to change, 0 to destroy.

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

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Conflicts: keep remaining_manual_steps.md deleted (moved to [bespoke]
issues in #29); keep the Phase 0 line without the /v1 mention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

terraform plan (production)

This is exactly what merging will change in AWS.

Plan
random_password.mcp_session: Refreshing state... [id=none]
data.aws_route53_zone.main: Reading...
aws_kms_key.data: Refreshing state... [id=b0cda5a8-651e-419f-b8ab-61772bce79da]
data.aws_servicequotas_service_quota.lambda_concurrency: Reading...
module.stripe_webhook.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-stripe-webhook]
module.mcp.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-mcp]
module.stripe_webhook.aws_iam_role.this: Refreshing state... [id=substratal-prod-stripe-webhook]
module.webhook_worker.aws_iam_role.this: Refreshing state... [id=substratal-prod-webhook-worker]
aws_apigatewayv2_api.main: Refreshing state... [id=rv9n7e3ww4]
data.aws_caller_identity.me: Reading...
aws_rds_cluster.main: Refreshing state... [id=substratal-prod-db]
data.aws_caller_identity.me: Read complete after 0s [id=196936048600]
aws_kms_key.signing: Refreshing state... [id=0d2770cd-bbc8-4904-a432-2e9c9304a3ca]
aws_cloudwatch_log_group.api_access: Refreshing state... [id=/aws/apigateway/substratal-prod]
data.aws_servicequotas_service_quota.lambda_concurrency: Read complete after 0s [id=arn:aws:servicequotas:us-east-1::lambda/L-B99A9384]
aws_secretsmanager_secret.app: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:196936048600:secret:substratal-prod/app-1hXTGg]
aws_iam_role.scheduler: Refreshing state... [id=substratal-prod-scheduler]
aws_sesv2_email_identity.mail: Refreshing state... [id=mail.substratalapps.com]
module.api.aws_iam_role.this: Refreshing state... [id=substratal-prod-api]
aws_sqs_queue.webhooks_dlq: Refreshing state... [id=https://sqs.us-east-1.amazonaws.com/196936048600/substratal-prod-webhooks-dlq]
aws_sns_topic.alarms: Refreshing state... [id=arn:aws:sns:us-east-1:196936048600:substratal-prod-alarms]
module.api.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-api]
data.aws_route53_zone.main: Read complete after 1s [id=Z0783627X8ON1GH6I0XB]
module.jobs.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-jobs]
module.jobs.aws_iam_role.this: Refreshing state... [id=substratal-prod-jobs]
module.webhook_worker.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-webhook-worker]
aws_acm_certificate.api: Refreshing state... [id=arn:aws:acm:us-east-1:196936048600:certificate/a8e2136f-64d3-456b-904a-05ea74eba9b3]
module.mcp.aws_iam_role.this: Refreshing state... [id=substratal-prod-mcp]
aws_s3_bucket.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_kms_alias.signing: Refreshing state... [id=alias/substratal-prod-jwt-signing]
aws_kms_alias.data: Refreshing state... [id=alias/substratal-prod-data]
aws_apigatewayv2_stage.default: Refreshing state... [id=$default]
aws_rds_cluster_instance.main: Refreshing state... [id=substratal-prod-db-1]
aws_route53_record.dmarc: Refreshing state... [id=Z0783627X8ON1GH6I0XB__dmarc.mail.substratalapps.com_TXT]
terraform_data.app_secret_init: Refreshing state... [id=5474e6c3-4843-79d8-9346-9fc3d3f8fa87]
aws_route53_record.dkim[0]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_fmy2g7whqti56v5vn56es6izxo4yzayc._domainkey.mail.substratalapps.com_CNAME]
aws_route53_record.dkim[2]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_rp4dt5rjjkpnhdyxud6l2spetqo5nom4._domainkey.mail.substratalapps.com_CNAME]
aws_route53_record.dkim[1]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_fhl63kaowzjfd2splohuyaifnnansx5d._domainkey.mail.substratalapps.com_CNAME]
aws_sqs_queue.webhooks: Refreshing state... [id=https://sqs.us-east-1.amazonaws.com/196936048600/substratal-prod-webhooks]
module.stripe_webhook.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-stripe-webhook:logs]
module.webhook_worker.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-webhook-worker:logs]
aws_cloudwatch_log_metric_filter.security["destructive-blocked"]: Refreshing state... [id=substratal-prod-destructive-blocked]
aws_cloudwatch_log_metric_filter.security["failed-logins"]: Refreshing state... [id=substratal-prod-failed-logins]
aws_cloudwatch_log_metric_filter.security["refresh-reuse"]: Refreshing state... [id=substratal-prod-refresh-reuse]
module.api.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-api:logs]
aws_cloudwatch_metric_alarm.aurora_near_max: Refreshing state... [id=substratal-prod-aurora-acu-near-max]
aws_cloudwatch_metric_alarm.api_5xx: Refreshing state... [id=substratal-prod-api-5xx]
aws_cloudwatch_metric_alarm.webhook_dlq: Refreshing state... [id=substratal-prod-webhooks-dlq]
aws_sns_topic_subscription.alarms[0]: Refreshing state... [id=arn:aws:sns:us-east-1:196936048600:substratal-prod-alarms:fffb7317-8153-4e66-9212-eded800c9c05]
aws_cloudwatch_metric_alarm.security["destructive-blocked"]: Refreshing state... [id=substratal-prod-security-destructive-blocked]
aws_cloudwatch_metric_alarm.security["failed-logins"]: Refreshing state... [id=substratal-prod-security-failed-logins]
aws_cloudwatch_metric_alarm.security["refresh-reuse"]: Refreshing state... [id=substratal-prod-security-refresh-reuse]
module.mcp.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-mcp:logs]
module.jobs.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-jobs:logs]
aws_route53_record.cert_validation["api.substratalapps.com"]: Refreshing state... [id=Z0783627X8ON1GH6I0XB__45a2e898c02bdc2297dde6c5e5a798a8.api.substratalapps.com._CNAME]
module.stripe_webhook.aws_lambda_function.this: Refreshing state... [id=substratal-prod-stripe-webhook]
module.webhook_worker.aws_lambda_function.this: Refreshing state... [id=substratal-prod-webhook-worker]
module.api.aws_lambda_function.this: Refreshing state... [id=substratal-prod-api]
module.webhook_worker.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-webhook-worker:function]
module.api.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-api:function]
module.stripe_webhook.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-stripe-webhook:function]
module.mcp.aws_lambda_function.this: Refreshing state... [id=substratal-prod-mcp]
aws_acm_certificate_validation.api: Refreshing state... [id=2026-10-20 20:44:42.323 +0000 UTC]
aws_apigatewayv2_domain_name.api: Refreshing state... [id=api.substratalapps.com]
aws_apigatewayv2_api_mapping.api: Refreshing state... [id=zdmjg2]
aws_route53_record.api: Refreshing state... [id=Z0783627X8ON1GH6I0XB_api.substratalapps.com_A]
aws_s3_bucket_public_access_block.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_s3_bucket_versioning.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_s3_bucket_server_side_encryption_configuration.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
module.jobs.aws_lambda_function.this: Refreshing state... [id=substratal-prod-jobs]
module.jobs.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-jobs:function]
module.stripe_webhook.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:live]
module.webhook_worker.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:live]
module.api.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:live]
module.mcp.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:live]
aws_lambda_event_source_mapping.webhooks: Refreshing state... [id=bed6416f-6a72-474a-83e5-559c995cf8f3]
aws_cloudwatch_metric_alarm.lambda_throttles["api"]: Refreshing state... [id=substratal-prod-api-throttles]
aws_cloudwatch_metric_alarm.lambda_throttles["worker"]: Refreshing state... [id=substratal-prod-worker-throttles]
aws_lambda_permission.apigw["api"]: Refreshing state... [id=apigateway-api]
aws_apigatewayv2_integration.fn["stripe"]: Refreshing state... [id=34l24l9]
aws_apigatewayv2_integration.fn["api"]: Refreshing state... [id=3h3bk4c]
aws_lambda_permission.apigw["stripe"]: Refreshing state... [id=apigateway-stripe]
aws_apigatewayv2_integration.fn["mcp"]: Refreshing state... [id=8xk20ia]
aws_lambda_permission.apigw["mcp"]: Refreshing state... [id=apigateway-mcp]
aws_apigatewayv2_route.r["GET /mcp"]: Refreshing state... [id=s6ez97n]
aws_apigatewayv2_route.r["$default"]: Refreshing state... [id=cr82zzs]
aws_apigatewayv2_route.r["GET /.well-known/{proxy+}"]: Refreshing state... [id=kb35wrh]
aws_apigatewayv2_route.r["POST /mcp"]: Refreshing state... [id=a197cl8]
aws_apigatewayv2_route.r["DELETE /mcp"]: Refreshing state... [id=peme2oj]
aws_apigatewayv2_route.r["POST /internal/stripe/webhook"]: Refreshing state... [id=bczysio]
module.jobs.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:live]
aws_iam_role_policy.scheduler: Refreshing state... [id=substratal-prod-scheduler:terraform-339ca2ff2a164d299b046f72e0]
aws_cloudwatch_metric_alarm.lambda_errors["stripe"]: Refreshing state... [id=substratal-prod-stripe-errors]
aws_scheduler_schedule.job["webhook-health"]: Refreshing state... [id=default/substratal-prod-webhook-health]
aws_scheduler_schedule.job["subscription-access"]: Refreshing state... [id=default/substratal-prod-subscription-access]
aws_cloudwatch_metric_alarm.lambda_errors["worker"]: Refreshing state... [id=substratal-prod-worker-errors]
aws_cloudwatch_metric_alarm.lambda_errors["jobs"]: Refreshing state... [id=substratal-prod-jobs-errors]
aws_cloudwatch_metric_alarm.lambda_errors["api"]: Refreshing state... [id=substratal-prod-api-errors]
aws_cloudwatch_metric_alarm.lambda_errors["mcp"]: Refreshing state... [id=substratal-prod-mcp-errors]
aws_scheduler_schedule.job["cleanup"]: Refreshing state... [id=default/substratal-prod-cleanup]
aws_scheduler_schedule.job["settings-projections"]: Refreshing state... [id=default/substratal-prod-settings-projections]
aws_scheduler_schedule.job["push-maintenance"]: Refreshing state... [id=default/substratal-prod-push-maintenance]
aws_scheduler_schedule.job["erasure-cascade"]: Refreshing state... [id=default/substratal-prod-erasure-cascade]
aws_scheduler_schedule.job["entitlement-sweep"]: Refreshing state... [id=default/substratal-prod-entitlement-sweep]
aws_scheduler_schedule.job["audit-archive"]: Refreshing state... [id=default/substratal-prod-audit-archive]
aws_scheduler_schedule.job["webhook-dispatch"]: Refreshing state... [id=default/substratal-prod-webhook-dispatch]
aws_scheduler_schedule.job["seat-sync"]: Refreshing state... [id=default/substratal-prod-seat-sync]
aws_scheduler_schedule.job["stripe-events"]: Refreshing state... [id=default/substratal-prod-stripe-events]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.api.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "10" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.api.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-api"
      ~ last_modified                  = "2026-10-22T07:02:33.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:10" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:10/invocations" -> (known after apply)
      ~ s3_key                         = "9c3f30aec1e0b68e743eb9c01cfebb16e683a845/api.zip" -> "28561533ec3ec6f0aa8ce129f8dc814852d9cbef/api.zip"
        tags                           = {}
      ~ version                        = "10" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.jobs.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "10" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.jobs.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-jobs"
      ~ last_modified                  = "2026-10-22T07:02:33.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:10" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:10/invocations" -> (known after apply)
      ~ s3_key                         = "9c3f30aec1e0b68e743eb9c01cfebb16e683a845/jobs.zip" -> "28561533ec3ec6f0aa8ce129f8dc814852d9cbef/jobs.zip"
        tags                           = {}
      ~ version                        = "10" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.mcp.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "11" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.mcp.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-mcp"
      ~ last_modified                  = "2026-10-22T07:02:33.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:11" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:11/invocations" -> (known after apply)
      ~ s3_key                         = "9c3f30aec1e0b68e743eb9c01cfebb16e683a845/mcp.zip" -> "28561533ec3ec6f0aa8ce129f8dc814852d9cbef/mcp.zip"
        tags                           = {}
      ~ version                        = "11" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.stripe_webhook.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "10" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.stripe_webhook.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-stripe-webhook"
      ~ last_modified                  = "2026-10-22T07:02:33.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:10" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:10/invocations" -> (known after apply)
      ~ s3_key                         = "9c3f30aec1e0b68e743eb9c01cfebb16e683a845/stripe-webhook.zip" -> "28561533ec3ec6f0aa8ce129f8dc814852d9cbef/stripe-webhook.zip"
        tags                           = {}
      ~ version                        = "10" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.webhook_worker.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "10" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.webhook_worker.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-webhook-worker"
      ~ last_modified                  = "2026-10-22T07:02:33.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:10" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:10/invocations" -> (known after apply)
      ~ s3_key                         = "9c3f30aec1e0b68e743eb9c01cfebb16e683a845/webhook-worker.zip" -> "28561533ec3ec6f0aa8ce129f8dc814852d9cbef/webhook-worker.zip"
        tags                           = {}
      ~ version                        = "10" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 10 to change, 0 to destroy.

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

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

@Adron
Adron merged commit f66cb8d into main Oct 22, 2026
8 checks passed
@Adron
Adron deleted the feature/header-versioning-spec branch October 22, 2026 07:11
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