@reventlessdev/reventless-aws 3.0.0-alpha.292 → 3.0.0-alpha.294

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/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
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.294 (2026-08-14)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** provision the active-role write door outside the host-UI branch ([2ffc121](https://github.com/ReventlessDev/reventless-core/commit/2ffc12198dfedd151c298add42fbb9bb0dc95ca6))
11
+ ### Features
12
+
13
+ * **aws:** prove the pre-token-generation trigger before attaching it to a pool ([6e981a4](https://github.com/ReventlessDev/reventless-core/commit/6e981a4f6b4089a140cd7dda7df00177b98d4a65))
14
+
15
+
16
+ # 3.0.0-alpha.293 (2026-08-13)
17
+
18
+ ### Features
19
+
20
+ * **aws:** narrow the token to the role a caller chose to act as ([194332f](https://github.com/ReventlessDev/reventless-core/commit/194332fa0af8e13417f72d57904e2ed469747dde))
21
+
22
+
6
23
  # 3.0.0-alpha.292 (2026-08-13)
7
24
 
8
25
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.292",
3
+ "version": "3.0.0-alpha.294",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,17 +12,17 @@
12
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
13
  "sury": "11.0.0-alpha.4",
14
14
  "uuid": "^13.0.0",
15
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.8",
15
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.9",
16
16
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
17
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.73",
18
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
19
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.5",
19
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.74",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.231",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.232",
23
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.96",
23
24
  "@reventlessdev/reventless-infra": "3.0.0-alpha.139",
24
25
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
25
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.95",
26
26
  "@reventlessdev/reventless-spec": "3.0.0-alpha.112"
27
27
  },
28
28
  "devDependencies": {
package/src/Platform.res CHANGED
@@ -245,11 +245,12 @@ module MakeWithConfig = (
245
245
  // unified mode carries the admin base here instead; it declares no stores in practice,
246
246
  // so the upload resolvers below are never created there.)
247
247
  let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
248
- types: Array.concat(
249
- ReventlessCore.Platform_AdminApi.uploadTypes,
250
- ReventlessCore.Platform_AdminApi.geocodeTypes,
248
+ types: ReventlessCore.Platform_AdminApi.uploadTypes
249
+ ->Array.concat(ReventlessCore.Platform_AdminApi.geocodeTypes)
250
+ ->Array.concat(ReventlessCore.Platform_AdminApi.activeRoleTypes),
251
+ mutations: ReventlessCore.Platform_AdminApi.uploadMutationFields->Array.concat(
252
+ ReventlessCore.Platform_AdminApi.activeRoleMutationFields,
251
253
  ),
252
- mutations: ReventlessCore.Platform_AdminApi.uploadMutationFields,
253
254
  queries: Array.concat(
254
255
  [" Platform_ping: String"],
255
256
  ReventlessCore.Platform_AdminApi.geocodeQueryFields,
@@ -2257,6 +2258,44 @@ module MakeWithConfig = (
2257
2258
  Pulumi.Pulumi.export("hostShellUrl", distributionUrl)
2258
2259
  }
2259
2260
 
2261
+ // Where a caller records the role they want to act as
2262
+ // (docs/plans/active-role-narrows-the-token.md §6). The mutation only writes
2263
+ // a preference; the pre-token-generation trigger reads the row and re-checks
2264
+ // it against real membership before narrowing anything, so this door grants
2265
+ // nobody anything on its own.
2266
+ //
2267
+ // The table and the trigger were provisioned with the pool — they have to be,
2268
+ // since the pool is declared carrying the trigger's ARN. Only the write door
2269
+ // waits for the API, which is why it is here and not there.
2270
+ //
2271
+ // 🚨 **Outside `switch hostUiBundle`, and that is the whole point.** It was
2272
+ // written inside it, where the pool happens to be resolved — and a platform
2273
+ // whose shell ships from its own stack passes no bundle, so the door silently
2274
+ // did not exist there. The table and trigger still appeared, because
2275
+ // `resolveCognitoUserPool` is process-cached and `Auth_Cognito.make` reaches
2276
+ // it first: the feature deployed looking complete, with the trigger attached
2277
+ // to the pool and nothing able to write a row for it to read. Same shape as
2278
+ // the object-store endpoints at the top of this file, which sat in this branch
2279
+ // for the same reason and were moved out for the same one. Nothing that is not
2280
+ // *about* the host UI belongs inside this switch.
2281
+ //
2282
+ // Split-API only, like the upload and geocode resolvers: `Platform_SetActiveRole`
2283
+ // lives on the domain base document, which a unified-mode deployment's single
2284
+ // API does not carry. The table and trigger are provisioned either way — see
2285
+ // `Platform_Stack` for why that is not gated on this flag.
2286
+ if Config.splitApi {
2287
+ // Resolved here rather than reused from the branch above, which may not have
2288
+ // run. The call is process-cached, so this is the same pool either way.
2289
+ let pool = Platform_Stack.resolveCognitoUserPool()
2290
+ Auth_ActiveRoleStore.makeWriteDoor(
2291
+ ~api=domainApi,
2292
+ ~table=pool.activeRoleTable,
2293
+ ~cognitoUserPoolId=pool.poolId->Pulumi.Output.asInput,
2294
+ ~cognitoUserPoolArn=pool.poolArn->Pulumi.Output.asInput,
2295
+ ~opts={},
2296
+ )
2297
+ }
2298
+
2260
2299
  Pulumi.Pulumi.getOutputs()
2261
2300
  }
2262
2301
 
@@ -60,6 +60,7 @@ import * as StateTopic_AppSync$ReventlessAws from "./adapter/StateTopic/StateTop
60
60
  import * as AppSync_SdlDecorate$ReventlessAws from "./components/Api/AppSync_SdlDecorate.res.mjs";
61
61
  import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
62
62
  import * as Util_ResourceNaming$ReventlessAws from "./util/Util_ResourceNaming.res.mjs";
63
+ import * as Auth_ActiveRoleStore$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleStore.res.mjs";
63
64
  import * as ClonerRunner_Fargate$ReventlessAws from "./plugin/cloner/ClonerRunner_Fargate.res.mjs";
64
65
  import * as QueryEngine_DynamoDb$ReventlessAws from "./adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
65
66
  import * as PluginRuntime_Builder$ReventlessAws from "./plugin/runtime/PluginRuntime_Builder.res.mjs";
@@ -192,8 +193,8 @@ function MakeWithConfig(Config) {
192
193
  }
193
194
  };
194
195
  let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
195
- types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes),
196
- mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
196
+ types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes).concat(Platform_AdminApi$ReventlessCore.activeRoleTypes),
197
+ mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields.concat(Platform_AdminApi$ReventlessCore.activeRoleMutationFields),
197
198
  queries: [" Platform_ping: String"].concat(Platform_AdminApi$ReventlessCore.geocodeQueryFields),
198
199
  subscriptions: [],
199
200
  subscriptionSources: []
@@ -1230,6 +1231,10 @@ function MakeWithConfig(Config) {
1230
1231
  }
1231
1232
  Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
1232
1233
  }
1234
+ if (Config.splitApi) {
1235
+ let pool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
1236
+ Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, pool.activeRoleTable, pool.poolId, pool.poolArn, undefined, {});
1237
+ }
1233
1238
  return Pulumi$Pulumi.getOutputs();
1234
1239
  };
1235
1240
  let startServers = () => {};
@@ -1491,8 +1496,8 @@ function Make($star) {
1491
1496
  }
1492
1497
  };
1493
1498
  let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
1494
- types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes),
1495
- mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
1499
+ types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes).concat(Platform_AdminApi$ReventlessCore.activeRoleTypes),
1500
+ mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields.concat(Platform_AdminApi$ReventlessCore.activeRoleMutationFields),
1496
1501
  queries: [" Platform_ping: String"].concat(Platform_AdminApi$ReventlessCore.geocodeQueryFields),
