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.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/dist/cli.js +1187 -315
- package/dist/terraform/examples/greenfield/main.tf +325 -19
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
- package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
- package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
- package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
- package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
- package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
- package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
- package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
- package/dist/terraform/modules/app/static-site/main.tf +146 -5
- package/dist/terraform/modules/app/www-dns/main.tf +118 -15
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
- package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
- package/dist/terraform/modules/thinkwork/main.tf +439 -21
- package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
- package/dist/terraform/modules/thinkwork/variables.tf +153 -2
- package/dist/terraform/schema.graphql +17 -0
- 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
|
|
12
|
+
# - Thread, agent, and template CRUD
|
|
13
13
|
# - Skills, KB, memory handlers
|
|
14
|
-
# -
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
}
|