@reventlessdev/reventless-aws 3.0.0-alpha.369 → 3.0.0-alpha.371

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 (38) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +11 -11
  3. package/scripts/ProvisionAdmin.res +2 -2
  4. package/scripts/ProvisionAdmin.res.mjs +7 -6
  5. package/src/Platform.res +56 -7
  6. package/src/Platform.res.mjs +46 -7
  7. package/src/Platform_ProvidedGroups.res +97 -0
  8. package/src/Platform_ProvidedGroups.res.mjs +103 -0
  9. package/src/Platform_Stack.res +6 -3
  10. package/src/Platform_Stack.res.mjs +3 -3
  11. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -1
  12. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  13. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +1 -1
  14. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -1
  15. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
  16. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -0
  17. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -0
  18. package/src/components/Api/AppSync_Adapter.res +4 -3
  19. package/src/components/Api/AppSync_SdlDecorate.res +7 -3
  20. package/src/components/Api/AppSync_SdlDecorate.res.mjs +4 -3
  21. package/src/util/Util_OwnerScopeEnv.res +9 -0
  22. package/src/util/Util_OwnerScopeEnv.res.mjs +14 -1
  23. package/src/util/Util_ShellConfig.res +4 -0
  24. package/src/util/Util_ShellConfig.res.mjs +5 -1
  25. package/tests/AppSync_AdapterTest.res +46 -23
  26. package/tests/AppSync_AdapterTest.res.mjs +35 -27
  27. package/tests/PgPipeline_IntegrationTest.res +2 -1
  28. package/tests/PgQueryDbUnion_IntegrationTest.res +2 -1
  29. package/tests/Platform_ProvidedGroupsTest.res +52 -0
  30. package/tests/Platform_ProvidedGroupsTest.res.mjs +98 -0
  31. package/tests/Util_OwnerScopeEnvTest.res +48 -2
  32. package/tests/Util_OwnerScopeEnvTest.res.mjs +49 -3
  33. package/tests/Util_ShellConfigTest.res +17 -0
  34. package/tests/Util_ShellConfigTest.res.mjs +18 -0
  35. package/tests/integration/AggTestAggregate.res +2 -1
  36. package/tests/integration/EpCompositeSlice.res +2 -1
  37. package/tests/integration/EpTestSlice.res +2 -1
  38. package/tests/integration/QueryDbUnion_DynamoDb_IntegrationTest.res +2 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
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.371 (2026-10-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * check a plugin's roles against the user pool's groups ([ddb3c42](https://github.com/ReventlessDev/reventless-core/commit/ddb3c422b7226a19800e781abd5f03af18867007))
11
+
12
+
13
+ # 3.0.0-alpha.370 (2026-10-01)
14
+
15
+ * feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
16
+
17
+ ### BREAKING CHANGES
18
+
19
+ * AllowGroups and AdminGroup are removed — write AllowRoles over
20
+ the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
21
+ types declare `type role`, and their rules are Authorization.rule<role>; a spec
22
+ that writes its own rule declares `type role` (Reventless.Role.name for a rule
23
+ over Authorization.permission). Role.name is a private string: make one from a
24
+ string with Role.make.
25
+
26
+
27
+
6
28
  # 3.0.0-alpha.369 (2026-10-01)
7
29
 
8
30
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.369",
3
+ "version": "3.0.0-alpha.371",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -21,19 +21,19 @@
21
21
  "sury": "11.0.0",
22
22
  "uuid": "^13.0.0",
23
23
  "yaml": "^2.8.3",
24
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.27",
25
- "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
26
- "@reventlessdev/rescript-node": "2.0.0-alpha.20",
27
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.22",
24
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.28",
28
25
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
29
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
26
+ "@reventlessdev/rescript-node": "2.0.0-alpha.20",
27
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
28
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.23",
29
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
30
30
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
31
31
  "@reventlessdev/rescript-web": "1.0.0-alpha.7",
32
- "@reventlessdev/reventless-infra": "3.0.0-alpha.183",
33
- "@reventlessdev/reventless-core": "3.0.0-alpha.285",
34
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.149",
35
- "@reventlessdev/reventless-interop": "3.0.0-alpha.38",
36
- "@reventlessdev/reventless-spec": "3.0.0-alpha.155"
32
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.185",
33
+ "@reventlessdev/reventless-core": "3.0.0-alpha.287",
34
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.151",
35
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.157",
36
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.39"
37
37
  },
38
38
  "devDependencies": {
39
39
  "rescript": "12.3.1",
@@ -76,7 +76,7 @@ Make the first administrator of a Reventless deployment.
76
76
  one. The run always names the stack it used.
77
77
  --email <address> The address they sign in with.
78
78
 
79
- Creates the "${Reventless.AdminGroup.name}" group if it is missing, the account if
79
+ Creates the "${Reventless.Role.adminGroup()}" group if it is missing, the account if
80
80
  it is missing, a working password, and the membership between them. Provisions
81
81
  nothing that a platform stack owns. Region and credentials come from the
82
82
  environment, as for any AWS SDK call.
@@ -222,7 +222,7 @@ let run = async ({email, providerId, stack}: request): result<unit, string> =>
222
222
  switch await checkPoolAcceptsEmail(~providerId) {
223
223
  | Error(_) as e => e
224
224
  | Ok() =>
225
- let group = Reventless.AdminGroup.name
225
+ let group = Reventless.Role.adminGroup()
226
226
  let password = Reventless.Util_Password.generate()
227
227
  await ensureGroup(~providerId, ~group)
228
228
  await ensureUser(~providerId, ~email)
@@ -1,9 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
4
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
4
5
  import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
5
6
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
7
7
  import * as Util_Password$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Password.res.mjs";
8
8
  import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
9
9
  import * as ProvisionCognito$ReventlessAws from "./ProvisionCognito.res.mjs";
@@ -36,7 +36,7 @@ Make the first administrator of a Reventless deployment.
36
36
  one. The run always names the stack it used.
37
37
  --email <address> The address they sign in with.
38
38
 
39
- Creates the "` + AdminGroup$Reventless.name + `" group if it is missing, the account if
39
+ Creates the "` + Role$Reventless.adminGroup() + `" group if it is missing, the account if
40
40
  it is missing, a working password, and the membership between them. Provisions
41
41
  nothing that a platform stack owns. Region and credentials come from the
42
42
  environment, as for any AWS SDK call.
@@ -156,12 +156,13 @@ async function run(param) {
156
156
  if (e$1.TAG !== "Ok") {
157
157
  return e$1;
158
158
  }
159
+ let group = Role$Reventless.adminGroup();
159
160
  let password = Util_Password$Reventless.generate();
160
- await ensureGroup(providerId, AdminGroup$Reventless.name);
161
+ await ensureGroup(providerId, group);
161
162
  await ensureUser(providerId, email);
162
163
  await setPassword(providerId, email, password);
163
- await addToGroup(providerId, email, AdminGroup$Reventless.name);
164
- console.log(signInDetails(providerId, email, password, AdminGroup$Reventless.name));
164
+ await addToGroup(providerId, email, group);
165
+ console.log(signInDetails(providerId, email, password, group));
165
166
  return {
166
167
  TAG: "Ok",
167
168
  _0: undefined
@@ -212,4 +213,4 @@ export {
212
213
  cli,
213
214
  main,
214
215
  }
215
- /* CliArgs-Reventless Not a pure module */
216
+ /* usage Not a pure module */
package/src/Platform.res CHANGED
@@ -107,6 +107,47 @@ let messagingEmailSenderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.ma
107
107
  `""` means the platform named none, which the runtime reads as its default. */
108
108
  let messagingEmailProviderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
109
109
 
110
+ /** Which group stands for which role, only where the two differ; every other
111
+ role maps to the group of its own name. Call before `Make()`: the platform
112
+ resolves roles while it is built, and a mapping stated later is refused. */
113
+ let roleGroups = Reventless.Role.setGroups
114
+
115
+ /** Groups a root says its identity provider has, for a provider the platform
116
+ cannot list. Where it can (`loadProvidedGroups`), the listing decides, and a
117
+ declared group the pool lacks fails the deploy. */
118
+ let providedGroups = Reventless.Role.provideGroups
119
+
120
+ /** The platform stack a plugin stack deploys against (`platform:stack`), or
121
+ `None` in a program that is its own platform. One reference per program:
122
+ a second with the same name is a duplicate resource. */
123
+ let platformStackRefCache: ref<option<option<Pulumi.StackReference.t>>> = ref(None)
124
+
125
+ let platformStackReference = (): option<Pulumi.StackReference.t> =>
126
+ switch platformStackRefCache.contents {
127
+ | Some(stackRef) => stackRef
128
+ | None =>
129
+ let stackRef =
130
+ Pulumi.Config.make(Some("platform"))
131
+ ->Pulumi.Config.get("stack")
132
+ ->Option.map(stack => Pulumi.StackReference.make(stack))
133
+ platformStackRefCache := Some(stackRef)
134
+ stackRef
135
+ }
136
+
137
+ /** Ask the user pool which groups it has, so the role check compares each
138
+ plugin's roles with the pool. A plugin's deploy program calls `deployPlugin`
139
+ once this resolves; see [Platform_ProvidedGroups]. */
140
+ let loadProvidedGroups = (): promise<unit> =>
141
+ Platform_ProvidedGroups.load(~stackRef=platformStackReference())
142
+
143
+ /** The group a role resolves to — the name this platform writes into its
144
+ directives, its shell configuration and the elevated groups. For code that
145
+ grants access by group, so it never spells one itself. */
146
+ let groupOf = Reventless.Role.groupOf
147
+
148
+ /** `groupOf(Role.admin)`. */
149
+ let adminGroup = Reventless.Role.adminGroup
150
+
110
151
  module MakeWithConfig = (
111
152
  Config: {
112
153
  let splitApi: bool
@@ -144,7 +185,15 @@ module MakeWithConfig = (
144
185
  // A default, never a forcing: an explicit `setElevatedGroups` in a platform
145
186
  // root — including one naming nobody — means what it says and wins. See
146
187
  // [Reventless.OwnerScope.defaultElevatedGroups].
147
- Reventless.OwnerScope.defaultElevatedGroups([Reventless.AdminGroup.name])
188
+ Reventless.OwnerScope.defaultElevatedRoles([Reventless.Role.admin])
189
+
190
+ // What this deployment provides for the roles its plugins need, where the pool
191
+ // was not listed (`loadProvidedGroups`): the administrator group the stack
192
+ // declares, and the groups of the accounts provisioning creates. Read when each
193
+ // plugin is built, after the root has mapped its roles.
194
+ Reventless.Role.provideGroupsFrom(() =>
195
+ [Reventless.Role.adminGroup()]->Array.concat(Reventless.AccountsManifest.declaredGroups())
196
+ )
148
197
 
149
198
  // Dispatch per-flavor commandHandlerConfig records to the four runtime
150
199
  // builders. Every sub-record is optional; only branches the caller actually
@@ -202,10 +251,7 @@ module MakeWithConfig = (
202
251
  // Determine API source based on platform:stack config.
203
252
  // - Platform/monolithic mode (no config): create a real AppSync API resource.
204
253
  // - Plugin mode (config set): reference the platform's shared API via StackReference.
205
- let platformStackRef =
206
- Pulumi.Config.make(Some("platform"))
207
- ->Pulumi.Config.get("stack")
208
- ->Option.map(stack => Pulumi.StackReference.make(stack))
254
+ let platformStackRef = platformStackReference()
209
255
 
210
256
  // The declared object stores, as the upload claimer needs them: qualified
211
257
  // name, physical bucket, served prefix.
@@ -308,13 +354,16 @@ module MakeWithConfig = (
308
354
  // Admin base as a source-API document — auth-decorated (all fields Admin,
309
355
  // Cognito-only; the deploy-time SigV4 system-caller fields died with the
310
356
  // fragment registry) plus the canonical stamp.
311
- let adminSourceSdl = (): string =>
357
+ let adminSourceSdl = (): string => {
358
+ // The directive below fixes the administrator's group for this deploy.
359
+ Reventless.Role.freeze()
312
360
  assembleCanonicalSourceSdl(
313
361
  ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
314
362
  ReventlessCore.Platform_AdminApi.baseFragment(~cloner=Config.cloner),
315
- ~group=Reventless.AdminGroup.name,
363
+ ~group=Reventless.Role.adminGroup(),
316
364
  ),
317
365
  )
366
+ }
318
367
 
319
368
  // Split-mode Domain source document: relay base types + Platform_ping — the
320
369
  // Domain merged API's canonical owner (plugin fields come from plugin sources).
@@ -13,10 +13,10 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
13
13
  import * as Pulumi from "@pulumi/pulumi";
14
14
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
15
15
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
16
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
16
17
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
17
18
  import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
18
19
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
19
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
20
20
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
21
21
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
22
22
  import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
@@ -34,6 +34,7 @@ import * as Util_Bundle$ReventlessAws from "./util/Util_Bundle.res.mjs";
34
34
  import * as Plugin_Stack$ReventlessAws from "./plugin/stack/Plugin_Stack.res.mjs";
35
35
  import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
36
36
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
37
+ import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
37
38
  import * as Platform_Casts$ReventlessAws from "./Platform_Casts.res.mjs";
38
39
  import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
39
40
  import * as QueryDbBackend$ReventlessAws from "./adapter/QueryDb/QueryDbBackend.res.mjs";
@@ -79,6 +80,7 @@ import * as EventTopicPublisher_SNS$ReventlessAws from "./adapter/EventTopic/Eve
79
80
  import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
80
81
  import * as PgQueryResolver_Builder$ReventlessAws from "./adapter/QueryDb/PgQueryResolver_Builder.res.mjs";
81
82
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
83
+ import * as Platform_ProvidedGroups$ReventlessAws from "./Platform_ProvidedGroups.res.mjs";
82
84
  import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments_Projection.res.mjs";
83
85
  import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
84
86
  import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
@@ -151,8 +153,27 @@ let messagingEmailProviderRef = {
151
153
  contents: Pulumi.output("")
152
154
  };
153
155
 
156
+ let platformStackRefCache = {
157
+ contents: undefined
158
+ };
159
+
160
+ function platformStackReference() {
161
+ let stackRef = platformStackRefCache.contents;
162
+ if (stackRef !== undefined) {
163
+ return Primitive_option.valFromOption(stackRef);
164
+ }
165
+ let stackRef$1 = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
166
+ platformStackRefCache.contents = Primitive_option.some(stackRef$1);
167
+ return stackRef$1;
168
+ }
169
+
170
+ function loadProvidedGroups() {
171
+ return Platform_ProvidedGroups$ReventlessAws.load(platformStackReference());
172
+ }
173
+
154
174
  function MakeWithConfig(Config) {
155
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
175
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
176
+ Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
156
177
  Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
157
178
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
158
179
  Stdlib_Option.forEach(param.async, AggregateRuntime_Builder_Single_Async$ReventlessAws.setConfig);
@@ -181,7 +202,7 @@ function MakeWithConfig(Config) {
181
202
  let currentDeployTarget = {
182
203
  contents: "Domain"
183
204
  };
184
- let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
205
+ let platformStackRef = platformStackReference();
185
206
  let claimStores = () => {
186
207
  if (platformStackRef !== undefined) {
187
208
  return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
@@ -214,7 +235,8 @@ function MakeWithConfig(Config) {
214
235
  subscriptionSources: []
215
236
  });
216
237
  let adminSourceSdl = () => {
217
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), AdminGroup$Reventless.name, undefined);
238
+ Role$Reventless.freeze();
239
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), Role$Reventless.adminGroup(), undefined);
218
240
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
219
241
  };
220
242
  let match;
@@ -1566,7 +1588,8 @@ function MakeWithConfig(Config) {
1566
1588
 
1567
1589
  function Make($star) {
1568
1590
  let commandHandlerConfig = {};
1569
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
1591
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
1592
+ Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
1570
1593
  Stdlib_Option.forEach(commandHandlerConfig.aggregates, param => {
1571
1594
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
1572
1595
  Stdlib_Option.forEach(param.async, AggregateRuntime_Builder_Single_Async$ReventlessAws.setConfig);
@@ -1595,7 +1618,7 @@ function Make($star) {
1595
1618
  let currentDeployTarget = {
1596
1619
  contents: "Domain"
1597
1620
  };
1598
- let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
1621
+ let platformStackRef = platformStackReference();
1599
1622
  let claimStores = () => {
1600
1623
  if (platformStackRef !== undefined) {
1601
1624
  return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
@@ -1628,7 +1651,8 @@ function Make($star) {
1628
1651
  subscriptionSources: []
1629
1652
  });
1630
1653
  let adminSourceSdl = () => {
1631
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), AdminGroup$Reventless.name, undefined);
1654
+ Role$Reventless.freeze();
1655
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), Role$Reventless.adminGroup(), undefined);
1632
1656
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
1633
1657
  };
1634
1658
  let match;
@@ -2958,6 +2982,14 @@ function Make($star) {
2958
2982
  };
2959
2983
  }
2960
2984
 
2985
+ let roleGroups = Role$Reventless.setGroups;
2986
+
2987
+ let providedGroups = Role$Reventless.provideGroups;
2988
+
2989
+ let groupOf = Role$Reventless.groupOf;
2990
+
2991
+ let adminGroup = Role$Reventless.adminGroup;
2992
+
2961
2993
  export {
2962
2994
  log,
2963
2995
  apiConfigRef,
@@ -2969,6 +3001,13 @@ export {
2969
3001
  geocoderPlaceIndexRef,
2970
3002
  messagingEmailSenderRef,
2971
3003
  messagingEmailProviderRef,
3004
+ roleGroups,
3005
+ providedGroups,
3006
+ platformStackRefCache,
3007
+ platformStackReference,
3008
+ loadProvidedGroups,
3009
+ groupOf,
3010
+ adminGroup,
2972
3011
  MakeWithConfig,
2973
3012
  Make,
2974
3013
  }
@@ -0,0 +1,97 @@
1
+ /***
2
+ The user pool's groups, read before a plugin is built so the role check compares
3
+ its roles with the pool, not a manifest (`Role.provideListedGroups`). A group the
4
+ pool lacks refuses every caller, which nothing at runtime can tell from a caller
5
+ without the role. Works in a preview: `ListGroups` is a read, and the pool id is
6
+ read outside any `Output.apply`.
7
+ */
8
+
9
+ module Cognito = AwsSdk.CognitoIdentityServiceProvider
10
+
11
+ type pool = {id: string, region: option<string>}
12
+
13
+ type send = Cognito.ListGroupsCommand.input => promise<Cognito.ListGroupsCommand.output>
14
+
15
+ /** Every group of the pool, following `NextToken` across pages. */
16
+ let listAll = async (~send: send, ~userPoolId: string): array<string> => {
17
+ let rec page = async (nextToken, acc) => {
18
+ let out = await send({userPoolId, limit: 60, ?nextToken})
19
+ let names = out.groups->Option.getOr([])->Array.filterMap(g => g.groupName)
20
+ let acc = acc->Array.concat(names)
21
+ switch out.nextToken {
22
+ | Some(_) as next => await page(next, acc)
23
+ | None => acc
24
+ }
25
+ }
26
+ await page(None, [])
27
+ }
28
+
29
+ /** A plugin stack's pool, from the platform stack's exports. The new key before
30
+ the old one, and each directly before under `default`, where ESM programs
31
+ nest their exports. */
32
+ let poolFromExports = (~get: string => option<JSON.t>): option<pool> => {
33
+ let nested = get("default")->Option.flatMap(JSON.Decode.object)
34
+ let read = key =>
35
+ get(key)
36
+ ->Option.orElse(nested->Option.flatMap(d => d->Dict.get(key)))
37
+ ->Option.flatMap(JSON.Decode.string)
38
+ read("identityProviderId")
39
+ ->Option.orElse(read("cognitoUserPoolId"))
40
+ ->Option.map(id => {
41
+ id,
42
+ region: read("identityProviderRegion")->Option.orElse(read("cognitoRegion")),
43
+ })
44
+ }
45
+
46
+ /**
47
+ The pool this program deploys against, or `None` where it cannot be listed yet:
48
+ a pool this program creates does not exist before its first deploy.
49
+
50
+ A plugin stack reads its platform stack's exports, the same ones its resolvers
51
+ use. A program that is its own platform reads a supplied pool's id from config.
52
+ */
53
+ let resolvePool = async (~stackRef: option<Pulumi.StackReference.t>): option<pool> =>
54
+ switch stackRef {
55
+ | Some(stackRef) =>
56
+ let keys = [
57
+ "identityProviderId",
58
+ "cognitoUserPoolId",
59
+ "identityProviderRegion",
60
+ "cognitoRegion",
61
+ "default",
62
+ ]
63
+ let values = await Promise.all(
64
+ keys->Array.map(key => stackRef->Pulumi.StackReference.getOutputValue(key)),
65
+ )
66
+ let exports = Dict.fromArray(
67
+ keys->Array.mapWithIndex((key, i) => (key, values->Array.getUnsafe(i))),
68
+ )
69
+ poolFromExports(~get=key => exports->Dict.get(key)->Option.flatMap(v => v))
70
+ | None =>
71
+ Platform_Stack._identityProviderId(
72
+ ~cfg=Pulumi.Config.make(Some("platform")),
73
+ )->Option.map(id => {
74
+ id,
75
+ region: Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region"),
76
+ })
77
+ }
78
+
79
+ let sendTo = (~region: option<string>): send => {
80
+ let client = Cognito.Raw.client(~options={region: ?region})
81
+ input => Cognito.ListGroupsCommand.make(input)->Cognito.ListGroupsCommand.Raw.send(client, _)
82
+ }
83
+
84
+ /** List the pool's groups and record them for the role check. Leaves the check
85
+ on its other sources where there is no pool to ask yet. */
86
+ let load = async (~stackRef: option<Pulumi.StackReference.t>): unit =>
87
+ switch await resolvePool(~stackRef) {
88
+ | None => ()
89
+ | Some({id, region}) =>
90
+ let groups = try await listAll(~send=sendTo(~region), ~userPoolId=id) catch {
91
+ | exn if exn->Util_AwsError.hasCode(~code="AccessDeniedException") =>
92
+ JsError.throwWithMessage(
93
+ `Cannot list the groups of the user pool ${id}: the deploying principal needs ` ++ `cognito-idp:ListGroups on it, so the deploy can check that every role a plugin needs has a group.`,
94
+ )
95
+ }
96
+ Reventless.Role.provideListedGroups(~source=`the user pool ${id}`, groups)
97
+ }
@@ -0,0 +1,103 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Pulumi from "@pulumi/pulumi";
7
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
9
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+ import * as Util_AwsError$ReventlessAws from "./util/Util_AwsError.res.mjs";
12
+ import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
13
+ import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
14
+
15
+ async function listAll(send, userPoolId) {
16
+ let page = async (nextToken, acc) => {
17
+ let out = await send({
18
+ UserPoolId: userPoolId,
19
+ Limit: 60,
20
+ NextToken: nextToken
21
+ });
22
+ let names = Stdlib_Array.filterMap(Stdlib_Option.getOr(out.Groups, []), g => g.GroupName);
23
+ let acc$1 = acc.concat(names);
24
+ let next = out.NextToken;
25
+ if (next !== undefined) {
26
+ return await page(next, acc$1);
27
+ } else {
28
+ return acc$1;
29
+ }
30
+ };
31
+ return await page(undefined, []);
32
+ }
33
+
34
+ function poolFromExports(get) {
35
+ let nested = Stdlib_Option.flatMap(get("default"), Stdlib_JSON.Decode.object);
36
+ let read = key => Stdlib_Option.flatMap(Stdlib_Option.orElse(get(key), Stdlib_Option.flatMap(nested, d => d[key])), Stdlib_JSON.Decode.string);
37
+ return Stdlib_Option.map(Stdlib_Option.orElse(read("identityProviderId"), read("cognitoUserPoolId")), id => ({
38
+ id: id,
39
+ region: Stdlib_Option.orElse(read("identityProviderRegion"), read("cognitoRegion"))
40
+ }));
41
+ }
42
+
43
+ async function resolvePool(stackRef) {
44
+ if (stackRef === undefined) {
45
+ return Stdlib_Option.map(Platform_Stack$ReventlessAws._identityProviderId(new Pulumi.Config("platform")), id => ({
46
+ id: id,
47
+ region: new Pulumi.Config("aws").get("region")
48
+ }));
49
+ }
50
+ let stackRef$1 = Primitive_option.valFromOption(stackRef);
51
+ let keys = [
52
+ "identityProviderId",
53
+ "cognitoUserPoolId",
54
+ "identityProviderRegion",
55
+ "cognitoRegion",
56
+ "default"
57
+ ];
58
+ let values = await Promise.all(keys.map(key => stackRef$1.getOutputValue(key)));
59
+ let $$exports = Object.fromEntries(keys.map((key, i) => [
60
+ key,
61
+ values[i]
62
+ ]));
63
+ return poolFromExports(key => Stdlib_Option.flatMap($$exports[key], v => v));
64
+ }
65
+
66
+ function sendTo(region) {
67
+ let client = new ClientCognitoIdentityProvider.CognitoIdentityProviderClient({
68
+ region: region
69
+ });
70
+ return input => client.send(new ClientCognitoIdentityProvider.ListGroupsCommand(input));
71
+ }
72
+
73
+ async function load(stackRef) {
74
+ let match = await resolvePool(stackRef);
75
+ if (match === undefined) {
76
+ return;
77
+ }
78
+ let id = match.id;
79
+ let groups;
80
+ try {
81
+ groups = await listAll(sendTo(match.region), id);
82
+ } catch (raw_exn) {
83
+ let exn = Primitive_exceptions.internalToException(raw_exn);
84
+ if (Util_AwsError$ReventlessAws.hasCode(exn, "AccessDeniedException")) {
85
+ groups = Stdlib_JsError.throwWithMessage(`Cannot list the groups of the user pool ` + id + `: the deploying principal needs cognito-idp:ListGroups on it, so the deploy can check that every role a plugin needs has a group.`);
86
+ } else {
87
+ throw exn;
88
+ }
89
+ }
90
+ return Role$Reventless.provideListedGroups(`the user pool ` + id, groups);
91
+ }
92
+
93
+ let Cognito;
94
+
95
+ export {
96
+ Cognito,
97
+ listAll,
98
+ poolFromExports,
99
+ resolvePool,
100
+ sendTo,
101
+ load,
102
+ }
103
+ /* @pulumi/pulumi Not a pure module */
@@ -25,7 +25,7 @@ type cognitoUserPool = {
25
25
  * with SPA-friendly defaults (email sign-in unless `platform:loginIdentifier`
26
26
  * says otherwise, 12-char password policy, no MFA, and admin-only user
27
27
  * creation unless `platform:signUpMode` says otherwise), plus the
28
- * administrator group named by [Reventless.AdminGroup] and an elevated-groups
28
+ * administrator group named by [Reventless.Role.adminGroup] and an elevated-groups
29
29
  * default naming it. What it does **not** create is the first administrator:
30
30
  * accounts are not stack resources, and a pool full of them is not something a
31
31
  * `pulumi destroy` should be able to empty. `provision-admin` makes that one,
@@ -302,7 +302,7 @@ let _resolveUncached = (): cognitoUserPool => {
302
302
  // An ordinary child resource: the pool is ours, so this needs no out-of-band
303
303
  // call and Pulumi's state stays the authority for whether it exists.
304
304
  let _adminGroup = Util_CognitoGroupUser.addUserGroup(
305
- ~name=Reventless.AdminGroup.name,
305
+ ~name=Reventless.Role.adminGroup(),
306
306
  ~userPoolId=pool.id,
307
307
  )
308
308
 
@@ -400,7 +400,10 @@ let _resolveUncached = (): cognitoUserPool => {
400
400
  // name is what a caller needs either way, and an output that appeared in one
401
401
  // mode and not the other would read as "there is no administrator group here"
402
402
  // rather than "this stack did not create it".
403
- Pulumi.Pulumi.export("identityProviderAdminGroup", Pulumi.Output.make(Reventless.AdminGroup.name))
403
+ Pulumi.Pulumi.export(
404
+ "identityProviderAdminGroup",
405
+ Pulumi.Output.make(Reventless.Role.adminGroup()),
406
+ )
404
407
 
405
408
  // Exported so an operator can read back the store this provider resolved to —
406
409
  // the name is derived, not configured, so this is a convenience for checking
@@ -5,7 +5,7 @@ import * as Pulumi$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Pulumi
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
8
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
9
9
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
10
10
  import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
11
11
  import * as Auth_SignUpMode$ReventlessAws from "./adapter/Auth/Auth_SignUpMode.res.mjs";
@@ -133,7 +133,7 @@ function _resolveUncached() {
133
133
  mfaConfiguration: "OFF",
134
134
  tags: AWS_Tags$ReventlessAws.make("HostUiPool", "Platform", "Auth", "Platform", undefined, undefined, undefined, undefined)
135
135
  });
136
- Util_CognitoGroupUser$ReventlessAws.addUserGroup(AdminGroup$Reventless.name, pool.id);
136
+ Util_CognitoGroupUser$ReventlessAws.addUserGroup(Role$Reventless.adminGroup(), pool.id);
137
137
  let tokenUnits2_accessToken = "minutes";
138
138
  let tokenUnits2_idToken = "minutes";
139
139
  let tokenUnits2_refreshToken = "days";
@@ -181,7 +181,7 @@ function _resolveUncached() {
181
181
  Pulumi$Pulumi.$$export("cognitoUserPoolArn", result.poolArn);
182
182
  Pulumi$Pulumi.$$export("cognitoRegion", regionOutput);
183
183
  Pulumi$Pulumi.$$export("cognitoUserPoolManaged", managedStr);
184
- Pulumi$Pulumi.$$export("identityProviderAdminGroup", Pulumi.output(AdminGroup$Reventless.name));
184
+ Pulumi$Pulumi.$$export("identityProviderAdminGroup", Pulumi.output(Role$Reventless.adminGroup()));
185
185
  Pulumi$Pulumi.$$export("activeRoleStore", result.activeRoleTable.name);
186
186
  return result;
187
187
  }
@@ -194,7 +194,7 @@ let make = (
194
194
  let adminEntryJson =
195
195
  ReventlessCore.Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
196
196
  ~pluginId=ReventlessCore.Platform_Admin_Structure.pluginId,
197
- ReventlessCore.Platform_Admin_Structure.structure,
197
+ ReventlessCore.Platform_Admin_Structure.structure(),
198
198
  )->JSON.stringify
199
199
 
200
200
  // Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
@@ -107,7 +107,7 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
107
107
  encodeSelections(j.selections)
108
108
  ]
109
109
  ]))) : "";
110
- let adminEntryJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(Platform_Admin_Structure$ReventlessCore.pluginId, undefined, Platform_Admin_Structure$ReventlessCore.structure));
110
+ let adminEntryJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(Platform_Admin_Structure$ReventlessCore.pluginId, undefined, Platform_Admin_Structure$ReventlessCore.structure()));
111
111
  let packageDirs = Object.fromEntries([[
112
112
  "@reventlessdev/reventless-aws",
113
113
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
@@ -6,7 +6,7 @@
6
6
  // ([Auth_ActiveRoleTrigger.res] bundles it and attaches it to the user pool).
7
7
  //
8
8
  // The narrowing has to happen here rather than in our own enforcement code
9
- // because `@authorize(AllowGroups([...]))` compiles to a field directive AppSync
9
+ // because `@authorize(AllowRoles([...]))` compiles to a field directive AppSync
10
10
  // evaluates against `cognito:groups` **before any of our code executes**. A
11
11
  // request header we invent could scope reads correctly and still leave every
12
12
  // group-gated mutation callable — right about the data, wrong about the writes.
@@ -150,7 +150,7 @@ let registerBinding = (
150
150
  ),
151
151
  labelField: entry.labelField,
152
152
  includeIdParam: entry.includeIdParam,
153
- authorization: spec.authorization,
153
+ authorization: spec.authorization->Reventless.Authorization.named,
154
154
  // From the same schema `capability` is derived from, one line above, so the
155
155
  // two cannot end up disagreeing about this read model's fields.
156
156
  ownerField: Reventless.Owner.fieldNames(spec.stateSchema)->Array.get(0),