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

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 (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +7 -7
  3. package/scripts/ProvisionAdmin.res +2 -2
  4. package/scripts/ProvisionAdmin.res.mjs +7 -6
  5. package/src/Platform.res +32 -3
  6. package/src/Platform.res.mjs +22 -5
  7. package/src/Platform_Stack.res +6 -3
  8. package/src/Platform_Stack.res.mjs +3 -3
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -1
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  11. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +1 -1
  12. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -1
  13. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
  14. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -0
  15. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -0
  16. package/src/components/Api/AppSync_Adapter.res +4 -3
  17. package/src/components/Api/AppSync_SdlDecorate.res +7 -3
  18. package/src/components/Api/AppSync_SdlDecorate.res.mjs +4 -3
  19. package/src/util/Util_OwnerScopeEnv.res +9 -0
  20. package/src/util/Util_OwnerScopeEnv.res.mjs +14 -1
  21. package/src/util/Util_ShellConfig.res +4 -0
  22. package/src/util/Util_ShellConfig.res.mjs +5 -1
  23. package/tests/AppSync_AdapterTest.res +46 -23
  24. package/tests/AppSync_AdapterTest.res.mjs +35 -27
  25. package/tests/PgPipeline_IntegrationTest.res +2 -1
  26. package/tests/PgQueryDbUnion_IntegrationTest.res +2 -1
  27. package/tests/Util_OwnerScopeEnvTest.res +48 -2
  28. package/tests/Util_OwnerScopeEnvTest.res.mjs +49 -3
  29. package/tests/Util_ShellConfigTest.res +17 -0
  30. package/tests/Util_ShellConfigTest.res.mjs +18 -0
  31. package/tests/integration/AggTestAggregate.res +2 -1
  32. package/tests/integration/EpCompositeSlice.res +2 -1
  33. package/tests/integration/EpTestSlice.res +2 -1
  34. package/tests/integration/QueryDbUnion_DynamoDb_IntegrationTest.res +2 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.370 (2026-10-01)
7
+
8
+ * feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * AllowGroups and AdminGroup are removed — write AllowRoles over
13
+ the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
14
+ types declare `type role`, and their rules are Authorization.rule<role>; a spec
15
+ that writes its own rule declares `type role` (Reventless.Role.name for a rule
16
+ over Authorization.permission). Role.name is a private string: make one from a
17
+ string with Role.make.
18
+
19
+
20
+
6
21
  # 3.0.0-alpha.369 (2026-10-01)
7
22
 
8
23
  ### 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.370",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -22,18 +22,18 @@
22
22
  "uuid": "^13.0.0",
23
23
  "yaml": "^2.8.3",
24
24
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.27",
25
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
25
26
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
26
27
  "@reventlessdev/rescript-node": "2.0.0-alpha.20",
27
28
  "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.22",
28
- "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
29
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
30
29
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
30
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
31
+ "@reventlessdev/reventless-core": "3.0.0-alpha.286",
31
32
  "@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
33
  "@reventlessdev/reventless-interop": "3.0.0-alpha.38",
36
- "@reventlessdev/reventless-spec": "3.0.0-alpha.155"
34
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.150",
35
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.156",
36
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.184"
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,24 @@ 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
+ /** The groups of a user pool this deployment did not create, which no accounts
116
+ manifest names. They count as provided by the check that every role a plugin
117
+ needs has a group. */
118
+ let providedGroups = Reventless.Role.provideGroups
119
+
120
+ /** The group a role resolves to — the name this platform writes into its
121
+ directives, its shell configuration and the elevated groups. For code that
122
+ grants access by group, so it never spells one itself. */
123
+ let groupOf = Reventless.Role.groupOf
124
+
125
+ /** `groupOf(Role.admin)`. */
126
+ let adminGroup = Reventless.Role.adminGroup
127
+
110
128
  module MakeWithConfig = (
111
129
  Config: {
112
130
  let splitApi: bool
@@ -144,7 +162,15 @@ module MakeWithConfig = (
144
162
  // A default, never a forcing: an explicit `setElevatedGroups` in a platform
145
163
  // root — including one naming nobody — means what it says and wins. See
146
164
  // [Reventless.OwnerScope.defaultElevatedGroups].
147
- Reventless.OwnerScope.defaultElevatedGroups([Reventless.AdminGroup.name])
165
+ Reventless.OwnerScope.defaultElevatedRoles([Reventless.Role.admin])
166
+
167
+ // What this deployment provides for the roles its plugins need: the
168
+ // administrator group the stack declares, and the groups of the accounts
169
+ // provisioning creates. Read when each plugin is built, after the root has
170
+ // mapped its roles.
171
+ Reventless.Role.provideGroupsFrom(() =>
172
+ [Reventless.Role.adminGroup()]->Array.concat(Reventless.AccountsManifest.declaredGroups())
173
+ )
148
174
 
149
175
  // Dispatch per-flavor commandHandlerConfig records to the four runtime
150
176
  // builders. Every sub-record is optional; only branches the caller actually
@@ -308,13 +334,16 @@ module MakeWithConfig = (
308
334
  // Admin base as a source-API document — auth-decorated (all fields Admin,
309
335
  // Cognito-only; the deploy-time SigV4 system-caller fields died with the
310
336
  // fragment registry) plus the canonical stamp.
311
- let adminSourceSdl = (): string =>
337
+ let adminSourceSdl = (): string => {
338
+ // The directive below fixes the administrator's group for this deploy.
339
+ Reventless.Role.freeze()
312
340
  assembleCanonicalSourceSdl(
313
341
  ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
314
342
  ReventlessCore.Platform_AdminApi.baseFragment(~cloner=Config.cloner),
315
- ~group=Reventless.AdminGroup.name,
343
+ ~group=Reventless.Role.adminGroup(),
316
344
  ),
317
345
  )
346
+ }
318
347
 
319
348
  // Split-mode Domain source document: relay base types + Platform_ping — the
320
349
  // 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";
@@ -152,7 +153,8 @@ let messagingEmailProviderRef = {
152
153
  };
153
154
 
154
155
  function MakeWithConfig(Config) {
155
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
156
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
157
+ Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
156
158
  Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
157
159
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
158
160
  Stdlib_Option.forEach(param.async, AggregateRuntime_Builder_Single_Async$ReventlessAws.setConfig);
@@ -214,7 +216,8 @@ function MakeWithConfig(Config) {
214
216
  subscriptionSources: []
215
217
  });
216
218
  let adminSourceSdl = () => {
217
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), AdminGroup$Reventless.name, undefined);
219
+ Role$Reventless.freeze();
220
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), Role$Reventless.adminGroup(), undefined);
218
221
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
219
222
  };
