feat(infra): add HCP Terraform for procurement-ingest import-in-place

This commit is contained in:
Adam Moussa 2026-08-06 17:07:40 -04:00
parent 5b3e20bd35
commit 0ba3600177
No known key found for this signature in database
33 changed files with 5026 additions and 1 deletions

32
.github/workflows/ci-terraform.yaml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Terraform CI
on:
pull_request:
branches: [main]
paths:
- "terraform/**"
- ".github/workflows/ci-terraform.yaml"
permissions:
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: "1.9.8"
- name: Terraform fmt
run: terraform fmt -check -recursive
- name: Terraform init
run: terraform init -backend=false
- name: Terraform validate
run: terraform validate

View file

@ -335,9 +335,12 @@ The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This pr
## CI/CD ## CI/CD
> **PLAT-86 (in progress):** CDK CD is soft-frozen (`deploy.yaml` disabled on GitHub) while ownership moves to HCP Terraform workspace `procurement-ingest-prod`. Terraform lives under `terraform/`; import map under `docs/plat-86/`. HCP becomes the sole mutate path after import + green Manual apply; then `deploy.yaml` / `cd-cdk.yaml` are retired. Post-apply smoke still targets `po-email-processor`, `workorder-email-processor`, and `procurement-api` via `scripts/post-deploy-smoke.sh`.
GitHub Actions with reusable workflows from `Sea-Haven-Industries/.github` (all pinned to a commit SHA of `main`): GitHub Actions with reusable workflows from `Sea-Haven-Industries/.github` (all pinned to a commit SHA of `main`):
- **CI** (`ci.yaml`, PR to `main`): linting + `cdk synth` via `ci-python-sam.yaml`. `cdk synth`'s Docker-bundled asset build for `po-email-processor` and `workorder-email-processor` mounts the widened `../lambdas` asset root (Phase 2, see [Deploy-Pipeline Guards](#deploy-pipeline-guards-phase-0)) as build context, not just each function's own subdirectory — the `exclude` list on both `from_asset` calls strips local-only `__pycache__`/`package/` (and `tests/`) cruft from that wider mount's source fingerprint, so CI's asset hash matches a clean local checkout, and each function's scoped `cp` glob copies only its own pipeline's `*.py` into the zip. (The exclude does not, and under `SOURCE` hashing cannot, keep the *other* pipeline's tracked source out of the fingerprint (see the PO bundling note above on `SOURCE` hashing) — but that source is identical in CI and local, so it does not cause hash divergence.) - **CI** (`ci.yaml`, PR to `main`): linting + `cdk synth` via `ci-python-sam.yaml`. `cdk synth`'s Docker-bundled asset build for `po-email-processor` and `workorder-email-processor` mounts the widened `../lambdas` asset root (Phase 2, see [Deploy-Pipeline Guards](#deploy-pipeline-guards-phase-0)) as build context, not just each function's own subdirectory — the `exclude` list on both `from_asset` calls strips local-only `__pycache__`/`package/` (and `tests/`) cruft from that wider mount's source fingerprint, so CI's asset hash matches a clean local checkout, and each function's scoped `cp` glob copies only its own pipeline's `*.py` into the zip. (The exclude does not, and under `SOURCE` hashing cannot, keep the *other* pipeline's tracked source out of the fingerprint (see the PO bundling note above on `SOURCE` hashing) — but that source is identical in CI and local, so it does not cause hash divergence.)
- **CD** (`deploy.yaml`, push to `main`): CDK deploy via `cd-cdk.yaml` (OIDC auth), followed by the synchronous `post-deploy-script: scripts/post-deploy-smoke.sh` healthcheck gate (see [Deploy-Pipeline Guards](#deploy-pipeline-guards-phase-0)) — `cd-cdk.yaml`'s `stack-name` input only accepts one stack, so the smoke script itself enumerates `po-email-processor`, `workorder-email-processor`, and `procurement-api` - **Terraform CI** (`ci-terraform.yaml`, PR to `main` when `terraform/**` changes): `fmt -check`, `init -backend=false`, `validate`.
- **CD** (`deploy.yaml`, push to `main`): CDK deploy via `cd-cdk.yaml` (OIDC auth), followed by the synchronous `post-deploy-script: scripts/post-deploy-smoke.sh` healthcheck gate (see [Deploy-Pipeline Guards](#deploy-pipeline-guards-phase-0)) — `cd-cdk.yaml`'s `stack-name` input only accepts one stack, so the smoke script itself enumerates `po-email-processor`, `workorder-email-processor`, and `procurement-api`. **Soft-frozen for PLAT-86** (workflow disabled until HCP cutover seals).
- Plus dependency review and PR labeler workflows on every PR - Plus dependency review and PR labeler workflows on every PR
Branch protection on `main` — all changes through PR. Branch protection on `main` — all changes through PR.

View file

@ -0,0 +1,374 @@
[
{
"LogicalId": "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691",
"Type": "AWS::Lambda::Function",
"PhysicalId": "WorkorderIngestStack-BucketNotificationsHandler050-fBtoo7x6Azy3",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC",
"Type": "AWS::IAM::Role",
"PhysicalId": "WorkorderIngestStack-BucketNotificationsHandler050a-7KxpBKmlpFSf",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "CDKMetadata",
"Type": "AWS::CDK::Metadata",
"PhysicalId": "a85b0320-86dc-11f1-b0cf-0eccc7521e19",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "CommentsTableBBDBF0A8",
"Type": "AWS::DynamoDB::Table",
"PhysicalId": "WorkOrderComments",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "EmailBucket843A740F",
"Type": "AWS::S3::Bucket",
"PhysicalId": "workorder-ingest-emails-011934824531",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketAllowBucketNotificationsToworkorderingestEmailProcessor5B0B85DABA138779",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "WorkorderIngestStack-EmailBucketAllowBucketNotificationsToworkorderingestEmailProcessor-VuairMrX6Qir",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketNotificationsDDD5BEEA",
"Type": "Custom::S3BucketNotifications",
"PhysicalId": "EmailBucketNotificationsDDD5BEEA",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketNotificationsHandlerPolicy7D16942E",
"Type": "AWS::IAM::Policy",
"PhysicalId": "Worko-Email-YLl5HvyOvIg2",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketPolicyBBC68546",
"Type": "AWS::S3::BucketPolicy",
"PhysicalId": "workorder-ingest-emails-011934824531",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessor218EC076",
"Type": "AWS::Lambda::Function",
"PhysicalId": "workorder-email-processor",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorAiFallbackRejectedAlarm71D1F2CE",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-ai-fallback-rejected",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDlqA753DED5",
"Type": "AWS::SQS::Queue",
"PhysicalId": "https://sqs.us-east-1.amazonaws.com/011934824531/WorkorderIngestStack-EmailProcessorDlqA753DED5-qCTHrsoEucas",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDlqMessagesAlarmED574757",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-dlq-messages",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDlqPolicyB24007F0",
"Type": "AWS::SQS::QueuePolicy",
"PhysicalId": "WorkorderIngestStack-EmailProcessorDlqPolicyB24007F0-15VK27UU5Q19",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDurationAlarmF7530BED",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorErrorsAlarmC1BF0D18",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorLogGroup8A79A2C8",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/workorder-email-processor",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorSenderAuthRejectedAlarm3D9534B7",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-sender-auth-rejected",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorSenderAuthRejectedFilterC0DAEDFE",
"Type": "AWS::Logs::MetricFilter",
"PhysicalId": "EmailProcessorSenderAuthRejectedFilterC0DAEDFE-Wkbow1VNDeDM",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorServiceRole637381CE",
"Type": "AWS::IAM::Role",
"PhysicalId": "WorkorderIngestStack-EmailProcessorServiceRole63738-5l9OZXQLVvJn",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorServiceRoleDefaultPolicy0B15DAB8",
"Type": "AWS::IAM::Policy",
"PhysicalId": "Worko-Email-8PjqeVLLDLRv",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorTemplateFallbackRateAlarmF9B0866A",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-template-fallback-rate",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorThrottlesAlarmE102F9E9",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-email-processor-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ExistingRuleSetWorkorderEmailRuleEA29F845",
"Type": "AWS::SES::ReceiptRule",
"PhysicalId": "ExistingRuleSetWorkorderEmailRuleEA29F845-PKtaDBvIg61a",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterCD1F4C3E",
"Type": "AWS::Lambda::Function",
"PhysicalId": "workorder-shoc-emitter",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterDurationAlarm3F755835",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-emitter-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterDynamoDBEventSourceworkorderingestCommentsTableFD97F35C58A19C1A",
"Type": "AWS::Lambda::EventSourceMapping",
"PhysicalId": "da37d4a0-3086-4929-9ace-6eca5c20cd07",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterDynamoDBEventSourceworkorderingestWorkOrdersTable004CD4AC38D8BED2",
"Type": "AWS::Lambda::EventSourceMapping",
"PhysicalId": "cc149f4d-5375-4820-8fb8-c514accd3a85",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterErrorsAlarmCE2B702A",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-emitter-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterFailuresMessagesAlarmEC9798F9",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-emitter-failures-messages",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterFailuresQueue11D7C8E0",
"Type": "AWS::SQS::Queue",
"PhysicalId": "https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-failures",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterFailuresQueuePolicy0D60571C",
"Type": "AWS::SQS::QueuePolicy",
"PhysicalId": "WorkorderIngestStack-ShocEmitterFailuresQueuePolicy0D60571C-2AMBNJh0la3L",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterIteratorAgeAlarm90B55822",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-emitter-iterator-age",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterLogGroup80BDE724",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/workorder-shoc-emitter",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterRejectedMessagesAlarmAE9AFCDD",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-emitter-rejected-messages",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterRejectedQueueD754F41E",
"Type": "AWS::SQS::Queue",
"PhysicalId": "https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-rejected",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterRejectedQueuePolicy4B088C0D",
"Type": "AWS::SQS::QueuePolicy",
"PhysicalId": "WorkorderIngestStack-ShocEmitterRejectedQueuePolicy4B088C0D-vzXOuhHaGfNy",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterServiceRoleA0CA0C74",
"Type": "AWS::IAM::Role",
"PhysicalId": "WorkorderIngestStack-ShocEmitterServiceRoleA0CA0C74-LlFfZ6F9f6sr",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterServiceRoleDefaultPolicy5DE45717",
"Type": "AWS::IAM::Policy",
"PhysicalId": "Worko-ShocE-0txSITC4MbcW",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocEmitterThrottlesAlarm5503E07A",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-emitter-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorD4073E6B",
"Type": "AWS::Lambda::Function",
"PhysicalId": "workorder-shoc-hmac-rotator",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorDurationAlarm0725D01A",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-hmac-rotator-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorErrorsAlarm55E7BCC3",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-hmac-rotator-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc3085C896",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "WorkorderIngestStack-ShocHmacRotatorInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc3085C8-kJaePEGWmM3r",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorLogGroup1D109CC5",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/workorder-shoc-hmac-rotator",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorServiceRole7F2B6917",
"Type": "AWS::IAM::Role",
"PhysicalId": "WorkorderIngestStack-ShocHmacRotatorServiceRole7F2B-uKqx4ccGQTk5",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorServiceRoleDefaultPolicy492EC50B",
"Type": "AWS::IAM::Policy",
"PhysicalId": "Worko-ShocH-B5ATRJ8co1Hc",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocHmacRotatorThrottlesAlarm9963F2A3",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "workorder-shoc-hmac-rotator-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocWebhookHmacKey87DD3EC7",
"Type": "AWS::KMS::Key",
"PhysicalId": "d10fd1f0-a61a-4405-8568-85e9fd11ba18",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocWebhookHmacKeyAliasC83A07B2",
"Type": "AWS::KMS::Alias",
"PhysicalId": "alias/workorder-ingest-shoc-webhook-kms",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocWebhookHmacSecretA6CF6356",
"Type": "AWS::SecretsManager::Secret",
"PhysicalId": "arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocWebhookHmacSecretPolicy2B0A7DAC",
"Type": "AWS::SecretsManager::ResourcePolicy",
"PhysicalId": "arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ShocWebhookHmacSecretRotationA6130D74",
"Type": "AWS::SecretsManager::RotationSchedule",
"PhysicalId": "arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUI258D87A5",
"Type": "AWS::Lambda::Function",
"PhysicalId": "workorder-web-ui",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "WebUILogGroup8F43ABDC",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/workorder-web-ui",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUIServiceRoleB1B395BE",
"Type": "AWS::IAM::Role",
"PhysicalId": "WorkorderIngestStack-WebUIServiceRoleB1B395BE-0kRzDlAiLZVn",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUIServiceRoleDefaultPolicy8B510141",
"Type": "AWS::IAM::Policy",
"PhysicalId": "Worko-WebUI-TVYFGuD5kBVZ",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WorkOrderCommentsSystemErrorsAlarm4CD72F33",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "WorkOrderComments-system-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WorkOrderCommentsThrottlesAlarmA10AEFD9",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "WorkOrderComments-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WorkOrdersTable515B4C61",
"Type": "AWS::DynamoDB::Table",
"PhysicalId": "WorkOrders",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "WorkOrdersTableSystemErrorsAlarm117925DB",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "WorkOrders-system-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WorkOrdersTableThrottlesAlarmDACF2500",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "WorkOrders-throttles",
"Status": "CREATE_COMPLETE"
}
]

View file

@ -0,0 +1,52 @@
# PLAT-86 import map (seahaven-prod `011934824531` / us-east-1)
Frozen from live `DescribeStackResources` on 2026-08-06. CFN resource total: **164** (46 + 62 + 56). Estimated Terraform resources after expansion: ~220–320 — under HCP free-tier **500**.
Workspace: one `procurement-ingest-prod` covering all three former stacks.
## Out-of-band (data source / do not recreate)
| Dependency | Value |
|---|---|
| SES ruleset | `INBOUND_MAIL` |
| SNS | `arn:aws:sns:us-east-1:011934824531:site-alerts` |
| SSM CMK | `/seahaven/dynamodb/cmk-arn` → `arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12` |
| SSM ACM | `/procurement-api/custom-domain/certificate-arn` → `arn:aws:acm:us-east-1:011934824531:certificate/9eac4c03-6850-49f1-baa1-6c4e7fffd1c7` |
| Secret (imported shell) | `arn:aws:secretsmanager:us-east-1:011934824531:secret:procurement-ingest/web-ui-auth-token-ApAMmr` |
| Mgmt Route53 | `procurement-api.seahaven.com` alias — stays OOB |
## CFN disposal disposition
| Class | Disposition on CFN stack delete |
|---|---|
| DynamoDB tables, email S3 buckets, Lambda log groups | **RETAIN** (must already be TF-owned; never delete) |
| Named SQS (`workorder-shoc-emitter-*`), SHOC secret/KMS, API GW, Lambdas, alarms, SES receipt rules | TF-owned; remove from CFN via retain-on-delete or deletion_policy before stack delete |
| CDK custom resources (`Custom::S3BucketNotifications`, `BucketNotificationsHandler` Lambda/role) | **Destroy with CFN** — replaced by native `aws_s3_bucket_notification` |
| CDK Metadata | Destroy with CFN |
| CDK-generated IAM roles at path `/` | After Lambda repoint to `/tf-managed/`, delete with CFN or sweep |
## Key physical IDs to preserve
- Lambdas: `po-email-processor`, `po-web-ui`, `po-ingest-site-extractor`, `workorder-email-processor`, `workorder-web-ui`, `workorder-shoc-emitter`, `workorder-shoc-hmac-rotator`, `procurement-api`
- Tables: `purchase-orders`, `verified-sites`, `pending-site-review`, `WorkOrders`, `WorkOrderComments` (PascalCase kept for import)
- Buckets: `po-ingest-emails-011934824531`, `workorder-ingest-emails-011934824531`
- Queues: `workorder-shoc-emitter-failures`, `workorder-shoc-emitter-rejected`, plus CDK-named DLQs
- Domain: `procurement-api.seahaven.com` (mgmt Route53 OOB)
- API REST id: `mvul1efda2`
- SHOC KMS: alias `workorder-ingest-shoc-webhook-kms` (key id in live inventory JSON only)
- Secret: `workorder-ingest/shoc-webhook-hmac`
## Cross-account pins (must stay byte-stable)
- API resource policy principal: `arn:aws:iam::396287094661:role/shoc-backend-dev`
- SHOC KMS decrypt: exact ARN + `kms:ViaService` for Secrets Manager
- Webhook URL: `https://api.dev.seahaven.com/api/webhooks/work-orders`
## Raw dumps
- [`po-ingest-resources.json`](po-ingest-resources.json)
- [`WorkorderIngestStack-resources.json`](WorkorderIngestStack-resources.json)
- [`procurement-api-resources.json`](procurement-api-resources.json)
- [`raw-inventory.tsv`](raw-inventory.tsv)
Import blocks: [`terraform/imports.tf`](../../terraform/imports.tf)

View file

@ -0,0 +1,278 @@
[
{
"LogicalId": "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691",
"Type": "AWS::Lambda::Function",
"PhysicalId": "po-ingest-BucketNotificationsHandler050a0587b75445-eYKw7CRdcWxS",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC",
"Type": "AWS::IAM::Role",
"PhysicalId": "po-ingest-BucketNotificationsHandler050a0587b754454-kAyd3v80XLIz",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "CDKMetadata",
"Type": "AWS::CDK::Metadata",
"PhysicalId": "e287be30-86dc-11f1-a551-0affcf7dfc53",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "EmailBucket843A740F",
"Type": "AWS::S3::Bucket",
"PhysicalId": "po-ingest-emails-011934824531",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketAllowBucketNotificationsTopoingestEmailProcessor21BB6E7689B27E99",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "po-ingest-EmailBucketAllowBucketNotificationsTopoingestEmailProcessor21BB6E7689B27E99-2uvEuiUCEDgr",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketNotificationsDDD5BEEA",
"Type": "Custom::S3BucketNotifications",
"PhysicalId": "EmailBucketNotificationsDDD5BEEA",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketNotificationsHandlerPolicy7D16942E",
"Type": "AWS::IAM::Policy",
"PhysicalId": "po-in-Email-eep3xPAZrcL7",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailBucketPolicyBBC68546",
"Type": "AWS::S3::BucketPolicy",
"PhysicalId": "po-ingest-emails-011934824531",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessor218EC076",
"Type": "AWS::Lambda::Function",
"PhysicalId": "po-email-processor",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorAiFallbackRejectedAlarm71D1F2CE",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-ai-fallback-rejected",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDlqA753DED5",
"Type": "AWS::SQS::Queue",
"PhysicalId": "https://sqs.us-east-1.amazonaws.com/011934824531/po-ingest-EmailProcessorDlqA753DED5-Mn33HvhsDPEu",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDlqMessagesAlarmED574757",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-dlq-messages",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDlqPolicyB24007F0",
"Type": "AWS::SQS::QueuePolicy",
"PhysicalId": "po-ingest-EmailProcessorDlqPolicyB24007F0-ZBgpMipDtvH3",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorDurationAlarmF7530BED",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorErrorsAlarmC1BF0D18",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorLogGroup8A79A2C8",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/po-email-processor",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorSenderAuthRejectedAlarm3D9534B7",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-sender-auth-rejected",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorSenderAuthRejectedFilterC0DAEDFE",
"Type": "AWS::Logs::MetricFilter",
"PhysicalId": "EmailProcessorSenderAuthRejectedFilterC0DAEDFE-3K6wBb003iDv",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorServiceRole637381CE",
"Type": "AWS::IAM::Role",
"PhysicalId": "po-ingest-EmailProcessorServiceRole637381CE-hG1KGKCIivCd",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorServiceRoleDefaultPolicy0B15DAB8",
"Type": "AWS::IAM::Policy",
"PhysicalId": "po-in-Email-BbPUnOcJ71or",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorTemplateFallbackRateAlarmF9B0866A",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-template-fallback-rate",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "EmailProcessorThrottlesAlarmE102F9E9",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-email-processor-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ExistingRuleSetPoEmailRuleAC8E9C87",
"Type": "AWS::SES::ReceiptRule",
"PhysicalId": "ExistingRuleSetPoEmailRuleAC8E9C87-qwGDj9lBoL1G",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "PendingSiteReviewTableBF01B687",
"Type": "AWS::DynamoDB::Table",
"PhysicalId": "pending-site-review",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "PendingSiteReviewTableSystemErrorsAlarm36BA58D4",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "pending-site-review-system-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "PendingSiteReviewTableThrottlesAlarm15FB6DD5",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "pending-site-review-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "PurchaseOrdersTable491A23F2",
"Type": "AWS::DynamoDB::Table",
"PhysicalId": "purchase-orders",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "PurchaseOrdersTableSystemErrorsAlarmBC7F000F",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "purchase-orders-system-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "PurchaseOrdersTableThrottlesAlarmE5ACE81B",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "purchase-orders-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractor84CD7B81",
"Type": "AWS::Lambda::Function",
"PhysicalId": "po-ingest-site-extractor",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorDurationAlarm3D503607",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-ingest-site-extractor-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorDynamoDBEventSourcepoingestPurchaseOrdersTable8DBC21215DEFB913",
"Type": "AWS::Lambda::EventSourceMapping",
"PhysicalId": "64fbee1f-d9c3-4cfd-aced-19b9d29940f8",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorErrorsAlarm5DCF80DA",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-ingest-site-extractor-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorLogGroupD475D9DD",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/po-ingest-site-extractor",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorServiceRole11491F4D",
"Type": "AWS::IAM::Role",
"PhysicalId": "po-ingest-SiteExtractorServiceRole11491F4D-X3aKoS1Rwq1W",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorServiceRoleDefaultPolicy81255274",
"Type": "AWS::IAM::Policy",
"PhysicalId": "po-in-SiteE-JJJqSNBTUCT4",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "SiteExtractorThrottlesAlarm63246CEB",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-ingest-site-extractor-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "VerifiedSitesTable23AD7F59",
"Type": "AWS::DynamoDB::Table",
"PhysicalId": "verified-sites",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "VerifiedSitesTableSystemErrorsAlarm0408CCA4",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "verified-sites-system-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "VerifiedSitesTableThrottlesAlarmEC141C84",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "verified-sites-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUI258D87A5",
"Type": "AWS::Lambda::Function",
"PhysicalId": "po-web-ui",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "WebUILogGroup8F43ABDC",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/po-web-ui",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUIServiceRoleB1B395BE",
"Type": "AWS::IAM::Role",
"PhysicalId": "po-ingest-WebUIServiceRoleB1B395BE-UVNhr9c8b4YR",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUIServiceRoleDefaultPolicy8B510141",
"Type": "AWS::IAM::Policy",
"PhysicalId": "po-in-WebUI-jVyc9nvGbIx6",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUiDurationAlarm1A1A227D",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-web-ui-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "WebUiThrottlesAlarmA33C4B72",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "po-web-ui-throttles",
"Status": "CREATE_COMPLETE"
}
]

View file

@ -0,0 +1,338 @@
[
{
"LogicalId": "CDKMetadata",
"Type": "AWS::CDK::Metadata",
"PhysicalId": "472324e0-86ee-11f1-9b27-0afff03a5535",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "ProcurementApi187041AB",
"Type": "AWS::Lambda::Function",
"PhysicalId": "procurement-api",
"Status": "UPDATE_COMPLETE"
},
{
"LogicalId": "ProcurementApi5xxAlarmE69DCA02",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "procurement-api-5xx",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiDomain857EE934",
"Type": "AWS::ApiGateway::DomainName",
"PhysicalId": "procurement-api.seahaven.com",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiDomainMapprocurementapiProcurementRestApi7C05134920F11EB5",
"Type": "AWS::ApiGateway::BasePathMapping",
"PhysicalId": "procurement-api.seahaven.com|(none)",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiDurationAlarmBEC61762",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "procurement-api-duration",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiErrorsAlarmFA6549F2",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "procurement-api-errors",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiLogGroupD8265E36",
"Type": "AWS::Logs::LogGroup",
"PhysicalId": "/aws/lambda/procurement-api",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiServiceRoleD696D662",
"Type": "AWS::IAM::Role",
"PhysicalId": "procurement-api-ProcurementApiServiceRoleD696D662-W2RyELHYFwBL",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiServiceRoleDefaultPolicy1953BD71",
"Type": "AWS::IAM::Policy",
"PhysicalId": "procu-Procu-3dauci6ha7Eg",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementApiThrottlesAlarm7F98A1AD",
"Type": "AWS::CloudWatch::Alarm",
"PhysicalId": "procurement-api-throttles",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiDD8C37D6",
"Type": "AWS::ApiGateway::RestApi",
"PhysicalId": "mvul1efda2",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiDeployment526FECC37a13ca46329dbf433cb1289dad9c6539",
"Type": "AWS::ApiGateway::Deployment",
"PhysicalId": "uax9sq",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiDeploymentStageprodD4833AB9",
"Type": "AWS::ApiGateway::Stage",
"PhysicalId": "prod",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApidocsBC7A2DEC",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "k4vl85",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApidocsGET1935E06E",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|k4vl85|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApidocsGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETdocsFCC4F675",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApidocsGETApiPermissionTestprocurementapiProcurementRest-JkMEaKq9d2lY",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApidocsGETApiPermissionprocurementapiProcurementRestApi7C051349GETdocs00B2D21A",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApidocsGETApiPermissionprocurementapiProcurementRestApi7-fVn42qrClCjF",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiopenapijson945C2370",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "xos715",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiopenapijsonGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETopenapijson078A24BA",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiopenapijsonGETApiPermissionTestprocurementapiProcurem-VPxSN2DPhc4W",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiopenapijsonGETApiPermissionprocurementapiProcurementRestApi7C051349GETopenapijson86A01B87",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiopenapijsonGETApiPermissionprocurementapiProcurementR-I2fDmtO8vw2w",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiopenapijsonGETD1B98CF4",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|xos715|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseordersE380B6C5",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "pfn6qm",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseordersGET1A2C4DD8",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|pfn6qm|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseordersGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETpurchaseordersFF5C7CF2",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApipurchaseordersGETApiPermissionTestprocurementapiProcu-FPgglqrEyKO4",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseordersGETApiPermissionprocurementapiProcurementRestApi7C051349GETpurchaseordersC47C57EA",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApipurchaseordersGETApiPermissionprocurementapiProcureme-Db7wxlMFIJ1T",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseorderspoNumber11A73C81",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "nclnn3",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseorderspoNumberGET7331738A",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|nclnn3|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseorderspoNumberGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETpurchaseorderspoNumberC6737C34",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApipurchaseorderspoNumberGETApiPermissionTestprocurement-SxEdwssencAN",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApipurchaseorderspoNumberGETApiPermissionprocurementapiProcurementRestApi7C051349GETpurchaseorderspoNumber8DEED34E",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApipurchaseorderspoNumberGETApiPermissionprocurementapiP-qRCfwLHa2pa6",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsites4516F10B",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "vyst7e",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitesGET81D69959",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|vyst7e|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitesGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETverifiedsitesA7C29C4E",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiverifiedsitesGETApiPermissionTestprocurementapiProcur-1QHpXCoVpyKN",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitesGETApiPermissionprocurementapiProcurementRestApi7C051349GETverifiedsites16531E92",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiverifiedsitesGETApiPermissionprocurementapiProcuremen-b5nhaktRwejs",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitessiteCode48F86D1E",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "rmaawy",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitessiteCodeGET2EE78735",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|rmaawy|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitessiteCodeGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETverifiedsitessiteCode63EDC375",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiverifiedsitessiteCodeGETApiPermissionTestprocurementa-2PI3jCYWgXPU",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiverifiedsitessiteCodeGETApiPermissionprocurementapiProcurementRestApi7C051349GETverifiedsitessiteCode6457DF41",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiverifiedsitessiteCodeGETApiPermissionprocurementapiPr-QQ5mW4HnxKAr",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersD3A24F7A",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "h5iu4f",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersGET62D5D82A",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|h5iu4f|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkorders23C66297",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersGETApiPermissionTestprocurementapiProcureme-Ed6sN9siVtQv",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkorders067BAE83",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersGETApiPermissionprocurementapiProcurementRe-CdkJhuPaVDJv",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdE28A1181",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "4uk6uh",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkordersworkOrderId0451CF71",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdGETApiPermissionTestprocurementa-iZ1Jmq8djJRs",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkordersworkOrderId6619DA0E",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdGETApiPermissionprocurementapiPr-nbjmJdg8o0nc",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdGETFEEB7CE4",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|4uk6uh|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdPATCH7361320B",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|4uk6uh|PATCH",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionTestprocurementapiProcurementRestApi7C051349PATCHworkordersworkOrderIdC15330EB",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionTestprocuremen-BJYqqytadslV",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionprocurementapiProcurementRestApi7C051349PATCHworkordersworkOrderId4DF98886",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionprocurementapi-O70SvTPEoe0h",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcomments5C278482",
"Type": "AWS::ApiGateway::Resource",
"PhysicalId": "gfx0te",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcommentsGET032FCBD8",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|gfx0te|GET",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkordersworkOrderIdcomments9BD68D42",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionTestproc-AbYwSYiRKjxQ",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkordersworkOrderIdcommentsACDBEBB8",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionprocurem-uxRIv0NP6hKD",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcommentsPOST98588435",
"Type": "AWS::ApiGateway::Method",
"PhysicalId": "mvul1efda2|gfx0te|POST",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionTestprocurementapiProcurementRestApi7C051349POSTworkordersworkOrderIdcomments2194642A",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionTestpro-Ci7eIdxeqWFP",
"Status": "CREATE_COMPLETE"
},
{
"LogicalId": "ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionprocurementapiProcurementRestApi7C051349POSTworkordersworkOrderIdcommentsFBA9AF05",
"Type": "AWS::Lambda::Permission",
"PhysicalId": "procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionprocure-VQGemxZkkIu1",
"Status": "CREATE_COMPLETE"
}
]

View file

@ -0,0 +1,164 @@
po-ingest AWS::Lambda::Function BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691 po-ingest-BucketNotificationsHandler050a0587b75445-eYKw7CRdcWxS
po-ingest AWS::IAM::Role BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC po-ingest-BucketNotificationsHandler050a0587b754454-kAyd3v80XLIz
po-ingest AWS::CDK::Metadata CDKMetadata e287be30-86dc-11f1-a551-0affcf7dfc53
po-ingest AWS::S3::Bucket EmailBucket843A740F po-ingest-emails-011934824531
po-ingest AWS::Lambda::Permission EmailBucketAllowBucketNotificationsTopoingestEmailProcessor21BB6E7689B27E99 po-ingest-EmailBucketAllowBucketNotificationsTopoingestEmailProcessor21BB6E7689B27E99-2uvEuiUCEDgr
po-ingest Custom::S3BucketNotifications EmailBucketNotificationsDDD5BEEA EmailBucketNotificationsDDD5BEEA
po-ingest AWS::IAM::Policy EmailBucketNotificationsHandlerPolicy7D16942E po-in-Email-eep3xPAZrcL7
po-ingest AWS::S3::BucketPolicy EmailBucketPolicyBBC68546 po-ingest-emails-011934824531
po-ingest AWS::Lambda::Function EmailProcessor218EC076 po-email-processor
po-ingest AWS::CloudWatch::Alarm EmailProcessorAiFallbackRejectedAlarm71D1F2CE po-email-processor-ai-fallback-rejected
po-ingest AWS::SQS::Queue EmailProcessorDlqA753DED5 https://sqs.us-east-1.amazonaws.com/011934824531/po-ingest-EmailProcessorDlqA753DED5-Mn33HvhsDPEu
po-ingest AWS::CloudWatch::Alarm EmailProcessorDlqMessagesAlarmED574757 po-email-processor-dlq-messages
po-ingest AWS::SQS::QueuePolicy EmailProcessorDlqPolicyB24007F0 po-ingest-EmailProcessorDlqPolicyB24007F0-ZBgpMipDtvH3
po-ingest AWS::CloudWatch::Alarm EmailProcessorDurationAlarmF7530BED po-email-processor-duration
po-ingest AWS::CloudWatch::Alarm EmailProcessorErrorsAlarmC1BF0D18 po-email-processor-errors
po-ingest AWS::Logs::LogGroup EmailProcessorLogGroup8A79A2C8 /aws/lambda/po-email-processor
po-ingest AWS::CloudWatch::Alarm EmailProcessorSenderAuthRejectedAlarm3D9534B7 po-email-processor-sender-auth-rejected
po-ingest AWS::Logs::MetricFilter EmailProcessorSenderAuthRejectedFilterC0DAEDFE EmailProcessorSenderAuthRejectedFilterC0DAEDFE-3K6wBb003iDv
po-ingest AWS::IAM::Role EmailProcessorServiceRole637381CE po-ingest-EmailProcessorServiceRole637381CE-hG1KGKCIivCd
po-ingest AWS::IAM::Policy EmailProcessorServiceRoleDefaultPolicy0B15DAB8 po-in-Email-BbPUnOcJ71or
po-ingest AWS::CloudWatch::Alarm EmailProcessorTemplateFallbackRateAlarmF9B0866A po-email-processor-template-fallback-rate
po-ingest AWS::CloudWatch::Alarm EmailProcessorThrottlesAlarmE102F9E9 po-email-processor-throttles
po-ingest AWS::SES::ReceiptRule ExistingRuleSetPoEmailRuleAC8E9C87 ExistingRuleSetPoEmailRuleAC8E9C87-qwGDj9lBoL1G
po-ingest AWS::DynamoDB::Table PendingSiteReviewTableBF01B687 pending-site-review
po-ingest AWS::CloudWatch::Alarm PendingSiteReviewTableSystemErrorsAlarm36BA58D4 pending-site-review-system-errors
po-ingest AWS::CloudWatch::Alarm PendingSiteReviewTableThrottlesAlarm15FB6DD5 pending-site-review-throttles
po-ingest AWS::DynamoDB::Table PurchaseOrdersTable491A23F2 purchase-orders
po-ingest AWS::CloudWatch::Alarm PurchaseOrdersTableSystemErrorsAlarmBC7F000F purchase-orders-system-errors
po-ingest AWS::CloudWatch::Alarm PurchaseOrdersTableThrottlesAlarmE5ACE81B purchase-orders-throttles
po-ingest AWS::Lambda::Function SiteExtractor84CD7B81 po-ingest-site-extractor
po-ingest AWS::CloudWatch::Alarm SiteExtractorDurationAlarm3D503607 po-ingest-site-extractor-duration
po-ingest AWS::Lambda::EventSourceMapping SiteExtractorDynamoDBEventSourcepoingestPurchaseOrdersTable8DBC21215DEFB913 64fbee1f-d9c3-4cfd-aced-19b9d29940f8
po-ingest AWS::CloudWatch::Alarm SiteExtractorErrorsAlarm5DCF80DA po-ingest-site-extractor-errors
po-ingest AWS::Logs::LogGroup SiteExtractorLogGroupD475D9DD /aws/lambda/po-ingest-site-extractor
po-ingest AWS::IAM::Role SiteExtractorServiceRole11491F4D po-ingest-SiteExtractorServiceRole11491F4D-X3aKoS1Rwq1W
po-ingest AWS::IAM::Policy SiteExtractorServiceRoleDefaultPolicy81255274 po-in-SiteE-JJJqSNBTUCT4
po-ingest AWS::CloudWatch::Alarm SiteExtractorThrottlesAlarm63246CEB po-ingest-site-extractor-throttles
po-ingest AWS::DynamoDB::Table VerifiedSitesTable23AD7F59 verified-sites
po-ingest AWS::CloudWatch::Alarm VerifiedSitesTableSystemErrorsAlarm0408CCA4 verified-sites-system-errors
po-ingest AWS::CloudWatch::Alarm VerifiedSitesTableThrottlesAlarmEC141C84 verified-sites-throttles
po-ingest AWS::Lambda::Function WebUI258D87A5 po-web-ui
po-ingest AWS::Logs::LogGroup WebUILogGroup8F43ABDC /aws/lambda/po-web-ui
po-ingest AWS::IAM::Role WebUIServiceRoleB1B395BE po-ingest-WebUIServiceRoleB1B395BE-UVNhr9c8b4YR
po-ingest AWS::IAM::Policy WebUIServiceRoleDefaultPolicy8B510141 po-in-WebUI-jVyc9nvGbIx6
po-ingest AWS::CloudWatch::Alarm WebUiDurationAlarm1A1A227D po-web-ui-duration
po-ingest AWS::CloudWatch::Alarm WebUiThrottlesAlarmA33C4B72 po-web-ui-throttles
WorkorderIngestStack AWS::Lambda::Function BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691 WorkorderIngestStack-BucketNotificationsHandler050-fBtoo7x6Azy3
WorkorderIngestStack AWS::IAM::Role BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC WorkorderIngestStack-BucketNotificationsHandler050a-7KxpBKmlpFSf
WorkorderIngestStack AWS::CDK::Metadata CDKMetadata a85b0320-86dc-11f1-b0cf-0eccc7521e19
WorkorderIngestStack AWS::DynamoDB::Table CommentsTableBBDBF0A8 WorkOrderComments
WorkorderIngestStack AWS::S3::Bucket EmailBucket843A740F workorder-ingest-emails-011934824531
WorkorderIngestStack AWS::Lambda::Permission EmailBucketAllowBucketNotificationsToworkorderingestEmailProcessor5B0B85DABA138779 WorkorderIngestStack-EmailBucketAllowBucketNotificationsToworkorderingestEmailProcessor-VuairMrX6Qir
WorkorderIngestStack Custom::S3BucketNotifications EmailBucketNotificationsDDD5BEEA EmailBucketNotificationsDDD5BEEA
WorkorderIngestStack AWS::IAM::Policy EmailBucketNotificationsHandlerPolicy7D16942E Worko-Email-YLl5HvyOvIg2
WorkorderIngestStack AWS::S3::BucketPolicy EmailBucketPolicyBBC68546 workorder-ingest-emails-011934824531
WorkorderIngestStack AWS::Lambda::Function EmailProcessor218EC076 workorder-email-processor
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorAiFallbackRejectedAlarm71D1F2CE workorder-email-processor-ai-fallback-rejected
WorkorderIngestStack AWS::SQS::Queue EmailProcessorDlqA753DED5 https://sqs.us-east-1.amazonaws.com/011934824531/WorkorderIngestStack-EmailProcessorDlqA753DED5-qCTHrsoEucas
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorDlqMessagesAlarmED574757 workorder-email-processor-dlq-messages
WorkorderIngestStack AWS::SQS::QueuePolicy EmailProcessorDlqPolicyB24007F0 WorkorderIngestStack-EmailProcessorDlqPolicyB24007F0-15VK27UU5Q19
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorDurationAlarmF7530BED workorder-email-processor-duration
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorErrorsAlarmC1BF0D18 workorder-email-processor-errors
WorkorderIngestStack AWS::Logs::LogGroup EmailProcessorLogGroup8A79A2C8 /aws/lambda/workorder-email-processor
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorSenderAuthRejectedAlarm3D9534B7 workorder-email-processor-sender-auth-rejected
WorkorderIngestStack AWS::Logs::MetricFilter EmailProcessorSenderAuthRejectedFilterC0DAEDFE EmailProcessorSenderAuthRejectedFilterC0DAEDFE-Wkbow1VNDeDM
WorkorderIngestStack AWS::IAM::Role EmailProcessorServiceRole637381CE WorkorderIngestStack-EmailProcessorServiceRole63738-5l9OZXQLVvJn
WorkorderIngestStack AWS::IAM::Policy EmailProcessorServiceRoleDefaultPolicy0B15DAB8 Worko-Email-8PjqeVLLDLRv
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorTemplateFallbackRateAlarmF9B0866A workorder-email-processor-template-fallback-rate
WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorThrottlesAlarmE102F9E9 workorder-email-processor-throttles
WorkorderIngestStack AWS::SES::ReceiptRule ExistingRuleSetWorkorderEmailRuleEA29F845 ExistingRuleSetWorkorderEmailRuleEA29F845-PKtaDBvIg61a
WorkorderIngestStack AWS::Lambda::Function ShocEmitterCD1F4C3E workorder-shoc-emitter
WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterDurationAlarm3F755835 workorder-shoc-emitter-duration
WorkorderIngestStack AWS::Lambda::EventSourceMapping ShocEmitterDynamoDBEventSourceworkorderingestCommentsTableFD97F35C58A19C1A da37d4a0-3086-4929-9ace-6eca5c20cd07
WorkorderIngestStack AWS::Lambda::EventSourceMapping ShocEmitterDynamoDBEventSourceworkorderingestWorkOrdersTable004CD4AC38D8BED2 cc149f4d-5375-4820-8fb8-c514accd3a85
WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterErrorsAlarmCE2B702A workorder-shoc-emitter-errors
WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterFailuresMessagesAlarmEC9798F9 workorder-shoc-emitter-failures-messages
WorkorderIngestStack AWS::SQS::Queue ShocEmitterFailuresQueue11D7C8E0 https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-failures
WorkorderIngestStack AWS::SQS::QueuePolicy ShocEmitterFailuresQueuePolicy0D60571C WorkorderIngestStack-ShocEmitterFailuresQueuePolicy0D60571C-2AMBNJh0la3L
WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterIteratorAgeAlarm90B55822 workorder-shoc-emitter-iterator-age
WorkorderIngestStack AWS::Logs::LogGroup ShocEmitterLogGroup80BDE724 /aws/lambda/workorder-shoc-emitter
WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterRejectedMessagesAlarmAE9AFCDD workorder-shoc-emitter-rejected-messages
WorkorderIngestStack AWS::SQS::Queue ShocEmitterRejectedQueueD754F41E https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-rejected
WorkorderIngestStack AWS::SQS::QueuePolicy ShocEmitterRejectedQueuePolicy4B088C0D WorkorderIngestStack-ShocEmitterRejectedQueuePolicy4B088C0D-vzXOuhHaGfNy
WorkorderIngestStack AWS::IAM::Role ShocEmitterServiceRoleA0CA0C74 WorkorderIngestStack-ShocEmitterServiceRoleA0CA0C74-LlFfZ6F9f6sr
WorkorderIngestStack AWS::IAM::Policy ShocEmitterServiceRoleDefaultPolicy5DE45717 Worko-ShocE-0txSITC4MbcW
WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterThrottlesAlarm5503E07A workorder-shoc-emitter-throttles
WorkorderIngestStack AWS::Lambda::Function ShocHmacRotatorD4073E6B workorder-shoc-hmac-rotator
WorkorderIngestStack AWS::CloudWatch::Alarm ShocHmacRotatorDurationAlarm0725D01A workorder-shoc-hmac-rotator-duration
WorkorderIngestStack AWS::CloudWatch::Alarm ShocHmacRotatorErrorsAlarm55E7BCC3 workorder-shoc-hmac-rotator-errors
WorkorderIngestStack AWS::Lambda::Permission ShocHmacRotatorInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc3085C896 WorkorderIngestStack-ShocHmacRotatorInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc3085C8-kJaePEGWmM3r
WorkorderIngestStack AWS::Logs::LogGroup ShocHmacRotatorLogGroup1D109CC5 /aws/lambda/workorder-shoc-hmac-rotator
WorkorderIngestStack AWS::IAM::Role ShocHmacRotatorServiceRole7F2B6917 WorkorderIngestStack-ShocHmacRotatorServiceRole7F2B-uKqx4ccGQTk5
WorkorderIngestStack AWS::IAM::Policy ShocHmacRotatorServiceRoleDefaultPolicy492EC50B Worko-ShocH-B5ATRJ8co1Hc
WorkorderIngestStack AWS::CloudWatch::Alarm ShocHmacRotatorThrottlesAlarm9963F2A3 workorder-shoc-hmac-rotator-throttles
WorkorderIngestStack AWS::KMS::Key ShocWebhookHmacKey87DD3EC7 d10fd1f0-a61a-4405-8568-85e9fd11ba18
WorkorderIngestStack AWS::KMS::Alias ShocWebhookHmacKeyAliasC83A07B2 alias/workorder-ingest-shoc-webhook-kms
WorkorderIngestStack AWS::SecretsManager::Secret ShocWebhookHmacSecretA6CF6356 arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB
WorkorderIngestStack AWS::SecretsManager::ResourcePolicy ShocWebhookHmacSecretPolicy2B0A7DAC arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB
WorkorderIngestStack AWS::SecretsManager::RotationSchedule ShocWebhookHmacSecretRotationA6130D74 arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB
WorkorderIngestStack AWS::Lambda::Function WebUI258D87A5 workorder-web-ui
WorkorderIngestStack AWS::Logs::LogGroup WebUILogGroup8F43ABDC /aws/lambda/workorder-web-ui
WorkorderIngestStack AWS::IAM::Role WebUIServiceRoleB1B395BE WorkorderIngestStack-WebUIServiceRoleB1B395BE-0kRzDlAiLZVn
WorkorderIngestStack AWS::IAM::Policy WebUIServiceRoleDefaultPolicy8B510141 Worko-WebUI-TVYFGuD5kBVZ
WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrderCommentsSystemErrorsAlarm4CD72F33 WorkOrderComments-system-errors
WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrderCommentsThrottlesAlarmA10AEFD9 WorkOrderComments-throttles
WorkorderIngestStack AWS::DynamoDB::Table WorkOrdersTable515B4C61 WorkOrders
WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrdersTableSystemErrorsAlarm117925DB WorkOrders-system-errors
WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrdersTableThrottlesAlarmDACF2500 WorkOrders-throttles
procurement-api AWS::CDK::Metadata CDKMetadata 472324e0-86ee-11f1-9b27-0afff03a5535
procurement-api AWS::Lambda::Function ProcurementApi187041AB procurement-api
procurement-api AWS::CloudWatch::Alarm ProcurementApi5xxAlarmE69DCA02 procurement-api-5xx
procurement-api AWS::ApiGateway::DomainName ProcurementApiDomain857EE934 procurement-api.seahaven.com
procurement-api AWS::ApiGateway::BasePathMapping ProcurementApiDomainMapprocurementapiProcurementRestApi7C05134920F11EB5 procurement-api.seahaven.com|(none)
procurement-api AWS::CloudWatch::Alarm ProcurementApiDurationAlarmBEC61762 procurement-api-duration
procurement-api AWS::CloudWatch::Alarm ProcurementApiErrorsAlarmFA6549F2 procurement-api-errors
procurement-api AWS::Logs::LogGroup ProcurementApiLogGroupD8265E36 /aws/lambda/procurement-api
procurement-api AWS::IAM::Role ProcurementApiServiceRoleD696D662 procurement-api-ProcurementApiServiceRoleD696D662-W2RyELHYFwBL
procurement-api AWS::IAM::Policy ProcurementApiServiceRoleDefaultPolicy1953BD71 procu-Procu-3dauci6ha7Eg
procurement-api AWS::CloudWatch::Alarm ProcurementApiThrottlesAlarm7F98A1AD procurement-api-throttles
procurement-api AWS::ApiGateway::RestApi ProcurementRestApiDD8C37D6 mvul1efda2
procurement-api AWS::ApiGateway::Deployment ProcurementRestApiDeployment526FECC37a13ca46329dbf433cb1289dad9c6539 uax9sq
procurement-api AWS::ApiGateway::Stage ProcurementRestApiDeploymentStageprodD4833AB9 prod
procurement-api AWS::ApiGateway::Resource ProcurementRestApidocsBC7A2DEC k4vl85
procurement-api AWS::ApiGateway::Method ProcurementRestApidocsGET1935E06E mvul1efda2|k4vl85|GET
procurement-api AWS::Lambda::Permission ProcurementRestApidocsGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETdocsFCC4F675 procurement-api-ProcurementRestApidocsGETApiPermissionTestprocurementapiProcurementRest-JkMEaKq9d2lY
procurement-api AWS::Lambda::Permission ProcurementRestApidocsGETApiPermissionprocurementapiProcurementRestApi7C051349GETdocs00B2D21A procurement-api-ProcurementRestApidocsGETApiPermissionprocurementapiProcurementRestApi7-fVn42qrClCjF
procurement-api AWS::ApiGateway::Resource ProcurementRestApiopenapijson945C2370 xos715
procurement-api AWS::Lambda::Permission ProcurementRestApiopenapijsonGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETopenapijson078A24BA procurement-api-ProcurementRestApiopenapijsonGETApiPermissionTestprocurementapiProcurem-VPxSN2DPhc4W
procurement-api AWS::Lambda::Permission ProcurementRestApiopenapijsonGETApiPermissionprocurementapiProcurementRestApi7C051349GETopenapijson86A01B87 procurement-api-ProcurementRestApiopenapijsonGETApiPermissionprocurementapiProcurementR-I2fDmtO8vw2w
procurement-api AWS::ApiGateway::Method ProcurementRestApiopenapijsonGETD1B98CF4 mvul1efda2|xos715|GET
procurement-api AWS::ApiGateway::Resource ProcurementRestApipurchaseordersE380B6C5 pfn6qm
procurement-api AWS::ApiGateway::Method ProcurementRestApipurchaseordersGET1A2C4DD8 mvul1efda2|pfn6qm|GET
procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseordersGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETpurchaseordersFF5C7CF2 procurement-api-ProcurementRestApipurchaseordersGETApiPermissionTestprocurementapiProcu-FPgglqrEyKO4
procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseordersGETApiPermissionprocurementapiProcurementRestApi7C051349GETpurchaseordersC47C57EA procurement-api-ProcurementRestApipurchaseordersGETApiPermissionprocurementapiProcureme-Db7wxlMFIJ1T
procurement-api AWS::ApiGateway::Resource ProcurementRestApipurchaseorderspoNumber11A73C81 nclnn3
procurement-api AWS::ApiGateway::Method ProcurementRestApipurchaseorderspoNumberGET7331738A mvul1efda2|nclnn3|GET
procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseorderspoNumberGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETpurchaseorderspoNumberC6737C34 procurement-api-ProcurementRestApipurchaseorderspoNumberGETApiPermissionTestprocurement-SxEdwssencAN
procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseorderspoNumberGETApiPermissionprocurementapiProcurementRestApi7C051349GETpurchaseorderspoNumber8DEED34E procurement-api-ProcurementRestApipurchaseorderspoNumberGETApiPermissionprocurementapiP-qRCfwLHa2pa6
procurement-api AWS::ApiGateway::Resource ProcurementRestApiverifiedsites4516F10B vyst7e
procurement-api AWS::ApiGateway::Method ProcurementRestApiverifiedsitesGET81D69959 mvul1efda2|vyst7e|GET
procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitesGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETverifiedsitesA7C29C4E procurement-api-ProcurementRestApiverifiedsitesGETApiPermissionTestprocurementapiProcur-1QHpXCoVpyKN
procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitesGETApiPermissionprocurementapiProcurementRestApi7C051349GETverifiedsites16531E92 procurement-api-ProcurementRestApiverifiedsitesGETApiPermissionprocurementapiProcuremen-b5nhaktRwejs
procurement-api AWS::ApiGateway::Resource ProcurementRestApiverifiedsitessiteCode48F86D1E rmaawy
procurement-api AWS::ApiGateway::Method ProcurementRestApiverifiedsitessiteCodeGET2EE78735 mvul1efda2|rmaawy|GET
procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitessiteCodeGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETverifiedsitessiteCode63EDC375 procurement-api-ProcurementRestApiverifiedsitessiteCodeGETApiPermissionTestprocurementa-2PI3jCYWgXPU
procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitessiteCodeGETApiPermissionprocurementapiProcurementRestApi7C051349GETverifiedsitessiteCode6457DF41 procurement-api-ProcurementRestApiverifiedsitessiteCodeGETApiPermissionprocurementapiPr-QQ5mW4HnxKAr
procurement-api AWS::ApiGateway::Resource ProcurementRestApiworkordersD3A24F7A h5iu4f
procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersGET62D5D82A mvul1efda2|h5iu4f|GET
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkorders23C66297 procurement-api-ProcurementRestApiworkordersGETApiPermissionTestprocurementapiProcureme-Ed6sN9siVtQv
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkorders067BAE83 procurement-api-ProcurementRestApiworkordersGETApiPermissionprocurementapiProcurementRe-CdkJhuPaVDJv
procurement-api AWS::ApiGateway::Resource ProcurementRestApiworkordersworkOrderIdE28A1181 4uk6uh
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkordersworkOrderId0451CF71 procurement-api-ProcurementRestApiworkordersworkOrderIdGETApiPermissionTestprocurementa-iZ1Jmq8djJRs
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkordersworkOrderId6619DA0E procurement-api-ProcurementRestApiworkordersworkOrderIdGETApiPermissionprocurementapiPr-nbjmJdg8o0nc
procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdGETFEEB7CE4 mvul1efda2|4uk6uh|GET
procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdPATCH7361320B mvul1efda2|4uk6uh|PATCH
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionTestprocurementapiProcurementRestApi7C051349PATCHworkordersworkOrderIdC15330EB procurement-api-ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionTestprocuremen-BJYqqytadslV
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionprocurementapiProcurementRestApi7C051349PATCHworkordersworkOrderId4DF98886 procurement-api-ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionprocurementapi-O70SvTPEoe0h
procurement-api AWS::ApiGateway::Resource ProcurementRestApiworkordersworkOrderIdcomments5C278482 gfx0te
procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdcommentsGET032FCBD8 mvul1efda2|gfx0te|GET
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkordersworkOrderIdcomments9BD68D42 procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionTestproc-AbYwSYiRKjxQ
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkordersworkOrderIdcommentsACDBEBB8 procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionprocurem-uxRIv0NP6hKD
procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdcommentsPOST98588435 mvul1efda2|gfx0te|POST
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionTestprocurementapiProcurementRestApi7C051349POSTworkordersworkOrderIdcomments2194642A procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionTestpro-Ci7eIdxeqWFP
procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionprocurementapiProcurementRestApi7C051349POSTworkordersworkOrderIdcommentsFBA9AF05 procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionprocure-VQGemxZkkIu1
1 po-ingest AWS::Lambda::Function BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691 po-ingest-BucketNotificationsHandler050a0587b75445-eYKw7CRdcWxS
2 po-ingest AWS::IAM::Role BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC po-ingest-BucketNotificationsHandler050a0587b754454-kAyd3v80XLIz
3 po-ingest AWS::CDK::Metadata CDKMetadata e287be30-86dc-11f1-a551-0affcf7dfc53
4 po-ingest AWS::S3::Bucket EmailBucket843A740F po-ingest-emails-011934824531
5 po-ingest AWS::Lambda::Permission EmailBucketAllowBucketNotificationsTopoingestEmailProcessor21BB6E7689B27E99 po-ingest-EmailBucketAllowBucketNotificationsTopoingestEmailProcessor21BB6E7689B27E99-2uvEuiUCEDgr
6 po-ingest Custom::S3BucketNotifications EmailBucketNotificationsDDD5BEEA EmailBucketNotificationsDDD5BEEA
7 po-ingest AWS::IAM::Policy EmailBucketNotificationsHandlerPolicy7D16942E po-in-Email-eep3xPAZrcL7
8 po-ingest AWS::S3::BucketPolicy EmailBucketPolicyBBC68546 po-ingest-emails-011934824531
9 po-ingest AWS::Lambda::Function EmailProcessor218EC076 po-email-processor
10 po-ingest AWS::CloudWatch::Alarm EmailProcessorAiFallbackRejectedAlarm71D1F2CE po-email-processor-ai-fallback-rejected
11 po-ingest AWS::SQS::Queue EmailProcessorDlqA753DED5 https://sqs.us-east-1.amazonaws.com/011934824531/po-ingest-EmailProcessorDlqA753DED5-Mn33HvhsDPEu
12 po-ingest AWS::CloudWatch::Alarm EmailProcessorDlqMessagesAlarmED574757 po-email-processor-dlq-messages
13 po-ingest AWS::SQS::QueuePolicy EmailProcessorDlqPolicyB24007F0 po-ingest-EmailProcessorDlqPolicyB24007F0-ZBgpMipDtvH3
14 po-ingest AWS::CloudWatch::Alarm EmailProcessorDurationAlarmF7530BED po-email-processor-duration
15 po-ingest AWS::CloudWatch::Alarm EmailProcessorErrorsAlarmC1BF0D18 po-email-processor-errors
16 po-ingest AWS::Logs::LogGroup EmailProcessorLogGroup8A79A2C8 /aws/lambda/po-email-processor
17 po-ingest AWS::CloudWatch::Alarm EmailProcessorSenderAuthRejectedAlarm3D9534B7 po-email-processor-sender-auth-rejected
18 po-ingest AWS::Logs::MetricFilter EmailProcessorSenderAuthRejectedFilterC0DAEDFE EmailProcessorSenderAuthRejectedFilterC0DAEDFE-3K6wBb003iDv
19 po-ingest AWS::IAM::Role EmailProcessorServiceRole637381CE po-ingest-EmailProcessorServiceRole637381CE-hG1KGKCIivCd
20 po-ingest AWS::IAM::Policy EmailProcessorServiceRoleDefaultPolicy0B15DAB8 po-in-Email-BbPUnOcJ71or
21 po-ingest AWS::CloudWatch::Alarm EmailProcessorTemplateFallbackRateAlarmF9B0866A po-email-processor-template-fallback-rate
22 po-ingest AWS::CloudWatch::Alarm EmailProcessorThrottlesAlarmE102F9E9 po-email-processor-throttles
23 po-ingest AWS::SES::ReceiptRule ExistingRuleSetPoEmailRuleAC8E9C87 ExistingRuleSetPoEmailRuleAC8E9C87-qwGDj9lBoL1G
24 po-ingest AWS::DynamoDB::Table PendingSiteReviewTableBF01B687 pending-site-review
25 po-ingest AWS::CloudWatch::Alarm PendingSiteReviewTableSystemErrorsAlarm36BA58D4 pending-site-review-system-errors
26 po-ingest AWS::CloudWatch::Alarm PendingSiteReviewTableThrottlesAlarm15FB6DD5 pending-site-review-throttles
27 po-ingest AWS::DynamoDB::Table PurchaseOrdersTable491A23F2 purchase-orders
28 po-ingest AWS::CloudWatch::Alarm PurchaseOrdersTableSystemErrorsAlarmBC7F000F purchase-orders-system-errors
29 po-ingest AWS::CloudWatch::Alarm PurchaseOrdersTableThrottlesAlarmE5ACE81B purchase-orders-throttles
30 po-ingest AWS::Lambda::Function SiteExtractor84CD7B81 po-ingest-site-extractor
31 po-ingest AWS::CloudWatch::Alarm SiteExtractorDurationAlarm3D503607 po-ingest-site-extractor-duration
32 po-ingest AWS::Lambda::EventSourceMapping SiteExtractorDynamoDBEventSourcepoingestPurchaseOrdersTable8DBC21215DEFB913 64fbee1f-d9c3-4cfd-aced-19b9d29940f8
33 po-ingest AWS::CloudWatch::Alarm SiteExtractorErrorsAlarm5DCF80DA po-ingest-site-extractor-errors
34 po-ingest AWS::Logs::LogGroup SiteExtractorLogGroupD475D9DD /aws/lambda/po-ingest-site-extractor
35 po-ingest AWS::IAM::Role SiteExtractorServiceRole11491F4D po-ingest-SiteExtractorServiceRole11491F4D-X3aKoS1Rwq1W
36 po-ingest AWS::IAM::Policy SiteExtractorServiceRoleDefaultPolicy81255274 po-in-SiteE-JJJqSNBTUCT4
37 po-ingest AWS::CloudWatch::Alarm SiteExtractorThrottlesAlarm63246CEB po-ingest-site-extractor-throttles
38 po-ingest AWS::DynamoDB::Table VerifiedSitesTable23AD7F59 verified-sites
39 po-ingest AWS::CloudWatch::Alarm VerifiedSitesTableSystemErrorsAlarm0408CCA4 verified-sites-system-errors
40 po-ingest AWS::CloudWatch::Alarm VerifiedSitesTableThrottlesAlarmEC141C84 verified-sites-throttles
41 po-ingest AWS::Lambda::Function WebUI258D87A5 po-web-ui
42 po-ingest AWS::Logs::LogGroup WebUILogGroup8F43ABDC /aws/lambda/po-web-ui
43 po-ingest AWS::IAM::Role WebUIServiceRoleB1B395BE po-ingest-WebUIServiceRoleB1B395BE-UVNhr9c8b4YR
44 po-ingest AWS::IAM::Policy WebUIServiceRoleDefaultPolicy8B510141 po-in-WebUI-jVyc9nvGbIx6
45 po-ingest AWS::CloudWatch::Alarm WebUiDurationAlarm1A1A227D po-web-ui-duration
46 po-ingest AWS::CloudWatch::Alarm WebUiThrottlesAlarmA33C4B72 po-web-ui-throttles
47 WorkorderIngestStack AWS::Lambda::Function BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691 WorkorderIngestStack-BucketNotificationsHandler050-fBtoo7x6Azy3
48 WorkorderIngestStack AWS::IAM::Role BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC WorkorderIngestStack-BucketNotificationsHandler050a-7KxpBKmlpFSf
49 WorkorderIngestStack AWS::CDK::Metadata CDKMetadata a85b0320-86dc-11f1-b0cf-0eccc7521e19
50 WorkorderIngestStack AWS::DynamoDB::Table CommentsTableBBDBF0A8 WorkOrderComments
51 WorkorderIngestStack AWS::S3::Bucket EmailBucket843A740F workorder-ingest-emails-011934824531
52 WorkorderIngestStack AWS::Lambda::Permission EmailBucketAllowBucketNotificationsToworkorderingestEmailProcessor5B0B85DABA138779 WorkorderIngestStack-EmailBucketAllowBucketNotificationsToworkorderingestEmailProcessor-VuairMrX6Qir
53 WorkorderIngestStack Custom::S3BucketNotifications EmailBucketNotificationsDDD5BEEA EmailBucketNotificationsDDD5BEEA
54 WorkorderIngestStack AWS::IAM::Policy EmailBucketNotificationsHandlerPolicy7D16942E Worko-Email-YLl5HvyOvIg2
55 WorkorderIngestStack AWS::S3::BucketPolicy EmailBucketPolicyBBC68546 workorder-ingest-emails-011934824531
56 WorkorderIngestStack AWS::Lambda::Function EmailProcessor218EC076 workorder-email-processor
57 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorAiFallbackRejectedAlarm71D1F2CE workorder-email-processor-ai-fallback-rejected
58 WorkorderIngestStack AWS::SQS::Queue EmailProcessorDlqA753DED5 https://sqs.us-east-1.amazonaws.com/011934824531/WorkorderIngestStack-EmailProcessorDlqA753DED5-qCTHrsoEucas
59 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorDlqMessagesAlarmED574757 workorder-email-processor-dlq-messages
60 WorkorderIngestStack AWS::SQS::QueuePolicy EmailProcessorDlqPolicyB24007F0 WorkorderIngestStack-EmailProcessorDlqPolicyB24007F0-15VK27UU5Q19
61 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorDurationAlarmF7530BED workorder-email-processor-duration
62 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorErrorsAlarmC1BF0D18 workorder-email-processor-errors
63 WorkorderIngestStack AWS::Logs::LogGroup EmailProcessorLogGroup8A79A2C8 /aws/lambda/workorder-email-processor
64 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorSenderAuthRejectedAlarm3D9534B7 workorder-email-processor-sender-auth-rejected
65 WorkorderIngestStack AWS::Logs::MetricFilter EmailProcessorSenderAuthRejectedFilterC0DAEDFE EmailProcessorSenderAuthRejectedFilterC0DAEDFE-Wkbow1VNDeDM
66 WorkorderIngestStack AWS::IAM::Role EmailProcessorServiceRole637381CE WorkorderIngestStack-EmailProcessorServiceRole63738-5l9OZXQLVvJn
67 WorkorderIngestStack AWS::IAM::Policy EmailProcessorServiceRoleDefaultPolicy0B15DAB8 Worko-Email-8PjqeVLLDLRv
68 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorTemplateFallbackRateAlarmF9B0866A workorder-email-processor-template-fallback-rate
69 WorkorderIngestStack AWS::CloudWatch::Alarm EmailProcessorThrottlesAlarmE102F9E9 workorder-email-processor-throttles
70 WorkorderIngestStack AWS::SES::ReceiptRule ExistingRuleSetWorkorderEmailRuleEA29F845 ExistingRuleSetWorkorderEmailRuleEA29F845-PKtaDBvIg61a
71 WorkorderIngestStack AWS::Lambda::Function ShocEmitterCD1F4C3E workorder-shoc-emitter
72 WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterDurationAlarm3F755835 workorder-shoc-emitter-duration
73 WorkorderIngestStack AWS::Lambda::EventSourceMapping ShocEmitterDynamoDBEventSourceworkorderingestCommentsTableFD97F35C58A19C1A da37d4a0-3086-4929-9ace-6eca5c20cd07
74 WorkorderIngestStack AWS::Lambda::EventSourceMapping ShocEmitterDynamoDBEventSourceworkorderingestWorkOrdersTable004CD4AC38D8BED2 cc149f4d-5375-4820-8fb8-c514accd3a85
75 WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterErrorsAlarmCE2B702A workorder-shoc-emitter-errors
76 WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterFailuresMessagesAlarmEC9798F9 workorder-shoc-emitter-failures-messages
77 WorkorderIngestStack AWS::SQS::Queue ShocEmitterFailuresQueue11D7C8E0 https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-failures
78 WorkorderIngestStack AWS::SQS::QueuePolicy ShocEmitterFailuresQueuePolicy0D60571C WorkorderIngestStack-ShocEmitterFailuresQueuePolicy0D60571C-2AMBNJh0la3L
79 WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterIteratorAgeAlarm90B55822 workorder-shoc-emitter-iterator-age
80 WorkorderIngestStack AWS::Logs::LogGroup ShocEmitterLogGroup80BDE724 /aws/lambda/workorder-shoc-emitter
81 WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterRejectedMessagesAlarmAE9AFCDD workorder-shoc-emitter-rejected-messages
82 WorkorderIngestStack AWS::SQS::Queue ShocEmitterRejectedQueueD754F41E https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-rejected
83 WorkorderIngestStack AWS::SQS::QueuePolicy ShocEmitterRejectedQueuePolicy4B088C0D WorkorderIngestStack-ShocEmitterRejectedQueuePolicy4B088C0D-vzXOuhHaGfNy
84 WorkorderIngestStack AWS::IAM::Role ShocEmitterServiceRoleA0CA0C74 WorkorderIngestStack-ShocEmitterServiceRoleA0CA0C74-LlFfZ6F9f6sr
85 WorkorderIngestStack AWS::IAM::Policy ShocEmitterServiceRoleDefaultPolicy5DE45717 Worko-ShocE-0txSITC4MbcW
86 WorkorderIngestStack AWS::CloudWatch::Alarm ShocEmitterThrottlesAlarm5503E07A workorder-shoc-emitter-throttles
87 WorkorderIngestStack AWS::Lambda::Function ShocHmacRotatorD4073E6B workorder-shoc-hmac-rotator
88 WorkorderIngestStack AWS::CloudWatch::Alarm ShocHmacRotatorDurationAlarm0725D01A workorder-shoc-hmac-rotator-duration
89 WorkorderIngestStack AWS::CloudWatch::Alarm ShocHmacRotatorErrorsAlarm55E7BCC3 workorder-shoc-hmac-rotator-errors
90 WorkorderIngestStack AWS::Lambda::Permission ShocHmacRotatorInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc3085C896 WorkorderIngestStack-ShocHmacRotatorInvokeN0a2GKfZP0JmDqDEVhhu6A0TUv3NyNbk4YMFKNc3085C8-kJaePEGWmM3r
91 WorkorderIngestStack AWS::Logs::LogGroup ShocHmacRotatorLogGroup1D109CC5 /aws/lambda/workorder-shoc-hmac-rotator
92 WorkorderIngestStack AWS::IAM::Role ShocHmacRotatorServiceRole7F2B6917 WorkorderIngestStack-ShocHmacRotatorServiceRole7F2B-uKqx4ccGQTk5
93 WorkorderIngestStack AWS::IAM::Policy ShocHmacRotatorServiceRoleDefaultPolicy492EC50B Worko-ShocH-B5ATRJ8co1Hc
94 WorkorderIngestStack AWS::CloudWatch::Alarm ShocHmacRotatorThrottlesAlarm9963F2A3 workorder-shoc-hmac-rotator-throttles
95 WorkorderIngestStack AWS::KMS::Key ShocWebhookHmacKey87DD3EC7 d10fd1f0-a61a-4405-8568-85e9fd11ba18
96 WorkorderIngestStack AWS::KMS::Alias ShocWebhookHmacKeyAliasC83A07B2 alias/workorder-ingest-shoc-webhook-kms
97 WorkorderIngestStack AWS::SecretsManager::Secret ShocWebhookHmacSecretA6CF6356 arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB
98 WorkorderIngestStack AWS::SecretsManager::ResourcePolicy ShocWebhookHmacSecretPolicy2B0A7DAC arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB
99 WorkorderIngestStack AWS::SecretsManager::RotationSchedule ShocWebhookHmacSecretRotationA6130D74 arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB
100 WorkorderIngestStack AWS::Lambda::Function WebUI258D87A5 workorder-web-ui
101 WorkorderIngestStack AWS::Logs::LogGroup WebUILogGroup8F43ABDC /aws/lambda/workorder-web-ui
102 WorkorderIngestStack AWS::IAM::Role WebUIServiceRoleB1B395BE WorkorderIngestStack-WebUIServiceRoleB1B395BE-0kRzDlAiLZVn
103 WorkorderIngestStack AWS::IAM::Policy WebUIServiceRoleDefaultPolicy8B510141 Worko-WebUI-TVYFGuD5kBVZ
104 WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrderCommentsSystemErrorsAlarm4CD72F33 WorkOrderComments-system-errors
105 WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrderCommentsThrottlesAlarmA10AEFD9 WorkOrderComments-throttles
106 WorkorderIngestStack AWS::DynamoDB::Table WorkOrdersTable515B4C61 WorkOrders
107 WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrdersTableSystemErrorsAlarm117925DB WorkOrders-system-errors
108 WorkorderIngestStack AWS::CloudWatch::Alarm WorkOrdersTableThrottlesAlarmDACF2500 WorkOrders-throttles
109 procurement-api AWS::CDK::Metadata CDKMetadata 472324e0-86ee-11f1-9b27-0afff03a5535
110 procurement-api AWS::Lambda::Function ProcurementApi187041AB procurement-api
111 procurement-api AWS::CloudWatch::Alarm ProcurementApi5xxAlarmE69DCA02 procurement-api-5xx
112 procurement-api AWS::ApiGateway::DomainName ProcurementApiDomain857EE934 procurement-api.seahaven.com
113 procurement-api AWS::ApiGateway::BasePathMapping ProcurementApiDomainMapprocurementapiProcurementRestApi7C05134920F11EB5 procurement-api.seahaven.com|(none)
114 procurement-api AWS::CloudWatch::Alarm ProcurementApiDurationAlarmBEC61762 procurement-api-duration
115 procurement-api AWS::CloudWatch::Alarm ProcurementApiErrorsAlarmFA6549F2 procurement-api-errors
116 procurement-api AWS::Logs::LogGroup ProcurementApiLogGroupD8265E36 /aws/lambda/procurement-api
117 procurement-api AWS::IAM::Role ProcurementApiServiceRoleD696D662 procurement-api-ProcurementApiServiceRoleD696D662-W2RyELHYFwBL
118 procurement-api AWS::IAM::Policy ProcurementApiServiceRoleDefaultPolicy1953BD71 procu-Procu-3dauci6ha7Eg
119 procurement-api AWS::CloudWatch::Alarm ProcurementApiThrottlesAlarm7F98A1AD procurement-api-throttles
120 procurement-api AWS::ApiGateway::RestApi ProcurementRestApiDD8C37D6 mvul1efda2
121 procurement-api AWS::ApiGateway::Deployment ProcurementRestApiDeployment526FECC37a13ca46329dbf433cb1289dad9c6539 uax9sq
122 procurement-api AWS::ApiGateway::Stage ProcurementRestApiDeploymentStageprodD4833AB9 prod
123 procurement-api AWS::ApiGateway::Resource ProcurementRestApidocsBC7A2DEC k4vl85
124 procurement-api AWS::ApiGateway::Method ProcurementRestApidocsGET1935E06E mvul1efda2|k4vl85|GET
125 procurement-api AWS::Lambda::Permission ProcurementRestApidocsGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETdocsFCC4F675 procurement-api-ProcurementRestApidocsGETApiPermissionTestprocurementapiProcurementRest-JkMEaKq9d2lY
126 procurement-api AWS::Lambda::Permission ProcurementRestApidocsGETApiPermissionprocurementapiProcurementRestApi7C051349GETdocs00B2D21A procurement-api-ProcurementRestApidocsGETApiPermissionprocurementapiProcurementRestApi7-fVn42qrClCjF
127 procurement-api AWS::ApiGateway::Resource ProcurementRestApiopenapijson945C2370 xos715
128 procurement-api AWS::Lambda::Permission ProcurementRestApiopenapijsonGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETopenapijson078A24BA procurement-api-ProcurementRestApiopenapijsonGETApiPermissionTestprocurementapiProcurem-VPxSN2DPhc4W
129 procurement-api AWS::Lambda::Permission ProcurementRestApiopenapijsonGETApiPermissionprocurementapiProcurementRestApi7C051349GETopenapijson86A01B87 procurement-api-ProcurementRestApiopenapijsonGETApiPermissionprocurementapiProcurementR-I2fDmtO8vw2w
130 procurement-api AWS::ApiGateway::Method ProcurementRestApiopenapijsonGETD1B98CF4 mvul1efda2|xos715|GET
131 procurement-api AWS::ApiGateway::Resource ProcurementRestApipurchaseordersE380B6C5 pfn6qm
132 procurement-api AWS::ApiGateway::Method ProcurementRestApipurchaseordersGET1A2C4DD8 mvul1efda2|pfn6qm|GET
133 procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseordersGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETpurchaseordersFF5C7CF2 procurement-api-ProcurementRestApipurchaseordersGETApiPermissionTestprocurementapiProcu-FPgglqrEyKO4
134 procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseordersGETApiPermissionprocurementapiProcurementRestApi7C051349GETpurchaseordersC47C57EA procurement-api-ProcurementRestApipurchaseordersGETApiPermissionprocurementapiProcureme-Db7wxlMFIJ1T
135 procurement-api AWS::ApiGateway::Resource ProcurementRestApipurchaseorderspoNumber11A73C81 nclnn3
136 procurement-api AWS::ApiGateway::Method ProcurementRestApipurchaseorderspoNumberGET7331738A mvul1efda2|nclnn3|GET
137 procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseorderspoNumberGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETpurchaseorderspoNumberC6737C34 procurement-api-ProcurementRestApipurchaseorderspoNumberGETApiPermissionTestprocurement-SxEdwssencAN
138 procurement-api AWS::Lambda::Permission ProcurementRestApipurchaseorderspoNumberGETApiPermissionprocurementapiProcurementRestApi7C051349GETpurchaseorderspoNumber8DEED34E procurement-api-ProcurementRestApipurchaseorderspoNumberGETApiPermissionprocurementapiP-qRCfwLHa2pa6
139 procurement-api AWS::ApiGateway::Resource ProcurementRestApiverifiedsites4516F10B vyst7e
140 procurement-api AWS::ApiGateway::Method ProcurementRestApiverifiedsitesGET81D69959 mvul1efda2|vyst7e|GET
141 procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitesGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETverifiedsitesA7C29C4E procurement-api-ProcurementRestApiverifiedsitesGETApiPermissionTestprocurementapiProcur-1QHpXCoVpyKN
142 procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitesGETApiPermissionprocurementapiProcurementRestApi7C051349GETverifiedsites16531E92 procurement-api-ProcurementRestApiverifiedsitesGETApiPermissionprocurementapiProcuremen-b5nhaktRwejs
143 procurement-api AWS::ApiGateway::Resource ProcurementRestApiverifiedsitessiteCode48F86D1E rmaawy
144 procurement-api AWS::ApiGateway::Method ProcurementRestApiverifiedsitessiteCodeGET2EE78735 mvul1efda2|rmaawy|GET
145 procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitessiteCodeGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETverifiedsitessiteCode63EDC375 procurement-api-ProcurementRestApiverifiedsitessiteCodeGETApiPermissionTestprocurementa-2PI3jCYWgXPU
146 procurement-api AWS::Lambda::Permission ProcurementRestApiverifiedsitessiteCodeGETApiPermissionprocurementapiProcurementRestApi7C051349GETverifiedsitessiteCode6457DF41 procurement-api-ProcurementRestApiverifiedsitessiteCodeGETApiPermissionprocurementapiPr-QQ5mW4HnxKAr
147 procurement-api AWS::ApiGateway::Resource ProcurementRestApiworkordersD3A24F7A h5iu4f
148 procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersGET62D5D82A mvul1efda2|h5iu4f|GET
149 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkorders23C66297 procurement-api-ProcurementRestApiworkordersGETApiPermissionTestprocurementapiProcureme-Ed6sN9siVtQv
150 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkorders067BAE83 procurement-api-ProcurementRestApiworkordersGETApiPermissionprocurementapiProcurementRe-CdkJhuPaVDJv
151 procurement-api AWS::ApiGateway::Resource ProcurementRestApiworkordersworkOrderIdE28A1181 4uk6uh
152 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkordersworkOrderId0451CF71 procurement-api-ProcurementRestApiworkordersworkOrderIdGETApiPermissionTestprocurementa-iZ1Jmq8djJRs
153 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkordersworkOrderId6619DA0E procurement-api-ProcurementRestApiworkordersworkOrderIdGETApiPermissionprocurementapiPr-nbjmJdg8o0nc
154 procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdGETFEEB7CE4 mvul1efda2|4uk6uh|GET
155 procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdPATCH7361320B mvul1efda2|4uk6uh|PATCH
156 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionTestprocurementapiProcurementRestApi7C051349PATCHworkordersworkOrderIdC15330EB procurement-api-ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionTestprocuremen-BJYqqytadslV
157 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionprocurementapiProcurementRestApi7C051349PATCHworkordersworkOrderId4DF98886 procurement-api-ProcurementRestApiworkordersworkOrderIdPATCHApiPermissionprocurementapi-O70SvTPEoe0h
158 procurement-api AWS::ApiGateway::Resource ProcurementRestApiworkordersworkOrderIdcomments5C278482 gfx0te
159 procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdcommentsGET032FCBD8 mvul1efda2|gfx0te|GET
160 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionTestprocurementapiProcurementRestApi7C051349GETworkordersworkOrderIdcomments9BD68D42 procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionTestproc-AbYwSYiRKjxQ
161 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionprocurementapiProcurementRestApi7C051349GETworkordersworkOrderIdcommentsACDBEBB8 procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsGETApiPermissionprocurem-uxRIv0NP6hKD
162 procurement-api AWS::ApiGateway::Method ProcurementRestApiworkordersworkOrderIdcommentsPOST98588435 mvul1efda2|gfx0te|POST
163 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionTestprocurementapiProcurementRestApi7C051349POSTworkordersworkOrderIdcomments2194642A procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionTestpro-Ci7eIdxeqWFP
164 procurement-api AWS::Lambda::Permission ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionprocurementapiProcurementRestApi7C051349POSTworkordersworkOrderIdcommentsFBA9AF05 procurement-api-ProcurementRestApiworkordersworkOrderIdcommentsPOSTApiPermissionprocure-VQGemxZkkIu1

11
terraform/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
build/
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
!terraform.tfvars.example
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json

74
terraform/.terraform.lock.hcl generated Normal file
View file

@ -0,0 +1,74 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/archive" {
version = "2.8.0"
constraints = "~> 2.0"
hashes = [
"h1:WB6H5ksIZiyq1lQlD/PWeh+tn4FLsbSjVnRW3+4xe2Y=",
"h1:cMBtvdHEgvTmglbioVehZCaOIPocumu9+vlGw5Dsaro=",
"h1:jdmKm+xl6ZcQrijxapnZ94RVuz/G4vk7hsIa1N0VT5Q=",
"h1:oRWx6ZDIdlNBF90L8TzV9X7pQ+P403hoCDiBfmUfhC0=",
"zh:0d14713fdc259fb377d0b899ad3c650a34194bd52194c863303ef22a65a580e2",
"zh:369b56040c7a8085d04e7e8ffac1e2b321a3170e502f788819bc34b868ec016f",
"zh:4d1a3b983ed6af5a52bfe12794674ae55cbadfa6021b37106ade68b433ad216a",
"zh:5c547549e26e083573c78a966ca68ce6d7df6bb8f3948f66a575f07da46b74ea",
"zh:6de093e62a975eb19a5e3017ce38e6e3cb639c17b79648d2000e0a8348f0e997",
"zh:7267936c2cdbc448efeb594d73e6b56a53d6a7ae14fe88cdd2a4133adc3302f0",
"zh:7482f023050ed426b4b45116e1761643bc33b1fd4ce4a6fab207ae2571f35940",
"zh:76bbd93b234e5a2927d98b511d86565700f549b570871a194c35f944b96cefb7",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:c6afc4bc1f002bac9c173007dd4da05fde788cd14c2916089f958c33fedb0dfa",
"zh:d3ba40bd806a3a08e9237dece679193c99afb2085de6b45d7f5d1f673cfcd368",
"zh:e1ad7ded53ecd6f0e5b473a3b44eae2b2e885653a56050ab583d387332be02e4",
"zh:e93e78575ce82be6084cc153c24ba8f385dc8d6880888ee66e918460c870953d",
]
}
provider "registry.terraform.io/hashicorp/aws" {
version = "6.58.0"
constraints = "~> 6.57"
hashes = [
"h1:1im6ypdeXLXq3sHElserTE62qmIqNiHLAyC3R5EnFFw=",
"h1:2kpake4zZKRX5437QVIRU3qFYH6Bjw/QE1fgVCPOrUg=",
"h1:OWl47Bo8Vzlf5srTUCmA6v4kvQGfah/P1joRtIYUUMc=",
"h1:UFot9S97tuAPvjKvoxm08sDG/gKYdDK+lMwsZKtLieY=",
"zh:1221253beee5629fb503d79cebc9bc661279cbc4be5d01db9ab4c1b702108250",
"zh:132bd0925bdc4b72446ac750b7ccb1e19b9ba8fbb6df57b2c1423314d2195d4f",
"zh:18cda250b9e82b753808715893c8927f132273c00ffae7a697d65ac1cb577e48",
"zh:204c944f1fb7f440a335bb2083c9691a9d1f677aea9701025dd5816aee41f0ba",
"zh:2dc41df289f2b10a01e650cdd73699955f0ab0645d09cfb114a8cd0f4cc4ede7",
"zh:345633dfa9a234659d52aadd126e6dce658518c3ab5cbf6d871221287ed5ec56",
"zh:4dadcced73e742903158bc9838936d911f3fa4c2c37b5591c1a28f8f2a1902a6",
"zh:5bc60cc2b8c093da98b211d9f6c21c9ecec0f21b944d9ecbe3961fba33086e80",
"zh:6cc8f084938b0033a9c0c910989919dad6b1683e76e0afa1a5c604e39f398a75",
"zh:7db214647f79de9a033b5dfd6cbfaa42d53c4d056b32cb3acc7ad99306dd548a",
"zh:9078589ec881cee7ed9403af262c98ff257fb3e1baae72ff6429a398b1c730af",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:bd5bce6aec4d4922b1127b8575688bd4bc4279670ee28d198ede404709826c7c",
"zh:cd900ecf56d21023873898b06e40234f3f4d350f2343b7d9b980d6c5cb604fae",
"zh:dbe93b276a84421026b956c3c5b4eb8897da6cbb54b93cb89f5d6ebbd30805ca",
"zh:f6b6c7bb2dbf04ee085e5c22f7a65b3ccaebf368ed95584dc0dfee8a22771056",
]
}
provider "registry.terraform.io/hashicorp/external" {
version = "2.4.0"
constraints = "~> 2.0"
hashes = [
"h1:AmY6ZeIvqoTT5ZjzD+P49PeQH6Va1QLMkX+7MUQfYoA=",
"zh:0772afb42b658468ac5e15df33bf2080456f8f0b8ab163bfe9c50d2b2ea02135",
"zh:0ac31a9aaa43dfcff5944b791596cdc94e153348e4bb4642282d034dff548134",
"zh:32d8492b1bdcc956ca3c6d00c6392d0a83942ff11d4820c7ee63ca6796e06950",
"zh:3c0482e894429f528ce6655a76ab0d8a9f7c0dacc6c828865e1515d4a7dbb852",
"zh:61e68100b4db2f930b31491f23c602126382fd5e51252be1b551f0e17f8ddbee",
"zh:6d60f615a0ad85eb962c9eb94f25e3eba7a72684ce276ba5dfb23f36b295a8f8",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:9ced2745eb5f1346203027d2dd7bf856ad1d279a25730ff7dbc6eec187aaca0c",
"zh:a8378558a177d43f55aa0d79d4fae91a704695122a1b109668c1daa8fb76f09d",
"zh:aadd98086133d3ebea67437d56512fdcc6dfb3bd34dfc23f276c0db9272e27b4",
"zh:beff701b653841e70441978137768f54e7dc6c27e7bf12a4589087f01f5bbcee",
"zh:c91c2223b29fdbc0044d20e1936ccc051d010727a13f2ff1e75e51f09bff33a3",
"zh:d491f9c2d32a39dc4031628469ae7c8aec0074312a7c1f0286b173cdcf854a54",
]
}

328
terraform/api.tf Normal file
View file

@ -0,0 +1,328 @@
locals {
api_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ShocBackendDevDataRead"
Effect = "Allow"
Principal = {
AWS = local.shoc_consumer_role_arn
}
Action = "execute-api:Invoke"
Resource = [
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/work-orders",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/work-orders/*",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/purchase-orders",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/purchase-orders/*",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/verified-sites",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/verified-sites/*",
]
},
{
Sid = "DocsTokenGatedRoutes"
Effect = "Allow"
Principal = { AWS = "*" }
Action = "execute-api:Invoke"
Resource = [
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/docs",
"arn:aws:execute-api:${var.aws_region}:${local.account_id}:${local.api_rest_api_id}/${local.api_stage_name}/GET/openapi.json",
]
}
]
})
# Live resource IDs (import). parent_key null => API root.
api_resources = {
work_orders = {
id = "h5iu4f"
parent_key = null
path_part = "work-orders"
}
work_order_id = {
id = "4uk6uh"
parent_key = "work_orders"
path_part = "{workOrderId}"
}
work_order_comments = {
id = "gfx0te"
parent_key = "work_order_id"
path_part = "comments"
}
purchase_orders = {
id = "pfn6qm"
parent_key = null
path_part = "purchase-orders"
}
po_number = {
id = "nclnn3"
parent_key = "purchase_orders"
path_part = "{poNumber}"
}
verified_sites = {
id = "vyst7e"
parent_key = null
path_part = "verified-sites"
}
site_code = {
id = "rmaawy"
parent_key = "verified_sites"
path_part = "{siteCode}"
}
docs = {
id = "k4vl85"
parent_key = null
path_part = "docs"
}
openapi_json = {
id = "xos715"
parent_key = null
path_part = "openapi.json"
}
}
api_iam_methods = {
work_orders_get = { resource = "work_orders", method = "GET" }
work_order_id_get = { resource = "work_order_id", method = "GET" }
work_order_id_patch = { resource = "work_order_id", method = "PATCH" }
work_order_comments_get = { resource = "work_order_comments", method = "GET" }
work_order_comments_post = { resource = "work_order_comments", method = "POST" }
purchase_orders_get = { resource = "purchase_orders", method = "GET" }
po_number_get = { resource = "po_number", method = "GET" }
verified_sites_get = { resource = "verified_sites", method = "GET" }
site_code_get = { resource = "site_code", method = "GET" }
}
api_none_methods = {
docs_get = { resource = "docs", method = "GET" }
openapi_json_get = { resource = "openapi_json", method = "GET" }
}
}
resource "aws_cloudwatch_log_group" "procurement_api" {
name = "/aws/lambda/procurement-api"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "procurement_api" {
function_name = "procurement-api"
role = aws_iam_role.procurement_api.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 30
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["procurement_api"].key
source_code_hash = data.archive_file.lambda["procurement_api"].output_base64sha256
environment {
variables = {
VERIFIED_SITES_TABLE = aws_dynamodb_table.verified_sites.name
WORK_ORDERS_TABLE = aws_dynamodb_table.work_orders.name
COMMENTS_TABLE = aws_dynamodb_table.work_order_comments.name
PO_TABLE = aws_dynamodb_table.purchase_orders.name
WEB_UI_AUTH_TOKEN_SECRET_ARN = var.web_ui_auth_token_secret_arn
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.procurement_api,
aws_iam_role_policy_attachment.procurement_api_basic,
aws_iam_role_policy.procurement_api_ddb,
aws_iam_role_policy.procurement_api_kms,
aws_iam_role_policy.procurement_api_secrets,
]
}
resource "aws_api_gateway_rest_api" "procurement" {
name = "procurement-api"
description = "Read API over procurement-ingest work orders + purchase orders; token-gated OpenAPI docs at /docs"
policy = local.api_policy
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_resource" "work_orders" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "work-orders"
}
resource "aws_api_gateway_resource" "work_order_id" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.work_orders.id
path_part = "{workOrderId}"
}
resource "aws_api_gateway_resource" "work_order_comments" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.work_order_id.id
path_part = "comments"
}
resource "aws_api_gateway_resource" "purchase_orders" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "purchase-orders"
}
resource "aws_api_gateway_resource" "po_number" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.purchase_orders.id
path_part = "{poNumber}"
}
resource "aws_api_gateway_resource" "verified_sites" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "verified-sites"
}
resource "aws_api_gateway_resource" "site_code" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_resource.verified_sites.id
path_part = "{siteCode}"
}
resource "aws_api_gateway_resource" "docs" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "docs"
}
resource "aws_api_gateway_resource" "openapi_json" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
parent_id = aws_api_gateway_rest_api.procurement.root_resource_id
path_part = "openapi.json"
}
locals {
api_resource_ids = {
work_orders = aws_api_gateway_resource.work_orders.id
work_order_id = aws_api_gateway_resource.work_order_id.id
work_order_comments = aws_api_gateway_resource.work_order_comments.id
purchase_orders = aws_api_gateway_resource.purchase_orders.id
po_number = aws_api_gateway_resource.po_number.id
verified_sites = aws_api_gateway_resource.verified_sites.id
site_code = aws_api_gateway_resource.site_code.id
docs = aws_api_gateway_resource.docs.id
openapi_json = aws_api_gateway_resource.openapi_json.id
}
}
resource "aws_api_gateway_method" "iam" {
for_each = local.api_iam_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = each.value.method
authorization = "AWS_IAM"
}
resource "aws_api_gateway_method" "none" {
for_each = local.api_none_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = each.value.method
authorization = "NONE"
}
resource "aws_api_gateway_integration" "iam" {
for_each = local.api_iam_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = aws_api_gateway_method.iam[each.key].http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.procurement_api.invoke_arn
}
resource "aws_api_gateway_integration" "none" {
for_each = local.api_none_methods
rest_api_id = aws_api_gateway_rest_api.procurement.id
resource_id = local.api_resource_ids[each.value.resource]
http_method = aws_api_gateway_method.none[each.key].http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.procurement_api.invoke_arn
}
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.procurement_api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.procurement.execution_arn}/*/*"
}
resource "aws_api_gateway_deployment" "procurement" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
triggers = {
redeployment = sha1(jsonencode({
resources = local.api_resources
iam = local.api_iam_methods
none = local.api_none_methods
policy = local.api_policy
lambda_hash = data.archive_file.lambda["procurement_api"].output_base64sha256
}))
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_integration.iam,
aws_api_gateway_integration.none,
]
}
resource "aws_api_gateway_stage" "prod" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
deployment_id = aws_api_gateway_deployment.procurement.id
stage_name = local.api_stage_name
xray_tracing_enabled = false
}
resource "aws_api_gateway_method_settings" "prod_all" {
rest_api_id = aws_api_gateway_rest_api.procurement.id
stage_name = aws_api_gateway_stage.prod.stage_name
method_path = "*/*"
settings {
metrics_enabled = false
logging_level = "OFF"
data_trace_enabled = false
throttling_burst_limit = 100
throttling_rate_limit = 50
}
}
resource "aws_api_gateway_domain_name" "procurement" {
domain_name = local.api_domain_name
regional_certificate_arn = data.aws_ssm_parameter.procurement_api_cert_arn.value
security_policy = "TLS_1_2"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_base_path_mapping" "procurement" {
api_id = aws_api_gateway_rest_api.procurement.id
stage_name = aws_api_gateway_stage.prod.stage_name
domain_name = aws_api_gateway_domain_name.procurement.domain_name
}

