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
@@ -0,0 +1,93 @@
1
+ ################################################################################
2
+ # Compliance Audit Bucket — Variables
3
+ ################################################################################
4
+
5
+ variable "stage" {
6
+ description = "Deployment stage"
7
+ type = string
8
+ }
9
+
10
+ variable "account_id" {
11
+ description = "AWS account ID. Used in the anchor Lambda role's trust policy as an aws:SourceAccount condition for confused-deputy defense — even if a malicious principal somehow obtained the role's ARN, they would need to assume it from inside this account."
12
+ type = string
13
+ }
14
+
15
+ variable "region" {
16
+ description = "AWS region. Used in the anchor Lambda role's trust-policy aws:SourceArn pin to constrain AssumeRole to the predictable function ARN `arn:aws:lambda:{region}:{account_id}:function:thinkwork-{stage}-api-compliance-anchor`. Required. Whitespace is rejected (a trailing space silently produces a malformed ARN that never matches at AssumeRole time)."
17
+ type = string
18
+
19
+ validation {
20
+ condition = length(var.region) > 0 && var.region == trimspace(var.region)
21
+ error_message = "region must be non-empty and free of leading/trailing whitespace. A whitespace-padded value silently produces a malformed aws:SourceArn that never matches."
22
+ }
23
+ }
24
+
25
+ variable "compliance_reader_secret_arn" {
26
+ description = "ARN of the Secrets Manager secret holding the `compliance_reader` Aurora role credentials (Phase 3 U2). The U8a anchor Lambda's anchor_secrets policy enumerates this ARN explicitly."
27
+ type = string
28
+
29
+ validation {
30
+ condition = length(var.compliance_reader_secret_arn) > 0
31
+ error_message = "compliance_reader_secret_arn must be non-empty."
32
+ }
33
+ }
34
+
35
+ variable "compliance_drainer_secret_arn" {
36
+ description = "ARN of the Secrets Manager secret holding the `compliance_drainer` Aurora role credentials (Phase 3 U2 / PR #887). The U8a anchor Lambda uses this to UPDATE compliance.tenant_anchor_state."
37
+ type = string
38
+
39
+ validation {
40
+ condition = length(var.compliance_drainer_secret_arn) > 0
41
+ error_message = "compliance_drainer_secret_arn must be non-empty."
42
+ }
43
+ }
44
+
45
+ variable "bucket_name" {
46
+ description = "Name of the compliance audit-anchor S3 bucket (master-plan canonical: thinkwork-{stage}-compliance-anchors)"
47
+ type = string
48
+ }
49
+
50
+ variable "kms_key_arn" {
51
+ description = "ARN of the customer-managed KMS key used for SSE-KMS encryption of anchor objects. Must be non-empty - wired from module.kms.key_arn at the composite root."
52
+ type = string
53
+
54
+ validation {
55
+ condition = length(var.kms_key_arn) > 0
56
+ error_message = "kms_key_arn must be non-empty. Check that module.kms is enabled in the composite root (var.create_kms_key = true)."
57
+ }
58
+ }
59
+
60
+ variable "mode" {
61
+ description = "S3 Object Lock retention mode. GOVERNANCE allows a privileged role with s3:BypassGovernanceRetention to delete or shorten retention; COMPLIANCE is irreversible (even AWS root cannot delete or shorten until retention expires). Default GOVERNANCE per master plan Decision #2; flip to COMPLIANCE in prod via tfvars at audit-engagement time."
62
+ type = string
63
+ default = "GOVERNANCE"
64
+
65
+ validation {
66
+ condition = contains(["GOVERNANCE", "COMPLIANCE"], var.mode)
67
+ error_message = "mode must be either GOVERNANCE or COMPLIANCE."
68
+ }
69
+ }
70
+
71
+ variable "retention_days" {
72
+ description = "Default Object Lock retention in days, applied to every PutObject under the anchors/ prefix unless an explicit per-object retention overrides it. SOC2 Type 1 baseline is 12 months (365)."
73
+ type = number
74
+ default = 365
75
+
76
+ validation {
77
+ condition = var.retention_days > 0
78
+ error_message = "retention_days must be greater than 0."
79
+ }
80
+ }
81
+
82
+ # Phase 3 U8b — explicit override for COMPLIANCE-mode in non-prod stages.
83
+ # COMPLIANCE mode is irreversible; once any object is written under it, that
84
+ # object is undeleteable for the full retention window — even AWS root cannot
85
+ # remove it. Defaulting to false keeps a "stage typo" (stage = "prod-canary"
86
+ # instead of "prod") from silently producing an unrecoverable dev bucket. The
87
+ # operator must set this true in tfvars at audit-engagement time on the
88
+ # specific non-prod stage that should run COMPLIANCE.
89
+ variable "allow_compliance_in_non_prod" {
90
+ description = "Permit var.mode = COMPLIANCE on non-prod stages. Default false. Set true ONLY at audit-engagement time on the specific non-prod stage where COMPLIANCE is intended; the bucket's WORM bytes are unrecoverable once retention starts."
91
+ type = bool
92
+ default = false
93
+ }
@@ -0,0 +1,269 @@
1
+ ################################################################################
2
+ # Compliance Exports Bucket — Data Module (U11.U2)
3
+ #
4
+ # Ephemeral S3 bucket for SOC2 walkthrough export artifacts produced by the
5
+ # U11 export runner Lambda. Distinct from the WORM-protected
6
+ # compliance-audit-bucket: this is a 7-day-lifecycle short-lived artifact
7
+ # bucket; nothing here is the system of record. The system of record is
8
+ # `compliance.audit_events` in Aurora; the runner just rematerializes
9
+ # filtered slices into CSV/NDJSON for auditor download.
10
+ #
11
+ # Why not Object Lock:
12
+ # - Exports are derivable from the Aurora rows + the runner code; losing
13
+ # an export does not lose evidence.
14
+ # - Object Lock + 7-day retention is incoherent (the lock makes the
15
+ # object undeleteable for 7 days then it does become deletable, which
16
+ # is just a reverse-engineered version of expiration with extra cost).
17
+ # - Auditors want a presigned URL that works for ~15 minutes; the bucket
18
+ # itself is plumbing, not a trust anchor.
19
+ #
20
+ # Why versioning suspended:
21
+ # - Export artifacts are write-once-by-name (key includes jobId UUID); a
22
+ # second write would imply a runner bug, not a legitimate update. We
23
+ # prefer the second write to fail noisily on a precondition rather than
24
+ # silently produce a v2 object.
25
+ #
26
+ # Bucket policy (defense-in-depth):
27
+ # - EnforceHTTPS: deny any s3:* over plain HTTP.
28
+ #
29
+ # IAM role (runner Lambda's eventual identity, defined below):
30
+ # - Allow s3:PutObject + s3:GetObject + s3:GetObjectAttributes +
31
+ # s3:AbortMultipartUpload on ${bucket}/* (any prefix; runner picks).
32
+ # - Allow s3:ListBucket on ${bucket} (multipart upload listing).
33
+ # - Explicit Deny on every other S3 ARN (NotResource defense).
34
+ #
35
+ # Inert seam (U11.U2):
36
+ # - Module ships the role; the standalone Lambda function in
37
+ # terraform/modules/app/lambda-api/handlers.tf assumes this role.
38
+ # - The Lambda body is a stub that throws "not implemented" —
39
+ # U11.U3 swaps in the live runner.
40
+ ################################################################################
41
+
42
+ resource "aws_s3_bucket" "exports" {
43
+ bucket = var.bucket_name
44
+
45
+ # Hardcoded false — even though objects expire after 7 days, an
46
+ # accidental terraform destroy on a bucket holding in-flight export
47
+ # artifacts would interrupt an auditor mid-download.
48
+ force_destroy = false
49
+
50
+ tags = {
51
+ Name = var.bucket_name
52
+ Stage = var.stage
53
+ Purpose = "compliance-exports"
54
+ Expiration = "${var.expiration_days}d"
55
+ }
56
+ }
57
+
58
+ resource "aws_s3_bucket_versioning" "exports" {
59
+ bucket = aws_s3_bucket.exports.id
60
+
61
+ # Suspended — write-once-by-jobId; we don't want v1/v2 of the same key.
62
+ versioning_configuration {
63
+ status = "Suspended"
64
+ }
65
+ }
66
+
67
+ resource "aws_s3_bucket_server_side_encryption_configuration" "exports" {
68
+ bucket = aws_s3_bucket.exports.id
69
+
70
+ rule {
71
+ apply_server_side_encryption_by_default {
72
+ sse_algorithm = "AES256"
73
+ }
74
+ }
75
+ }
76
+
77
+ resource "aws_s3_bucket_public_access_block" "exports" {
78
+ bucket = aws_s3_bucket.exports.id
79
+
80
+ block_public_acls = true
81
+ block_public_policy = true
82
+ ignore_public_acls = true
83
+ restrict_public_buckets = true
84
+ }
85
+
86
+ resource "aws_s3_bucket_lifecycle_configuration" "exports" {
87
+ bucket = aws_s3_bucket.exports.id
88
+
89
+ rule {
90
+ id = "exports-expiration"
91
+ status = "Enabled"
92
+
93
+ # Apply to every object — exports always live in keyed prefixes the
94
+ # runner picks, but the lifecycle is uniform across the bucket.
95
+ filter {}
96
+
97
+ expiration {
98
+ days = var.expiration_days
99
+ }
100
+
101
+ # Belt-and-suspenders for any failed multipart uploads — abort after
102
+ # 1 day so we don't accumulate orphaned upload IDs.
103
+ abort_incomplete_multipart_upload {
104
+ days_after_initiation = 1
105
+ }
106
+ }
107
+ }
108
+
109
+ resource "aws_s3_bucket_policy" "exports" {
110
+ bucket = aws_s3_bucket.exports.id
111
+
112
+ depends_on = [aws_s3_bucket_public_access_block.exports]
113
+
114
+ policy = jsonencode({
115
+ Version = "2012-10-17"
116
+ Statement = [
117
+ {
118
+ Sid = "EnforceHTTPS"
119
+ Effect = "Deny"
120
+ Principal = "*"
121
+ Action = "s3:*"
122
+ Resource = [
123
+ aws_s3_bucket.exports.arn,
124
+ "${aws_s3_bucket.exports.arn}/*",
125
+ ]
126
+ Condition = {
127
+ Bool = {
128
+ "aws:SecureTransport" = "false"
129
+ }
130
+ }
131
+ },
132
+ ]
133
+ })
134
+ }
135
+
136
+ ################################################################################
137
+ # IAM role for the runner Lambda (U11.U3 will assume this role; in U11.U2
138
+ # the standalone Lambda function references this role's ARN, but its body
139
+ # is the inert stub).
140
+ ################################################################################
141
+
142
+ resource "aws_iam_role" "runner_lambda" {
143
+ name = "thinkwork-${var.stage}-compliance-export-runner-role"
144
+
145
+ # Trust policy: Lambda service principal + account-pin only.
146
+ #
147
+ # We do NOT pin `aws:SourceArn` to the function ARN. The runner is
148
+ # invoked via `aws_lambda_event_source_mapping` (SQS → Lambda); when
149
+ # AWS Lambda calls `sts:AssumeRole` to validate the mapping at
150
+ # CreateEventSourceMapping time, the `aws:SourceArn` context key is
151
+ # the SQS queue ARN, not the Lambda function ARN. Pinning to the
152
+ # function ARN — even with `StringEqualsIfExists` — caused "Please
153
+ # add Lambda as a Trusted Entity for ..." failures
154
+ # (deploy runs 25557118131 and 25560679065).
155
+ #
156
+ # `aws:SourceAccount` strict-equals is the substantive confused-deputy
157
+ # guard — even if the role ARN leaks, only this account can use it.
158
+ # The Lambda service principal restriction in `Principal` keeps
159
+ # non-Lambda services from assuming.
160
+ #
161
+ # The anchor Lambda role (compliance-audit-bucket) keeps a strict
162
+ # SourceArn pin to the function ARN because it's scheduler-triggered;
163
+ # EventBridge Scheduler always passes the function ARN as SourceArn.
164
+ # SQS event source mapping does not — that's what makes this role
165
+ # different.
166
+ assume_role_policy = jsonencode({
167
+ Version = "2012-10-17"
168
+ Statement = [{
169
+ Effect = "Allow"
170
+ Principal = { Service = "lambda.amazonaws.com" }
171
+ Action = "sts:AssumeRole"
172
+ Condition = {
173
+ StringEquals = {
174
+ "aws:SourceAccount" = var.account_id
175
+ }
176
+ }
177
+ }]
178
+ })
179
+ }
180
+
181
+ resource "aws_iam_role_policy_attachment" "runner_basic" {
182
+ role = aws_iam_role.runner_lambda.name
183
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
184
+ }
185
+
186
+ resource "aws_iam_role_policy" "runner_s3_allow" {
187
+ name = "exports-s3-allow"
188
+ role = aws_iam_role.runner_lambda.id
189
+
190
+ policy = jsonencode({
191
+ Version = "2012-10-17"
192
+ Statement = [
193
+ {
194
+ Sid = "ExportsObjectsAllow"
195
+ Effect = "Allow"
196
+ Action = [
197
+ "s3:PutObject",
198
+ "s3:GetObject",
199
+ "s3:GetObjectAttributes",
200
+ "s3:AbortMultipartUpload",
201
+ ]
202
+ Resource = "${aws_s3_bucket.exports.arn}/*"
203
+ },
204
+ {
205
+ Sid = "ExportsBucketAllow"
206
+ Effect = "Allow"
207
+ Action = [
208
+ "s3:ListBucket",
209
+ "s3:ListBucketMultipartUploads",
210
+ "s3:GetBucketLocation",
211
+ ]
212
+ Resource = aws_s3_bucket.exports.arn
213
+ },
214
+ ]
215
+ })
216
+ }
217
+
218
+ resource "aws_iam_role_policy" "runner_secrets" {
219
+ count = length(var.database_secret_arn) > 0 ? 1 : 0
220
+ name = "exports-runner-secrets"
221
+ role = aws_iam_role.runner_lambda.id
222
+
223
+ # The runner reads the writer-pool DB credentials at module-load via
224
+ # GetSecretValue. Without this grant the runner throws AccessDenied
225
+ # at the first SQS-triggered invocation (deploy run 25561658625
226
+ # failed the smoke gate with this exact error). Scoped to the single
227
+ # secret ARN — no wildcard.
228
+ policy = jsonencode({
229
+ Version = "2012-10-17"
230
+ Statement = [
231
+ {
232
+ Sid = "RunnerDatabaseSecretRead"
233
+ Effect = "Allow"
234
+ Action = ["secretsmanager:GetSecretValue"]
235
+ Resource = var.database_secret_arn
236
+ },
237
+ ]
238
+ })
239
+ }
240
+
241
+ resource "aws_iam_role_policy" "runner_s3_deny_other_buckets" {
242
+ name = "exports-s3-deny-other-buckets"
243
+ role = aws_iam_role.runner_lambda.id
244
+
245
+ # Defense-in-depth: even if a future inline-policy attachment or AWS-
246
+ # managed-policy widens the role's S3 grants, this explicit deny
247
+ # restricts the runner to the exports bucket only. NotResource ensures
248
+ # the deny applies to every S3 ARN that ISN'T the exports bucket.
249
+ policy = jsonencode({
250
+ Version = "2012-10-17"
251
+ Statement = [
252
+ {
253
+ Sid = "DenyAllOtherS3Buckets"
254
+ Effect = "Deny"
255
+ Action = [
256
+ "s3:PutObject",
257
+ "s3:GetObject",
258
+ "s3:DeleteObject",
259
+ "s3:ListBucket",
260
+ "s3:AbortMultipartUpload",
261
+ ]
262
+ NotResource = [
263
+ aws_s3_bucket.exports.arn,
264
+ "${aws_s3_bucket.exports.arn}/*",
265
+ ]
266
+ },
267
+ ]
268
+ })
269
+ }
@@ -0,0 +1,23 @@
1
+ ################################################################################
2
+ # Compliance Exports Bucket — Outputs
3
+ ################################################################################
4
+
5
+ output "bucket_name" {
6
+ description = "Name (id) of the compliance exports S3 bucket."
7
+ value = aws_s3_bucket.exports.id
8
+ }
9
+
10
+ output "bucket_arn" {
11
+ description = "ARN of the compliance exports S3 bucket."
12
+ value = aws_s3_bucket.exports.arn
13
+ }
14
+
15
+ output "runner_role_arn" {
16
+ description = "ARN of the IAM role the U11 export runner Lambda assumes. Inert in U11.U2 — the function exists with a stub body until U11.U3."
17
+ value = aws_iam_role.runner_lambda.arn
18
+ }
19
+
20
+ output "runner_role_name" {
21
+ description = "Name of the IAM role the runner Lambda assumes. Used by sibling app-tier modules that need to attach inline policies (e.g., DLQ SendMessage, SQS receive) to this role without re-deriving the name from the ARN."
22
+ value = aws_iam_role.runner_lambda.name
23
+ }
@@ -0,0 +1,50 @@
1
+ ################################################################################
2
+ # Compliance Exports Bucket — Variables
3
+ ################################################################################
4
+
5
+ variable "stage" {
6
+ description = "Deployment stage."
7
+ type = string
8
+ }
9
+
10
+ variable "account_id" {
11
+ description = "AWS account ID. Pinned in the runner Lambda's trust policy as aws:SourceAccount for confused-deputy defense."
12
+ type = string
13
+ }
14
+
15
+ variable "region" {
16
+ description = "AWS region. Used in the runner Lambda's trust-policy aws:SourceArn pin to constrain AssumeRole to the predictable function ARN `arn:aws:lambda:{region}:{account_id}:function:thinkwork-{stage}-api-compliance-export-runner`."
17
+ type = string
18
+
19
+ validation {
20
+ condition = length(var.region) > 0 && var.region == trimspace(var.region)
21
+ error_message = "region must be non-empty and free of leading/trailing whitespace."
22
+ }
23
+ }
24
+
25
+ variable "bucket_name" {
26
+ description = "Name of the compliance exports S3 bucket. Canonical pattern: `thinkwork-{stage}-compliance-exports`."
27
+ type = string
28
+
29
+ validation {
30
+ condition = length(var.bucket_name) > 0
31
+ error_message = "bucket_name must be non-empty."
32
+ }
33
+ }
34
+
35
+ variable "database_secret_arn" {
36
+ description = "ARN of the Secrets Manager secret holding the writer-pool DB credentials. The runner Lambda reads this at module-load to construct the Aurora connection string for INSERT/UPDATE on compliance.export_jobs and SELECT on compliance.audit_events. Default empty so existing module consumers don't break before they wire it; the runner throws a deterministic error if unset at runtime."
37
+ type = string
38
+ default = ""
39
+ }
40
+
41
+ variable "expiration_days" {
42
+ description = "Number of days an export object lives before lifecycle expiration. SOC2 walkthrough auditors typically download artifacts within hours; 7 days is the audit-window default."
43
+ type = number
44
+ default = 7
45
+
46
+ validation {
47
+ condition = var.expiration_days >= 1 && var.expiration_days <= 90
48
+ error_message = "expiration_days must be between 1 and 90 inclusive."
49
+ }
50
+ }
@@ -0,0 +1,123 @@
1
+ ################################################################################
2
+ # S3 Backups Bucket — Data Module
3
+ #
4
+ # Separate bucket for pre-destructive-migration CSV exports and other
5
+ # operational snapshots. Distinct from the primary storage bucket because:
6
+ #
7
+ # - No CORS (client browsers never read this bucket)
8
+ # - Block public access fully (public-access tickets cannot accidentally
9
+ # grant access via aws_s3_bucket_public_access_block)
10
+ # - Server-side encryption on by default
11
+ # - Lifecycle rule auto-expires `pre-drop/` objects after 90 days so cost
12
+ # stays bounded without a separate sweeper
13
+ # - HTTPS-only bucket policy
14
+ #
15
+ # Used by:
16
+ # - packages/database-pg/drizzle/0027_thread_cleanup_drops.sql (U5 of the
17
+ # thread-detail cleanup plan) via `aws_s3.query_export_to_s3` calls.
18
+ # - Any future destructive migration that wants a pre-apply row-data
19
+ # snapshot.
20
+ #
21
+ # Pairs with an IAM role on the Aurora cluster (see
22
+ # `terraform/modules/data/aurora-postgres/main.tf`) so the cluster can
23
+ # PutObject directly without credentials in the SQL file.
24
+ ################################################################################
25
+
26
+ variable "stage" {
27
+ description = "Deployment stage"
28
+ type = string
29
+ }
30
+
31
+ variable "bucket_name" {
32
+ description = "Name of the S3 backups bucket"
33
+ type = string
34
+ }
35
+
36
+ resource "aws_s3_bucket" "backups" {
37
+ bucket = var.bucket_name
38
+
39
+ tags = {
40
+ Name = var.bucket_name
41
+ Stage = var.stage
42
+ # Identifies the bucket's purpose so operators can spot accidental reads
43
+ # in CloudTrail and so cost-allocation tags split backups out of the
44
+ # primary storage bucket.
45
+ Purpose = "operational-backups"
46
+ }
47
+ }
48
+
49
+ resource "aws_s3_bucket_public_access_block" "backups" {
50
+ bucket = aws_s3_bucket.backups.id
51
+
52
+ block_public_acls = true
53
+ block_public_policy = true
54
+ ignore_public_acls = true
55
+ restrict_public_buckets = true
56
+ }
57
+
58
+ resource "aws_s3_bucket_server_side_encryption_configuration" "backups" {
59
+ bucket = aws_s3_bucket.backups.id
60
+
61
+ rule {
62
+ apply_server_side_encryption_by_default {
63
+ sse_algorithm = "AES256"
64
+ }
65
+ }
66
+ }
67
+
68
+ resource "aws_s3_bucket_lifecycle_configuration" "backups" {
69
+ bucket = aws_s3_bucket.backups.id
70
+
71
+ rule {
72
+ id = "expire-pre-drop-snapshots"
73
+ status = "Enabled"
74
+
75
+ filter {
76
+ prefix = "pre-drop/"
77
+ }
78
+
79
+ expiration {
80
+ days = 90
81
+ }
82
+ }
83
+ }
84
+
85
+ resource "aws_s3_bucket_policy" "backups_https_only" {
86
+ bucket = aws_s3_bucket.backups.id
87
+
88
+ # aws_s3_bucket_public_access_block must apply first; otherwise account-
89
+ # level BPA defaults can intermittently reject the bucket-policy PUT with
90
+ # AccessDenied during initial apply.
91
+ depends_on = [aws_s3_bucket_public_access_block.backups]
92
+
93
+ policy = jsonencode({
94
+ Version = "2012-10-17"
95
+ Statement = [
96
+ {
97
+ Sid = "EnforceHTTPS"
98
+ Effect = "Deny"
99
+ Principal = "*"
100
+ Action = "s3:*"
101
+ Resource = [
102
+ aws_s3_bucket.backups.arn,
103
+ "${aws_s3_bucket.backups.arn}/*",
104
+ ]
105
+ Condition = {
106
+ Bool = {
107
+ "aws:SecureTransport" = "false"
108
+ }
109
+ }
110
+ },
111
+ ]
112
+ })
113
+ }
114
+
115
+ output "bucket_name" {
116
+ description = "Name of the S3 backups bucket"
117
+ value = aws_s3_bucket.backups.id
118
+ }
119
+
120
+ output "bucket_arn" {
121
+ description = "ARN of the S3 backups bucket"
122
+ value = aws_s3_bucket.backups.arn
123
+ }
@@ -35,6 +35,14 @@ resource "aws_s3_bucket" "main" {
35
35
  }