220
223
  let match;
@@ -1566,7 +1569,8 @@ function MakeWithConfig(Config) {
1566
1569
 
1567
1570
  function Make($star) {
1568
1571
  let commandHandlerConfig = {};
1569
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
1572
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
1573
+ Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
1570
1574
  Stdlib_Option.forEach(commandHandlerConfig.aggregates, param => {
1571
1575
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
1572
1576
  Stdlib_Option.forEach(param.async, AggregateRuntime_Builder_Single_Async$ReventlessAws.setConfig);
@@ -1628,7 +1632,8 @@ function Make($star) {
1628
1632
  subscriptionSources: []
1629
1633
  });
1630
1634
  let adminSourceSdl = () => {
1631
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), AdminGroup$Reventless.name, undefined);
1635
+ Role$Reventless.freeze();
1636
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), Role$Reventless.adminGroup(), undefined);
1632
1637
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
1633
1638
  };
1634
1639
  let match;
@@ -2958,6 +2963,14 @@ function Make($star) {
2958
2963
  };
2959
2964
  }
2960
2965
 
2966
+ let roleGroups = Role$Reventless.setGroups;
2967
+
2968
+ let providedGroups = Role$Reventless.provideGroups;
2969
+
2970
+ let groupOf = Role$Reventless.groupOf;
2971
+
2972
+ let adminGroup = Role$Reventless.adminGroup;
2973
+
2961
2974
  export {
2962
2975
  log,
2963
2976
  apiConfigRef,
@@ -2969,6 +2982,10 @@ export {
2969
2982
  geocoderPlaceIndexRef,
2970
2983
  messagingEmailSenderRef,
2971
2984
  messagingEmailProviderRef,
2985
+ roleGroups,
2986
+ providedGroups,
2987
+ groupOf,
2988
+ adminGroup,
2972
2989
  MakeWithConfig,
2973
2990
  Make,
2974
2991
  }
@@ -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),
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
7
7
  import * as RowImage$Reventless from "@reventlessdev/reventless-spec/src/semantic/RowImage.res.mjs";
8
8
  import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
9
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
9
10
  import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
10
11
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
11
12
  import * as PgQueryResolver_Lambda$ReventlessAws from "../QueryDb/PgQueryResolver_Lambda.res.mjs";
@@ -75,7 +76,7 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
75
76
  capability: GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(entry.readModelName, spec.stateSchema),
76
77
  labelField: entry.labelField,
77
78
  includeIdParam: entry.includeIdParam,
78
- authorization: spec.authorization,
79
+ authorization: Authorization$Reventless.named(spec.authorization),
79
80
  ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0],
80
81
  retiredField: Stdlib_Option.map(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.field),
81
82
  retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values),
@@ -246,6 +246,7 @@ let makeFromCodeAsset: (
246
246
  // concludes nobody is elevated and stamps an operator's on-behalf write with
247
247
  // the operator's own id.
248
248
  Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables)
249
+ Util_OwnerScopeEnv.applyRoleGroupsDefault(variables)
249
250
 
250
251
  // ESM self-containment (Option C): every code archive built by
251
252
  // Util_Bundle.buildCodeArchive ships register-hook.mjs + layer-resolver.mjs at
@@ -104,6 +104,7 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
104
104
  });
105
105
  Util_LambdaLogging$ReventlessAws.applyLogLevelDefault(variables);
106
106
  Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables);
107
+ Util_OwnerScopeEnv$ReventlessAws.applyRoleGroupsDefault(variables);
107
108
  variables["NODE_OPTIONS"] = Util_Bundle$ReventlessAws.esmLoaderNodeOptions;
108
109
  variables["ESM_FALLBACK_DIRS"] = Util_Bundle$ReventlessAws.esmFallbackDirs;
