thinkwork-cli 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +1187 -315
  4. package/dist/terraform/examples/greenfield/main.tf +325 -19
  5. package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
  6. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
  7. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
  8. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
  9. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
  10. package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
  11. package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
  12. package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
  13. package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
  14. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
  15. package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
  16. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
  17. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
  18. package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
  19. package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
  20. package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
  21. package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
  22. package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
  23. package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
  24. package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
  25. package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
  26. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
  27. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
  28. package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
  29. package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
  30. package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
  31. package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
  32. package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
  33. package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
  34. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
  35. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
  36. package/dist/terraform/modules/app/static-site/main.tf +146 -5
  37. package/dist/terraform/modules/app/www-dns/main.tf +118 -15
  38. package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
  39. package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
  40. package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
  41. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
  42. package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
  43. package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
  44. package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
  45. package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
  46. package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
  47. package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
  48. package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
  49. package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
  50. package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
  51. package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
  52. package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
  53. package/dist/terraform/modules/thinkwork/main.tf +439 -21
  54. package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
  55. package/dist/terraform/modules/thinkwork/variables.tf +153 -2
  56. package/dist/terraform/schema.graphql +17 -0
  57. package/package.json +15 -14
@@ -9,9 +9,9 @@
9
9
  # In production this module will contain 30+ Lambda functions covering:
10
10
  # - GraphQL HTTP handler (the main API entry point)
11
11
  # - Agent invoke / chat
12
- # - Thread, agent, template, connector CRUD
12
+ # - Thread, agent, and template CRUD
13
13
  # - Skills, KB, memory handlers
14
- # - Connectors (Slack, GitHub, Google)
14
+ # - Webhook, MCP, and OAuth handlers
15
15
  # - Email inbound/outbound
16
16
  # - OAuth callbacks
17
17
  ################################################################################
@@ -70,6 +70,58 @@ resource "aws_apigatewayv2_api_mapping" "main" {
70
70
  stage = aws_apigatewayv2_stage.default.id
71
71
  }
72
72
 
73
+ ################################################################################
74
+ # MCP Custom Domain (optional) — second domain on the same HTTP API.
75
+ #
76
+ # Two-apply dance because ACM requires DNS validation before a Regional
77
+ # custom domain can bind the cert. `var.mcp_custom_domain_ready = false`
78
+ # (first apply) creates just the cert in pending-validation state and
79
+ # surfaces the validation record via `mcp_custom_domain_validation` output.
80
+ # The operator adds that record to Cloudflare (via `pnpm cf:sync-mcp`),
81
+ # waits ~5 min for ACM validation, then sets `mcp_custom_domain_ready =
82
+ # true` for the second apply, which creates the domain + API mapping.
83
+ # A final `pnpm cf:sync-mcp --finalize` adds the `mcp.thinkwork.ai`
84
+ # CNAME pointing at the regional domain target.
85
+ #
86
+ # See docs/solutions/patterns/mcp-custom-domain-setup-2026-04-23.md.
87
+ ################################################################################
88
+
89
+ resource "aws_acm_certificate" "mcp" {
90
+ count = var.mcp_custom_domain != "" ? 1 : 0
91
+ domain_name = var.mcp_custom_domain
92
+ validation_method = "DNS"
93
+
94
+ lifecycle {
95
+ create_before_destroy = true
96
+ }
97
+
98
+ tags = {
99
+ Name = "thinkwork-${var.stage}-mcp-cert"
100
+ }
101
+ }
102
+
103
+ resource "aws_apigatewayv2_domain_name" "mcp" {
104
+ count = var.mcp_custom_domain != "" && var.mcp_custom_domain_ready ? 1 : 0
105
+ domain_name = var.mcp_custom_domain
106
+
107
+ domain_name_configuration {
108
+ certificate_arn = aws_acm_certificate.mcp[0].arn
109
+ endpoint_type = "REGIONAL"
110
+ security_policy = "TLS_1_2"
111
+ }
112
+
113
+ tags = {
114
+ Name = "thinkwork-${var.stage}-mcp-domain"
115
+ }
116
+ }
117
+
118
+ resource "aws_apigatewayv2_api_mapping" "mcp" {
119
+ count = var.mcp_custom_domain != "" && var.mcp_custom_domain_ready ? 1 : 0
120
+ api_id = aws_apigatewayv2_api.main.id
121
+ domain_name = aws_apigatewayv2_domain_name.mcp[0].id
122
+ stage = aws_apigatewayv2_stage.default.id
123
+ }
124
+
73
125
  ################################################################################