36
36
  }
37
37
 
38
+ resource "aws_s3_bucket_versioning" "main" {
39
+ bucket = aws_s3_bucket.main.id
40
+
41
+ versioning_configuration {
42
+ status = "Enabled"
43
+ }
44
+ }
45
+
38
46
  resource "aws_s3_bucket_cors_configuration" "main" {
39
47
  bucket = aws_s3_bucket.main.id
40
48
 
@@ -47,6 +55,11 @@ resource "aws_s3_bucket_cors_configuration" "main" {
47
55
  }
48
56
  }
49
57
 
58
+ resource "aws_s3_bucket_notification" "eventbridge" {
59
+ bucket = aws_s3_bucket.main.id
60
+ eventbridge = true
61
+ }
62
+
50
63
  resource "aws_s3_bucket_policy" "https_only" {
51
64
  bucket = aws_s3_bucket.main.id
52
65
 
@@ -107,20 +107,23 @@ variable "admin_logout_urls" {
107
107
  }
108
108
 
109
109
  variable "mobile_callback_urls" {
110
- description = "OAuth callback URLs for the mobile client"
110
+ description = "OAuth callback URLs for the mobile client. Host apps that embed the SDK register their own deep-link here. Proper per-host app client isolation is 0.3.0 work — this is the stopgap capture of the drift from the CLI-applied URIs."
111
111
  type = list(string)
112
112
  default = [
113
113
  "exp://localhost:8081",
114
114
  "thinkwork://",
115
115
  "thinkwork://auth/callback",
116
+ "myapp://",
117
+ "myapp://oauth/callback",
116
118
  ]
117
119
  }
118
120
 
119
121
  variable "mobile_logout_urls" {
120
- description = "OAuth logout URLs for the mobile client"
122
+ description = "OAuth logout URLs for the mobile client. Host apps that embed the SDK register their own deep-link here (see `mobile_callback_urls` for rationale)."
121
123
  type = list(string)
122
124
  default = [
123
125
  "exp://localhost:8081",
124
126
  "thinkwork://",
127
+ "myapp://",
125
128
  ]
126
129
  }