thinkwork-cli 0.8.2 → 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 +18 -2
  3. package/dist/cli.js +3004 -215
  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 +204 -4
  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 +1557 -42
  25. package/dist/terraform/modules/app/lambda-api/main.tf +299 -15
  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 +5 -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 +165 -6
  56. package/dist/terraform/schema.graphql +45 -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/*"
@@ -196,12 +262,21 @@ resource "aws_iam_role_policy" "lambda_bedrock" {
196
262
  name = "bedrock-invoke"
197
263
  role = aws_iam_role.lambda.id
198
264
 
265
+ # Cross-region inference profiles (us.anthropic.claude-*) require
266
+ # `bedrock:InvokeModel` on the *inference-profile* ARN AND on the
267
+ # underlying foundation-model ARN in *every* region the profile can
268
+ # route to (e.g. us-east-2 for us.anthropic.claude-haiku-4-5). The
269
+ # region wildcard below covers all of them. Needed by the eval-runner
270
+ # llm-rubric judge and any handler that calls Converse with a profile ID.
199
271
  policy = jsonencode({
200
272
  Version = "2012-10-17"
201
273
  Statement = [{
202
- Effect = "Allow"
203
- Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
204
- Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
274
+ Effect = "Allow"
275
+ Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
276
+ Resource = [
277
+ "arn:aws:bedrock:*::foundation-model/*",
278
+ "arn:aws:bedrock:*:${var.account_id}:inference-profile/*",
279
+ ]
205
280
  }]
206
281
  })
207
282
  }
@@ -244,14 +319,72 @@ resource "aws_iam_role_policy" "lambda_agentcore_invoke" {
244
319
  Action = [
245
320
  "lambda:InvokeFunction",
246
321
  ]
247
- Resource = [
322
+ Resource = compact([
248
323
  var.agentcore_function_arn,
249
324
  "${var.agentcore_function_arn}:*",
250
- ]
325
+ var.agentcore_flue_function_arn,
326
+ var.agentcore_flue_function_arn != "" ? "${var.agentcore_flue_function_arn}:*" : "",
327
+ ])
251
328
  }]
252
329
  })
253
330
  }
254
331
 
332
+ # Eval-runner: invoke the AgentCore Runtime data plane to run an agent
333
+ # under test, and call AgentCore Evaluations.Evaluate to score the
334
+ # resulting spans. Both APIs are on the bedrock-agentcore service. Also
335
+ # allow reading spans + log events from CloudWatch Logs (aws/spans is
336
+ # the Transaction Search destination; the runtime log groups carry the
337
+ # OTel scope=strands.telemetry.tracer log records that EvaluateCommand
338
+ # requires alongside the spans).
339
+ resource "aws_iam_role_policy" "lambda_eval_runner" {
340
+ name = "eval-runner-bedrock-agentcore"
341
+ role = aws_iam_role.lambda.id
342
+
343
+ policy = jsonencode({
344
+ Version = "2012-10-17"
345
+ Statement = [
346
+ {
347
+ Sid = "AgentCoreInvokeRuntime"
348
+ Effect = "Allow"
349
+ Action = ["bedrock-agentcore:InvokeAgentRuntime"]
350
+ Resource = "arn:aws:bedrock-agentcore:${var.region}:${var.account_id}:runtime/*"
351
+ },
352
+ {
353
+ Sid = "AgentCoreEvaluate"
354
+ Effect = "Allow"
355
+ Action = [
356
+ "bedrock-agentcore:Evaluate",
357
+ "bedrock-agentcore:GetEvaluator",
358
+ "bedrock-agentcore:ListEvaluators",
359
+ ]
360
+ Resource = "*"
361
+ },
362
+ {
363
+ Sid = "EvalSpansRead"
364
+ Effect = "Allow"
365
+ Action = [
366
+ "logs:FilterLogEvents",
367
+ "logs:GetLogEvents",
368
+ "logs:DescribeLogGroups",
369
+ "logs:DescribeLogStreams",
370
+ ]
371
+ Resource = [
372
+ "arn:aws:logs:${var.region}:${var.account_id}:log-group:aws/spans",
373
+ "arn:aws:logs:${var.region}:${var.account_id}:log-group:aws/spans:*",
374
+ "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*",
375
+ "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:*",
376
+ ]
377
+ },
378
+ {
379
+ Sid = "SsmReadEvalRunnerCfg"
380
+ Effect = "Allow"
381
+ Action = ["ssm:GetParameter", "ssm:GetParameters"]
382
+ Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/agentcore/runtime-id-*"
383
+ },
384
+ ]
385
+ })
386
+ }
387
+
255
388
  # AgentCore Memory read access for the GraphQL memory resolvers.
256
389
  # memoryRecords / memorySearch call ListMemoryRecordsCommand to fetch
257
390
  # records across the tenant's agents.
@@ -291,14 +424,33 @@ resource "aws_iam_role_policy" "lambda_ssm_read" {
291
424
 
292
425
  policy = jsonencode({
293
426
  Version = "2012-10-17"
294
- Statement = [{
295
- Effect = "Allow"
296
- Action = [
297
- "ssm:GetParameter",
298
- "ssm:GetParameters",
299
- ]
300
- Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/*"
301
- }]
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
+ ]
302
454
  })
303
455
  }
304
456
 
@@ -362,11 +514,143 @@ resource "aws_iam_role_policy" "lambda_api_cross_invoke" {
362
514
  "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-chat-agent-invoke",
363
515
  "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-knowledge-base-manager",
364
516
  "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-job-schedule-manager",
517
+ # eval-runner: graphql-http's startEvalRun mutation Event-invokes
518
+ # this asynchronously after inserting the eval_runs row.
519
+ "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-eval-runner",
520
+ # wiki-compile: memory-retain Event-invokes this after a successful
521
+ # retainTurn when the tenant's wiki_compile_enabled flag is on.
522
+ # compileWikiNow admin mutation also Event-invokes.
523
+ "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-wiki-compile",
524
+ # wiki-bootstrap-import: bootstrapJournalImport admin mutation
525
+ # Event-invokes this for the long-running ingest path.
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",
365
536
  ]
366
537
  }]
367
538
  })
368
539
  }
369
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
+
370
654
  ################################################################################
371
655
  # Placeholder Lambda — proves the infrastructure works
372
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
+ }