74
126
  # Shared Lambda Execution Role
75
127
  ################################################################################
@@ -92,6 +144,19 @@ resource "aws_iam_role_policy_attachment" "lambda_basic" {
92
144
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
93
145
  }
94
146
 
147
+ resource "aws_iam_role_policy_attachment" "computer_runtime_manager" {
148
+ # No count guard: module.computer_runtime is unconditional (see
149
+ # terraform/modules/thinkwork/main.tf), so the policy ARN is always
150
+ # populated. A `count = var.X != "" ? 1 : 0` guard fails terraform
151
+ # plan with "Invalid count argument" because var.X resolves from
152
+ # aws_iam_policy.manager.arn — a computed attribute only known at
153
+ # apply time, not at plan time. If a future caller wants to make
154
+ # this attachment conditional, gate it on a known-at-plan-time
155
+ # boolean variable rather than the ARN string.
156
+ role = aws_iam_role.lambda.name
157
+ policy_arn = var.computer_runtime_manager_policy_arn
158
+ }
159
+
95
160
  resource "aws_iam_role_policy" "lambda_rds" {
96
161
  name = "rds-data-api"
97
162
  role = aws_iam_role.lambda.id
@@ -129,6 +194,7 @@ resource "aws_iam_role_policy" "lambda_secrets" {
129
194
  Action = [
130
195
  "secretsmanager:CreateSecret",
131
196
  "secretsmanager:UpdateSecret",
197
+ "secretsmanager:DeleteSecret",
132
198
  "secretsmanager:GetSecretValue"
133
199
  ]
134
200
  Resource = "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:thinkwork/*"
@@ -253,10 +319,12 @@ resource "aws_iam_role_policy" "lambda_agentcore_invoke" {
253
319
  Action = [
254
320
  "lambda:InvokeFunction",
255
321
  ]
256
- Resource = [
322
+ Resource = compact([
257
323
  var.agentcore_function_arn,
258
324
  "${var.agentcore_function_arn}:*",
259
- ]
325
+ var.agentcore_flue_function_arn,
326
+ var.agentcore_flue_function_arn != "" ? "${var.agentcore_flue_function_arn}:*" : "",
327
+ ])
260
328
  }]
261
329
  })
262
330
  }