1497
1502
  subscriptions: [],
1498
1503
  subscriptionSources: []
@@ -2506,6 +2511,8 @@ function Make($star) {
2506
2511
  }
2507
2512
  Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
2508
2513
  }
2514
+ let pool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
2515
+ Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, pool.activeRoleTable, pool.poolId, pool.poolArn, undefined, {});
2509
2516
  return Pulumi$Pulumi.getOutputs();
2510
2517
  };
2511
2518
  let startServers = () => {};
@@ -6,6 +6,12 @@ type cognitoUserPool = {
6
6
  clientId: Pulumi.Output.t<string>,
7
7
  poolArn: Pulumi.Output.t<string>,
8
8
  managed: bool,
9
+ /** Rows holding the role each caller chose to act as, read by the pool's
10
+ pre-token-generation trigger. Carried on the resolved pool because the table
11
+ has to be provisioned *before* the pool (the trigger reads it, and the pool
12
+ is declared with the trigger's ARN), while the mutation that writes it is
13
+ provisioned after the API — see [Auth_ActiveRoleStore.res]. */
14
+ activeRoleTable: Auth_ActiveRoleStore.storeTable,
9
15
  }
10
16
 
11
17
  /**
@@ -32,6 +38,11 @@ type cognitoUserPool = {
32
38
  * `cognitoUserPoolArn`, `cognitoRegion`, and `cognitoUserPoolManaged` as
33
39
  * stack outputs so downstream stacks (and Stage D AppSync wiring) can read
34
40
  * them via `StackReference`.
41
+ *
42
+ * Also provisions the role-state table and the pre-token-generation trigger that
43
+ * narrows `cognito:groups` to a caller's chosen role. They belong here rather
44
+ * than beside the mutation that writes the table because the pool must be
45
+ * declared carrying the trigger's ARN — see the ordering note below.
35
46
  */