73
terraform/api_alarms.tf Normal file
View file

@ -0,0 +1,73 @@
resource "aws_cloudwatch_metric_alarm" "procurement_api_errors" {
alarm_name = "procurement-api-errors"
alarm_description = "procurement-api Lambda raised (bundle/init failures; the handler catches request errors, so any signal here is structural)"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.procurement_api.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "procurement_api_throttles" {
alarm_name = "procurement-api-throttles"
alarm_description = "procurement-api Lambda throttled"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.procurement_api.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "procurement_api_duration" {
alarm_name = "procurement-api-duration"
alarm_description = "procurement-api p99 duration >= 22.5s (75% of the 30s timeout; scans degrading toward timeout)"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p99"
threshold = 22500
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.procurement_api.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "procurement_api_5xx" {
alarm_name = "procurement-api-5xx"
alarm_description = "procurement-api gateway 5XX responses"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "5XXError"
namespace = "AWS/ApiGateway"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
ApiName = "procurement-api"
Stage = local.api_stage_name
}
}

55
terraform/artifacts.tf Normal file
View file

@ -0,0 +1,55 @@
# HCP plan and apply run on separate workers. archive_file paths from plan are
# not on the apply worker, so zip bytes are carried in the plan via
# content_base64 and uploaded to S3 at apply time for Lambda to consume.
#
# Package build runs during plan via external data (local-exec provisioners
# only run on apply; archive_file needs build/ present at plan time).
data "external" "package_build" {
program = ["bash", "${path.module}/build_packages_external.sh"]
}
resource "aws_s3_bucket" "artifacts" {
bucket = "procurement-ingest-artifacts-${local.account_id}"
}
resource "aws_s3_bucket_public_access_block" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
locals {
lambda_packages = {
po_email_processor = "po-email-processor.zip"
po_web_ui = "po-web-ui.zip"
po_site_extractor = "po-ingest-site-extractor.zip"
wo_email_processor = "workorder-email-processor.zip"
wo_web_ui = "workorder-web-ui.zip"
wo_shoc_emitter = "workorder-shoc-emitter.zip"
wo_shoc_hmac_rotator = "workorder-shoc-hmac-rotator.zip"
procurement_api = "procurement-api.zip"
}
}
data "archive_file" "lambda" {
for_each = local.lambda_packages
type = "zip"
source_dir = "${path.module}/build/${each.key}"
output_path = "${path.module}/build/${each.value}"
depends_on = [data.external.package_build]
}
resource "aws_s3_object" "lambda" {
for_each = local.lambda_packages
bucket = aws_s3_bucket.artifacts.id
key = each.value
content_base64 = filebase64(data.archive_file.lambda[each.key].output_path)
source_hash = data.archive_file.lambda[each.key].output_base64sha256
}

152
terraform/build_packages.sh Executable file
View file

@ -0,0 +1,152 @@
#!/usr/bin/env bash
# Package Lambda zips for HCP plan/apply. Runs on the Terraform worker.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
BUILD="${ROOT}/build"
SRC="$(cd "${ROOT}/../lambdas" && pwd)"
# Copy only regular files that resolve inside SRC (no symlink escape).
copy_src_file() {
local rel="$1"
local dest="$2"
local src_path="${SRC}/${rel}"
if [[ -L "${src_path}" ]]; then
echo "error: refusing symlink source: ${src_path}" >&2
exit 1
fi
if [[ ! -f "${src_path}" ]]; then
echo "error: missing regular file: ${src_path}" >&2
exit 1
fi
local resolved
resolved="$(cd "$(dirname "${src_path}")" && pwd)/$(basename "${src_path}")"
case "${resolved}" in
"${SRC}"/*) ;;
*)
echo "error: path escapes src tree: ${resolved}" >&2
exit 1
;;
esac
mkdir -p "$(dirname "${dest}")"
cp -P "${src_path}" "${dest}"
}
copy_py_dir() {
local rel_dir="$1"
local dest_dir="$2"
local f
mkdir -p "${dest_dir}"
for f in "${SRC}/${rel_dir}"/*.py; do
[[ -f "${f}" ]] || continue
copy_src_file "${rel_dir}/$(basename "${f}")" "${dest_dir}/$(basename "${f}")"
done
}
copy_shared_all() {
local dest_dir="$1"
local f
for f in "${SRC}/shared"/*.py; do
[[ -f "${f}" ]] || continue
copy_src_file "shared/$(basename "${f}")" "${dest_dir}/$(basename "${f}")"
done
}
maybe_pip_install() {
local dest_dir="$1"
local req="${dest_dir}/requirements.txt"
if [[ ! -f "${req}" ]]; then
return 0
fi
local deps
deps="$(grep -Ev '^[[:space:]]*(#|$)' "${req}" || true)"
if [[ -z "${deps}" ]]; then
rm -f "${req}"
return 0
fi
# boto3-only pins are Dependabot anchors; runtime provides boto3.
if ! printf '%s\n' "${deps}" | grep -Eqv '^[[:space:]]*boto3([=<!> ]|$)'; then
rm -f "${req}"
return 0
fi
python3 -m pip install \
--quiet \
--disable-pip-version-check \
-r "${req}" \
-t "${dest_dir}/" \
--platform manylinux2014_aarch64 \
--implementation cp \
--python-version 3.12 \
--only-binary=:all: \
--upgrade
rm -rf "${dest_dir}/boto3" "${dest_dir}/botocore" \
"${dest_dir}/s3transfer" "${dest_dir}/jmespath" \
"${dest_dir}/"*.dist-info 2>/dev/null || true
rm -f "${req}"
}
rm -rf "${BUILD}"
mkdir -p \
"${BUILD}/po_email_processor" \
"${BUILD}/po_web_ui" \
"${BUILD}/po_site_extractor" \
"${BUILD}/wo_email_processor" \
"${BUILD}/wo_web_ui" \
"${BUILD}/wo_shoc_emitter" \
"${BUILD}/wo_shoc_hmac_rotator" \
"${BUILD}/procurement_api"
copy_py_dir "po/email_processor" "${BUILD}/po_email_processor"
copy_shared_all "${BUILD}/po_email_processor"
if [[ -f "${SRC}/po/email_processor/requirements.txt" ]]; then
copy_src_file "po/email_processor/requirements.txt" "${BUILD}/po_email_processor/requirements.txt"
fi
maybe_pip_install "${BUILD}/po_email_processor"
copy_py_dir "po/web_ui" "${BUILD}/po_web_ui"
copy_src_file "shared/web_ui_auth.py" "${BUILD}/po_web_ui/web_ui_auth.py"
if [[ -f "${SRC}/po/web_ui/requirements.txt" ]]; then
copy_src_file "po/web_ui/requirements.txt" "${BUILD}/po_web_ui/requirements.txt"
fi
maybe_pip_install "${BUILD}/po_web_ui"
copy_py_dir "po/site_extractor" "${BUILD}/po_site_extractor"
if [[ -f "${SRC}/po/site_extractor/requirements.txt" ]]; then
copy_src_file "po/site_extractor/requirements.txt" "${BUILD}/po_site_extractor/requirements.txt"
fi
maybe_pip_install "${BUILD}/po_site_extractor"
copy_py_dir "wo/email_processor" "${BUILD}/wo_email_processor"
copy_shared_all "${BUILD}/wo_email_processor"
if [[ -f "${SRC}/wo/email_processor/requirements.txt" ]]; then
copy_src_file "wo/email_processor/requirements.txt" "${BUILD}/wo_email_processor/requirements.txt"
fi
maybe_pip_install "${BUILD}/wo_email_processor"
copy_py_dir "wo/web_ui" "${BUILD}/wo_web_ui"
copy_src_file "shared/web_ui_auth.py" "${BUILD}/wo_web_ui/web_ui_auth.py"
if [[ -f "${SRC}/wo/web_ui/requirements.txt" ]]; then
copy_src_file "wo/web_ui/requirements.txt" "${BUILD}/wo_web_ui/requirements.txt"
fi
maybe_pip_install "${BUILD}/wo_web_ui"
copy_py_dir "wo/shoc_emitter" "${BUILD}/wo_shoc_emitter"
copy_py_dir "wo/shoc_hmac_rotator" "${BUILD}/wo_shoc_hmac_rotator"
copy_py_dir "api" "${BUILD}/procurement_api"
for f in openapi.json docs.html redoc.standalone.js fonts.css; do
copy_src_file "api/${f}" "${BUILD}/procurement_api/${f}"
done
copy_src_file "shared/web_ui_auth.py" "${BUILD}/procurement_api/web_ui_auth.py"
if [[ -f "${SRC}/api/requirements.txt" ]]; then
copy_src_file "api/requirements.txt" "${BUILD}/procurement_api/requirements.txt"
fi
maybe_pip_install "${BUILD}/procurement_api"

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Terraform external data source entrypoint. Stdout must be JSON only.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
"${ROOT}/build_packages.sh" >&2
hash="$(
{
find -P \
"${ROOT}/build/po_email_processor" \
"${ROOT}/build/po_web_ui" \
"${ROOT}/build/po_site_extractor" \
"${ROOT}/build/wo_email_processor" \
"${ROOT}/build/wo_web_ui" \
"${ROOT}/build/wo_shoc_emitter" \
"${ROOT}/build/wo_shoc_hmac_rotator" \
"${ROOT}/build/procurement_api" \
-type f -print0 2>/dev/null \
| sort -z \
| xargs -0 sha256sum
} | sha256sum | awk '{print $1}'
)"
printf '{"status":"ok","hash":"%s"}\n' "${hash}"

658
terraform/iam.tf Normal file
View file

@ -0,0 +1,658 @@
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
locals {
bedrock_invoke_policy_json = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
]
Resource = [
"arn:aws:bedrock:${var.aws_region}:${local.account_id}:inference-profile/${local.bedrock_model_id}",
"arn:aws:bedrock:${var.aws_region}::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0",
"arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0",
"arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0",
]
}
]
})
dynamodb_cmk_rw_policy_json = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"kms:Decrypt",
"kms:DescribeKey",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*",
]
Resource = [data.aws_ssm_parameter.dynamodb_cmk_arn.value]
Condition = {
StringEquals = {
"kms:ViaService" = "dynamodb.${var.aws_region}.amazonaws.com"
}
}
}
]
})
dynamodb_cmk_read_policy_json = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"kms:Decrypt",
"kms:DescribeKey",
]
Resource = [data.aws_ssm_parameter.dynamodb_cmk_arn.value]
Condition = {
StringEquals = {
"kms:ViaService" = "dynamodb.${var.aws_region}.amazonaws.com"
}
}
}
]
})
web_ui_auth_secret_policy_json = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [var.web_ui_auth_token_secret_arn]
}
]
})
}
# ---------------------------------------------------------------------------
# po-email-processor
# ---------------------------------------------------------------------------
resource "aws_iam_role" "po_email_processor" {
name = "po-email-processor"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "po_email_processor_basic" {
role = aws_iam_role.po_email_processor.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "po_email_processor_s3" {
name = "s3-read-emails"
role = aws_iam_role.po_email_processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket",
]
Resource = [
aws_s3_bucket.po_emails.arn,
"${aws_s3_bucket.po_emails.arn}/*",
]
}
]
})
}
resource "aws_iam_role_policy" "po_email_processor_ddb" {
name = "dynamodb-rw"
role = aws_iam_role.po_email_processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
]
Resource = [
aws_dynamodb_table.purchase_orders.arn,
"${aws_dynamodb_table.purchase_orders.arn}/index/*",
]
}
]
})
}
resource "aws_iam_role_policy" "po_email_processor_kms" {
name = "dynamodb-cmk"
role = aws_iam_role.po_email_processor.id
policy = local.dynamodb_cmk_rw_policy_json
}
resource "aws_iam_role_policy" "po_email_processor_bedrock" {
name = "bedrock-invoke"
role = aws_iam_role.po_email_processor.id
policy = local.bedrock_invoke_policy_json
}
resource "aws_iam_role_policy" "po_email_processor_dlq" {
name = "sqs-dlq-send"
role = aws_iam_role.po_email_processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["sqs:SendMessage"]
Resource = [aws_sqs_queue.po_email_processor_dlq.arn]
}
]
})
}
# ---------------------------------------------------------------------------
# po-web-ui
# ---------------------------------------------------------------------------
resource "aws_iam_role" "po_web_ui" {
name = "po-web-ui"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "po_web_ui_basic" {
role = aws_iam_role.po_web_ui.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "po_web_ui_ddb" {
name = "dynamodb-read"
role = aws_iam_role.po_web_ui.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:BatchGetItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
]
Resource = [
aws_dynamodb_table.purchase_orders.arn,
"${aws_dynamodb_table.purchase_orders.arn}/index/*",
]
}
]
})
}
resource "aws_iam_role_policy" "po_web_ui_kms" {
name = "dynamodb-cmk"
role = aws_iam_role.po_web_ui.id
policy = local.dynamodb_cmk_read_policy_json
}
resource "aws_iam_role_policy" "po_web_ui_secrets" {
name = "secretsmanager-get"
role = aws_iam_role.po_web_ui.id
policy = local.web_ui_auth_secret_policy_json
}
# ---------------------------------------------------------------------------
# po-site-extractor
# ---------------------------------------------------------------------------
resource "aws_iam_role" "po_site_extractor" {
name = "po-site-extractor"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "po_site_extractor_basic" {
role = aws_iam_role.po_site_extractor.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "po_site_extractor_ddb" {
name = "dynamodb-rw-sites"
role = aws_iam_role.po_site_extractor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
]
Resource = [
aws_dynamodb_table.verified_sites.arn,
"${aws_dynamodb_table.verified_sites.arn}/index/*",
aws_dynamodb_table.pending_site_review.arn,
"${aws_dynamodb_table.pending_site_review.arn}/index/*",
]
},
{
Effect = "Allow"
Action = [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams",
]
Resource = [aws_dynamodb_table.purchase_orders.stream_arn]
},
{
Effect = "Allow"
Action = ["dynamodb:DescribeTable"]
Resource = [aws_dynamodb_table.purchase_orders.arn]
}
]
})
}
resource "aws_iam_role_policy" "po_site_extractor_kms" {
name = "dynamodb-cmk"
role = aws_iam_role.po_site_extractor.id
policy = local.dynamodb_cmk_read_policy_json
}
# ---------------------------------------------------------------------------
# workorder-email-processor
# ---------------------------------------------------------------------------
resource "aws_iam_role" "wo_email_processor" {
name = "workorder-email-processor"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "wo_email_processor_basic" {
role = aws_iam_role.wo_email_processor.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "wo_email_processor_s3" {
name = "s3-read-emails"
role = aws_iam_role.wo_email_processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket",
]
Resource = [
aws_s3_bucket.wo_emails.arn,
"${aws_s3_bucket.wo_emails.arn}/*",
]
}
]
})
}
resource "aws_iam_role_policy" "wo_email_processor_ddb" {
name = "dynamodb-rw"
role = aws_iam_role.wo_email_processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
]
Resource = [
aws_dynamodb_table.work_orders.arn,
"${aws_dynamodb_table.work_orders.arn}/index/*",
aws_dynamodb_table.work_order_comments.arn,
"${aws_dynamodb_table.work_order_comments.arn}/index/*",
]
}
]
})
}
resource "aws_iam_role_policy" "wo_email_processor_bedrock" {
name = "bedrock-invoke"
role = aws_iam_role.wo_email_processor.id
policy = local.bedrock_invoke_policy_json
}
resource "aws_iam_role_policy" "wo_email_processor_dlq" {
name = "sqs-dlq-send"
role = aws_iam_role.wo_email_processor.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["sqs:SendMessage"]
Resource = [aws_sqs_queue.wo_email_processor_dlq.arn]
}
]
})
}
# ---------------------------------------------------------------------------
# workorder-web-ui
# ---------------------------------------------------------------------------
resource "aws_iam_role" "wo_web_ui" {
name = "workorder-web-ui"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "wo_web_ui_basic" {
role = aws_iam_role.wo_web_ui.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "wo_web_ui_ddb" {
name = "dynamodb-read"
role = aws_iam_role.wo_web_ui.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:BatchGetItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
]
Resource = [
aws_dynamodb_table.work_orders.arn,
"${aws_dynamodb_table.work_orders.arn}/index/*",
aws_dynamodb_table.work_order_comments.arn,
"${aws_dynamodb_table.work_order_comments.arn}/index/*",
]
}
]
})
}
resource "aws_iam_role_policy" "wo_web_ui_secrets" {
name = "secretsmanager-get"
role = aws_iam_role.wo_web_ui.id
policy = local.web_ui_auth_secret_policy_json
}
# ---------------------------------------------------------------------------
# workorder-shoc-emitter
# ---------------------------------------------------------------------------
resource "aws_iam_role" "wo_shoc_emitter" {
name = "workorder-shoc-emitter"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "wo_shoc_emitter_basic" {
role = aws_iam_role.wo_shoc_emitter.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "wo_shoc_emitter_streams" {
name = "dynamodb-stream-read"
role = aws_iam_role.wo_shoc_emitter.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams",
]
Resource = [
aws_dynamodb_table.work_orders.stream_arn,
aws_dynamodb_table.work_order_comments.stream_arn,
]
},
{
Effect = "Allow"
Action = ["dynamodb:DescribeTable"]
Resource = [
aws_dynamodb_table.work_orders.arn,
aws_dynamodb_table.work_order_comments.arn,
]
}
]
})
}
resource "aws_iam_role_policy" "wo_shoc_emitter_secrets" {
name = "secretsmanager-get"
role = aws_iam_role.wo_shoc_emitter.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"]
Resource = [var.shoc_hmac_secret_arn]
}
]
})
}
resource "aws_iam_role_policy" "wo_shoc_emitter_kms" {
name = "shoc-hmac-kms"
role = aws_iam_role.wo_shoc_emitter.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["kms:Decrypt"]
Resource = [aws_kms_key.shoc_webhook.arn]
Condition = {
StringEquals = {
"kms:ViaService" = "secretsmanager.${var.aws_region}.amazonaws.com"
}
}
}
]
})
}
resource "aws_iam_role_policy" "wo_shoc_emitter_sqs" {
name = "sqs-send"
role = aws_iam_role.wo_shoc_emitter.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["sqs:SendMessage"]
Resource = [
aws_sqs_queue.shoc_emitter_rejected.arn,
aws_sqs_queue.shoc_emitter_failures.arn,
]
}
]
})
}
# ---------------------------------------------------------------------------
# workorder-shoc-hmac-rotator
# ---------------------------------------------------------------------------
resource "aws_iam_role" "wo_shoc_hmac_rotator" {
name = "workorder-shoc-hmac-rotator"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "wo_shoc_hmac_rotator_basic" {
role = aws_iam_role.wo_shoc_hmac_rotator.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "wo_shoc_hmac_rotator_secrets" {
name = "secretsmanager-rotate"
role = aws_iam_role.wo_shoc_hmac_rotator.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage",
]
Resource = [var.shoc_hmac_secret_arn]
}
]
})
}
resource "aws_iam_role_policy" "wo_shoc_hmac_rotator_kms" {
name = "shoc-hmac-kms"
role = aws_iam_role.wo_shoc_hmac_rotator.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*",
]
Resource = [aws_kms_key.shoc_webhook.arn]
Condition = {
StringEquals = {
"kms:ViaService" = "secretsmanager.${var.aws_region}.amazonaws.com"
}
}
}
]
})
}
# ---------------------------------------------------------------------------
# procurement-api
# ---------------------------------------------------------------------------
resource "aws_iam_role" "procurement_api" {
name = "procurement-api"
path = "/tf-managed/"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = local.boundary_arn
}
resource "aws_iam_role_policy_attachment" "procurement_api_basic" {
role = aws_iam_role.procurement_api.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "procurement_api_ddb" {
name = "dynamodb-read"
role = aws_iam_role.procurement_api.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:BatchGetItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
]
Resource = [
aws_dynamodb_table.purchase_orders.arn,
"${aws_dynamodb_table.purchase_orders.arn}/index/*",
aws_dynamodb_table.verified_sites.arn,
"${aws_dynamodb_table.verified_sites.arn}/index/*",
aws_dynamodb_table.work_orders.arn,
"${aws_dynamodb_table.work_orders.arn}/index/*",
aws_dynamodb_table.work_order_comments.arn,
"${aws_dynamodb_table.work_order_comments.arn}/index/*",
]
}
]
})
}
resource "aws_iam_role_policy" "procurement_api_kms" {
name = "dynamodb-cmk"
role = aws_iam_role.procurement_api.id
policy = local.dynamodb_cmk_read_policy_json
}
resource "aws_iam_role_policy" "procurement_api_secrets" {
name = "secretsmanager-get"
role = aws_iam_role.procurement_api.id
policy = local.web_ui_auth_secret_policy_json
}

595
terraform/imports.tf Normal file
View file

@ -0,0 +1,595 @@
# Import map: docs/plat-86/import-map.md (seahaven-prod, 2026-08-06).
# IAM roles are NOT imported — new /tf-managed/ roles replace CDK roles on cutover.
# --- DynamoDB ---
import {
to = aws_dynamodb_table.purchase_orders
id = "purchase-orders"
}
import {
to = aws_dynamodb_table.verified_sites
id = "verified-sites"
}
import {
to = aws_dynamodb_table.pending_site_review
id = "pending-site-review"
}
import {
to = aws_dynamodb_table.work_orders
id = "WorkOrders"
}
import {
to = aws_dynamodb_table.work_order_comments
id = "WorkOrderComments"
}
# --- S3 email buckets ---
import {
to = aws_s3_bucket.po_emails
id = "po-ingest-emails-011934824531"
}
import {
to = aws_s3_bucket.wo_emails
id = "workorder-ingest-emails-011934824531"
}
# --- SQS ---
import {
to = aws_sqs_queue.po_email_processor_dlq
id = "https://sqs.us-east-1.amazonaws.com/011934824531/po-ingest-EmailProcessorDlqA753DED5-Mn33HvhsDPEu"
}
import {
to = aws_sqs_queue.wo_email_processor_dlq
id = "https://sqs.us-east-1.amazonaws.com/011934824531/WorkorderIngestStack-EmailProcessorDlqA753DED5-qCTHrsoEucas"
}
import {
to = aws_sqs_queue.shoc_emitter_failures
id = "https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-failures"
}
import {
to = aws_sqs_queue.shoc_emitter_rejected
id = "https://sqs.us-east-1.amazonaws.com/011934824531/workorder-shoc-emitter-rejected"
}
# --- Lambdas ---
import {
to = aws_lambda_function.po_email_processor
id = "po-email-processor"
}
import {
to = aws_lambda_function.po_web_ui
id = "po-web-ui"
}
import {
to = aws_lambda_function.po_site_extractor
id = "po-ingest-site-extractor"
}
import {
to = aws_lambda_function.wo_email_processor
id = "workorder-email-processor"
}
import {
to = aws_lambda_function.wo_web_ui
id = "workorder-web-ui"
}
import {
to = aws_lambda_function.wo_shoc_emitter
id = "workorder-shoc-emitter"
}
import {
to = aws_lambda_function.wo_shoc_hmac_rotator
id = "workorder-shoc-hmac-rotator"
}
import {
to = aws_lambda_function.procurement_api
id = "procurement-api"
}
# --- Event source mappings ---
import {
to = aws_lambda_event_source_mapping.po_site_extractor
id = "64fbee1f-d9c3-4cfd-aced-19b9d29940f8"
}
import {
to = aws_lambda_event_source_mapping.wo_shoc_emitter_work_orders
id = "cc149f4d-5375-4820-8fb8-c514accd3a85"
}
import {
to = aws_lambda_event_source_mapping.wo_shoc_emitter_comments
id = "da37d4a0-3086-4929-9ace-6eca5c20cd07"
}
# --- Log groups ---
import {
to = aws_cloudwatch_log_group.po_email_processor
id = "/aws/lambda/po-email-processor"
}
import {
to = aws_cloudwatch_log_group.po_web_ui
id = "/aws/lambda/po-web-ui"
}
import {
to = aws_cloudwatch_log_group.po_site_extractor
id = "/aws/lambda/po-ingest-site-extractor"
}
import {
to = aws_cloudwatch_log_group.wo_email_processor
id = "/aws/lambda/workorder-email-processor"
}
import {
to = aws_cloudwatch_log_group.wo_web_ui
id = "/aws/lambda/workorder-web-ui"
}
import {
to = aws_cloudwatch_log_group.wo_shoc_emitter
id = "/aws/lambda/workorder-shoc-emitter"
}
import {
to = aws_cloudwatch_log_group.wo_shoc_hmac_rotator
id = "/aws/lambda/workorder-shoc-hmac-rotator"
}
import {
to = aws_cloudwatch_log_group.procurement_api
id = "/aws/lambda/procurement-api"
}
# --- SES receipt rules ---
import {
to = aws_ses_receipt_rule.po_email
id = "INBOUND_MAIL:ExistingRuleSetPoEmailRuleAC8E9C87-qwGDj9lBoL1G"
}
import {
to = aws_ses_receipt_rule.wo_email
id = "INBOUND_MAIL:ExistingRuleSetWorkorderEmailRuleEA29F845-PKtaDBvIg61a"
}
# --- SHOC KMS + secret ---
import {
to = aws_kms_key.shoc_webhook
id = "d10fd1f0-a61a-4405-8568-85e9fd11ba18"
}
import {
to = aws_kms_alias.shoc_webhook
id = "alias/workorder-ingest-shoc-webhook-kms"
}
import {
to = aws_secretsmanager_secret.shoc_webhook_hmac
id = "arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB"
}
import {
to = aws_secretsmanager_secret_rotation.shoc_webhook_hmac
id = "arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB"
}
# --- Metric filters ---
import {
to = aws_cloudwatch_log_metric_filter.po_sender_auth_rejected
id = "/aws/lambda/po-email-processor:EmailProcessorSenderAuthRejectedFilterC0DAEDFE-3K6wBb003iDv"
}
import {
to = aws_cloudwatch_log_metric_filter.wo_sender_auth_rejected
id = "/aws/lambda/workorder-email-processor:EmailProcessorSenderAuthRejectedFilterC0DAEDFE-Wkbow1VNDeDM"
}
# --- API Gateway ---
import {
to = aws_api_gateway_rest_api.procurement
id = "mvul1efda2"
}
import {
to = aws_api_gateway_domain_name.procurement
id = "procurement-api.seahaven.com"
}
import {
to = aws_api_gateway_stage.prod
id = "mvul1efda2/prod"
}
import {
to = aws_api_gateway_base_path_mapping.procurement
id = "procurement-api.seahaven.com/"
}
import {
to = aws_api_gateway_deployment.procurement
id = "mvul1efda2/uax9sq"
}
import {
to = aws_api_gateway_resource.work_orders
id = "mvul1efda2/h5iu4f"
}
import {
to = aws_api_gateway_resource.work_order_id
id = "mvul1efda2/4uk6uh"
}
import {
to = aws_api_gateway_resource.work_order_comments
id = "mvul1efda2/gfx0te"
}
import {
to = aws_api_gateway_resource.purchase_orders
id = "mvul1efda2/pfn6qm"
}
import {
to = aws_api_gateway_resource.po_number
id = "mvul1efda2/nclnn3"
}
import {
to = aws_api_gateway_resource.verified_sites
id = "mvul1efda2/vyst7e"
}
import {
to = aws_api_gateway_resource.site_code
id = "mvul1efda2/rmaawy"
}
import {
to = aws_api_gateway_resource.docs
id = "mvul1efda2/k4vl85"
}
import {
to = aws_api_gateway_resource.openapi_json
id = "mvul1efda2/xos715"
}
import {
to = aws_api_gateway_method.iam["work_orders_get"]
id = "mvul1efda2/h5iu4f/GET"
}
import {
to = aws_api_gateway_method.iam["work_order_id_get"]
id = "mvul1efda2/4uk6uh/GET"
}
import {
to = aws_api_gateway_method.iam["work_order_id_patch"]
id = "mvul1efda2/4uk6uh/PATCH"
}
import {
to = aws_api_gateway_method.iam["work_order_comments_get"]
id = "mvul1efda2/gfx0te/GET"
}
import {
to = aws_api_gateway_method.iam["work_order_comments_post"]
id = "mvul1efda2/gfx0te/POST"
}
import {
to = aws_api_gateway_method.iam["purchase_orders_get"]
id = "mvul1efda2/pfn6qm/GET"
}
import {
to = aws_api_gateway_method.iam["po_number_get"]
id = "mvul1efda2/nclnn3/GET"
}
import {
to = aws_api_gateway_method.iam["verified_sites_get"]
id = "mvul1efda2/vyst7e/GET"
}
import {
to = aws_api_gateway_method.iam["site_code_get"]
id = "mvul1efda2/rmaawy/GET"
}
import {
to = aws_api_gateway_method.none["docs_get"]
id = "mvul1efda2/k4vl85/GET"
}
import {
to = aws_api_gateway_method.none["openapi_json_get"]
id = "mvul1efda2/xos715/GET"
}
import {
to = aws_api_gateway_integration.iam["work_orders_get"]
id = "mvul1efda2/h5iu4f/GET"
}
import {
to = aws_api_gateway_integration.iam["work_order_id_get"]
id = "mvul1efda2/4uk6uh/GET"
}
import {
to = aws_api_gateway_integration.iam["work_order_id_patch"]
id = "mvul1efda2/4uk6uh/PATCH"
}
import {
to = aws_api_gateway_integration.iam["work_order_comments_get"]
id = "mvul1efda2/gfx0te/GET"
}
import {
to = aws_api_gateway_integration.iam["work_order_comments_post"]
id = "mvul1efda2/gfx0te/POST"
}
import {
to = aws_api_gateway_integration.iam["purchase_orders_get"]
id = "mvul1efda2/pfn6qm/GET"
}
import {
to = aws_api_gateway_integration.iam["po_number_get"]
id = "mvul1efda2/nclnn3/GET"
}
import {
to = aws_api_gateway_integration.iam["verified_sites_get"]
id = "mvul1efda2/vyst7e/GET"
}
import {
to = aws_api_gateway_integration.iam["site_code_get"]
id = "mvul1efda2/rmaawy/GET"
}
import {
to = aws_api_gateway_integration.none["docs_get"]
id = "mvul1efda2/k4vl85/GET"
}
import {
to = aws_api_gateway_integration.none["openapi_json_get"]
id = "mvul1efda2/xos715/GET"
}
# --- CloudWatch alarms (PO) ---
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_errors
id = "po-email-processor-errors"
}
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_throttles
id = "po-email-processor-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_dlq
id = "po-email-processor-dlq-messages"
}
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_duration
id = "po-email-processor-duration"
}
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_sender_auth_rejected
id = "po-email-processor-sender-auth-rejected"
}
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_fallback_rate
id = "po-email-processor-template-fallback-rate"
}
import {
to = aws_cloudwatch_metric_alarm.po_email_processor_ai_fallback_rejected
id = "po-email-processor-ai-fallback-rejected"
}
import {
to = aws_cloudwatch_metric_alarm.po_web_ui_throttles
id = "po-web-ui-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.po_web_ui_duration
id = "po-web-ui-duration"
}
import {
to = aws_cloudwatch_metric_alarm.po_site_extractor_errors
id = "po-ingest-site-extractor-errors"
}
import {
to = aws_cloudwatch_metric_alarm.po_site_extractor_throttles
id = "po-ingest-site-extractor-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.po_site_extractor_duration
id = "po-ingest-site-extractor-duration"
}
import {
to = aws_cloudwatch_metric_alarm.po_ddb_throttles["purchase-orders"]
id = "purchase-orders-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.po_ddb_system_errors["purchase-orders"]
id = "purchase-orders-system-errors"
}
import {
to = aws_cloudwatch_metric_alarm.po_ddb_throttles["verified-sites"]
id = "verified-sites-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.po_ddb_system_errors["verified-sites"]
id = "verified-sites-system-errors"
}
import {
to = aws_cloudwatch_metric_alarm.po_ddb_throttles["pending-site-review"]
id = "pending-site-review-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.po_ddb_system_errors["pending-site-review"]
id = "pending-site-review-system-errors"
}
# --- CloudWatch alarms (WO) ---
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_errors
id = "workorder-email-processor-errors"
}
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_throttles
id = "workorder-email-processor-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_dlq
id = "workorder-email-processor-dlq-messages"
}
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_duration
id = "workorder-email-processor-duration"
}
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_sender_auth_rejected
id = "workorder-email-processor-sender-auth-rejected"
}
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_fallback_rate
id = "workorder-email-processor-template-fallback-rate"
}
import {
to = aws_cloudwatch_metric_alarm.wo_email_processor_ai_fallback_rejected
id = "workorder-email-processor-ai-fallback-rejected"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_hmac_rotator_errors
id = "workorder-shoc-hmac-rotator-errors"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_hmac_rotator_throttles
id = "workorder-shoc-hmac-rotator-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_hmac_rotator_duration
id = "workorder-shoc-hmac-rotator-duration"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_emitter_errors
id = "workorder-shoc-emitter-errors"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_emitter_throttles
id = "workorder-shoc-emitter-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_emitter_duration
id = "workorder-shoc-emitter-duration"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_emitter_iterator_age
id = "workorder-shoc-emitter-iterator-age"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_emitter_failures_messages
id = "workorder-shoc-emitter-failures-messages"
}
import {
to = aws_cloudwatch_metric_alarm.wo_shoc_emitter_rejected_messages
id = "workorder-shoc-emitter-rejected-messages"
}
import {
to = aws_cloudwatch_metric_alarm.wo_ddb_throttles["WorkOrders"]
id = "WorkOrders-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.wo_ddb_system_errors["WorkOrders"]
id = "WorkOrders-system-errors"
}
import {
to = aws_cloudwatch_metric_alarm.wo_ddb_throttles["WorkOrderComments"]
id = "WorkOrderComments-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.wo_ddb_system_errors["WorkOrderComments"]
id = "WorkOrderComments-system-errors"
}
# --- CloudWatch alarms (API) ---
import {
to = aws_cloudwatch_metric_alarm.procurement_api_errors
id = "procurement-api-errors"
}
import {
to = aws_cloudwatch_metric_alarm.procurement_api_throttles
id = "procurement-api-throttles"
}
import {
to = aws_cloudwatch_metric_alarm.procurement_api_duration
id = "procurement-api-duration"
}
import {
to = aws_cloudwatch_metric_alarm.procurement_api_5xx
id = "procurement-api-5xx"
}

56
terraform/locals.tf Normal file
View file

@ -0,0 +1,56 @@
locals {
account_id = "011934824531"
boundary_arn = "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary"
bedrock_model_id = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
shoc_consumer_role_arn = var.shoc_consumer_role_arn
po_email_bucket_name = "po-ingest-emails-${local.account_id}"
wo_email_bucket_name = "workorder-ingest-emails-${local.account_id}"
po_email_processor_dlq_name = "po-ingest-EmailProcessorDlqA753DED5-Mn33HvhsDPEu"
wo_email_processor_dlq_name = "WorkorderIngestStack-EmailProcessorDlqA753DED5-qCTHrsoEucas"
po_ses_rule_name = "ExistingRuleSetPoEmailRuleAC8E9C87-qwGDj9lBoL1G"
wo_ses_rule_name = "ExistingRuleSetWorkorderEmailRuleEA29F845-PKtaDBvIg61a"
po_sender_auth_filter_name = "EmailProcessorSenderAuthRejectedFilterC0DAEDFE-3K6wBb003iDv"
wo_sender_auth_filter_name = "EmailProcessorSenderAuthRejectedFilterC0DAEDFE-Wkbow1VNDeDM"
api_rest_api_id = "mvul1efda2"
api_root_id = "babtawlzki"
api_domain_name = "procurement-api.seahaven.com"
api_stage_name = "prod"
api_deployment_id = "uax9sq"
ddb_alarm_operations = [
"GetItem",
"BatchGetItem",
"Query",
"Scan",
"PutItem",
"UpdateItem",
"DeleteItem",
"BatchWriteItem",
]
ddb_throttle_expr = "getitem + batchgetitem + query + scan + putitem + updateitem + deleteitem + batchwriteitem"
# Active SES receipt rule set in prod is INBOUND_MAIL (import-map OOB).
ses_rule_set_name = data.aws_ses_active_receipt_rule_set.inbound.rule_set_name
}
data "aws_sns_topic" "site_alerts" {
name = "site-alerts"
}
data "aws_ssm_parameter" "dynamodb_cmk_arn" {
name = "/seahaven/dynamodb/cmk-arn"
}
data "aws_ssm_parameter" "procurement_api_cert_arn" {
name = "/procurement-api/custom-domain/certificate-arn"
}
data "aws_ses_active_receipt_rule_set" "inbound" {}

34
terraform/outputs.tf Normal file
View file

@ -0,0 +1,34 @@
output "po_email_processor_arn" {
description = "ARN of po-email-processor"
value = aws_lambda_function.po_email_processor.arn
}
output "wo_email_processor_arn" {
description = "ARN of workorder-email-processor"
value = aws_lambda_function.wo_email_processor.arn
}
output "procurement_api_arn" {
description = "ARN of procurement-api Lambda"
value = aws_lambda_function.procurement_api.arn
}
output "procurement_api_invoke_url" {
description = "API Gateway stage invoke URL"
value = aws_api_gateway_stage.prod.invoke_url
}
output "procurement_api_custom_domain" {
description = "Custom domain (Route53 alias stays OOB in mgmt)"
value = "https://${aws_api_gateway_domain_name.procurement.domain_name}/"
}
output "shoc_hmac_secret_arn" {
description = "SHOC webhook HMAC secret ARN"
value = aws_secretsmanager_secret.shoc_webhook_hmac.arn
}
output "artifacts_bucket" {
description = "Lambda artifact bucket"
value = aws_s3_bucket.artifacts.bucket
}

270
terraform/po_alarms.tf Normal file
View file

@ -0,0 +1,270 @@
resource "aws_cloudwatch_log_metric_filter" "po_sender_auth_rejected" {
name = local.po_sender_auth_filter_name
log_group_name = aws_cloudwatch_log_group.po_email_processor.name
pattern = "\"sender_auth_rejected\""
metric_transformation {
name = "po-email-processor-sender-auth-rejected"
namespace = "Seahaven/ProcurementIngest"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_errors" {
alarm_name = "po-email-processor-errors"
alarm_description = "po-email-processor async invocation errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_email_processor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_throttles" {
alarm_name = "po-email-processor-throttles"
alarm_description = "po-email-processor invocation throttles"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_email_processor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_dlq" {
alarm_name = "po-email-processor-dlq-messages"
alarm_description = "po-email-processor DLQ has messages (dropped PO emails)"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Maximum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
QueueName = aws_sqs_queue.po_email_processor_dlq.name
}
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_duration" {
alarm_name = "po-email-processor-duration"
alarm_description = "po-email-processor p99 duration approaching the 60s timeout"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p99"
threshold = 45000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_email_processor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_sender_auth_rejected" {
alarm_name = "po-email-processor-sender-auth-rejected"
alarm_description = "po-email-processor rejected inbound mail on sender authentication (possible allowlist/DKIM-domain drift silently dropping real mail)"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 6
datapoints_to_alarm = 2
metric_name = "po-email-processor-sender-auth-rejected"
namespace = "Seahaven/ProcurementIngest"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_fallback_rate" {
alarm_name = "po-email-processor-template-fallback-rate"
alarm_description = "po-email-processor deterministic-template coverage collapse: >20% of parses fell back to the Bedrock AI extractor"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 4
datapoints_to_alarm = 2
threshold = 20
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = "IF((FILL(fb,0)+FILL(tmpl,0))>=8, 100*FILL(fb,0)/(FILL(fb,0)+FILL(tmpl,0)), 0)"
label = "TemplateFallbackRatePct"
return_data = true
}
metric_query {
id = "fb"
metric {
metric_name = "ParseOutcome"
namespace = "Seahaven/PoIngest"
period = 21600
stat = "Sum"
dimensions = {
ParseMethod = "ai_fallback"
}
}
return_data = false
}
metric_query {
id = "tmpl"
metric {
metric_name = "ParseOutcome"
namespace = "Seahaven/PoIngest"
period = 21600
stat = "Sum"
dimensions = {
ParseMethod = "template"
}
}
return_data = false
}
}
resource "aws_cloudwatch_metric_alarm" "po_email_processor_ai_fallback_rejected" {
alarm_name = "po-email-processor-ai-fallback-rejected"
alarm_description = "po-email-processor is rejecting Bedrock AI-fallback output at the validation gate (possible prompt-injection probing or template drift silently dropping real mail)"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 4
datapoints_to_alarm = 2
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = "IF(FILL(rej,0)>=1, FILL(rej,0), 0)"
label = "AiFallbackRejectedCount"
return_data = true
}
metric_query {
id = "rej"
metric {
metric_name = "ParseOutcome"
namespace = "Seahaven/PoIngest"
period = 21600
stat = "Sum"
dimensions = {
ParseMethod = "ai_fallback_rejected"
}
}
return_data = false
}
}
resource "aws_cloudwatch_metric_alarm" "po_web_ui_throttles" {
alarm_name = "po-web-ui-throttles"
alarm_description = "po-web-ui invocation throttles"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_web_ui.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_web_ui_duration" {
alarm_name = "po-web-ui-duration"
alarm_description = "po-web-ui p99 duration approaching the 60s timeout"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p99"
threshold = 45000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_web_ui.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_site_extractor_errors" {
alarm_name = "po-ingest-site-extractor-errors"
alarm_description = "po-ingest-site-extractor invocation errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_site_extractor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_site_extractor_throttles" {
alarm_name = "po-ingest-site-extractor-throttles"
alarm_description = "po-ingest-site-extractor invocation throttles"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_site_extractor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "po_site_extractor_duration" {
alarm_name = "po-ingest-site-extractor-duration"
alarm_description = "po-ingest-site-extractor p99 duration approaching the 60s timeout"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p99"
threshold = 45000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.po_site_extractor.function_name
}
}

134
terraform/po_ddb.tf Normal file
View file

@ -0,0 +1,134 @@
resource "aws_dynamodb_table" "purchase_orders" {
name = "purchase-orders"
billing_mode = "PAY_PER_REQUEST"
hash_key = "po_number"
attribute {
name = "po_number"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_IMAGE"
server_side_encryption {
enabled = true
kms_key_arn = data.aws_ssm_parameter.dynamodb_cmk_arn.value
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_dynamodb_table" "verified_sites" {
name = "verified-sites"
billing_mode = "PAY_PER_REQUEST"
hash_key = "siteCode"
attribute {
name = "siteCode"
type = "S"
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_dynamodb_table" "pending_site_review" {
name = "pending-site-review"
billing_mode = "PAY_PER_REQUEST"
hash_key = "po_number"
attribute {
name = "po_number"
type = "S"
}
lifecycle {
prevent_destroy = true
}
}
locals {
po_ddb_alarm_tables = {
"purchase-orders" = aws_dynamodb_table.purchase_orders.name
"verified-sites" = aws_dynamodb_table.verified_sites.name
"pending-site-review" = aws_dynamodb_table.pending_site_review.name
}
}
resource "aws_cloudwatch_metric_alarm" "po_ddb_throttles" {
for_each = local.po_ddb_alarm_tables
alarm_name = "${each.key}-throttles"
alarm_description = "${each.key} DynamoDB throttled requests"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = local.ddb_throttle_expr
label = "Sum of throttled requests across all operations"
return_data = true
}
dynamic "metric_query" {
for_each = local.ddb_alarm_operations
content {
id = lower(metric_query.value)
metric {
metric_name = "ThrottledRequests"
namespace = "AWS/DynamoDB"
period = 300
stat = "Sum"
dimensions = {
TableName = each.value
Operation = metric_query.value
}
}
return_data = false
}
}
}
resource "aws_cloudwatch_metric_alarm" "po_ddb_system_errors" {
for_each = local.po_ddb_alarm_tables
alarm_name = "${each.key}-system-errors"
alarm_description = "${each.key} DynamoDB server-side (5xx) errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = local.ddb_throttle_expr
label = "Sum of system errors across all operations"
return_data = true
}
dynamic "metric_query" {
for_each = local.ddb_alarm_operations
content {
id = lower(metric_query.value)
metric {
metric_name = "SystemErrors"
namespace = "AWS/DynamoDB"
period = 300
stat = "Sum"
dimensions = {
TableName = each.value
Operation = metric_query.value
}
}
return_data = false
}
}
}

134
terraform/po_lambda.tf Normal file
View file

@ -0,0 +1,134 @@
resource "aws_cloudwatch_log_group" "po_email_processor" {
name = "/aws/lambda/po-email-processor"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_cloudwatch_log_group" "po_web_ui" {
name = "/aws/lambda/po-web-ui"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_cloudwatch_log_group" "po_site_extractor" {
name = "/aws/lambda/po-ingest-site-extractor"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "po_email_processor" {
function_name = "po-email-processor"
role = aws_iam_role.po_email_processor.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["po_email_processor"].key
source_code_hash = data.archive_file.lambda["po_email_processor"].output_base64sha256
dead_letter_config {
target_arn = aws_sqs_queue.po_email_processor_dlq.arn
}
environment {
variables = {
PO_TABLE = aws_dynamodb_table.purchase_orders.name
ALLOWED_DKIM_DOMAINS = "amazon.coupahost.com"
BEDROCK_MODEL_ID = local.bedrock_model_id
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.po_email_processor,
aws_iam_role_policy_attachment.po_email_processor_basic,
aws_iam_role_policy.po_email_processor_s3,
aws_iam_role_policy.po_email_processor_ddb,
aws_iam_role_policy.po_email_processor_kms,
aws_iam_role_policy.po_email_processor_bedrock,
aws_iam_role_policy.po_email_processor_dlq,
]
}
resource "aws_lambda_function" "po_web_ui" {
function_name = "po-web-ui"
role = aws_iam_role.po_web_ui.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["po_web_ui"].key
source_code_hash = data.archive_file.lambda["po_web_ui"].output_base64sha256
environment {
variables = {
PO_TABLE = aws_dynamodb_table.purchase_orders.name
WEB_UI_AUTH_TOKEN_SECRET_ARN = var.web_ui_auth_token_secret_arn
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.po_web_ui,
aws_iam_role_policy_attachment.po_web_ui_basic,
aws_iam_role_policy.po_web_ui_ddb,
aws_iam_role_policy.po_web_ui_kms,
aws_iam_role_policy.po_web_ui_secrets,
]
}
resource "aws_lambda_function" "po_site_extractor" {
function_name = "po-ingest-site-extractor"
role = aws_iam_role.po_site_extractor.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["po_site_extractor"].key
source_code_hash = data.archive_file.lambda["po_site_extractor"].output_base64sha256
environment {
variables = {
VERIFIED_SITES_TABLE = aws_dynamodb_table.verified_sites.name
PENDING_REVIEW_TABLE = aws_dynamodb_table.pending_site_review.name
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.po_site_extractor,
aws_iam_role_policy_attachment.po_site_extractor_basic,
aws_iam_role_policy.po_site_extractor_ddb,
aws_iam_role_policy.po_site_extractor_kms,
]
}
resource "aws_lambda_event_source_mapping" "po_site_extractor" {
event_source_arn = aws_dynamodb_table.purchase_orders.stream_arn
function_name = aws_lambda_function.po_site_extractor.arn
starting_position = "TRIM_HORIZON"
batch_size = 10
maximum_batching_window_in_seconds = 30
bisect_batch_on_function_error = true
maximum_retry_attempts = 3
parallelization_factor = 1
enabled = true
}

96
terraform/po_s3.tf Normal file
View file

@ -0,0 +1,96 @@
resource "aws_s3_bucket" "po_emails" {
bucket = local.po_email_bucket_name
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "po_emails" {
bucket = aws_s3_bucket.po_emails.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "po_emails" {
bucket = aws_s3_bucket.po_emails.id
rule {
id = "expire-90-days"
status = "Enabled"
filter {}
expiration {
days = 90
}
}
}
resource "aws_s3_bucket_policy" "po_emails" {
bucket = aws_s3_bucket.po_emails.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "ses.amazonaws.com" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.po_emails.arn}/inbound/*"
Condition = {
StringEquals = {
"aws:SourceAccount" = local.account_id
}
}
}
]
})
}
resource "aws_s3_bucket_notification" "po_emails" {
bucket = aws_s3_bucket.po_emails.id
lambda_function {
id = "po-email-processor-inbound"
lambda_function_arn = aws_lambda_function.po_email_processor.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "inbound/"
}
depends_on = [aws_lambda_permission.po_emails_invoke]
}
resource "aws_lambda_permission" "po_emails_invoke" {
statement_id = "AllowS3InvokePoEmailProcessor"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.po_email_processor.function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.po_emails.arn
}
resource "aws_sqs_queue" "po_email_processor_dlq" {
name = local.po_email_processor_dlq_name
message_retention_seconds = 1209600
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue_policy" "po_email_processor_dlq" {
queue_url = aws_sqs_queue.po_email_processor_dlq.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Principal = { AWS = "*" }
Action = "sqs:*"
Resource = aws_sqs_queue.po_email_processor_dlq.arn
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}
]
})
}

14
terraform/po_ses.tf Normal file
View file

@ -0,0 +1,14 @@
resource "aws_ses_receipt_rule" "po_email" {
name = local.po_ses_rule_name
rule_set_name = local.ses_rule_set_name
recipients = ["amazon_po@int.seahaven.com"]
enabled = true
scan_enabled = false
tls_policy = "Optional"
s3_action {
bucket_name = aws_s3_bucket.po_emails.id
object_key_prefix = "inbound/"
position = 1
}
}

11
terraform/providers.tf Normal file
View file

@ -0,0 +1,11 @@
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "procurement-ingest"
ManagedBy = "terraform"
Workspace = "procurement-ingest-prod"
}
}
}

View file

@ -0,0 +1,8 @@
# Wire these as HCP workspace Terraform variables (never commit real .tfvars).
# Exact ARNs only; secret VALUES must never appear in this repo.
web_ui_auth_token_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:procurement-ingest/web-ui-auth-token-ApAMmr"
shoc_hmac_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB"
# Required: no Terraform default. Live emitter target today (contract Rev 2026-07-23).
shoc_webhook_url = "https://api.dev.seahaven.com/api/webhooks/work-orders"
# Required exact-ARN pin for cross-account HMAC read (docs/shoc-webhook-contract.md).
shoc_consumer_role_arn = "arn:aws:iam::396287094661:role/shoc-backend-dev"

25
terraform/variables.tf Normal file
View file

@ -0,0 +1,25 @@
variable "aws_region" {
type = string
description = "AWS region for all resources"
default = "us-east-1"
}
variable "web_ui_auth_token_secret_arn" {
type = string
description = "Secrets Manager ARN for the shared web UI / docs auth token (exact ARN, including suffix)"
}
variable "shoc_hmac_secret_arn" {
type = string
description = "Secrets Manager ARN for the SHOC webhook HMAC secret (exact ARN, including suffix)"
}
variable "shoc_webhook_url" {
type = string
description = "SHOC webhook HTTPS endpoint URL (required; no default — set explicitly in HCP workspace vars)"
}
variable "shoc_consumer_role_arn" {
type = string
description = "Exact IAM role ARN allowed to GetSecretValue / kms:Decrypt the SHOC HMAC secret (cross-account consumer). Live pin today is arn:aws:iam::396287094661:role/shoc-backend-dev per docs/shoc-webhook-contract.md."
}

26
terraform/versions.tf Normal file
View file

@ -0,0 +1,26 @@
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.57"
}
archive = {
source = "hashicorp/archive"
version = "~> 2.0"
}
external = {
source = "hashicorp/external"
version = "~> 2.0"
}
}
cloud {
organization = "seahaven"
workspaces {
name = "procurement-ingest-prod"
}
}
}

344
terraform/wo_alarms.tf Normal file
View file

@ -0,0 +1,344 @@
resource "aws_cloudwatch_log_metric_filter" "wo_sender_auth_rejected" {
name = local.wo_sender_auth_filter_name
log_group_name = aws_cloudwatch_log_group.wo_email_processor.name
pattern = "\"sender_auth_rejected\""
metric_transformation {
name = "workorder-email-processor-sender-auth-rejected"
namespace = "Seahaven/ProcurementIngest"
value = "1"
default_value = "0"
}
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_errors" {
alarm_name = "workorder-email-processor-errors"
alarm_description = "workorder-email-processor async invocation errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_email_processor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_throttles" {
alarm_name = "workorder-email-processor-throttles"
alarm_description = "workorder-email-processor invocation throttles"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_email_processor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_dlq" {
alarm_name = "workorder-email-processor-dlq-messages"
alarm_description = "workorder-email-processor DLQ has visible messages (dropped emails)"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Maximum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
QueueName = aws_sqs_queue.wo_email_processor_dlq.name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_duration" {
alarm_name = "workorder-email-processor-duration"
alarm_description = "workorder-email-processor p95 duration approaching the 60s timeout"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p95"
threshold = 45000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_email_processor.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_sender_auth_rejected" {
alarm_name = "workorder-email-processor-sender-auth-rejected"
alarm_description = "workorder-email-processor rejected inbound mail on sender authentication (possible allowlist/DKIM-domain drift silently dropping real mail)"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 6
datapoints_to_alarm = 2
metric_name = "workorder-email-processor-sender-auth-rejected"
namespace = "Seahaven/ProcurementIngest"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_fallback_rate" {
alarm_name = "workorder-email-processor-template-fallback-rate"
alarm_description = "workorder-email-processor deterministic-template coverage collapse: >15% of parses fell back to the Bedrock AI extractor"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
threshold = 15
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = "IF((FILL(fb,0)+FILL(rej,0)+FILL(tmpl,0))>=10, 100*(FILL(fb,0)+FILL(rej,0))/(FILL(fb,0)+FILL(rej,0)+FILL(tmpl,0)), 0)"
label = "TemplateFallbackRatePct"
return_data = true
}
metric_query {
id = "fb"
metric {
metric_name = "ParseOutcome"
namespace = "Seahaven/WorkorderIngest"
period = 900
stat = "Sum"
dimensions = {
ParseMethod = "ai_fallback"
}
}
return_data = false
}
metric_query {
id = "rej"
metric {
metric_name = "ParseOutcome"
namespace = "Seahaven/WorkorderIngest"
period = 900
stat = "Sum"
dimensions = {
ParseMethod = "ai_fallback_rejected"
}
}
return_data = false
}
metric_query {
id = "tmpl"
metric {
metric_name = "ParseOutcome"
namespace = "Seahaven/WorkorderIngest"
period = 900
stat = "Sum"
dimensions = {
ParseMethod = "template"
}
}
return_data = false
}
}
resource "aws_cloudwatch_metric_alarm" "wo_email_processor_ai_fallback_rejected" {
alarm_name = "workorder-email-processor-ai-fallback-rejected"
alarm_description = "workorder-email-processor is rejecting Bedrock AI-fallback output at the validation gate (possible prompt-injection probing or template drift silently dropping real mail)"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 6
datapoints_to_alarm = 2
metric_name = "ParseOutcome"
namespace = "Seahaven/WorkorderIngest"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
ParseMethod = "ai_fallback_rejected"
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_hmac_rotator_errors" {
alarm_name = "workorder-shoc-hmac-rotator-errors"
alarm_description = "workorder-shoc-hmac-rotator invocation errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_hmac_rotator.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_hmac_rotator_throttles" {
alarm_name = "workorder-shoc-hmac-rotator-throttles"
alarm_description = "workorder-shoc-hmac-rotator invocation throttles"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_hmac_rotator.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_hmac_rotator_duration" {
alarm_name = "workorder-shoc-hmac-rotator-duration"
alarm_description = "workorder-shoc-hmac-rotator p99 duration approaching the 60s timeout"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p99"
threshold = 45000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_hmac_rotator.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_emitter_errors" {
alarm_name = "workorder-shoc-emitter-errors"
alarm_description = "workorder-shoc-emitter invocation errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_emitter.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_emitter_throttles" {
alarm_name = "workorder-shoc-emitter-throttles"
alarm_description = "workorder-shoc-emitter invocation throttles"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "Throttles"
namespace = "AWS/Lambda"
period = 300
statistic = "Sum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_emitter.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_emitter_duration" {
alarm_name = "workorder-shoc-emitter-duration"
alarm_description = "workorder-shoc-emitter p99 duration approaching the 60s timeout"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "Duration"
namespace = "AWS/Lambda"
period = 300
extended_statistic = "p99"
threshold = 45000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_emitter.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_emitter_iterator_age" {
alarm_name = "workorder-shoc-emitter-iterator-age"
alarm_description = "workorder-shoc-emitter stream lag >= 10 min (SHOC receiver likely down; shard blocking on retries)"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
datapoints_to_alarm = 2
metric_name = "IteratorAge"
namespace = "AWS/Lambda"
period = 300
statistic = "Maximum"
threshold = 600000
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
FunctionName = aws_lambda_function.wo_shoc_emitter.function_name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_emitter_failures_messages" {
alarm_name = "workorder-shoc-emitter-failures-messages"
alarm_description = "workorder-shoc-emitter retry-exhausted stream records parked (ESM failure metadata; replay rebuilds from DynamoDB)"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Maximum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
QueueName = aws_sqs_queue.shoc_emitter_failures.name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_shoc_emitter_rejected_messages" {
alarm_name = "workorder-shoc-emitter-rejected-messages"
alarm_description = "workorder-shoc-emitter parked non-retryable 4xx deliveries (contract bug; inspect payloads and replay)"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Maximum"
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
dimensions = {
QueueName = aws_sqs_queue.shoc_emitter_rejected.name
}
}

122
terraform/wo_ddb.tf Normal file
View file

@ -0,0 +1,122 @@
resource "aws_dynamodb_table" "work_orders" {
name = "WorkOrders"
billing_mode = "PAY_PER_REQUEST"
hash_key = "work_order_id"
attribute {
name = "work_order_id"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
lifecycle {
prevent_destroy = true
}
}
resource "aws_dynamodb_table" "work_order_comments" {
name = "WorkOrderComments"
billing_mode = "PAY_PER_REQUEST"
hash_key = "work_order_id"
range_key = "comment_id"
attribute {
name = "work_order_id"
type = "S"
}
attribute {
name = "comment_id"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
lifecycle {
prevent_destroy = true
}
}
locals {
wo_ddb_alarm_tables = {
"WorkOrders" = aws_dynamodb_table.work_orders.name
"WorkOrderComments" = aws_dynamodb_table.work_order_comments.name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_ddb_throttles" {
for_each = local.wo_ddb_alarm_tables
alarm_name = "${each.key}-throttles"
alarm_description = "${each.key} DynamoDB throttled requests"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = local.ddb_throttle_expr
label = "Sum of throttled requests across all operations"
return_data = true
}
dynamic "metric_query" {
for_each = local.ddb_alarm_operations
content {
id = lower(metric_query.value)
metric {
metric_name = "ThrottledRequests"
namespace = "AWS/DynamoDB"
period = 300
stat = "Sum"
dimensions = {
TableName = each.value
Operation = metric_query.value
}
}
return_data = false
}
}
}
resource "aws_cloudwatch_metric_alarm" "wo_ddb_system_errors" {
for_each = local.wo_ddb_alarm_tables
alarm_name = "${each.key}-system-errors"
alarm_description = "${each.key} DynamoDB server-side (5xx) errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = local.ddb_throttle_expr
label = "Sum of system errors across all operations"
return_data = true
}
dynamic "metric_query" {
for_each = local.ddb_alarm_operations
content {
id = lower(metric_query.value)
metric {
metric_name = "SystemErrors"
namespace = "AWS/DynamoDB"
period = 300
stat = "Sum"
dimensions = {
TableName = each.value
Operation = metric_query.value
}
}
return_data = false
}
}
}

84
terraform/wo_lambda.tf Normal file
View file

@ -0,0 +1,84 @@
resource "aws_cloudwatch_log_group" "wo_email_processor" {
name = "/aws/lambda/workorder-email-processor"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_cloudwatch_log_group" "wo_web_ui" {
name = "/aws/lambda/workorder-web-ui"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "wo_email_processor" {
function_name = "workorder-email-processor"
role = aws_iam_role.wo_email_processor.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["wo_email_processor"].key
source_code_hash = data.archive_file.lambda["wo_email_processor"].output_base64sha256
dead_letter_config {
target_arn = aws_sqs_queue.wo_email_processor_dlq.arn
}
environment {
variables = {
WORK_ORDERS_TABLE = aws_dynamodb_table.work_orders.name
COMMENTS_TABLE = aws_dynamodb_table.work_order_comments.name
ALLOWED_DKIM_DOMAINS = "seahaven.com"
BEDROCK_MODEL_ID = local.bedrock_model_id
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.wo_email_processor,
aws_iam_role_policy_attachment.wo_email_processor_basic,
aws_iam_role_policy.wo_email_processor_s3,
aws_iam_role_policy.wo_email_processor_ddb,
aws_iam_role_policy.wo_email_processor_bedrock,
aws_iam_role_policy.wo_email_processor_dlq,
]
}
resource "aws_lambda_function" "wo_web_ui" {
function_name = "workorder-web-ui"
role = aws_iam_role.wo_web_ui.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 128
timeout = 15
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["wo_web_ui"].key
source_code_hash = data.archive_file.lambda["wo_web_ui"].output_base64sha256
environment {
variables = {
WORK_ORDERS_TABLE = aws_dynamodb_table.work_orders.name
COMMENTS_TABLE = aws_dynamodb_table.work_order_comments.name
WEB_UI_AUTH_TOKEN_SECRET_ARN = var.web_ui_auth_token_secret_arn
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.wo_web_ui,
aws_iam_role_policy_attachment.wo_web_ui_basic,
aws_iam_role_policy.wo_web_ui_ddb,
aws_iam_role_policy.wo_web_ui_secrets,
]
}

96
terraform/wo_s3.tf Normal file
View file

@ -0,0 +1,96 @@
resource "aws_s3_bucket" "wo_emails" {
bucket = local.wo_email_bucket_name
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "wo_emails" {
bucket = aws_s3_bucket.wo_emails.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "wo_emails" {
bucket = aws_s3_bucket.wo_emails.id
rule {
id = "expire-90-days"
status = "Enabled"
filter {}
expiration {
days = 90
}
}
}
resource "aws_s3_bucket_policy" "wo_emails" {
bucket = aws_s3_bucket.wo_emails.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "ses.amazonaws.com" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.wo_emails.arn}/inbound/*"
Condition = {
StringEquals = {
"aws:SourceAccount" = local.account_id
}
}
}
]
})
}
resource "aws_s3_bucket_notification" "wo_emails" {
bucket = aws_s3_bucket.wo_emails.id
lambda_function {
id = "wo-email-processor-inbound"
lambda_function_arn = aws_lambda_function.wo_email_processor.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "inbound/"
}
depends_on = [aws_lambda_permission.wo_emails_invoke]
}
resource "aws_lambda_permission" "wo_emails_invoke" {
statement_id = "AllowS3InvokeWoEmailProcessor"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.wo_email_processor.function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.wo_emails.arn
}
resource "aws_sqs_queue" "wo_email_processor_dlq" {
name = local.wo_email_processor_dlq_name
message_retention_seconds = 1209600
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue_policy" "wo_email_processor_dlq" {
queue_url = aws_sqs_queue.wo_email_processor_dlq.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Principal = { AWS = "*" }
Action = "sqs:*"
Resource = aws_sqs_queue.wo_email_processor_dlq.arn
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}
]
})
}

14
terraform/wo_ses.tf Normal file
View file

@ -0,0 +1,14 @@
resource "aws_ses_receipt_rule" "wo_email" {
name = local.wo_ses_rule_name
rule_set_name = local.ses_rule_set_name
recipients = ["apm@int.seahaven.com"]
enabled = true
scan_enabled = false
tls_policy = "Optional"
s3_action {
bucket_name = aws_s3_bucket.wo_emails.id
object_key_prefix = "inbound/"
position = 1
}
}

345
terraform/wo_shoc.tf Normal file
View file

@ -0,0 +1,345 @@
data "aws_iam_policy_document" "shoc_webhook_kms" {
statement {
sid = "EnableRootAccountPermissions"
effect = "Allow"
actions = ["kms:*"]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${local.account_id}:root"]
}
}
statement {
sid = "ShocBackendDevDecrypt"
effect = "Allow"
actions = ["kms:Decrypt"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [local.shoc_consumer_role_arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "AllowSecretsManagerViaServiceAccount"
effect = "Allow"
actions = [
"kms:Decrypt",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${local.account_id}:root"]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "AllowSecretsManagerGrants"
effect = "Allow"
actions = [
"kms:CreateGrant",
"kms:DescribeKey",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${local.account_id}:root"]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "ShocHmacRotator"
effect = "Allow"
actions = [
"kms:Decrypt",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.wo_shoc_hmac_rotator.arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "ShocEmitterDecrypt"
effect = "Allow"
actions = ["kms:Decrypt"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.wo_shoc_emitter.arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
}
resource "aws_kms_key" "shoc_webhook" {
description = "Dedicated CMK for the workorder-ingest/shoc-webhook-hmac secret (cross-account readable by the SHOC backend)"
enable_key_rotation = true
deletion_window_in_days = 7
policy = data.aws_iam_policy_document.shoc_webhook_kms.json
}
resource "aws_kms_alias" "shoc_webhook" {
name = "alias/workorder-ingest-shoc-webhook-kms"
target_key_id = aws_kms_key.shoc_webhook.key_id
}
resource "aws_secretsmanager_secret" "shoc_webhook_hmac" {
name = "workorder-ingest/shoc-webhook-hmac"
description = "HMAC signing keys for the SHOC work-order webhook (docs/shoc-webhook-contract.md section 6)"
kms_key_id = aws_kms_key.shoc_webhook.arn
tags = {
Purpose = "shoc-webhook-hmac"
ManagedBy = "terraform"
}
}
resource "aws_secretsmanager_secret_policy" "shoc_webhook_hmac" {
secret_arn = aws_secretsmanager_secret.shoc_webhook_hmac.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = local.shoc_consumer_role_arn
}
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
]
Resource = "*"
},
{
Effect = "Deny"
Principal = {
AWS = "arn:aws:iam::${local.account_id}:root"
}
Action = "secretsmanager:DeleteSecret"
Resource = "*"
}
]
})
}
resource "aws_cloudwatch_log_group" "wo_shoc_hmac_rotator" {
name = "/aws/lambda/workorder-shoc-hmac-rotator"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "wo_shoc_hmac_rotator" {
function_name = "workorder-shoc-hmac-rotator"
role = aws_iam_role.wo_shoc_hmac_rotator.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 128
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["wo_shoc_hmac_rotator"].key
source_code_hash = data.archive_file.lambda["wo_shoc_hmac_rotator"].output_base64sha256
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.wo_shoc_hmac_rotator,
aws_iam_role_policy_attachment.wo_shoc_hmac_rotator_basic,
aws_iam_role_policy.wo_shoc_hmac_rotator_secrets,
aws_iam_role_policy.wo_shoc_hmac_rotator_kms,
]
}
resource "aws_lambda_permission" "wo_shoc_hmac_rotator" {
statement_id = "AllowSecretsManagerRotate"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.wo_shoc_hmac_rotator.function_name
principal = "secretsmanager.amazonaws.com"
source_account = local.account_id
source_arn = aws_secretsmanager_secret.shoc_webhook_hmac.arn
}
resource "aws_secretsmanager_secret_rotation" "shoc_webhook_hmac" {
secret_id = aws_secretsmanager_secret.shoc_webhook_hmac.id
rotation_lambda_arn = aws_lambda_function.wo_shoc_hmac_rotator.arn
rotation_rules {
automatically_after_days = 30
}
depends_on = [aws_lambda_permission.wo_shoc_hmac_rotator]
}
resource "aws_sqs_queue" "shoc_emitter_failures" {
name = "workorder-shoc-emitter-failures"
message_retention_seconds = 1209600
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue_policy" "shoc_emitter_failures" {
queue_url = aws_sqs_queue.shoc_emitter_failures.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Principal = { AWS = "*" }
Action = "sqs:*"
Resource = aws_sqs_queue.shoc_emitter_failures.arn
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}
]
})
}
resource "aws_sqs_queue" "shoc_emitter_rejected" {
name = "workorder-shoc-emitter-rejected"
message_retention_seconds = 1209600
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue_policy" "shoc_emitter_rejected" {
queue_url = aws_sqs_queue.shoc_emitter_rejected.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Principal = { AWS = "*" }
Action = "sqs:*"
Resource = aws_sqs_queue.shoc_emitter_rejected.arn
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}
]
})
}
resource "aws_cloudwatch_log_group" "wo_shoc_emitter" {
name = "/aws/lambda/workorder-shoc-emitter"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "wo_shoc_emitter" {
function_name = "workorder-shoc-emitter"
role = aws_iam_role.wo_shoc_emitter.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["wo_shoc_emitter"].key
source_code_hash = data.archive_file.lambda["wo_shoc_emitter"].output_base64sha256
environment {
variables = {
HMAC_SECRET_ARN = var.shoc_hmac_secret_arn
REJECTED_QUEUE_URL = aws_sqs_queue.shoc_emitter_rejected.url
SHOC_WEBHOOK_URL = var.shoc_webhook_url
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.wo_shoc_emitter,
aws_iam_role_policy_attachment.wo_shoc_emitter_basic,
aws_iam_role_policy.wo_shoc_emitter_streams,
aws_iam_role_policy.wo_shoc_emitter_secrets,
aws_iam_role_policy.wo_shoc_emitter_kms,
aws_iam_role_policy.wo_shoc_emitter_sqs,
]
}
resource "aws_lambda_event_source_mapping" "wo_shoc_emitter_work_orders" {
event_source_arn = aws_dynamodb_table.work_orders.stream_arn
function_name = aws_lambda_function.wo_shoc_emitter.arn
starting_position = "LATEST"
batch_size = 10
parallelization_factor = 1
maximum_record_age_in_seconds = 86400
maximum_retry_attempts = -1
bisect_batch_on_function_error = false
function_response_types = ["ReportBatchItemFailures"]
enabled = true
destination_config {
on_failure {
destination_arn = aws_sqs_queue.shoc_emitter_failures.arn
}
}
}
resource "aws_lambda_event_source_mapping" "wo_shoc_emitter_comments" {
event_source_arn = aws_dynamodb_table.work_order_comments.stream_arn
function_name = aws_lambda_function.wo_shoc_emitter.arn
starting_position = "LATEST"
batch_size = 10
parallelization_factor = 1
maximum_record_age_in_seconds = 86400
maximum_retry_attempts = -1
bisect_batch_on_function_error = false
function_response_types = ["ReportBatchItemFailures"]
enabled = true
destination_config {
on_failure {
destination_arn = aws_sqs_queue.shoc_emitter_failures.arn
}
}
}