@@ -356,14 +424,33 @@ resource "aws_iam_role_policy" "lambda_ssm_read" {
356
424
 
357
425
  policy = jsonencode({
358
426
  Version = "2012-10-17"
359
- Statement = [{
360
- Effect = "Allow"
361
- Action = [
362
- "ssm:GetParameter",
363
- "ssm:GetParameters",
364
- ]
365
- Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/*"
366
- }]
427
+ Statement = [
428
+ {
429
+ Effect = "Allow"
430
+ Action = [
431
+ "ssm:GetParameter",
432
+ "ssm:GetParameters",
433
+ ]
434
+ Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/*"
435
+ },
436
+ # SecureString parameters (e.g. /thinkwork/<stage>/google-places/api-key)
437
+ # are encrypted with the default AWS-managed SSM key. The default key's
438
+ # resource policy auto-grants Decrypt to any IAM principal with
439
+ # ssm:GetParameter on the parameter via `kms:ViaService = ssm.*`, so
440
+ # this explicit grant is a belt-and-suspenders clarification. If we
441
+ # later move to a customer-managed KMS key, this is the scope that
442
+ # needs updating.
443
+ {
444
+ Effect = "Allow"
445
+ Action = ["kms:Decrypt"]
446
+ Resource = "*"
447
+ Condition = {
448
+ StringEquals = {
449
+ "kms:ViaService" = "ssm.${var.region}.amazonaws.com"
450
+ }
451
+ }
452
+ },
453
+ ]
367
454
  })
368
455
  }
369
456
 
@@ -437,11 +524,133 @@ resource "aws_iam_role_policy" "lambda_api_cross_invoke" {
437
524
  # wiki-bootstrap-import: bootstrapJournalImport admin mutation
438
525
  # Event-invokes this for the long-running ingest path.
439
526
  "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-wiki-bootstrap-import",
527
+ # routine-resume: routine-approval-bridge (Phase B U8) invokes
528
+ # this with RequestResponse after a HITL decideInboxItem
529
+ # decision. Calls SendTaskSuccess/SendTaskFailure on the SFN
530
+ # task token; idempotent on already-consumed tokens.
531
+ "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-routine-resume",
532
+ # workspace-files-efs: workspace-files invokes this (RequestResponse)
533
+ # for Computer-target list/get to bypass the computer_tasks queue
534
+ # and read EFS directly. Standalone resource below.
535
+ "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-workspace-files-efs",
440
536
  ]
441
537
  }]
442
538
  })
443
539
  }
444
540
 
541
+ # Step Functions admin operations — for createRoutine / publishRoutineVersion
542
+ # / triggerRoutineRun / updateRoutine resolvers (Phase B U7) and the
543
+ # routine-asl-validator Lambda (Phase A U5). State-machine ARNs follow the
544
+ # naming convention `thinkwork-${stage}-routine-*`; aliases follow the
545
+ # state-machine ARN with a colon-separated alias name.
546
+ resource "aws_iam_role_policy" "lambda_routines_stepfunctions" {
547
+ name = "routines-step-functions"
548
+ role = aws_iam_role.lambda.id
549
+
550
+ policy = jsonencode({
551
+ Version = "2012-10-17"
552
+ Statement = [
553
+ {
554
+ Sid = "RoutineStateMachineLifecycle"
555
+ Effect = "Allow"
556
+ Action = [
557
+ "states:CreateStateMachine",
558
+ "states:UpdateStateMachine",
559
+ "states:DeleteStateMachine",
560
+ "states:DescribeStateMachine",
561
+ "states:ListStateMachines",
562
+ "states:TagResource",
563
+ "states:UntagResource",
564
+ "states:PublishStateMachineVersion",
565
+ "states:DeleteStateMachineVersion",
566
+ "states:ListStateMachineVersions",
567
+ "states:CreateStateMachineAlias",
568
+ "states:UpdateStateMachineAlias",
569
+ "states:DeleteStateMachineAlias",
570
+ "states:DescribeStateMachineAlias",
571
+ "states:ListStateMachineAliases",
572
+ "states:DescribeStateMachineForExecution",
573
+ ]
574
+ Resource = "arn:aws:states:${var.region}:${var.account_id}:stateMachine:thinkwork-${var.stage}-routine-*"
575
+ },
576
+ {
577
+ Sid = "RoutineExecution"
578
+ Effect = "Allow"
579
+ Action = [
580
+ "states:StartExecution",
581
+ "states:StartSyncExecution",
582
+ "states:StopExecution",
583
+ "states:DescribeExecution",
584
+ "states:ListExecutions",
585
+ "states:GetExecutionHistory",
586
+ ]
587
+ Resource = [
588
+ "arn:aws:states:${var.region}:${var.account_id}:stateMachine:thinkwork-${var.stage}-routine-*",
589
+ "arn:aws:states:${var.region}:${var.account_id}:execution:thinkwork-${var.stage}-routine-*:*",
590
+ ]
591
+ },
592
+ {
593
+ Sid = "RoutineTaskTokens"
594
+ Effect = "Allow"
595
+ Action = [
596
+ "states:SendTaskSuccess",
597
+ "states:SendTaskFailure",
598
+ "states:SendTaskHeartbeat",
599
+ ]
600
+ Resource = "*"
601
+ },
602
+ {
603
+ Sid = "RoutineValidate"
604
+ Effect = "Allow"
605
+ Action = ["states:ValidateStateMachineDefinition"]
606
+ Resource = "*"
607
+ },
608
+ {
609
+ # PassRole so the createRoutine resolver can hand the routines
610
+ # execution role to a newly-created state machine. Scoped to the
611
+ # specific role created by the routines-stepfunctions module.
612
+ Sid = "RoutinePassExecutionRole"
613
+ Effect = "Allow"
614
+ Action = ["iam:PassRole"]
615
+ Resource = "arn:aws:iam::${var.account_id}:role/thinkwork-${var.stage}-routines-execution-role"
616
+ Condition = {
617
+ StringEquals = {
618
+ "iam:PassedToService" = "states.amazonaws.com"
619
+ }
620
+ }
621
+ },
622
+ {
623
+ # routine-task-python (Phase B U6) wraps the AgentCore code
624
+ # interpreter so SFN can run `python` recipe states. Three calls
625
+ # per Task: Start session, Invoke, Stop. Resource is `*` because
626
+ # interpreter sessions are runtime-scoped, not provisioned.
627
+ Sid = "RoutineTaskPythonCodeInterpreter"
628
+ Effect = "Allow"
629
+ Action = [
630
+ "bedrock-agentcore:StartCodeInterpreterSession",
631
+ "bedrock-agentcore:InvokeCodeInterpreter",
632
+ "bedrock-agentcore:StopCodeInterpreterSession",
633
+ "bedrock-agentcore:GetCodeInterpreterSession",
634
+ ]
635
+ Resource = "*"
636
+ },
637
+ {
638
+ # routine-task-python S3 offload — full stdout/stderr land in
639
+ # the per-stage routine-output bucket under
640
+ # <tenantId>/<sfn-execution-id>/<nodeId>/{stdout,stderr}.log.
641
+ # PutObject only — the read path is GraphQL-fronted and runs
642
+ # under the graphql-http handler's role, not this one.
643
+ Sid = "RoutineTaskPythonS3Offload"
644
+ Effect = "Allow"
645
+ Action = [
646
+ "s3:PutObject",
647
+ ]
648
+ Resource = "arn:aws:s3:::thinkwork-${var.stage}-routine-output/*"
649
+ },
650
+ ]
651
+ })
652
+ }
653
+
445
654
  ################################################################################
446
655
  # Placeholder Lambda — proves the infrastructure works
447
656
  #
@@ -0,0 +1,118 @@
1
+ locals {
2
+ mcp_oauth_api_base_url = "https://${aws_apigatewayv2_api.main.id}.execute-api.${var.region}.amazonaws.com"
3
+ mcp_oauth_cognito_base_url = var.cognito_auth_domain != "" ? "https://${var.cognito_auth_domain}.auth.${var.region}.amazoncognito.com" : ""
4
+ mcp_oauth_identity_providers = var.google_oauth_client_id != "" ? ["Google", "COGNITO"] : ["COGNITO"]
5
+ mcp_oauth_logo_path = "${path.module}/../../../../apps/admin/public/logo.png"
6
+ }
7
+
8
+ resource "aws_dynamodb_table" "mcp_oauth_revocations" {
9
+ name = "thinkwork-${var.stage}-mcp-oauth-revocations"
10
+ billing_mode = "PAY_PER_REQUEST"
11
+ hash_key = "token_id_hash"
12
+
13
+ attribute {
14
+ name = "token_id_hash"
15
+ type = "S"
16
+ }
17
+
18
+ ttl {
19
+ attribute_name = "expires_at"
20
+ enabled = true
21
+ }
22
+
23
+ tags = {
24
+ Name = "thinkwork-${var.stage}-mcp-oauth-revocations"
25
+ }
26
+ }
27
+
28
+ resource "aws_cognito_user_pool_client" "mcp_oauth" {
29
+ name = "ThinkworkMcpOAuth"
30
+ user_pool_id = var.user_pool_id
31
+
32
+ allowed_oauth_flows_user_pool_client = true
33
+ allowed_oauth_flows = ["code"]
34
+ allowed_oauth_scopes = ["openid", "email", "profile"]
35
+
36
+ supported_identity_providers = local.mcp_oauth_identity_providers
37
+
38
+ callback_urls = ["${local.mcp_oauth_api_base_url}/mcp/oauth/callback"]
39
+ logout_urls = [local.mcp_oauth_api_base_url]
40
+
41
+ access_token_validity = 1
42
+ id_token_validity = 1
43
+
44
+ token_validity_units {
45
+ access_token = "hours"
46
+ id_token = "hours"
47
+ }
48
+ }
49
+
50
+ resource "aws_cognito_user_pool_ui_customization" "mcp_oauth" {
51
+ user_pool_id = var.user_pool_id
52
+ client_id = aws_cognito_user_pool_client.mcp_oauth.id
53
+ image_file = fileexists(local.mcp_oauth_logo_path) ? filebase64(local.mcp_oauth_logo_path) : null
54
+
55
+ css = <<-CSS
56
+ .background-customizable {
57
+ background-color: #080808;
58
+ }
59
+
60
+ .banner-customizable {
61
+ background-color: #080808;
62
+ padding: 32px 0 18px;
63
+ }
64
+
65
+ .label-customizable {
66
+ color: #f5f5f5;
67
+ }
68
+
69
+ .legalText-customizable {
70
+ color: #f5f5f5;
71
+ }
72
+
73
+ .inputField-customizable {
74
+ background-color: #232323;
75
+ border: 1px solid #555555;
76
+ border-radius: 8px;
77
+ color: #ffffff;
78
+ min-height: 48px;
79
+ }
80
+
81
+ .inputField-customizable:focus {
82
+ border-color: #d8d8d8;
83
+ box-shadow: 0 0 0 3px rgba(216, 216, 216, 0.2);
84
+ }
85
+
86
+ .submitButton-customizable {
87
+ background-color: #f4f4f4;
88
+ border: 0;
89
+ border-radius: 8px;
90
+ color: #111111;
91
+ font-weight: 700;
92
+ min-height: 48px;
93
+ }
94
+
95
+ .submitButton-customizable:hover {
96
+ background-color: #ffffff;
97
+ }
98
+ CSS
99
+ }
100
+
101
+ resource "aws_iam_role_policy" "lambda_mcp_oauth_revocations" {
102
+ name = "mcp-oauth-revocations"
103
+ role = aws_iam_role.lambda.id
104
+
105
+ policy = jsonencode({
106
+ Version = "2012-10-17"
107
+ Statement = [
108
+ {
109
+ Effect = "Allow"
110
+ Action = [
111
+ "dynamodb:GetItem",
112
+ "dynamodb:PutItem"
113
+ ]
114
+ Resource = aws_dynamodb_table.mcp_oauth_revocations.arn
115
+ }
116
+ ]
117
+ })
118
+ }
@@ -0,0 +1,49 @@
1
+ ################################################################################
2
+ # Per-user OAuth client credentials (Google Workspace, Microsoft 365)
3
+ #
4
+ # Stored in Secrets Manager as JSON blobs `{"client_id","client_secret"}`.
5
+ # Fetched by oauth-authorize, oauth-callback, oauth-token at Lambda cold-start
6
+ # via packages/api/src/lib/oauth-client-credentials.ts, which caches values in
7
+ # module scope so warm containers pay zero additional round-trips.
8
+ #
9
+ # Secret names use the existing `thinkwork/${stage}/...` prefix so the shared
10
+ # Lambda role's `secretsmanager:GetSecretValue` policy on `thinkwork/*`
11
+ # (main.tf:128-135) covers them — no new IAM attachment needed.
12
+ #
13
+ # Security posture vs. the prior common_env env-var approach: client secrets
14
+ # are no longer baked into Lambda configuration (invisible in
15
+ # `aws lambda get-function-configuration`, in CloudWatch event streams, and
16
+ # in terraform state for the Lambda resource). The secret value itself is
17
+ # still readable by any Lambda using the shared role — per-consumer-role
18
+ # scoping is a separate (larger) refactor tracked for prod.
19
+ ################################################################################
20
+
21
+ resource "aws_secretsmanager_secret" "oauth_google_productivity" {
22
+ name = "thinkwork/${var.stage}/oauth/google-productivity"
23
+ description = "Google Workspace OAuth client credentials (per-user Gmail/Calendar integration). Fetched at Lambda cold-start by oauth-client-credentials.ts."
24
+ tags = {
25
+ Name = "thinkwork-${var.stage}-oauth-google-productivity"
26
+ Stage = var.stage
27
+ Provider = "google_productivity"
28
+ }
29
+ }
30
+
31
+ resource "aws_secretsmanager_secret_version" "oauth_google_productivity" {
32
+ secret_id = aws_secretsmanager_secret.oauth_google_productivity.id
33
+ secret_string = jsonencode({
34
+ client_id = var.google_oauth_client_id
35
+ client_secret = var.google_oauth_client_secret
36
+ })
37
+
38
+ # If the operator rotates the secret via AWS console / CLI without touching
39
+ # tfvars, terraform shouldn't clobber it on next apply. Matches the pattern
40
+ # used for google_places_api_key in the wiki-compile handler.
41
+ lifecycle {
42
+ ignore_changes = [secret_string]
43
+ }
44
+ }
45
+
46
+ # Microsoft 365 deferred to a follow-up — needs Azure app registration first.
47
+ # When ready, mirror the google-productivity resources + add
48
+ # `microsoft_oauth_client_id` / `_secret` variables to this module and the
49
+ # thinkwork module, then add MICROSOFT_OAUTH_SECRET_ARN to common_env.
@@ -13,6 +13,11 @@ output "api_execution_arn" {
13
13
  value = aws_apigatewayv2_api.main.execution_arn
14
14
  }
15
15
 
16
+ output "extension_proxy_route_prefix" {
17
+ description = "Route prefix private Admin extensions call through for tenant-scoped backend proxying."
18
+ value = "/api/extensions"
19
+ }
20
+
16
21
  output "lambda_role_arn" {
17
22
  description = "Shared Lambda execution role ARN (for other modules that add routes)"
18
23
  value = aws_iam_role.lambda.arn
@@ -42,3 +47,36 @@ output "email_inbound_fn_name" {
42
47
  description = "email-inbound Lambda function name. Used by the SES module for lambda:InvokeFunction permissions."
43
48
  value = local.use_local_zips ? aws_lambda_function.handler["email-inbound"].function_name : ""
44
49
  }
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # MCP custom domain — outputs consumed by `pnpm cf:sync-mcp`.
53
+ # ---------------------------------------------------------------------------
54
+
55
+ output "mcp_custom_domain" {
56
+ description = "Configured MCP custom domain (e.g., mcp.thinkwork.ai), or empty string when disabled. The CF sync script reads this to know the target hostname."
57
+ value = var.mcp_custom_domain
58
+ }
59
+
60
+ output "mcp_custom_domain_cert_arn" {
61
+ description = "ACM certificate ARN for the MCP custom domain, or empty when disabled. The sync script can use this to poll ACM validation status via aws acm describe-certificate."
62
+ value = var.mcp_custom_domain != "" ? aws_acm_certificate.mcp[0].arn : ""
63
+ }
64
+
65
+ output "mcp_custom_domain_validation" {
66
+ description = "List of ACM DNS-validation records that must exist in Cloudflare before the cert is issued. Each record is { name, type, value }. Consumed by scripts/cloudflare-sync-mcp.ts."
67
+ value = var.mcp_custom_domain != "" ? [
68
+ for dvo in aws_acm_certificate.mcp[0].domain_validation_options : {
69
+ name = dvo.resource_record_name
70
+ type = dvo.resource_record_type
71
+ value = dvo.resource_record_value
72
+ }
73
+ ] : []
74
+ }
75
+
76
+ output "mcp_custom_domain_target" {
77
+ description = "Regional target for the final mcp.thinkwork.ai → API Gateway CNAME. Only populated on the second apply (when mcp_custom_domain_ready = true). Includes { target_domain_name, hosted_zone_id } so the CF sync script can upsert the record."
78
+ value = var.mcp_custom_domain != "" && var.mcp_custom_domain_ready ? {
79
+ target_domain_name = aws_apigatewayv2_domain_name.mcp[0].domain_name_configuration[0].target_domain_name
80
+ hosted_zone_id = aws_apigatewayv2_domain_name.mcp[0].domain_name_configuration[0].hosted_zone_id
81
+ } : null
82
+ }
@@ -0,0 +1,43 @@
1
+ ################################################################################
2
+ # Slack workspace app credentials
3
+ #
4
+ # Stored in Secrets Manager as a JSON blob with three fields:
5
+ #
6
+ # {
7
+ # "signing_secret": "Slack request signing secret",
8
+ # "client_id": "Slack OAuth client id",
9
+ # "client_secret": "Slack OAuth client secret"
10
+ # }
11
+ #
12
+ # Slack request handlers and the OAuth install handler receive only this secret
13
+ # ARN in Lambda configuration. The shared Lambda role already has access to the
14
+ # `thinkwork/*` prefix, so no additional IAM attachment is needed.
15
+ #
16
+ # Operators populate the real value out-of-band. Terraform creates an initial
17
+ # empty version so Lambdas fail with a clear missing-field error before setup,
18
+ # and lifecycle.ignore_changes prevents later applies from overwriting rotated
19
+ # credentials.
20
+ ################################################################################
21
+
22
+ resource "aws_secretsmanager_secret" "slack_app_credentials" {
23
+ name = "thinkwork/${var.stage}/slack/app"
24
+ description = "Slack workspace app credentials (signing_secret, client_id, client_secret). Populate via Secrets Manager; never via tfvars."
25
+ tags = {
26
+ Name = "thinkwork-${var.stage}-slack-app"
27
+ Stage = var.stage
28
+ Provider = "slack"
29
+ }
30
+ }
31
+
32
+ resource "aws_secretsmanager_secret_version" "slack_app_credentials_initial" {
33
+ secret_id = aws_secretsmanager_secret.slack_app_credentials.id
34
+ secret_string = jsonencode({
35
+ signing_secret = ""
36
+ client_id = ""
37
+ client_secret = ""
38
+ })
39
+
40
+ lifecycle {
41
+ ignore_changes = [secret_string]
42
+ }
43
+ }
@@ -0,0 +1,53 @@
1
+ ################################################################################
2
+ # Stripe API credentials
3
+ #
4
+ # Stored in Secrets Manager as a JSON blob with three fields:
5
+ #
6
+ # {
7
+ # "secret_key": "sk_test_... | sk_live_...",
8
+ # "publishable_key": "pk_test_... | pk_live_...",
9
+ # "webhook_signing_secret": "whsec_..."
10
+ # }
11
+ #
12
+ # Fetched at Lambda cold-start by packages/api/src/lib/stripe-credentials.ts,
13
+ # which caches values in module scope so warm containers pay zero additional
14
+ # round-trips. Secret name uses the existing `thinkwork/${stage}/...` prefix so
15
+ # the shared Lambda role's `secretsmanager:GetSecretValue` policy on
16
+ # `thinkwork/*` (main.tf) covers it — no new IAM attachment needed.
17
+ #
18
+ # Operator populates the secret value out-of-band (never via tfvars), so Stripe
19
+ # keys are absent from terraform state for this resource:
20
+ #
21
+ # aws secretsmanager put-secret-value \
22
+ # --secret-id thinkwork/${stage}/stripe/api-credentials \
23
+ # --secret-string file://stripe-creds.json
24
+ #
25
+ # The initial version is a placeholder of empty strings so the secret has at
26
+ # least one version immediately after apply (Lambdas produce a clearer error
27
+ # shape when a field is empty than when the secret has zero versions).
28
+ ################################################################################
29
+
30
+ resource "aws_secretsmanager_secret" "stripe_api_credentials" {
31
+ name = "thinkwork/${var.stage}/stripe/api-credentials"
32
+ description = "Stripe API credentials (secret_key, publishable_key, webhook_signing_secret). Populate via `aws secretsmanager put-secret-value`; never via tfvars."
33
+ tags = {
34
+ Name = "thinkwork-${var.stage}-stripe-api-credentials"
35
+ Stage = var.stage
36
+ Provider = "stripe"
37
+ }
38
+ }
39
+
40
+ resource "aws_secretsmanager_secret_version" "stripe_api_credentials_initial" {
41
+ secret_id = aws_secretsmanager_secret.stripe_api_credentials.id
42
+ secret_string = jsonencode({
43
+ secret_key = ""
44
+ publishable_key = ""
45
+ webhook_signing_secret = ""
46
+ })
47
+
48
+ # Operator rotates via `aws secretsmanager put-secret-value`; terraform
49
+ # should never clobber that value on subsequent applies.
50
+ lifecycle {
51
+ ignore_changes = [secret_string]
52
+ }
53
+ }