36
47
  let _resolveUncached = (): cognitoUserPool => {
37
48
  let cfg = Pulumi.Config.make(Some("platform"))
@@ -40,6 +51,33 @@ let _resolveUncached = (): cognitoUserPool => {
40
51
  | None => cfg->Pulumi.Config.get("cognitoUserPoolId")
41
52
  }
42
53
 
54
+ // Provisioned ahead of the pool in both pool modes: the pre-token-generation
55
+ // trigger reads these rows, and in auto mode the pool is declared carrying the
56
+ // trigger's ARN — so table → trigger → pool is a forced order, not a choice.
57
+ // See [docs/plans/active-role-narrows-the-token.md] §6.
58
+ //
59
+ // Unconditional, including for a unified-API platform that mounts no
60
+ // `Platform_SetActiveRole` and so can never write a row.
61
+ //
62
+ // This was briefly gated on whether a write door would exist, to save a
63
+ // DynamoDB read per sign-in in that mode. The gate was a parameter on this
64
+ // function — and because the result is process-cached, it made the feature's
65
+ // existence depend on *which caller resolved the pool first*. `Auth_Cognito.make`
66
+ // reaches this through an adapter record and gets there before the platform
67
+ // does, so the whole feature silently vanished from the deploy: no error, no
68
+ // resource, nothing to notice. A local `pulumi preview` was the only thing that
69
+ // caught it.
70
+ //
71
+ // Saving one GetItem in a legacy mode is not worth a switch whose failure mode
72
+ // is a feature that quietly is not there. If the cost ever matters, the fix is
73
+ // a design that does not hinge on call order — not this parameter again.
74
+ let activeRoleTable = Auth_ActiveRoleStore.makeTable(~opts={})
75
+ let activeRoleTrigger = Auth_ActiveRoleTrigger.make(
76
+ ~activeRoleTableName=activeRoleTable.name->Pulumi.Output.asInput,
77
+ ~activeRoleTableArn=activeRoleTable.arn->Pulumi.Output.asInput,
78
+ ~opts={},
79
+ )
80
+
43
81
  let result = switch existingPoolId {
44
82
  | Some(poolId) =>
45
83
  let tokenUnits: PulumiAws.Cognito.UserPoolClient.tokenValidityUnits = {
@@ -66,11 +104,29 @@ let _resolveUncached = (): cognitoUserPool => {
66
104
 
67
105
  let lookup = PulumiAws.Cognito.UserPool.getUserPoolOutput(~args={userPoolId: poolId})
68
106
 
107
+ // BYO: we hold no handle to set `lambdaConfig` on, and Cognito offers no
108
+ // separate attachment resource — so the trigger is attached by a declared
109
+ // resource that describes the pool, merges into what it finds, and sends the
110
+ // result back whole. Never a hand-run command: that would leave the
111
+ // deployment's behaviour depending on an act no source describes.
112
+ // The code hash is an input so a new build of the trigger reaches this
113
+ // resource: the function ARN is stable, so it alone would let a broken
114
+ // version onto the pool unchecked after the first deploy.
115
+ let _attachment = Auth_ActiveRolePoolAttachment.make(
116
+ ~props={
117
+ userPoolId: Pulumi.Input.make(poolId),
118
+ preTokenGenerationArn: activeRoleTrigger.functionArn->Pulumi.Output.asInput,
119
+ codeHash: Pulumi.Input.make(activeRoleTrigger.sourceCodeHash),
120
+ },
121
+ ~opts={},
122
+ )
123
+
69
124
  {
70
125
  poolId: Pulumi.Output.make(poolId),
71
126
  clientId: client.id,
72
127
  poolArn: lookup->Pulumi.Output.apply(r => r.arn),
73
128
  managed: false,
129
+ activeRoleTable,
74
130
  }
75
131
 
76
132
  | None =>
@@ -83,6 +139,11 @@ let _resolveUncached = (): cognitoUserPool => {
83
139
  requireUppercase: Pulumi.Input.make(true),
84
140
  requireNumbers: Pulumi.Input.make(true),
85
141
  }
142
+ // Auto: the pool is ours, so the trigger is an ordinary declared property and
143
+ // no out-of-band `UpdateUserPool` is involved. Declaring it here rather than
144
+ // attaching it afterwards also keeps Pulumi's own state the authority — an
145
+ // attachment made behind the resource's back would read as drift on the next
146
+ // refresh and be reverted.
86
147
  let pool = PulumiAws.Cognito.UserPool.make(
87
148
  ~name="HostUiPool",
88
149
  ~args={
@@ -90,6 +151,9 @@ let _resolveUncached = (): cognitoUserPool => {
90
151
  usernameAttributes: Pulumi.Input.make([Pulumi.Input.make("email")]),
91
152
  passwordPolicy: Pulumi.Input.make(pwdPolicy),
92
153
  mfaConfiguration: Pulumi.Input.make("OFF"),
154
+ lambdaConfig: Pulumi.Input.make({
155
+ PulumiAws.Cognito.UserPool.preTokenGeneration: activeRoleTrigger.functionArn->Pulumi.Output.asInput,
156
+ }),
93
157
  tags: AWS.Tags.make(
94
158
  ~name="HostUiPool",
95
159
  ~kind=ReventlessCore.ComponentType.Platform,
@@ -126,9 +190,18 @@ let _resolveUncached = (): cognitoUserPool => {
126
190
  clientId: client.id,
127
191
  poolArn: pool.arn,
128
192
  managed: true,
193
+ activeRoleTable,
129
194
  }
130
195
  }
131
196
 
197
+ // After the branch, because the permission is scoped to the pool's ARN and the
198
+ // pool is what the branch produces.
199
+ Auth_ActiveRoleTrigger.grantInvoke(
200
+ ~trigger=activeRoleTrigger,
201
+ ~cognitoUserPoolArn=result.poolArn->Pulumi.Output.asInput,
202
+ ~opts={},
203
+ )
204
+
132
205
  let regionStr =
133
206
  Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown")
134
207
 
@@ -149,6 +222,15 @@ let _resolveUncached = (): cognitoUserPool => {
149
222
  // stack-export calls happen exactly once per Pulumi run.
150
223
  let _cached: ref<option<cognitoUserPool>> = ref(None)
151
224
 
225
+ /**
226
+ 🚨 **Takes no arguments, and must not grow any.** Everything here is provisioned
227
+ on the *first* call and every later caller gets that same result — so a
228
+ parameter would not configure the pool, it would configure whichever caller
229
+ happened to arrive first. `Auth_Cognito.make` reaches this through an adapter
230
+ record and gets here before the platform's own call does, which is not obvious
231
+ from any call site. Anything that needs to vary belongs in [_resolveUncached],
232
+ read from config or environment where every caller sees the same answer.
233
+ */
152
234
  let resolveCognitoUserPool = (): cognitoUserPool =>
153
235
  switch _cached.contents {
154
236
  | Some(p) => p
@@ -6,11 +6,16 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
8
8
  import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
9
+ import * as Auth_ActiveRoleStore$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleStore.res.mjs";
10
+ import * as Auth_ActiveRoleTrigger$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleTrigger.res.mjs";
11
+ import * as Auth_ActiveRolePoolAttachment$ReventlessAws from "./adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs";
9
12
 
10
13
  function _resolveUncached() {
11
14
  let cfg = new Pulumi.Config("platform");
12
15
  let v = Util_LocalConfig$ReventlessAws.get("cognitoUserPoolId");
13
16
  let existingPoolId = v !== undefined ? v : cfg.get("cognitoUserPoolId");
17
+ let activeRoleTable = Auth_ActiveRoleStore$ReventlessAws.makeTable(undefined, {});
18
+ let activeRoleTrigger = Auth_ActiveRoleTrigger$ReventlessAws.make(activeRoleTable.name, activeRoleTable.arn, undefined, {});
14
19
  let result;
15
20
  if (existingPoolId !== undefined) {
16
21
  let tokenUnits_accessToken = "minutes";
@@ -37,11 +42,17 @@ function _resolveUncached() {
37
42
  let lookup = Aws.cognito.getUserPoolOutput({
38
43
  userPoolId: existingPoolId
39
44
  });
45
+ Auth_ActiveRolePoolAttachment$ReventlessAws.make(undefined, {
46
+ userPoolId: existingPoolId,
47
+ preTokenGenerationArn: activeRoleTrigger.functionArn,
48
+ codeHash: activeRoleTrigger.sourceCodeHash
49
+ }, {});
40
50
  result = {
41
51
  poolId: Pulumi.output(existingPoolId),
42
52
  clientId: client.id,
43
53
  poolArn: lookup.apply(r => r.arn),
44
- managed: false
54
+ managed: false,
55
+ activeRoleTable: activeRoleTable
45
56
  };
46
57
  } else {
47
58
  let adminConfig = {
@@ -58,6 +69,9 @@ function _resolveUncached() {
58
69
  requireUppercase: pwdPolicy_requireUppercase
59
70
  };
60
71
  let pool = new (Aws.cognito.UserPool)("HostUiPool", {
72
+ lambdaConfig: {
73
+ preTokenGeneration: activeRoleTrigger.functionArn
74
+ },
61
75
  adminCreateUserConfig: adminConfig,
62
76
  usernameAttributes: ["email"],
63
77
  passwordPolicy: pwdPolicy,
@@ -89,9 +103,11 @@ function _resolveUncached() {
89
103
  poolId: pool.id,
90
104
  clientId: client$1.id,
91
105
  poolArn: pool.arn,
92
- managed: true
106
+ managed: true,
107
+ activeRoleTable: activeRoleTable
93
108
  };
94
109
  }
110
+ Auth_ActiveRoleTrigger$ReventlessAws.grantInvoke(activeRoleTrigger, result.poolArn, undefined, {});
95
111
  let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
96
112
  Pulumi$Pulumi.$$export("cognitoUserPoolId", result.poolId);
97
113
  Pulumi$Pulumi.$$export("cognitoUserPoolClientId", result.clientId);