@reventlessdev/reventless-aws 3.0.0-alpha.319 → 3.0.0-alpha.321

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 (47) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +12 -8
  3. package/rescript.json +4 -0
  4. package/run-provision-identity.mjs +3 -0
  5. package/scripts/ProvisionIdentity.res +303 -0
  6. package/scripts/ProvisionIdentity.res.mjs +376 -0
  7. package/src/Platform.res +54 -30
  8. package/src/Platform.res.mjs +34 -32
  9. package/src/Platform_Stack.res +102 -19
  10. package/src/Platform_Stack.res.mjs +43 -7
  11. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +267 -32
  12. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +124 -14
  13. package/src/adapter/Auth/Auth_ActiveRoleStore.res +93 -5
  14. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +53 -4
  15. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +50 -4
  16. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +24 -3
  17. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res +92 -0
  18. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs +49 -0
  19. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +21 -7
  20. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +15 -7
  21. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +34 -18
  22. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +14 -4
  23. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +29 -14
  24. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs +11 -1
  25. package/src/util/Util_AwsError.res +56 -0
  26. package/src/util/Util_AwsError.res.mjs +59 -0
  27. package/src/util/Util_ShellConfig.res +28 -0
  28. package/src/util/Util_ShellConfig.res.mjs +22 -0
  29. package/tests/AppSync_RetirementNarrowingTest.res +10 -12
  30. package/tests/AppSync_RetirementNarrowingTest.res.mjs +4 -2
  31. package/tests/Auth_ActiveRolePoolAttachmentTest.res +175 -0
  32. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +136 -0
  33. package/tests/Auth_ActiveRoleStoreTest.res +65 -0
  34. package/tests/Auth_ActiveRoleStoreTest.res.mjs +48 -0
  35. package/tests/Auth_ActiveRoleStore_SchemaTest.res +98 -0
  36. package/tests/Auth_ActiveRoleStore_SchemaTest.res.mjs +97 -0
  37. package/tests/AwsErrorFixtures.mjs +16 -0
  38. package/tests/ProvisionIdentityTest.res +109 -0
  39. package/tests/ProvisionIdentityTest.res.mjs +132 -0
  40. package/tests/QueryDbOwnerIndexTableTest.res +53 -0
  41. package/tests/QueryDbOwnerIndexTableTest.res.mjs +50 -0
  42. package/tests/QueryDbResolvers_AppSyncTest.res +19 -26
  43. package/tests/QueryDbResolvers_AppSyncTest.res.mjs +15 -17
  44. package/tests/Util_AwsErrorTest.res +127 -0
  45. package/tests/Util_AwsErrorTest.res.mjs +76 -0
  46. package/tests/Util_ShellConfigTest.res +51 -0
  47. package/tests/Util_ShellConfigTest.res.mjs +42 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.321 (2026-08-23)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** serve a full page from a filtered AppSync read ([b7dca5d](https://github.com/ReventlessDev/reventless-core/commit/b7dca5dac02e8c35c4848828152b90ade0f3c8ba))
11
+ ### Features
12
+
13
+ * **querydb:** serve owner-scoped lists from an index instead of a filtered Scan ([ad26c28](https://github.com/ReventlessDev/reventless-core/commit/ad26c284b4b0650fa3f3b6109ed33bbfe8608f6d))
14
+
15
+
16
+ # 3.0.0-alpha.320 (2026-08-23)
17
+
18
+ ### Bug Fixes
19
+
20
+ * **aws:** make the identity provisioning script reachable and legible ([a5e7059](https://github.com/ReventlessDev/reventless-core/commit/a5e7059044cf8ab65a1dea92b0d70cbd6d306717))
21
+ * feat(aws)!: scope the active-role store to the identity provider ([08e287d](https://github.com/ReventlessDev/reventless-core/commit/08e287db9721977b818bad76671ec4c62be6f9af))
22
+ ### Features
23
+
24
+ * **aws:** publish identity names alongside the cognito ones ([e240d4f](https://github.com/ReventlessDev/reventless-core/commit/e240d4f01fbda20b71edf822596e634b0d4cdbc0))
25
+
26
+ ### BREAKING CHANGES
27
+
28
+ * platform:cognitoUserPoolId is now
29
+ platform:identityProviderId (REVENTLESS_IDENTITY_PROVIDER_ID). The old
30
+ spelling is still read and warns — do not drop it until the new secret
31
+ is in place, because an unset provider id is auto mode, and auto mode
32
+ creates a new user pool.
33
+
34
+
35
+
6
36
  # 3.0.0-alpha.319 (2026-08-22)
7
37
 
8
38
  * feat(core)!: a slice that has given up says so ([f5559ab](https://github.com/ReventlessDev/reventless-core/commit/f5559abede3a9cf2c9a48aadc69a666c46cc75bf))
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.319",
3
+ "version": "3.0.0-alpha.321",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
+ "bin": {
7
+ "provision-identity": "./run-provision-identity.mjs"
8
+ },
6
9
  "dependencies": {
7
10
  "@aws-sdk/client-appsync": "3.970.0",
8
11
  "@aws-sdk/client-cloudfront": "3.970.0",
@@ -12,18 +15,18 @@
12
15
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
16
  "sury": "11.0.0-rc.2",
14
17
  "uuid": "^13.0.0",
15
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
18
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.13",
16
19
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.2",
20
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.4",
19
21
  "@reventlessdev/rescript-node": "2.0.0-alpha.8",
22
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
20
23
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
- "@reventlessdev/reventless-core": "3.0.0-alpha.247",
22
24
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.150",
25
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.151",
24
26
  "@reventlessdev/reventless-interop": "3.0.0-alpha.33",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.122",
26
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.111"
27
+ "@reventlessdev/reventless-core": "3.0.0-alpha.249",
28
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.113",
29
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.123"
27
30
  },
28
31
  "devDependencies": {
29
32
  "rescript": "12.3.0",
@@ -69,6 +72,7 @@
69
72
  "clean": "rescript clean",
70
73
  "rebuild": "pnpm run clean && pnpm run build",
71
74
  "deploy": "./scripts/deploy.sh",
75
+ "provision:identity": "node ./run-provision-identity.mjs",
72
76
  "test": "NODE_OPTIONS='--experimental-vm-modules' npx jest",
73
77
  "coverage": "nyc report --temp-directory=coverage --reporter=text-lcov | coveralls",
74
78
  "dev": "jest --watchAll",
package/rescript.json CHANGED
@@ -19,6 +19,10 @@
19
19
  {
20
20
  "dir": "tests",
21
21
  "subdirs": true
22
+ },
23
+ {
24
+ "dir": "scripts",
25
+ "subdirs": false
22
26
  }
23
27
  ],
24
28
  "dependencies": [
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./scripts/ProvisionIdentity.res.mjs"
3
+ await main()
@@ -0,0 +1,303 @@
1
+ /***
2
+ Provision an identity provider that no Pulumi stack owns, and the active-role
3
+ store that belongs to it.
4
+
5
+ This is the BYO half of `Platform_Stack.resolveCognitoUserPool`. A stack given
6
+ `platform:identityProviderId` creates neither the pool nor the store — it looks
7
+ both up, and fails if either is missing. Something has to make them, once,
8
+ outside every stack. That is this.
9
+
10
+ 🚨 **It does not attach a pre-token-generation trigger, and must not.** The pool's
11
+ one trigger slot is the deploy's business: a script that filled it would be the
12
+ hand-run `UpdateUserPool` this whole area refuses, leaving the deployment's
13
+ behaviour depending on an act no source describes.
14
+
15
+ The store's name and key schema come from [Auth_ActiveRoleStore_Schema] rather
16
+ than from constants here. A second copy of either would be one edit away from
17
+ creating a table the deploy does not look for — which surfaces as "table not
18
+ found" long after whoever ran this has moved on.
19
+
20
+ Reached as the `provision-identity` bin, from any package depending on
21
+ `@reventlessdev/reventless-aws` — an app's `platform-aws`, typically:
22
+
23
+ ```
24
+ pnpm exec provision-identity --name MyIdentity
25
+ pnpm exec provision-identity --provider-id eu-west-1_AbCdEfGhI
26
+ ```
27
+
28
+ Re-running is safe: existing resources are adopted and reported, never recreated.
29
+ The first thing anyone does with a provisioning script is run it twice.
30
+ */
31
+
32
+ open AwsSdk
33
+
34
+ module Schema = Auth_ActiveRoleStore_Schema
35
+
36
+ // ── Arguments ────────────────────────────────────────────────────────────────
37
+
38
+ type args = {
39
+ poolName: string,
40
+ providerId: option<string>,
41
+ help: bool,
42
+ }
43
+
44
+ let defaultPoolName = "ReventlessIdentity"
45
+
46
+ /** Parsed rather than positional, and unknown flags are an error: a typo'd
47
+ `--provider-id` that parsed as "absent" would create a *second* pool beside the
48
+ one the operator meant to extend. */
49
+ let parseArgs = (argv: array<string>): result<args, string> => {
50
+ let acc = ref(Ok({poolName: defaultPoolName, providerId: None, help: false}))
51
+ let i = ref(0)
52
+ let count = argv->Array.length
53
+ while i.contents < count {
54
+ let flag = argv->Array.getUnsafe(i.contents)
55
+ let value = argv->Array.get(i.contents + 1)
56
+ switch (acc.contents, flag, value) {
57
+ | (Error(_), _, _) => i := count
58
+ | (Ok(a), "--name", Some(v)) =>
59
+ acc := Ok({...a, poolName: v})
60
+ i := i.contents + 2
61
+ | (Ok(a), "--provider-id", Some(v)) =>
62
+ acc := Ok({...a, providerId: Some(v)})
63
+ i := i.contents + 2
64
+ | (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
65
+ acc := Ok({...a, help: true})
66
+ i := i.contents + 1
67
+ | (Ok(_), "--name", None) | (Ok(_), "--provider-id", None) =>
68
+ acc := Error(`${flag} needs a value`)
69
+ | (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
70
+ }
71
+ }
72
+ acc.contents
73
+ }
74
+
75
+ let usage = `
76
+ Provision a Reventless identity provider and its active-role store.
77
+
78
+ --name <name> Pool name to create or adopt (default: ${defaultPoolName})
79
+ --provider-id <id> Use this existing pool instead of creating one; only the
80
+ store is provisioned
81
+
82
+ Creates nothing that a platform stack owns, and never attaches a trigger.
83
+ Region and credentials come from the environment, as for any AWS SDK call.
84
+ `
85
+
86
+ // ── The pool ─────────────────────────────────────────────────────────────────
87
+
88
+ /** SPA-friendly defaults, matching what auto mode declares: email sign-in, a
89
+ 12-character password policy, no MFA, admin-only user creation. An operator
90
+ wanting something else edits the pool afterwards rather than having this script
91
+ grow a flag per Cognito setting. */
92
+ let poolSettings = (~poolName: string): CognitoIdentityServiceProvider.CreateUserPoolCommand.input => {
93
+ poolName,
94
+ usernameAttributes: ["email"],
95
+ mfaConfiguration: "OFF",
96
+ adminCreateUserConfig: {allowAdminCreateUserOnly: true},
97
+ policies: {
98
+ passwordPolicy: {
99
+ minimumLength: 12,
100
+ requireLowercase: true,
101
+ requireUppercase: true,
102
+ requireNumbers: true,
103
+ },
104
+ },
105
+ }
106
+
107
+ /** The pool with this name, if exactly one has it.
108
+
109
+ Cognito does not enforce unique pool names and offers no lookup by name, so
110
+ adoption means paging the list. Without this a second run silently creates a
111
+ second pool with the same name, and the operator then has two with no way to
112
+ tell from the console which one their stacks are configured for.
113
+
114
+ Two matches is an error rather than a choice — picking either would be a guess
115
+ at which one holds the accounts. */
116
+ let findPoolByName = async (~poolName: string): result<option<string>, string> => {
117
+ let nextToken = ref(None)
118
+ let more = ref(true)
119
+ let found = []
120
+ while more.contents {
121
+ let page = await CognitoIdentityServiceProvider.ListUserPoolsCommand.make({
122
+ maxResults: 60,
123
+ nextToken: ?nextToken.contents,
124
+ })->CognitoIdentityServiceProvider.ListUserPoolsCommand.send
125
+ page.userPools
126
+ ->Option.getOr([])
127
+ ->Array.forEach(p =>
128
+ switch (p.name, p.id) {
129
+ | (Some(name), Some(id)) if name == poolName => found->Array.push(id)
130
+ | _ => ()
131
+ }
132
+ )
133
+ switch page.nextToken {
134
+ | Some(_) as t => nextToken := t
135
+ | None => more := false
136
+ }
137
+ }
138
+ switch found {
139
+ | [] => Ok(None)
140
+ | [only] => Ok(Some(only))
141
+ | several =>
142
+ Error(
143
+ `${several->Array.length->Int.toString} user pools are named "${poolName}" (${several->Array.join(
144
+ ", ",
145
+ )}). Pass --provider-id to say which one to use; this script will not guess.`,
146
+ )
147
+ }
148
+ }
149
+
150
+ let resolvePool = async (~args: args): result<string, string> =>
151
+ switch args.providerId {
152
+ | Some(id) =>
153
+ // Described rather than trusted: the store's name is derived from this id, so
154
+ // a typo would produce a correctly-created table that nothing ever reads.
155
+ let _ = await CognitoIdentityServiceProvider.DescribeUserPoolCommand.make({
156
+ userPoolId: id,
157
+ })->CognitoIdentityServiceProvider.DescribeUserPoolCommand.send
158
+ Console.log(`pool ${id} (given, unchanged)`)
159
+ Ok(id)
160
+ | None =>
161
+ switch await findPoolByName(~poolName=args.poolName) {
162
+ | Error(_) as e => e
163
+ | Ok(Some(existing)) =>
164
+ Console.log(`pool ${existing} (adopted, named "${args.poolName}")`)
165
+ Ok(existing)
166
+ | Ok(None) =>
167
+ let created = await CognitoIdentityServiceProvider.CreateUserPoolCommand.make(
168
+ poolSettings(~poolName=args.poolName),
169
+ )->CognitoIdentityServiceProvider.CreateUserPoolCommand.send
170
+ switch created.userPool->Option.flatMap(p => p.id) {
171
+ | None => Error("CreateUserPool returned no pool id")
172
+ | Some(id) =>
173
+ Console.log(`pool ${id} (created, named "${args.poolName}")`)
174
+ Ok(id)
175
+ }
176
+ }
177
+ }
178
+
179
+ // ── The store ────────────────────────────────────────────────────────────────
180
+
181
+ let keySchema: array<DynamoDb.DynamoDb.DescribeTableCommand.keySchemaElement> = [
182
+ {attributeName: Schema.partitionKey, keyType: "HASH"},
183
+ {attributeName: Schema.sortKey, keyType: "RANGE"},
184
+ ]
185
+
186
+ /** The SDK's shape mapped onto the plain pairs [Schema.keySchemaRefusal] compares.
187
+ The refusal itself lives with the schema it is about, since the schema is what
188
+ it is a statement about — and it is shared with the deploy, which never sees
189
+ this file. */
190
+ let asPairs = (elements: array<DynamoDb.DynamoDb.DescribeTableCommand.keySchemaElement>) =>
191
+ elements->Array.map(k => (k.attributeName, k.keyType))
192
+
193
+ let describeTable = async (~tableName: string): option<
194
+ DynamoDb.DynamoDb.DescribeTableCommand.tableDescription,
195
+ > =>
196
+ try {
197
+ let out = await DynamoDb.DynamoDb.DescribeTableCommand.make({
198
+ tableName: tableName,
199
+ })->DynamoDb.DynamoDb.DescribeTableCommand.send
200
+ out.table
201
+ } catch {
202
+ | exn if Util_AwsError.isNotFound(exn) => None
203
+ }
204
+
205
+ let provisionStore = async (~tableName: string): result<unit, string> =>
206
+ switch await describeTable(~tableName) {
207
+ | Some(described) =>
208
+ switch Schema.keySchemaRefusal(
209
+ ~tableName,
210
+ ~actual=described.keySchema->Option.getOr([])->asPairs,
211
+ ) {
212
+ | Some(message) => Error(message)
213
+ | None =>
214
+ Console.log(`store ${tableName} (adopted)`)
215
+ Ok()
216
+ }
217
+ | None =>
218
+ let _ = await DynamoDb.DynamoDb.CreateTableCommand.make({
219
+ tableName,
220
+ keySchema,
221
+ attributeDefinitions: [
222
+ {attributeName: Schema.partitionKey, attributeType: "S"},
223
+ {attributeName: Schema.sortKey, attributeType: "S"},
224
+ ],
225
+ billingMode: "PAY_PER_REQUEST",
226
+ })->DynamoDb.DynamoDb.CreateTableCommand.send
227
+
228
+ let waited = await DynamoDb.DynamoDb.TableExistsWaiter.wait(~tableName)
229
+ if !DynamoDb.DynamoDb.TableExistsWaiter.succeeded(waited) {
230
+ Error(
231
+ `table "${tableName}" was created but did not become ACTIVE (${waited.state}). Re-run this script — it adopts what is already there.`,
232
+ )
233
+ } else {
234
+ // Matches what the framework's own tables carry, and it can only be turned
235
+ // on once the table is ACTIVE — hence the wait above.
236
+ let _ = await DynamoDb.DynamoDb.UpdateContinuousBackupsCommand.make({
237
+ tableName,
238
+ pointInTimeRecoverySpecification: {pointInTimeRecoveryEnabled: true},
239
+ })->DynamoDb.DynamoDb.UpdateContinuousBackupsCommand.send
240
+ Console.log(`store ${tableName} (created)`)
241
+ Ok()
242
+ }
243
+ }
244
+
245
+ // ── Main ─────────────────────────────────────────────────────────────────────
246
+
247
+ /** Printed rather than assumed: nothing here owns these resources, so nothing
248
+ here can wire them up either. */
249
+ let nextSteps = (~providerId: string) => `
250
+ Configure each platform stack that should use this provider:
251
+
252
+ pulumi config set platform:identityProviderId ${providerId}
253
+
254
+ or set REVENTLESS_IDENTITY_PROVIDER_ID in CI. Every stack on this provider
255
+ derives the same store, so there is nothing else to configure.
256
+
257
+ The pre-token-generation trigger is attached by the deploy, not by this script.
258
+ Users and groups are yours to create.`
259
+
260
+ let run = async (): result<unit, string> =>
261
+ // argv[0] is node, argv[1] this script.
262
+ switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
263
+ | Error(_) as e => e
264
+ | Ok(args) if args.help =>
265
+ Console.log(usage)
266
+ Ok()
267
+ | Ok(args) =>
268
+ switch await resolvePool(~args) {
269
+ | Error(_) as e => e
270
+ | Ok(providerId) =>
271
+ let tableName = Schema.derivedStoreName(~identityProviderId=providerId)
272
+ switch await provisionStore(~tableName) {
273
+ | Error(_) as e => e
274
+ | Ok() =>
275
+ Console.log(nextSteps(~providerId))
276
+ Ok()
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ 🚨 **Catches thrown exceptions, not only `Error` results.**
283
+
284
+ Without this an SDK failure escapes an async function nothing awaits, and Node
285
+ reports `UnhandledPromiseRejection ... "#<Object>"` — which names neither the call
286
+ that failed nor why. Every AWS call in this script can throw, so the top level has
287
+ to turn any of them into something an operator can read.
288
+ */
289
+ let main = async () =>
290
+ switch await run() {
291
+ | Ok() => ()
292
+ | Error(message) =>
293
+ Console.error(`provision-identity: ${message}`)
294
+ NodeProcess.exit(1)
295
+ | exception exn =>
296
+ Console.error(`provision-identity: ${Util_AwsError.describe(exn)}`)
297
+ NodeProcess.exit(1)
298
+ }
299
+
300
+ // 🚨 **No top-level call.** `../run-provision-identity.mjs` invokes [main]; this
301
+ // module only defines it. A module that ran itself on import could not be
302
+ // imported by a test — which is exactly how a guard that could never match
303
+ // shipped in this file once already.