@reventlessdev/reventless-local 3.0.0-alpha.275 → 3.0.0-alpha.277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,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.277 (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.276 (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.275 (2026-10-01)
7
29
 
8
30
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.275",
3
+ "version": "3.0.0-alpha.277",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -34,24 +34,24 @@
34
34
  "graphql-yoga": "^5.21.0",
35
35
  "sury": "11.0.0",
36
36
  "ws": "^8.18.0",
37
- "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
38
37
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
38
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
40
39
  "@reventlessdev/rescript-node": "2.0.0-alpha.20",
41
40
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
42
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
43
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.133",
44
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.232",
45
- "@reventlessdev/reventless-infra": "3.0.0-alpha.183",
46
- "@reventlessdev/reventless-core": "3.0.0-alpha.285",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.149",
48
- "@reventlessdev/reventless-seed": "1.0.0-alpha.31",
49
- "@reventlessdev/reventless-spec": "3.0.0-alpha.155"
41
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
42
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.287",
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.234",
45
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.151",
46
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.185",
47
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.135",
48
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.157",
49
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.31"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
53
53
  "sury-ppx": "11.0.0",
54
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.95"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.96"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "^12.3.1"
package/src/Platform.res CHANGED
@@ -73,6 +73,24 @@ let decodeUiFragmentRegistryEventEnvelope = (eventJson: JSON.t): option<
73
73
  // splitApi=true to serve core and plugin APIs on separate ports,
74
74
  // backend=Backend.Sqlite({...}) to opt into file-backed SQLite persistence.
75
75
  // Usage: module Platform = Platform.MakeWithConfig({let silent = true; let splitApi = false; let cloner = false; let backend = Backend.Memory})
76
+ /** Which group stands for which role, only where the two differ; every other
77
+ role maps to the group of its own name. Call before `Make()`: the platform
78
+ resolves roles while it is built, and a mapping stated later is refused. */
79
+ let roleGroups = Reventless.Role.setGroups
80
+
81
+ /** The groups of a user pool this deployment did not create, which no accounts
82
+ manifest names. They count as provided by the check that every role a plugin
83
+ needs has a group. */
84
+ let providedGroups = Reventless.Role.provideGroups
85
+
86
+ /** The group a role resolves to — the name this platform writes into its
87
+ directives, its shell configuration and the elevated groups. For code that
88
+ grants access by group, so it never spells one itself. */
89
+ let groupOf = Reventless.Role.groupOf
90
+
91
+ /** `groupOf(Role.admin)`. */
92
+ let adminGroup = Reventless.Role.adminGroup
93
+
76
94
  module MakeWithConfig = (
77
95
  Config: {
78
96
  let silent: bool
@@ -87,6 +105,11 @@ module MakeWithConfig = (
87
105
  // …) are no-ops in-memory. envVars are not propagated here because the
88
106
  // local platform has no per-component process boundary.
89
107
  let _ = Config.commandHandlerConfig
108
+
109
+ // What this platform provides for the roles its plugins need: every account it
110
+ // can sign in as, built-in and loaded alike, or the file the store will load
111
+ // when the servers start — after the plugins are built and checked.
112
+ Reventless.Role.provideGroupsFrom(UserStore.providedGroups)
90
113
  // Activate Pulumi mock mode — must happen before any component creation.
91
114
  // Idempotent, so safe to call even if TestRunner.setup() was already called.
92
115
  let _ = TestRunner.setup()
@@ -1789,7 +1812,7 @@ module MakeWithConfig = (
1789
1812
  // structure manually.
1790
1813
  pluginStructuresStore.contents->Dict.set(
1791
1814
  ReventlessCore.Platform_Admin_Structure.pluginId,
1792
- ReventlessCore.Platform_Admin_Structure.structure,
1815
+ ReventlessCore.Platform_Admin_Structure.structure(),
1793
1816
  )
1794
1817
 
1795
1818
  let pluginQueryDbName = ReventlessCore.PluginsReadModelSpec.name
@@ -2422,6 +2445,10 @@ module MakeWithConfig = (
2422
2445
  }}`,
2423
2446
  )
2424
2447
 
2448
+ // The role check below reads the accounts as they stand; a user store
2449
+ // loaded from elsewhere after this would leave it compared with the wrong ones.
2450
+ UserStore.pluginChecked := true
2451
+
2425
2452
  // Set the active deploy target so resolveTargetGraphQL/MCP() and QueryDb serverRef/relayRef
2426
2453
  // route registrations to the correct server. Mirrors AWS resolveTargetApi() pattern.
2427
2454
  currentDeployTarget.contents = apiTarget
@@ -12,6 +12,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
12
12
  import * as Effect from "effect/Effect";
13
13
  import * as Pulumi from "@pulumi/pulumi";
14
14
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
15
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
15
16
  import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
16
17
  import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiStyle.res.mjs";
17
18
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
@@ -140,6 +141,7 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
140
141
  }
141
142
 
142
143
  function MakeWithConfig(Config) {
144
+ Role$Reventless.provideGroupsFrom(UserStore$ReventlessLocal.providedGroups);
143
145
  TestRunner$ReventlessLocal.setup();
144
146
  let match = Config.backend;
145
147
  if (typeof match !== "object") {
@@ -1360,7 +1362,7 @@ function MakeWithConfig(Config) {
1360
1362
  connectPlugin(plugins$1);
1361
1363
  seedPluginStructuresStore(plugins$1);
1362
1364
  }
1363
- pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
1365
+ pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
1364
1366
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
1365
1367
  let parts = field.split("_");
1366
1368
  let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
@@ -1753,6 +1755,7 @@ function MakeWithConfig(Config) {
1753
1755
  let tmp;
1754
1756
  tmp = apiTarget === "Domain" ? "Domain" : "Platform";
1755
1757
  log.info("Platform", undefined, `deployPlugin target=` + tmp);
1758
+ UserStore$ReventlessLocal.pluginChecked.contents = true;
1756
1759
  currentDeployTarget.contents = apiTarget;
1757
1760
  StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
1758
1761
  let tmp$1;
@@ -1905,6 +1908,7 @@ function MakeWithConfig(Config) {
1905
1908
 
1906
1909
  function Make($star) {
1907
1910
  let backend = Backend$ReventlessLocal.fromEnv();
1911
+ Role$Reventless.provideGroupsFrom(UserStore$ReventlessLocal.providedGroups);
1908
1912
  TestRunner$ReventlessLocal.setup();
1909
1913
  if (typeof backend !== "object") {
1910
1914
  BackendState$ReventlessLocal.setMemory();
@@ -3113,7 +3117,7 @@ function Make($star) {
3113
3117
  connectPlugin(plugins$1);
3114
3118
  seedPluginStructuresStore(plugins$1);
3115
3119
  }
3116
- pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
3120
+ pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
3117
3121
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
3118
3122
  let parts = field.split("_");
3119
3123
  let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
@@ -3497,6 +3501,7 @@ function Make($star) {
3497
3501
  let tmp;
3498
3502
  tmp = apiTarget === "Domain" ? "Domain" : "Platform";
3499
3503
  log.info("Platform", undefined, `deployPlugin target=` + tmp);
3504
+ UserStore$ReventlessLocal.pluginChecked.contents = true;
3500
3505
  currentDeployTarget.contents = apiTarget;
3501
3506
  StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
3502
3507
  let tmp$1;
@@ -3642,6 +3647,14 @@ function Make($star) {
3642
3647
  };
3643
3648
  }
3644
3649
 
3650
+ let roleGroups = Role$Reventless.setGroups;
3651
+
3652
+ let providedGroups = Role$Reventless.provideGroups;
3653
+
3654
+ let groupOf = Role$Reventless.groupOf;
3655
+
3656
+ let adminGroup = Role$Reventless.adminGroup;
3657
+
3645
3658
  export {
3646
3659
  log,
3647
3660
  platformGraphQLRef,
@@ -3649,6 +3662,10 @@ export {
3649
3662
  platformMCPRef,
3650
3663
  decodePluginEventEnvelope,
3651
3664
  decodeUiFragmentRegistryEventEnvelope,
3665
+ roleGroups,
3666
+ providedGroups,
3667
+ groupOf,
3668
+ adminGroup,
3652
3669
  MakeWithConfig,
3653
3670
  Make,
3654
3671
  }
@@ -41,7 +41,12 @@ let manifestUrlOf = (config: ReventlessInfra.Platform.bakedManifest): string =>
41
41
  // caller matching no declared group gets and what every existing deployment has.
42
42
  let journeyManifestsKey = "journeyManifestUrls"
43
43
 
44
- let computedKeys = ["manifestUrl", journeyManifestsKey, ReventlessCore.Platform_UiSlots.configKey]
44
+ let computedKeys = [
45
+ "manifestUrl",
46
+ journeyManifestsKey,
47
+ ReventlessCore.Platform_UiSlots.configKey,
48
+ ReventlessCore.Platform_AdminGroup.configKey,
49
+ ]
45
50
 
46
51
  /**
47
52
  The overlay this platform puts on top of the shipped `config.json`.
@@ -69,6 +74,8 @@ let overlay = (
69
74
  )
70
75
  )
71
76
 
77
+ ReventlessCore.Platform_AdminGroup.configEntry()->Option.forEach(((k, v)) => out->Dict.set(k, v))
78
+
72
79
  bakedManifest->Option.forEach(config => {
73
80
  out->Dict.set("manifestUrl", JSON.Encode.string(manifestUrlOf(config)))
74
81
  // Omitted entirely when nothing is declared, so a single-audience deployment
@@ -9,6 +9,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
9
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
10
10
  import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
11
11
  import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
12
+ import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
12
13
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
13
14
 
14
15
  let log = Logger$ReventlessCore.fromEnv();
@@ -26,7 +27,8 @@ let journeyManifestsKey = "journeyManifestUrls";
26
27
  let computedKeys = [
27
28
  "manifestUrl",
28
29
  journeyManifestsKey,
29
- Platform_UiSlots$ReventlessCore.configKey
30
+ Platform_UiSlots$ReventlessCore.configKey,
31
+ Platform_AdminGroup$ReventlessCore.configKey
30
32
  ];
31
33
 
32
34
  function overlay(bakedManifest, uiSlotsFile, shellConfig) {
@@ -34,6 +36,9 @@ function overlay(bakedManifest, uiSlotsFile, shellConfig) {
34
36
  Stdlib_Option.forEach(uiSlotsFile, param => {
35
37
  out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
36
38
  });
39
+ Stdlib_Option.forEach(Platform_AdminGroup$ReventlessCore.configEntry(), param => {
40
+ out[param[0]] = param[1];
41
+ });
37
42
  Stdlib_Option.forEach(bakedManifest, config => {
38
43
  out["manifestUrl"] = Platform_BakedManifest$ReventlessCore.urlForKey(config.key);
39
44
  let urls = Platform_BakedManifest$ReventlessCore.journeyUrls(config);
@@ -21,19 +21,22 @@ let defaultUser: Identity.t = {
21
21
  provider: InMemory,
22
22
  }
23
23
 
24
- let adminUser: Identity.t = {
24
+ // A function, not a value: the administrator's group is the one the platform
25
+ // root maps `Role.admin` to, and this module is evaluated before the root runs.
26
+ let adminUser = (): Identity.t => {
25
27
  userId: "local-admin",
26
28
  username: "admin",
27
- groups: [AdminGroup.name, "User"],
29
+ groups: [Role.adminGroup(), "User"],
28
30
  provider: InMemory,
29
31
  }
30
32
 
31
33
  // ── User registry ─────────────────────────────────────────────────────────
32
34
  //
33
35
  // A `dict<Identity.t>` keyed by `username`. A4 hydrates this from
34
- // `.reventless/users.yaml`; tests inject via `registerUser`.
36
+ // `.reventless/users.yaml`; tests inject via `registerUser`. The built-in
37
+ // `admin` is answered on lookup, for the reason above, unless one is registered.
35
38
 
36
- let initialUsers = () => Dict.fromArray([("user", defaultUser), ("admin", adminUser)])
39
+ let initialUsers = () => Dict.fromArray([("user", defaultUser)])
37
40
 
38
41
  let users: ref<dict<Identity.t>> = ref(initialUsers())
39
42
 
@@ -42,7 +45,19 @@ let registerUser = (~username: string, ~identity: Identity.t): unit =>
42
45
 
43
46
  let resetUsers = (): unit => users := initialUsers()
44
47
 
45
- let lookupUser = (username: string): option<Identity.t> => users.contents->Dict.get(username)
48
+ let lookupUser = (username: string): option<Identity.t> =>
49
+ switch users.contents->Dict.get(username) {
50
+ | None if username == "admin" => Some(adminUser())
51
+ | found => found
52
+ }
53
+
54
+ /** The groups of every account this adapter can authenticate as, built-in
55
+ included — what the local platform provides. */
56
+ let knownGroups = (): array<string> =>
57
+ users.contents
58
+ ->Dict.valuesToArray
59
+ ->Array.concat([adminUser()])
60
+ ->Array.flatMap(identity => identity.groups)
46
61
 
47
62
  // ── Header parsing ────────────────────────────────────────────────────────
48
63
 
@@ -6,9 +6,9 @@ import * as Nodepath from "node:path";
6
6
  import * as Nodecrypto from "node:crypto";
7
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
8
  import * as Pulumi from "@pulumi/pulumi";
9
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
9
10
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
10
11
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
11
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
12
12
  import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
13
13
 
14
14
  let defaultUser_groups = ["User"];
@@ -20,29 +20,23 @@ let defaultUser = {
20
20
  provider: "InMemory"
21
21
  };
22
22
 
23
- let adminUser_groups = [
24
- AdminGroup$Reventless.name,
25
- "User"
26
- ];
27
-
28
- let adminUser = {
29
- userId: "local-admin",
30
- username: "admin",
31
- groups: adminUser_groups,
32
- provider: "InMemory"
33
- };
23
+ function adminUser() {
24
+ return {
25
+ userId: "local-admin",
26
+ username: "admin",
27
+ groups: [
28
+ Role$Reventless.adminGroup(),
29
+ "User"
30
+ ],
31
+ provider: "InMemory"
32
+ };
33
+ }
34
34
 
35
35
  function initialUsers() {
36
- return Object.fromEntries([
37
- [
36
+ return Object.fromEntries([[
38
37
  "user",
39
38
  defaultUser
40
- ],
41
- [
42
- "admin",
43
- adminUser
44
- ]
45
- ]);
39
+ ]]);
46
40
  }
47
41
 
48
42
  let users = {
@@ -58,7 +52,16 @@ function resetUsers() {
58
52
  }
59
53
 
60
54
  function lookupUser(username) {
61
- return users.contents[username];
55
+ let found = users.contents[username];
56
+ if (found !== undefined || username !== "admin") {
57
+ return found;
58
+ } else {
59
+ return adminUser();
60
+ }
61
+ }
62
+
63
+ function knownGroups() {
64
+ return Object.values(users.contents).concat([adminUser()]).flatMap(identity => identity.groups);
62
65
  }
63
66
 
64
67
  function parseGroups(s) {
@@ -329,7 +332,7 @@ async function authenticate(ctx) {
329
332
  let userHeader = ctx.headers["X-User".toLowerCase()];
330
333
  let groupsHeader = ctx.headers["X-Groups".toLowerCase()];
331
334
  if (userHeader !== undefined) {
332
- let identity$1 = users.contents[userHeader];
335
+ let identity$1 = lookupUser(userHeader);
333
336
  if (identity$1 === undefined) {
334
337
  return "Anonymous";
335
338
  }
@@ -371,6 +374,7 @@ export {
371
374
  registerUser,
372
375
  resetUsers,
373
376
  lookupUser,
377
+ knownGroups,
374
378
  parseGroups,
375
379
  getHeader,
376
380
  Login,
@@ -56,12 +56,35 @@ type resolution =
56
56
  // auto-discovery before `Platform.startServers`.
57
57
  let resolved: ref<bool> = ref(false)
58
58
 
59
- let resetResolution = (): unit => resolved := false
59
+ /** Set once a plugin has been built, and with it the check that every role it
60
+ needs has a group among these accounts. */
61
+ let pluginChecked: ref<bool> = ref(false)
62
+
63
+ let resetResolution = (): unit => {
64
+ resolved := false
65
+ pluginChecked := false
66
+ }
67
+
68
+ /**
69
+ The groups the store's accounts will belong to, for the role check, which runs
70
+ before the servers start and load it: the loaded accounts once `load` has run,
71
+ otherwise the default file `autoLoadOnce` will read (or the template `setup`
72
+ makes it from). Never the working directory once another source was loaded.
73
+ */
74
+ let providedGroups = (): array<string> =>
75
+ LocalAuth.knownGroups()->Array.concat(resolved.contents ? [] : AccountsManifest.declaredGroups())
60
76
 
61
77
  let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): result<
62
78
  resolution,
63
79
  string,
64
80
  > => {
81
+ // The default file is what the check already read; any other source arrives
82
+ // after it, and the check would have compared the plugins with other accounts.
83
+ if pluginChecked.contents && (users->Option.isSome || usersFile->Option.isSome) {
84
+ JsError.throwWithMessage(
85
+ "UserStore.load(~users / ~usersFile) after a plugin was built — the check that every role it needs has a group read other accounts. Call UserStore.load before deploying plugins.",
86
+ )
87
+ }
65
88
  resolved := true
66
89
  switch users {
67
90
  | Some(entries) =>
@@ -3,6 +3,7 @@
3
3
  import * as Nodefs from "node:fs";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
7
  import * as LocalAuth$ReventlessLocal from "./LocalAuth.res.mjs";
7
8
  import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
8
9
 
@@ -25,11 +26,23 @@ let resolved = {
25
26
  contents: false
26
27
  };
27
28
 
29
+ let pluginChecked = {
30
+ contents: false
31
+ };
32
+
28
33
  function resetResolution() {
29
34
  resolved.contents = false;
35
+ pluginChecked.contents = false;
36
+ }
37
+
38
+ function providedGroups() {
39
+ return LocalAuth$ReventlessLocal.knownGroups().concat(resolved.contents ? [] : AccountsManifest$Reventless.declaredGroups());
30
40
  }
31
41
 
32
42
  function load(users, usersFile, param) {
43
+ if (pluginChecked.contents && (Stdlib_Option.isSome(users) || Stdlib_Option.isSome(usersFile))) {
44
+ Stdlib_JsError.throwWithMessage("UserStore.load(~users / ~usersFile) after a plugin was built — the check that every role it needs has a group read other accounts. Call UserStore.load before deploying plugins.");
45
+ }
33
46
  resolved.contents = true;
34
47
  if (users !== undefined) {
35
48
  _registerEntries(users);
@@ -84,7 +97,9 @@ export {
84
97
  _registerEntries,
85
98
  _defaultPath,
86
99
  resolved,
100
+ pluginChecked,
87
101
  resetResolution,
102
+ providedGroups,
88
103
  load,
89
104
  autoLoadOnce,
90
105
  }
@@ -41,7 +41,7 @@ let wrapAdmin = (resolvers: dict<ReventlessGraphqlServer.GraphQL_ServerInstance.
41
41
  resolvers
42
42
  ->Dict.toArray
43
43
  ->Array.forEach(((k, v)) =>
44
- wrapped->Dict.set(k, Auth_GraphqlContext.requireGroup(~group=Reventless.AdminGroup.name, v))
44
+ wrapped->Dict.set(k, Auth_GraphqlContext.requireGroup(~group=Reventless.Role.adminGroup(), v))
45
45
  )
46
46
  wrapped
47
47
  }
@@ -1,6 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
3
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
4
4
  import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
5
5
  import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
6
6
 
@@ -9,7 +9,7 @@ let instance = GraphQL_ServerInstance$ReventlessGraphqlServer.make("GraphQL:Plat
9
9
  function wrapAdmin(resolvers) {
10
10
  let wrapped = {};
11
11
  Object.entries(resolvers).forEach(param => {
12
- wrapped[param[0]] = Auth_GraphqlContext$ReventlessLocal.requireGroup(AdminGroup$Reventless.name, param[1]);
12
+ wrapped[param[0]] = Auth_GraphqlContext$ReventlessLocal.requireGroup(Role$Reventless.adminGroup(), param[1]);
13
13
  });
14
14
  return wrapped;
15
15
  }
@@ -268,3 +268,34 @@ describe("ShellConfig.overlay — journeys", () => {
268
268
  )->toBe(true)
269
269
  )
270
270
  })
271
+
272
+ describe("ShellConfig.overlay — adminGroup", () => {
273
+ testSync("is absent while the administrator role keeps its own name", () => {
274
+ Reventless.Role.clearGroups()
275
+ expect(
276
+ ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)->Dict.keysToArray,
277
+ )->toEqual([])
278
+ })
279
+
280
+ testSync("carries the group a renamed administrator role maps to", () => {
281
+ Reventless.Role.clearGroups()
282
+ Reventless.Role.setGroups([(Reventless.Role.admin, "shop-admins")])
283
+ let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
284
+ Reventless.Role.clearGroups()
285
+ expect(out->str("adminGroup"))->toEqual(Some("shop-admins"))
286
+ })
287
+
288
+ // Computed from the role mapping, so a passthrough cannot point the shell at
289
+ // another group than the one the server gates on.
290
+ testSync("a shellConfig naming it is refused", () =>
291
+ expect(
292
+ threw(
293
+ () =>
294
+ ShellConfig.overlay(
295
+ ~bakedManifest=None,
296
+ ~shellConfig=Some(Dict.fromArray([("adminGroup", JSON.Encode.string("x"))])),
297
+ )->ignore,
298
+ ),
299
+ )->toBe(true)
300
+ )
301
+ })
@@ -5,6 +5,7 @@ import * as Nodeos from "node:os";
5
5
  import * as Nodepath from "node:path";
6
6
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
7
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
8
9
  import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
9
10
  import * as ShellConfig$ReventlessLocal from "../src/ShellConfig.res.mjs";
10
11
 
@@ -215,6 +216,31 @@ globalThis.describe("ShellConfig.overlay — journeys", () => {
215
216
  });
216
217
  });
217
218
 
219
+ globalThis.describe("ShellConfig.overlay — adminGroup", () => {
220
+ globalThis.test("is absent while the administrator role keeps its own name", () => {
221
+ Role$Reventless.clearGroups();
222
+ globalThis.expect(Object.keys(ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined))).toEqual([]);
223
+ });
224
+ globalThis.test("carries the group a renamed administrator role maps to", () => {
225
+ Role$Reventless.clearGroups();
226
+ Role$Reventless.setGroups([[
227
+ Role$Reventless.admin,
228
+ "shop-admins"
229
+ ]]);
230
+ let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
231
+ Role$Reventless.clearGroups();
232
+ globalThis.expect(str(out, "adminGroup")).toEqual("shop-admins");
233
+ });
234
+ globalThis.test("a shellConfig naming it is refused", () => {
235
+ globalThis.expect(threw(() => {
236
+ ShellConfig$ReventlessLocal.overlay(undefined, undefined, Object.fromEntries([[
237
+ "adminGroup",
238
+ "x"
239
+ ]]));
240
+ })).toBe(true);
241
+ });
242
+ });
243
+
218
244
  export {
219
245
  manifest,
220
246
  str,
@@ -1,7 +1,7 @@
1
1
  // Resolver-boundary tests for per-constructor authorization (A3.3 + A3.3b).
2
2
  // Mirrors the catalog Category aggregate's command shape: a record-payload
3
3
  // constructor `Add({name})` defaulting to AllowAuthenticated, plus a
4
- // payload-less `Archive` constrained to AllowGroups(["Admin"]).
4
+ // payload-less `Archive` constrained to AllowRoles([Admin]).
5
5
  //
6
6
  // The PPX emits a `commandAuthorization` switch at compile time (see
7
7
  // examples/online-shop-hybrid/catalog/.../Category.res.mjs). This test hand-
@@ -23,11 +23,11 @@ type command =
23
23
  | Rename({name: string})
24
24
  | Archive
25
25
 
26
- // Matches the shape the PPX emits for `@authorize(AllowGroups(["Admin"]))` on
26
+ // Matches the shape the PPX emits for `@authorize(AllowRoles([Admin]))` on
27
27
  // the payload-less `Archive` constructor with a file-level default.
28
28
  let commandAuthorization = (cmd: unknown): Reventless.Authorization.permission =>
29
29
  if (cmd->Obj.magic: 'a) === "Archive" {
30
- AllowGroups(["Admin"])
30
+ AllowRoles([Reventless.Role.make("Admin")])
31
31
  } else {
32
32
  AllowAuthenticated
33
33
  }
@@ -189,6 +189,30 @@ describe("CommandGeneratorResolvers_GraphQL — per-constructor authorization",
189
189
  expect(calls.contents)->toEqual(["Add"])
190
190
  })
191
191
 
192
+ // The rule names the role; the resolver checks the group the platform maps it
193
+ // to, so a deployment that calls its administrators "ops" admits those.
194
+ testPromise("a renamed role admits its group and refuses the old name", async () => {
195
+ let (_, _, archiveResolver, calls) = buildFixture(~namespace="Cat8")
196
+ Reventless.Role.clearGroups()
197
+ Reventless.Role.setGroups([(Reventless.Role.make("Admin"), "ops")])
198
+ let asOps = await archiveResolver(
199
+ JSON.Encode.null,
200
+ JSON.Encode.object(Dict.make()),
201
+ ctxFor({...userIdentity, groups: ["ops"]}),
202
+ )
203
+ let asAdminGroup = await archiveResolver(
204
+ JSON.Encode.null,
205
+ JSON.Encode.object(Dict.make()),
206
+ ctxFor(adminIdentity),
207
+ )
208
+ Reventless.Role.clearGroups()
209
+ expect((getTypename(asOps), getErrorCode(asAdminGroup)))->toEqual((
210
+ "CommandAccepted",
211
+ "Forbidden",
212
+ ))
213
+ expect(calls.contents)->toEqual(["Archive"])
214
+ })
215
+
192
216
  testPromise(
193
217
  "payload-less Archive registered as a GraphQL mutation field (no longer filtered)",
194
218
  async () => {
@@ -6,6 +6,7 @@ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
9
10
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
10
11
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
12
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
@@ -30,7 +31,7 @@ let commandSchema = Sury.union([
30
31
  function commandAuthorization(cmd) {
31
32
  if (cmd === "Archive") {
32
33
  return {
33
- TAG: "AllowGroups",
34
+ TAG: "AllowRoles",
34
35
  _0: ["Admin"]
35
36
  };
36
37
  } else {
@@ -170,6 +171,27 @@ globalThis.describe("CommandGeneratorResolvers_GraphQL — per-constructor autho
170
171
  globalThis.expect(getTypename(response)).toEqual("CommandAccepted");
171
172
  globalThis.expect(match[3].contents).toEqual(["Add"]);
172
173
  });
174
+ globalThis.test("a renamed role admits its group and refuses the old name", async () => {
175
+ let match = buildFixture("Cat8");
176
+ let archiveResolver = match[2];
177
+ Role$Reventless.clearGroups();
178
+ Role$Reventless.setGroups([[
179
+ "Admin",
180
+ "ops"
181
+ ]]);
182
+ let newrecord = {...userIdentity};
183
+ let asOps = await archiveResolver(null, {}, ctxFor((newrecord.groups = ["ops"], newrecord)));
184
+ let asAdminGroup = await archiveResolver(null, {}, ctxFor(adminIdentity));
185
+ Role$Reventless.clearGroups();
186
+ globalThis.expect([
187
+ getTypename(asOps),
188
+ getErrorCode(asAdminGroup)
189
+ ]).toEqual([
190
+ "CommandAccepted",
191
+ "Forbidden"
192
+ ]);
193
+ globalThis.expect(match[3].contents).toEqual(["Archive"]);
194
+ });
173
195
  globalThis.test("payload-less Archive registered as a GraphQL mutation field (no longer filtered)", async () => {
174
196
  buildFixture("Cat7");
175
197
  let sdl = DomainGraphQL_Server$ReventlessLocal.asInterface.buildSdl();
@@ -160,3 +160,76 @@ testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
160
160
  | None => JsError.throwWithMessage("autoLoadOnce wiped explicit registration")
161
161
  }
162
162
  })
163
+
164
+ // What the role check counts as provided, before the servers load the store.
165
+ describe("UserStore.providedGroups", () => {
166
+ // Runs `f` in a directory holding `files` (path relative to it → contents).
167
+ let inDir = (files: array<(string, string)>, f) => {
168
+ let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-provided-"]))
169
+ files->Array.forEach(((file, contents)) => {
170
+ let path = NodePath.join([dir, file])
171
+ NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
172
+ NodeFs.writeFileSync(path, contents)
173
+ })
174
+ let cwd = NodeProcess.cwd()
175
+ NodeProcess.chdir(dir)
176
+ let result = try f() catch {
177
+ | exn =>
178
+ NodeProcess.chdir(cwd)
179
+ throw(exn)
180
+ }
181
+ NodeProcess.chdir(cwd)
182
+ NodeFs.rmSync(dir, {recursive: true, force: true})
183
+ result
184
+ }
185
+ let manifest = group => `- username: m\n password: m\n groups: [${group}]\n`
186
+ let has = (group, groups) => groups->Array.includes(group)
187
+
188
+ testSync("counts the default file the servers will load", () => {
189
+ resetAll()
190
+ let groups = inDir(
191
+ [(".reventless/users.yaml", manifest("Merchandiser"))],
192
+ () => UserStore.providedGroups(),
193
+ )
194
+ expect(has("Merchandiser", groups))->toBe(true)
195
+ })
196
+
197
+ testSync("counts the template where setup has not made the file yet", () => {
198
+ resetAll()
199
+ let groups = inDir(
200
+ [("users.example.yaml", manifest("Fulfilment"))],
201
+ () => UserStore.providedGroups(),
202
+ )
203
+ expect(has("Fulfilment", groups))->toBe(true)
204
+ })
205
+
206
+ testSync("once loaded from elsewhere, counts those accounts and not the directory's", () => {
207
+ resetAll()
208
+ let _ = UserStore.load(~users=[{username: "i", password: "i", groups: ["Inline"]}], ())
209
+ let groups = inDir(
210
+ [(".reventless/users.yaml", manifest("Merchandiser"))],
211
+ () => UserStore.providedGroups(),
212
+ )
213
+ expect((has("Inline", groups), has("Merchandiser", groups)))->toEqual((true, false))
214
+ })
215
+
216
+ testSync("loading other accounts after a plugin was checked is refused", () => {
217
+ resetAll()
218
+ UserStore.pluginChecked := true
219
+ let refused = switch UserStore.load(
220
+ ~users=[{username: "late", password: "l", groups: ["Late"]}],
221
+ (),
222
+ ) {
223
+ | _ => false
224
+ | exception JsExn(_) => true
225
+ }
226
+ expect(refused)->toBe(true)
227
+ })
228
+
229
+ testSync("the default load after a plugin was checked is the one the check read", () => {
230
+ resetAll()
231
+ UserStore.pluginChecked := true
232
+ let loaded = inDir([], () => UserStore.load()->Result.isOk)
233
+ expect(loaded)->toBe(true)
234
+ })
235
+ })
@@ -3,7 +3,9 @@
3
3
  import * as Nodefs from "node:fs";
4
4
  import * as Nodeos from "node:os";
5
5
  import * as Nodepath from "node:path";
6
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
7
9
  import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
8
10
  import * as UserStore$ReventlessLocal from "../../src/adapter/Auth/UserStore.res.mjs";
9
11
 
@@ -201,6 +203,96 @@ globalThis.test("autoLoadOnce is a no-op after an explicit load", async () => {
201
203
  }
202
204
  });
203
205
 
206
+ globalThis.describe("UserStore.providedGroups", () => {
207
+ let inDir = (files, f) => {
208
+ let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-provided-"));
209
+ files.forEach(param => {
210
+ let path = Nodepath.join(dir, param[0]);
211
+ Nodefs.mkdirSync(Nodepath.dirname(path), {
212
+ recursive: true
213
+ });
214
+ Nodefs.writeFileSync(path, param[1], "utf8");
215
+ });
216
+ let cwd = process.cwd();
217
+ process.chdir(dir);
218
+ let result;
219
+ try {
220
+ result = f();
221
+ } catch (exn) {
222
+ process.chdir(cwd);
223
+ throw exn;
224
+ }
225
+ process.chdir(cwd);
226
+ Nodefs.rmSync(dir, {
227
+ recursive: true,
228
+ force: true
229
+ });
230
+ return result;
231
+ };
232
+ globalThis.test("counts the default file the servers will load", () => {
233
+ resetAll();
234
+ let groups = inDir([[
235
+ ".reventless/users.yaml",
236
+ `- username: m\n password: m\n groups: [` + "Merchandiser" + `]\n`
237
+ ]], () => UserStore$ReventlessLocal.providedGroups());
238
+ globalThis.expect(groups.includes("Merchandiser")).toBe(true);
239
+ });
240
+ globalThis.test("counts the template where setup has not made the file yet", () => {
241
+ resetAll();
242
+ let groups = inDir([[
243
+ "users.example.yaml",
244
+ `- username: m\n password: m\n groups: [` + "Fulfilment" + `]\n`
245
+ ]], () => UserStore$ReventlessLocal.providedGroups());
246
+ globalThis.expect(groups.includes("Fulfilment")).toBe(true);
247
+ });
248
+ globalThis.test("once loaded from elsewhere, counts those accounts and not the directory's", () => {
249
+ resetAll();
250
+ UserStore$ReventlessLocal.load([{
251
+ username: "i",
252
+ password: "i",
253
+ groups: ["Inline"]
254
+ }], undefined, undefined);
255
+ let groups = inDir([[
256
+ ".reventless/users.yaml",
257
+ `- username: m\n password: m\n groups: [` + "Merchandiser" + `]\n`
258
+ ]], () => UserStore$ReventlessLocal.providedGroups());
259
+ globalThis.expect([
260
+ groups.includes("Inline"),
261
+ groups.includes("Merchandiser")
262
+ ]).toEqual([
263
+ true,
264
+ false
265
+ ]);
266
+ });
267
+ globalThis.test("loading other accounts after a plugin was checked is refused", () => {
268
+ resetAll();
269
+ UserStore$ReventlessLocal.pluginChecked.contents = true;
270
+ let refused;
271
+ try {
272
+ UserStore$ReventlessLocal.load([{
273
+ username: "late",
274
+ password: "l",
275
+ groups: ["Late"]
276
+ }], undefined, undefined);
277
+ refused = false;
278
+ } catch (raw_exn) {
279
+ let exn = Primitive_exceptions.internalToException(raw_exn);
280
+ if (exn.RE_EXN_ID === "JsExn") {
281
+ refused = true;
282
+ } else {
283
+ throw exn;
284
+ }
285
+ }
286
+ globalThis.expect(refused).toBe(true);
287
+ });
288
+ globalThis.test("the default load after a plugin was checked is the one the check read", () => {
289
+ resetAll();
290
+ UserStore$ReventlessLocal.pluginChecked.contents = true;
291
+ let loaded = inDir([], () => Stdlib_Result.isOk(UserStore$ReventlessLocal.load(undefined, undefined, undefined)));
292
+ globalThis.expect(loaded).toBe(true);
293
+ });
294
+ });
295
+
204
296
  export {
205
297
  resetAll,
206
298
  _writeYamlFile,
@@ -145,7 +145,8 @@ let buildFixture = async (~name: string, ~indexes: array<Reventless.ReadModel.in
145
145
  type state = rowState
146
146
  let config = Reventless.ReadModel.config()
147
147
  let subIdConfig = None
148
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
148
+ type role = Reventless.Role.name
149
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
149
150
  let visibility: Reventless.Visibility.t = Public
150
151
  }
151
152
 
@@ -646,7 +647,8 @@ let buildSubIdFixture = async (~name: string) => {
646
647
  Reventless.ReadModel.subIdField: "lineNo",
647
648
  getSubId: (state: lineState) => state.lineNo,
648
649
  })
649
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
650
+ type role = Reventless.Role.name
651
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
650
652
  let visibility: Reventless.Visibility.t = Public
651
653
  }
652
654
 
@@ -96,7 +96,8 @@ let buildFixture = async (
96
96
  type state = productRow
97
97
  let config = Reventless.ReadModel.config()
98
98
  let subIdConfig = None
99
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
99
+ type role = Reventless.Role.name
100
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
100
101
  let visibility: Reventless.Visibility.t = Public
101
102
  }
102
103
 
@@ -125,7 +126,8 @@ let buildFixture = async (
125
126
  ],
126
127
  )
127
128
  let subIdConfig = None
128
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
129
+ type role = Reventless.Role.name
130
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
129
131
  let visibility: Reventless.Visibility.t = Public
130
132
  }
131
133
 
@@ -222,7 +222,9 @@ describe("InboundTranslationSlice Callback", () => {
222
222
  ),
223
223
  )
224
224
 
225
- let commandAuthorization = (_: command): Reventless.Authorization.permission =>
225
+ type role = Reventless.Role.name
226
+
227
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
226
228
  AllowAuthenticated
227
229
  type lifecycleState = unit
228
230
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
@@ -289,7 +291,9 @@ describe("InboundTranslationSlice Callback", () => {
289
291
 
290
292
  let translate = (_input: externalInput) => Ok([])
291
293
 
292
- let commandAuthorization = (_: command): Reventless.Authorization.permission =>
294
+ type role = Reventless.Role.name
295
+
296
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
293
297
  AllowAuthenticated
294
298
  type lifecycleState = unit
295
299
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
@@ -9,13 +9,15 @@ let PluginTest = Behavior_GWT$ReventlessGwt.MakeFromAggregate({
9
9
  name: PluginSpec$ReventlessCore.name,
10
10
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
11
11
  eventSchema: PluginSpec$ReventlessCore.eventSchema,
12
- errorSchema: PluginSpec$ReventlessCore.errorSchema
12
+ errorSchema: PluginSpec$ReventlessCore.errorSchema,
13
+ commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
13
14
  })({
14
15
  Spec: {
15
16
  name: PluginSpec$ReventlessCore.name,
16
17
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
17
18
  eventSchema: PluginSpec$ReventlessCore.eventSchema,
18
- errorSchema: PluginSpec$ReventlessCore.errorSchema
19
+ errorSchema: PluginSpec$ReventlessCore.errorSchema,
20
+ commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
19
21
  },
20
22
  initialState: PluginBehavior$ReventlessCore.initialState,
21
23
  evolve: PluginBehavior$ReventlessCore.evolve,
@@ -10,7 +10,8 @@ let Test = Behavior_GWT$ReventlessGwt.Make({
10
10
  consumedEventSchema: UiFragmentRegistry$ReventlessCore.consumedEventSchema,
11
11
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
12
12
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
13
- eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema
13
+ eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
14
+ commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization
14
15
  })({
15
16
  initialState: undefined,
16
17
  evolve: UiFragmentRegistry_Behavior$ReventlessCore.evolve,