109
110
  if (!RuntimeExtension$ReventlessCore.isEmpty()) {
@@ -181,12 +181,13 @@ let rec waitForMergeSuccess = async (
181
181
  // from `@@reventless.authorize` / `@authorize` PPX annotations.
182
182
  //
183
183
  // When both are present on the same field, the spec-level permission wins
184
- // (it is more specific). `AllowGroups([g1, g2, ...])` emits
185
- // `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`.
184
+ // (it is more specific). `AllowRoles([r1, r2, ...])` emits
185
+ // `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`, each `g` the group
186
+ // the deployment maps that role to.
186
187
  // `AllowAuthenticated` emits the group-less `@aws_cognito_user_pools` — the same
187
188
  // reachability an undirectived field already has, but stated rather than left
188
189
  // implicit, so `assertGateable` can tell "deliberately open" from "nobody
189
- // stamped this". `AllowGroups([])` and `DenyAll` emit a sentinel `__deny_all__`
190
+ // stamped this". `AllowRoles([])` and `DenyAll` emit a sentinel `__deny_all__`
190
191
  // group that no Cognito user can belong to — effectively blocking the field at
191
192
  // the API layer.
192
193
  //
@@ -136,8 +136,12 @@ type fieldGate =
136
136
 
137
137
  let permissionToGate = (permission: Reventless.Authorization.permission): fieldGate =>
138
138
  switch permission {
139
- | AllowGroups([]) => Groups(["__deny_all__"])
140
- | AllowGroups(groups) => Groups(groups)
139
+ // The groups the roles map to: the deploy program is where the mapping is known.
140
+ | AllowRoles(_) =>
141
+ switch Reventless.Authorization.groupsOf(permission) {
142
+ | [] => Groups(["__deny_all__"])
143
+ | groups => Groups(groups)
144
+ }
141
145
  | DenyAll => Groups(["__deny_all__"])
142
146
  | AllowAuthenticated => AnyAuthenticated
143
147
  | AllowAnonymous => Anonymous
@@ -165,7 +169,7 @@ let refuseAnonymousFields = (fieldNames: array<string>): 'a =>
165
169
  `Anonymous fields on AWS are deferred. When they land it will be as a separate\n` ++
166
170
  `API carrying only the unauthenticated fields. Do not add API_KEY to this one:\n` ++
167
171
  `every field here would then have to say whether the key reaches it, and one\n` ++
168
- `that forgot would be open. Until then, declare the field AllowAuthenticated\n` ++ `(or AllowGroups) and mean it.`,
172
+ `that forgot would be open. Until then, declare the field AllowAuthenticated\n` ++ `(or AllowRoles) and mean it.`,
169
173
  )
170
174
 
171
175
  // Injects the Cognito group gate on ALL mutation, query, and subscription fields
@@ -4,6 +4,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
7
8
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
8
9
 
9
10
  function injectAwsSubscribe(sdl, sources) {
@@ -65,7 +66,7 @@ function permissionToGate(permission) {
65
66
  };
66
67
  }
67
68
  } else {
68
- let groups = permission._0;
69
+ let groups = Authorization$Reventless.groupsOf(permission);
69
70
  if (groups.length !== 0) {
70
71
  return {
71
72
  TAG: "Groups",
@@ -81,7 +82,7 @@ function permissionToGate(permission) {
81
82
  }
82
83
 
83
84
  function refuseAnonymousFields(fieldNames) {
84
- return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nAnonymous fields on AWS are deferred. When they land it will be as a separate\nAPI carrying only the unauthenticated fields. Do not add API_KEY to this one:\nevery field here would then have to say whether the key reaches it, and one\nthat forgot would be open. Until then, declare the field AllowAuthenticated\n(or AllowGroups) and mean it.`);
85
+ return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nAnonymous fields on AWS are deferred. When they land it will be as a separate\nAPI carrying only the unauthenticated fields. Do not add API_KEY to this one:\nevery field here would then have to say whether the key reaches it, and one\nthat forgot would be open. Until then, declare the field AllowAuthenticated\n(or AllowRoles) and mean it.`);
85
86
  }
86
87
 
87
88
  function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
@@ -388,4 +389,4 @@ export {
388
389
  canonicalTypeNames,
389
390
  stampCanonicalTypes,
390
391
  }
391
- /* GraphQL_Stitcher-ReventlessCore Not a pure module */
392
+ /* Authorization-Reventless Not a pure module */
@@ -46,3 +46,12 @@ let applyElevatedGroupsDefault = (variables: dict<Pulumi.Input.t<string>>) =>
46
46
  if variables->Dict.get(key)->Option.isNone {
47
47
  entry()->Option.forEach(((k, v)) => variables->Dict.set(k, v))
48
48
  }
49
+
50
+ /** The renamed roles, for the same two-process reason: a runtime that evaluates
51
+ a rule (the SQL-backed view reads) never runs the root that stated the
52
+ mapping. Only the renamed ones travel; every other role is its own group. */
53
+ let applyRoleGroupsDefault = (variables: dict<Pulumi.Input.t<string>>) =>
54
+ switch (variables->Dict.get(Reventless.Role.envKey), Reventless.Role.envValue()) {
55
+ | (None, Some(value)) => variables->Dict.set(Reventless.Role.envKey, value->Pulumi.Input.make)
56
+ | _ => ()
57
+ }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
4
5
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
5
6
 
6
7
  let key = "REVENTLESS_ELEVATED_GROUPS";
@@ -23,9 +24,21 @@ function applyElevatedGroupsDefault(variables) {
23
24
  }
24
25
  }
25
26
 
27
+ function applyRoleGroupsDefault(variables) {
28
+ let match = variables[Role$Reventless.envKey];
29
+ let match$1 = Role$Reventless.envValue();
30
+ if (match !== undefined || match$1 === undefined) {
31
+ return;
32
+ } else {
33
+ variables[Role$Reventless.envKey] = match$1;
34
+ return;
35
+ }
36
+ }
37
+
26
38
  export {
27
39
  key,
28
40
  entry,
29
41
  applyElevatedGroupsDefault,
42
+ applyRoleGroupsDefault,
30
43
  }
31
- /* OwnerScope-Reventless Not a pure module */
44
+ /* Role-Reventless Not a pure module */
@@ -98,6 +98,10 @@ let fields = (
98
98
  ): dict<JSON.t> => {
99
99
  let out = computed->Dict.fromArray
100
100
 
101
+ // Computed, like the endpoints: the group the admin API is gated on, from the
102
+ // deploy's own role mapping.
103
+ ReventlessCore.Platform_AdminGroup.configEntry()->Option.forEach(((k, v)) => out->Dict.set(k, v))
104
+
101
105
  // The shell imports the module this key names, and imports nothing when the
102
106
  // key is absent — so writing the object without naming it here would ship a
103
107
  // file nothing fetches. Computed rather than left to `shellConfig` for the
@@ -5,6 +5,7 @@ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Platform$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Platform.res.mjs";
7
7
  import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
8
+ import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
8
9
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
9
10
 
10
11
  let journeyManifestsKey = "journeyManifestUrls";
@@ -59,6 +60,9 @@ function subscriptionEndpoint(httpsEndpoint) {
59
60
 
60
61
  function fields(computed, viewModes, bakedManifest, uiSlotsFile, shellConfig) {
61
62
  let out = Object.fromEntries(computed);
63
+ Stdlib_Option.forEach(Platform_AdminGroup$ReventlessCore.configEntry(), param => {
64
+ out[param[0]] = param[1];
65
+ });
62
66
  Stdlib_Option.forEach(uiSlotsFile, param => {
63
67
  out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
64
68
  });
@@ -103,4 +107,4 @@ export {
103
107
  subscriptionEndpoint,
104
108
  fields,
105
109
  }
106
- /* No side effect */
110
+ /* Platform_AdminGroup-ReventlessCore Not a pure module */
@@ -183,20 +183,16 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
183
183
  ?permission,
184
184
  }
185
185
 
186
- testSync("AllowGroups([\"Admin\"]) emits cognito_groups: [\"Admin\"] on mutation", () => {
186
+ testSync("AllowRoles with several roles emits comma-separated groups", () => {
187
187
  let fp = Dict.fromArray([
188
- ("catalog_Category_Archive", Reventless.Authorization.AllowGroups(["Admin"])),
188
+ (
189
+ "p_X",
190
+ Reventless.Authorization.AllowRoles([
191
+ Reventless.Role.make("Admin"),
192
+ Reventless.Role.make("Editor"),
193
+ ]),
194
+ ),
189
195
  ])
190
- let entry = mutationEntry(~fieldNames=["catalog_Category_Archive"], ~fieldPermissions=fp)
191
- let frag = makeFragment(["catalog_Category_Archive(id: ID!): String"], [])
192
- let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
193
- let parts = decodeFragment(aug)
194
- let m = parts.mutations->Array.getUnsafe(0)
195
- expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
196
- })
197
-
198
- testSync("AllowGroups multi-group emits comma-separated groups", () => {
199
- let fp = Dict.fromArray([("p_X", Reventless.Authorization.AllowGroups(["Admin", "Editor"]))])
200
196
  let entry = mutationEntry(~fieldNames=["p_X"], ~fieldPermissions=fp)
201
197
  let frag = makeFragment(["p_X(id: ID!): String"], [])
202
198
  let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
@@ -206,6 +202,29 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
206
202
  )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`)
207
203
  })
208
204
 
205
+ // The deploy program knows the mapping, so the directive carries the group a
206
+ // role maps to — never the role, which no token carries.
207
+ testSync("AllowRoles emits the groups its roles map to", () => {
208
+ Reventless.Role.clearGroups()
209
+ Reventless.Role.setGroups([(Reventless.Role.make("Merchandiser"), "shop-merch-team")])
210
+ let fp = Dict.fromArray([
211
+ (
212
+ "p_Rename",
213
+ Reventless.Authorization.AllowRoles([
214
+ Reventless.Role.make("Admin"),
215
+ Reventless.Role.make("Merchandiser"),
216
+ ]),
217
+ ),
218
+ ])
219
+ let entry = mutationEntry(~fieldNames=["p_Rename"], ~fieldPermissions=fp)
220
+ let frag = makeFragment(["p_Rename(id: ID!): String"], [])
221
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
222
+ Reventless.Role.clearGroups()
223
+ expect(
224
+ decodeFragment(aug).mutations->Array.getUnsafe(0),
225
+ )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`)
226
+ })
227
+
209
228
  testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
210
229
  let fp = Dict.fromArray([("p_Add", Reventless.Authorization.AllowAuthenticated)])
211
230
  let entry = mutationEntry(~fieldNames=["p_Add"], ~fieldPermissions=fp)
@@ -230,7 +249,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
230
249
 
231
250
  testSync("Per-field permissions: only annotated fields get directives", () => {
232
251
  let fp = Dict.fromArray([
233
- ("p_Archive", Reventless.Authorization.AllowGroups(["Admin"])),
252
+ ("p_Archive", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
234
253
  ("p_Add", Reventless.Authorization.AllowAuthenticated),
235
254
  ])
236
255
  let entry = mutationEntry(~fieldNames=["p_Archive", "p_Add"], ~fieldPermissions=fp)
@@ -255,7 +274,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
255
274
  let entry = queryEntry(
256
275
  ~single="p_Item",
257
276
  ~list="p_Items",
258
- ~permission=Some(Reventless.Authorization.AllowGroups(["Manager"])),
277
+ ~permission=Some(Reventless.Authorization.AllowRoles([Reventless.Role.make("Manager")])),
259
278
  )
260
279
  // Production query SDL emitted by GraphQL_FragmentGenerator always has an
261
280
  // arg list (Relay pagination on the list field), so extractLeadingName's
@@ -362,7 +381,9 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
362
381
  testSync("Spec-level permission wins over legacy authorization field", () => {
363
382
  // Legacy {tableName, group} says "Admin"; spec-level says "Manager".
364
383
  // Spec-level must win.
365
- let fp = Dict.fromArray([("p_X", Reventless.Authorization.AllowGroups(["Manager"]))])
384
+ let fp = Dict.fromArray([
385
+ ("p_X", Reventless.Authorization.AllowRoles([Reventless.Role.make("Manager")])),
386
+ ])
366
387
  let entry: ReventlessInfra.Api.mutationSchemaEntry = {
367
388
  fieldNames: ["p_X"],
368
389
  commandSchema: S.unknown,
@@ -427,7 +448,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
427
448
  fieldNames: ["p_Sync"],
428
449
  commandSchema: S.unknown,
429
450
  fieldPermissions: Dict.fromArray([
430
- ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
451
+ ("p_Sync", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
431
452
  ]),
432
453
  systemCallable: true,
433
454
  }
@@ -464,8 +485,8 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
464
485
  fieldNames: ["p_Sync", "p_Other"],
465
486
  commandSchema: S.unknown,
466
487
  fieldPermissions: Dict.fromArray([
467
- ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
468
- ("p_Other", Reventless.Authorization.AllowGroups(["Admin"])),
488
+ ("p_Sync", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
489
+ ("p_Other", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
469
490
  ]),
470
491
  // systemCallable applies to every field in the entry; split the fields so only
471
492
  // p_Sync opts in.
@@ -475,7 +496,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
475
496
  fieldNames: ["p_Sync"],
476
497
  commandSchema: S.unknown,
477
498
  fieldPermissions: Dict.fromArray([
478
- ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
499
+ ("p_Sync", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
479
500
  ]),
480
501
  systemCallable: true,
481
502
  }
@@ -501,7 +522,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
501
522
  returnTypeName: "p_Item",
502
523
  stateSchema: S.unknown,
503
524
  authorization: None,
504
- permission: Reventless.Authorization.AllowGroups(["Admin"]),
525
+ permission: Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")]),
505
526
  systemCallable: true,
506
527
  }
507
528
  let frag = makeFragment(
@@ -532,7 +553,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
532
553
  returnTypeName: "p_Item",
533
554
  stateSchema: S.unknown,
534
555
  authorization: None,
535
- permission: Reventless.Authorization.AllowGroups(["Admin"]),
556
+ permission: Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")]),
536
557
  systemCallable: true,
537
558
  }
538
559
  let frag = makeFragment(
@@ -583,7 +604,7 @@ describe("AppSync_Adapter — type-level dual-auth", () => {
583
604
  returnTypeName: "p_Item",
584
605
  stateSchema: S.unknown,
585
606
  authorization: None,
586
- permission: Reventless.Authorization.AllowGroups(["Admin"]),
607
+ permission: Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")]),
587
608
  systemCallable: true,
588
609
  }
589
610
 
@@ -958,7 +979,9 @@ describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
958
979
  })
959
980
 
960
981
  testSync("a subscription named like a gated mutation inherits its groups", () => {
961
- let fp = Dict.fromArray([("p_Ship", Reventless.Authorization.AllowGroups(["Fulfilment"]))])
982
+ let fp = Dict.fromArray([
983
+ ("p_Ship", Reventless.Authorization.AllowRoles([Reventless.Role.make("Fulfilment")])),
984
+ ])
962
985
  let frag = makeFragment(["p_Ship: CommandResult"])
963
986
  let aug = AppSync_Adapter.injectAwsAuth(
964
987
  frag,
@@ -4,6 +4,7 @@ import * as Sury from "sury";
4
4
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
7
8
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
9
  import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
9
10
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
@@ -129,37 +130,44 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
129
130
  authorization: undefined,
130
131
  permission: permission
131
132
  });
132
- globalThis.test("AllowGroups([\"Admin\"]) emits cognito_groups: [\"Admin\"] on mutation", () => {
133
+ globalThis.test("AllowRoles with several roles emits comma-separated groups", () => {
133
134
  let fp = Object.fromEntries([[
134
- "catalog_Category_Archive",
135
+ "p_X",
135
136
  {
136
- TAG: "AllowGroups",
137
- _0: ["Admin"]
137
+ TAG: "AllowRoles",
138
+ _0: [
139
+ "Admin",
140
+ "Editor"
141
+ ]
138
142
  }
139
143
  ]]);
140
- let entry = mutationEntry(["catalog_Category_Archive"], fp);
141
- let frag = makeFragment(["catalog_Category_Archive(id: ID!): String"], []);
144
+ let entry = mutationEntry(["p_X"], fp);
145
+ let frag = makeFragment(["p_X(id: ID!): String"], []);
142
146
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
143
147
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
144
- let m = parts.mutations[0];
145
- globalThis.expect(m).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
148
+ globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
146
149
  });
147
- globalThis.test("AllowGroups multi-group emits comma-separated groups", () => {
150
+ globalThis.test("AllowRoles emits the groups its roles map to", () => {
151
+ Role$Reventless.clearGroups();
152
+ Role$Reventless.setGroups([[
153
+ "Merchandiser",
154
+ "shop-merch-team"
155
+ ]]);
148
156
  let fp = Object.fromEntries([[
149
- "p_X",
157
+ "p_Rename",
150
158
  {
151
- TAG: "AllowGroups",
159
+ TAG: "AllowRoles",
152
160
  _0: [
153
161
  "Admin",
154
- "Editor"
162
+ "Merchandiser"
155
163
  ]
156
164
  }
157
165
  ]]);
158
- let entry = mutationEntry(["p_X"], fp);
159
- let frag = makeFragment(["p_X(id: ID!): String"], []);
166
+ let entry = mutationEntry(["p_Rename"], fp);
167
+ let frag = makeFragment(["p_Rename(id: ID!): String"], []);
160
168
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
161
- let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
162
- globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
169
+ Role$Reventless.clearGroups();
170
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`);
163
171
  });
164
172
  globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
165
173
  let fp = Object.fromEntries([[
@@ -190,7 +198,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
190
198
  [
191
199
  "p_Archive",
192
200
  {
193
- TAG: "AllowGroups",
201
+ TAG: "AllowRoles",
194
202
  _0: ["Admin"]
195
203
  }
196
204
  ],
@@ -226,7 +234,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
226
234
  });
227
235
  globalThis.test("Query permission applies to BOTH single and list field names", () => {
228
236
  let entry = queryEntry("p_Item", "p_Items", {
229
- TAG: "AllowGroups",
237
+ TAG: "AllowRoles",
230
238
  _0: ["Manager"]
231
239
  });
232
240
  let frag = makeFragment([], [
@@ -318,7 +326,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
318
326
  let fp = Object.fromEntries([[
319
327
  "p_X",
320
328
  {
321
- TAG: "AllowGroups",
329
+ TAG: "AllowRoles",
322
330
  _0: ["Manager"]
323
331
  }
324
332
  ]]);
@@ -380,7 +388,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
380
388
  let entry_fieldPermissions = Object.fromEntries([[
381
389
  "p_Sync",
382
390
  {
383
- TAG: "AllowGroups",
391
+ TAG: "AllowRoles",
384
392
  _0: ["Admin"]
385
393
  }
386
394
  ]]);
@@ -427,14 +435,14 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
427
435
  [
428
436
  "p_Sync",
429
437
  {
430
- TAG: "AllowGroups",
438
+ TAG: "AllowRoles",
431
439
  _0: ["Admin"]
432
440
  }
433
441
  ],
434
442
  [
435
443
  "p_Other",
436
444
  {
437
- TAG: "AllowGroups",
445
+ TAG: "AllowRoles",
438
446
  _0: ["Admin"]
439
447
  }
440
448
  ]
@@ -450,7 +458,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
450
458
  let iamEntry_fieldPermissions = Object.fromEntries([[
451
459
  "p_Sync",
452
460
  {
453
- TAG: "AllowGroups",
461
+ TAG: "AllowRoles",
454
462
  _0: ["Admin"]
455
463
  }
456
464
  ]]);
@@ -481,7 +489,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
481
489
  });
482
490
  globalThis.test("systemCallable query applies dual-auth to BOTH single and list fields", () => {
483
491
  let entry_permission = {
484
- TAG: "AllowGroups",
492
+ TAG: "AllowRoles",
485
493
  _0: ["Admin"]
486
494
  };
487
495
  let entry_systemCallable = true;
@@ -514,7 +522,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
514
522
  });
515
523
  globalThis.test("systemCallable dual-auth extends to derived query fields (Items/ByIds/By<Index>)", () => {
516
524
  let entry_permission = {
517
- TAG: "AllowGroups",
525
+ TAG: "AllowRoles",
518
526
  _0: ["Admin"]
519
527
  };
520
528
  let entry_systemCallable = true;
@@ -554,7 +562,7 @@ globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
554
562
  subscriptionSources: []
555
563
  });
556
564
  let callableEntry_permission = {
557
- TAG: "AllowGroups",
565
+ TAG: "AllowRoles",
558
566
  _0: ["Admin"]
559
567
  };
560
568
  let callableEntry_systemCallable = true;
@@ -845,7 +853,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
845
853
  let fp = Object.fromEntries([[
846
854
  "p_Ship",
847
855
  {
848
- TAG: "AllowGroups",
856
+ TAG: "AllowRoles",
849
857
  _0: ["Fulfilment"]
850
858
  }
851
859
  ]]);
@@ -35,7 +35,8 @@ module OrdersSpec = {
35
35
 
36
36
  let config = Reventless.ReadModel.config()
37
37
  let subIdConfig = None
38
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
38
+ type role = Reventless.Role.name
39
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
39
40
  let visibility: Reventless.Visibility.t = Public
40
41
  }
41
42
 
@@ -30,7 +30,8 @@ module CustomersSpec = {
30
30
 
31
31
  let config = Reventless.ReadModel.config()
32
32
  let subIdConfig = None
33
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
33
+ type role = Reventless.Role.name
34
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
34
35
  let visibility: Reventless.Visibility.t = Public
35
36
  }
36
37
 
@@ -68,7 +68,7 @@ describe("Util_OwnerScopeEnv — the elevated-groups carrier", () => {
68
68
  testSync("the platform's administrator default reaches a composed environment", () => {
69
69
  Reventless.OwnerScope.clearElevatedGroups()
70
70
  NodeProcess.env->Dict.delete(Util_OwnerScopeEnv.key)
71
- Reventless.OwnerScope.defaultElevatedGroups([Reventless.AdminGroup.name])
71
+ Reventless.OwnerScope.defaultElevatedRoles([Reventless.Role.admin])
72
72
 
73
73
  let variables = Dict.make()
74
74
  Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables)
@@ -81,7 +81,53 @@ describe("Util_OwnerScopeEnv — the elevated-groups carrier", () => {
81
81
  // Round-tripped, like the first case above: the runtime reads this back with
82
82
  // `parseElevatedGroups`, so the encoding is asserted rather than the spelling.
83
83
  expect(Reventless.OwnerScope.parseElevatedGroups(encoded))->toEqual([
84
- Reventless.AdminGroup.name,
84
+ Reventless.Role.adminGroup(),
85
85
  ])
86
86
  })
87
+
88
+ // Runtimes read groups, never roles, so an elevated role travels as the group
89
+ // the deploy program mapped it to.
90
+ testSync("an elevated role travels as the group it is mapped to", () => {
91
+ Reventless.OwnerScope.clearElevatedGroups()
92
+ Reventless.Role.clearGroups()
93
+ NodeProcess.env->Dict.delete(Util_OwnerScopeEnv.key)
94
+ Reventless.Role.setGroups([(Reventless.Role.make("Fulfilment"), "shop-ops")])
95
+ Reventless.OwnerScope.setElevatedRoles([Reventless.Role.make("Fulfilment")])
96
+ Reventless.OwnerScope.setElevatedGroups(["Admin"])
97
+
98
+ let variables = Dict.make()
99
+ Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables)
100
+ let encoded = switch variables->Dict.get(Util_OwnerScopeEnv.key) {
101
+ | Some(value) => value->Obj.magic
102
+ | None => ""
103
+ }
104
+ Reventless.OwnerScope.clearElevatedGroups()
105
+ Reventless.Role.clearGroups()
106
+
107
+ expect(Reventless.OwnerScope.parseElevatedGroups(encoded))->toEqual(["Admin", "shop-ops"])
108
+ })
109
+ })
110
+
111
+ describe("Util_OwnerScopeEnv.applyRoleGroupsDefault", () => {
112
+ testSync("carries the renamed roles, and only those", () => {
113
+ Reventless.Role.clearGroups()
114
+ Reventless.Role.setGroups([(Reventless.Role.make("Merchandiser"), "shop-merch-team")])
115
+ let variables = Dict.make()
116
+ Util_OwnerScopeEnv.applyRoleGroupsDefault(variables)
117
+ Reventless.Role.clearGroups()
118
+ let encoded = switch variables->Dict.get(Reventless.Role.envKey) {
119
+ | Some(value) => value->Obj.magic
120
+ | None => ""
121
+ }
122
+ expect(Reventless.Role.parseGroups(encoded))->toEqual([
123
+ (Reventless.Role.make("Merchandiser"), "shop-merch-team"),
124
+ ])
125
+ })
126
+
127
+ testSync("adds nothing when no role is renamed", () => {
128
+ Reventless.Role.clearGroups()
129
+ let variables = Dict.make()
130
+ Util_OwnerScopeEnv.applyRoleGroupsDefault(variables)
131
+ expect(variables->Dict.get(Reventless.Role.envKey)->Option.isNone)->toBe(true)
132
+ })
87
133
  })
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
5
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
6
6
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
7
7
  import * as Util_OwnerScopeEnv$ReventlessAws from "../src/util/Util_OwnerScopeEnv.res.mjs";
8
8
 
@@ -59,13 +59,59 @@ globalThis.describe("Util_OwnerScopeEnv — the elevated-groups carrier", () =>
59
59
  globalThis.test("the platform's administrator default reaches a composed environment", () => {
60
60
  OwnerScope$Reventless.clearElevatedGroups();
61
61
  Stdlib_Dict.$$delete(process.env, Util_OwnerScopeEnv$ReventlessAws.key);
62
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
62
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
63
63
  let variables = {};
64
64
  Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables);
65
65
  let value = variables[Util_OwnerScopeEnv$ReventlessAws.key];
66
66
  let encoded = value !== undefined ? value : "";
67
67
  OwnerScope$Reventless.clearElevatedGroups();
68
- globalThis.expect(OwnerScope$Reventless.parseElevatedGroups(encoded)).toEqual([AdminGroup$Reventless.name]);
68
+ globalThis.expect(OwnerScope$Reventless.parseElevatedGroups(encoded)).toEqual([Role$Reventless.adminGroup()]);
69
+ });
70
+ globalThis.test("an elevated role travels as the group it is mapped to", () => {
71
+ OwnerScope$Reventless.clearElevatedGroups();
72
+ Role$Reventless.clearGroups();
73
+ Stdlib_Dict.$$delete(process.env, Util_OwnerScopeEnv$ReventlessAws.key);
74
+ Role$Reventless.setGroups([[
75
+ "Fulfilment",
76
+ "shop-ops"
77
+ ]]);
78
+ OwnerScope$Reventless.setElevatedRoles(["Fulfilment"]);
79
+ OwnerScope$Reventless.setElevatedGroups(["Admin"]);
80
+ let variables = {};
81
+ Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables);
82
+ let value = variables[Util_OwnerScopeEnv$ReventlessAws.key];
83
+ let encoded = value !== undefined ? value : "";
84
+ OwnerScope$Reventless.clearElevatedGroups();
85
+ Role$Reventless.clearGroups();
86
+ globalThis.expect(OwnerScope$Reventless.parseElevatedGroups(encoded)).toEqual([
87
+ "Admin",
88
+ "shop-ops"
89
+ ]);
90
+ });
91
+ });
92
+
93
+ globalThis.describe("Util_OwnerScopeEnv.applyRoleGroupsDefault", () => {
94
+ globalThis.test("carries the renamed roles, and only those", () => {
95
+ Role$Reventless.clearGroups();
96
+ Role$Reventless.setGroups([[
97
+ "Merchandiser",
98
+ "shop-merch-team"
99
+ ]]);
100
+ let variables = {};
101
+ Util_OwnerScopeEnv$ReventlessAws.applyRoleGroupsDefault(variables);
102
+ Role$Reventless.clearGroups();
103
+ let value = variables[Role$Reventless.envKey];
104
+ let encoded = value !== undefined ? value : "";
105
+ globalThis.expect(Role$Reventless.parseGroups(encoded)).toEqual([[
106
+ "Merchandiser",
107
+ "shop-merch-team"
108
+ ]]);
109
+ });
110
+ globalThis.test("adds nothing when no role is renamed", () => {
111
+ Role$Reventless.clearGroups();
112
+ let variables = {};
113
+ Util_OwnerScopeEnv$ReventlessAws.applyRoleGroupsDefault(variables);
114
+ globalThis.expect(Stdlib_Option.isNone(variables[Role$Reventless.envKey])).toBe(true);
69
115
  });
70
116
  });
71
117
 
@@ -328,3 +328,20 @@ describe("the subscription endpoint", () => {
328
328
  )
329
329
  )
330
330
  })
331
+
332
+ // The shell gates its admin discovery on this group; a mirror of the server's
333
+ // own resolution, so a renamed administrator role reaches the browser too.
334
+ describe("Util_ShellConfig.fields — adminGroup", () => {
335
+ testSync("is absent while the administrator role keeps its own name", () => {
336
+ Reventless.Role.clearGroups()
337
+ expect(Util_ShellConfig.fields(~computed)->Dict.get("adminGroup")->Option.isNone)->toBe(true)
338
+ })
339
+
340
+ testSync("carries the group a renamed administrator role maps to", () => {
341
+ Reventless.Role.clearGroups()
342
+ Reventless.Role.setGroups([(Reventless.Role.admin, "shop-admins")])
343
+ let out = Util_ShellConfig.fields(~computed)
344
+ Reventless.Role.clearGroups()
345
+ expect(out->get("adminGroup"))->toEqual(JSON.Encode.string("shop-admins"))
346
+ })
347
+ })
@@ -4,6 +4,7 @@ import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.m
4
4
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
7
8
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
9
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
9
10
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
@@ -341,6 +342,23 @@ globalThis.describe("the subscription endpoint", () => {
341
342
  });
342
343
  });
343
344
 
345
+ globalThis.describe("Util_ShellConfig.fields — adminGroup", () => {
346
+ globalThis.test("is absent while the administrator role keeps its own name", () => {
347
+ Role$Reventless.clearGroups();
348
+ globalThis.expect(Stdlib_Option.isNone(Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined)["adminGroup"])).toBe(true);
349
+ });
350
+ globalThis.test("carries the group a renamed administrator role maps to", () => {
351
+ Role$Reventless.clearGroups();
352
+ Role$Reventless.setGroups([[
353
+ Role$Reventless.admin,
354
+ "shop-admins"
355
+ ]]);
356
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined, undefined);
357
+ Role$Reventless.clearGroups();
358
+ globalThis.expect(get(out, "adminGroup")).toEqual("shop-admins");
359
+ });
360
+ });
361
+
344
362
  export {
345
363
  computed,
346
364
  get,
@@ -18,6 +18,7 @@ type error = AlreadyExists
18
18
 
19
19
  let name = "AggTestAggregate"
20
20
  let moduleUrl = "agg-test://AggTestAggregate"
21
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
21
+ type role = Reventless.Role.name
22
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAnonymous
22
23
  type lifecycleState = unit
23
24
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
@@ -63,7 +63,8 @@ type event =
63
63
 
64
64
  let name = "EpCompositeSlice"
65
65
  let moduleUrl = "ep-test://EpCompositeSlice"
66
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
66
+ type role = Reventless.Role.name
67
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAnonymous
67
68
  type lifecycleState = unit
68
69
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
69
70
  let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
@@ -21,7 +21,8 @@ type event = WidgetAdded({widgetId: @s.matches(Reventless.DcbTag.partition) stri
21
21
 
22
22
  let name = "EpTestSlice"
23
23
  let moduleUrl = "ep-test://EpTestSlice"
24
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
24
+ type role = Reventless.Role.name
25
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAnonymous
25
26
  type lifecycleState = unit
26
27
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
27
28
  let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
@@ -32,7 +32,8 @@ module CustomersSpec = {
32
32
 
33
33
  let config = Reventless.ReadModel.config()
34
34
  let subIdConfig = None
35
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
35
+ type role = Reventless.Role.name
36
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
36
37
  let visibility: Reventless.Visibility.t = Public
37
38
  }
38
39