@reventlessdev/reventless-local 3.0.0-alpha.274 → 3.0.0-alpha.276

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,29 @@
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.276 (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
+
21
+ # 3.0.0-alpha.275 (2026-10-01)
22
+
23
+ ### Features
24
+
25
+ * **spec, aws, core, local:** an app declares its environments, and every deployment records its commit ([c7cc925](https://github.com/ReventlessDev/reventless-core/commit/c7cc92573e97742b681a9000cea0bdfd6e9a33fc))
26
+ * **spec, core, gwt, ppx:** a command acts only on what its caller owns ([2636f0e](https://github.com/ReventlessDev/reventless-core/commit/2636f0ee44797ee2c0b0c40a7107cf306113173f))
27
+
28
+
6
29
  # 3.0.0-alpha.274 (2026-09-29)
7
30
 
8
31
  **Note:** Version bump only for package @reventlessdev/reventless-local
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.274",
3
+ "version": "3.0.0-alpha.276",
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": {
@@ -37,21 +37,21 @@
37
37
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
38
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
40
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
40
41
  "@reventlessdev/rescript-node": "2.0.0-alpha.20",
41
42
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
42
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
43
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.132",
44
- "@reventlessdev/reventless-core": "3.0.0-alpha.284",
45
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.231",
46
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.148",
47
- "@reventlessdev/reventless-infra": "3.0.0-alpha.182",
43
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.233",
44
+ "@reventlessdev/reventless-core": "3.0.0-alpha.286",
45
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.134",
46
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.184",
47
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.156",
48
48
  "@reventlessdev/reventless-seed": "1.0.0-alpha.31",
49
- "@reventlessdev/reventless-spec": "3.0.0-alpha.154"
49
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.150"
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,14 @@ 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 manifest alike. The manifest is read here rather
111
+ // than from the user store, which loads only when the servers start — after
112
+ // the plugins are built and checked.
113
+ Reventless.Role.provideGroupsFrom(() =>
114
+ LocalAuth.knownGroups()->Array.concat(Reventless.AccountsManifest.declaredGroups())
115
+ )
90
116
  // Activate Pulumi mock mode — must happen before any component creation.
91
117
  // Idempotent, so safe to call even if TestRunner.setup() was already called.
92
118
  let _ = TestRunner.setup()
@@ -1273,6 +1299,7 @@ module MakeWithConfig = (
1273
1299
  ~pluginOutputs=?,
1274
1300
  ) => {
1275
1301
  let environment = Pulumi.Pulumi.getStackName()
1302
+ let provenance = Reventless.DeploymentProvenance.current()
1276
1303
  builtInfos->Array.forEachWithIndex((info, i) => {
1277
1304
  switch ReventlessCore.Plugin_Helpers.onPluginDeployedHook.contents {
1278
1305
  | Some(hook) =>
@@ -1289,6 +1316,8 @@ module MakeWithConfig = (
1289
1316
  deployedAt: Date.make()->Date.toISOString,
1290
1317
  actor: "local",
1291
1318
  deploymentId: Date.make()->Date.toISOString,
1319
+ commit: ?provenance.commit,
1320
+ dirty: ?(provenance.dirty ? Some(true) : None),
1292
1321
  kind: ?info.kind,
1293
1322
  displayName: ?info.displayName,
1294
1323
  vendor: ?info.vendor,
@@ -1786,7 +1815,7 @@ module MakeWithConfig = (
1786
1815
  // structure manually.
1787
1816
  pluginStructuresStore.contents->Dict.set(
1788
1817
  ReventlessCore.Platform_Admin_Structure.pluginId,
1789
- ReventlessCore.Platform_Admin_Structure.structure,
1818
+ ReventlessCore.Platform_Admin_Structure.structure(),
1790
1819
  )
1791
1820
 
1792
1821
  let pluginQueryDbName = ReventlessCore.PluginsReadModelSpec.name
@@ -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";
@@ -25,10 +26,12 @@ import * as UiSlots$ReventlessLocal from "./UiSlots.res.mjs";
25
26
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
26
27
  import * as LocalBus$ReventlessLocal from "./adapter/LocalBus.res.mjs";
27
28
  import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
29
+ import * as LocalAuth$ReventlessLocal from "./adapter/Auth/LocalAuth.res.mjs";
28
30
  import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
29
31
  import * as UserStore$ReventlessLocal from "./adapter/Auth/UserStore.res.mjs";
30
32
  import * as TestRunner$ReventlessLocal from "./test/TestRunner.res.mjs";
31
33
  import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
34
+ import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
32
35
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
33
36
  import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
34
37
  import * as ShellConfig$ReventlessLocal from "./ShellConfig.res.mjs";
@@ -43,6 +46,7 @@ import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/s
43
46
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
44
47
  import * as Plugin_Builder$ReventlessLocal from "./components/Plugin_Builder.res.mjs";
45
48
  import * as Counter_Builder$ReventlessLocal from "./components/Counter_Builder.res.mjs";
49
+ import * as DeploymentProvenance$Reventless from "@reventlessdev/reventless-spec/src/types/DeploymentProvenance.res.mjs";
46
50
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
47
51
  import * as LocalPluginSpec$ReventlessLocal from "./adapter/LocalPluginSpec.res.mjs";
48
52
  import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/NoEventMappings.res.mjs";
@@ -139,6 +143,7 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
139
143
  }
140
144
 
141
145
  function MakeWithConfig(Config) {
146
+ Role$Reventless.provideGroupsFrom(() => LocalAuth$ReventlessLocal.knownGroups().concat(AccountsManifest$Reventless.declaredGroups()));
142
147
  TestRunner$ReventlessLocal.setup();
143
148
  let match = Config.backend;
144
149
  if (typeof match !== "object") {
@@ -1069,6 +1074,7 @@ function MakeWithConfig(Config) {
1069
1074
  }));
1070
1075
  let firePluginDeployedHooks = (builtInfos, pluginOutputs) => {
1071
1076
  let environment = Pulumi.getStack();
1077
+ let provenance = DeploymentProvenance$Reventless.current();
1072
1078
  builtInfos.forEach((info, i) => {
1073
1079
  let hook = Plugin_Helpers$ReventlessCore.onPluginDeployedHook.contents;
1074
1080
  if (hook === undefined) {
@@ -1081,6 +1087,8 @@ function MakeWithConfig(Config) {
1081
1087
  let deployedInfo_version = info.version;
1082
1088
  let deployedInfo_deployedAt = new Date().toISOString();
1083
1089
  let deployedInfo_deploymentId = new Date().toISOString();
1090
+ let deployedInfo_commit = provenance.commit;
1091
+ let deployedInfo_dirty = provenance.dirty ? true : undefined;
1084
1092
  let deployedInfo_kind = info.kind;
1085
1093
  let deployedInfo_displayName = info.displayName;
1086
1094
  let deployedInfo_vendor = info.vendor;
@@ -1100,6 +1108,8 @@ function MakeWithConfig(Config) {
1100
1108
  deployedAt: deployedInfo_deployedAt,
1101
1109
  actor: "local",
1102
1110
  deploymentId: deployedInfo_deploymentId,
1111
+ commit: deployedInfo_commit,
1112
+ dirty: deployedInfo_dirty,
1103
1113
  kind: deployedInfo_kind,
1104
1114
  displayName: deployedInfo_displayName,
1105
1115
  vendor: deployedInfo_vendor,
@@ -1354,7 +1364,7 @@ function MakeWithConfig(Config) {
1354
1364
  connectPlugin(plugins$1);
1355
1365
  seedPluginStructuresStore(plugins$1);
1356
1366
  }
1357
- pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
1367
+ pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
1358
1368
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
1359
1369
  let parts = field.split("_");
1360
1370
  let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
@@ -1899,6 +1909,7 @@ function MakeWithConfig(Config) {
1899
1909
 
1900
1910
  function Make($star) {
1901
1911
  let backend = Backend$ReventlessLocal.fromEnv();
1912
+ Role$Reventless.provideGroupsFrom(() => LocalAuth$ReventlessLocal.knownGroups().concat(AccountsManifest$Reventless.declaredGroups()));
1902
1913
  TestRunner$ReventlessLocal.setup();
1903
1914
  if (typeof backend !== "object") {
1904
1915
  BackendState$ReventlessLocal.setMemory();
@@ -2822,6 +2833,7 @@ function Make($star) {
2822
2833
  }));
2823
2834
  let firePluginDeployedHooks = (builtInfos, pluginOutputs) => {
2824
2835
  let environment = Pulumi.getStack();
2836
+ let provenance = DeploymentProvenance$Reventless.current();
2825
2837
  builtInfos.forEach((info, i) => {
2826
2838
  let hook = Plugin_Helpers$ReventlessCore.onPluginDeployedHook.contents;
2827
2839
  if (hook === undefined) {
@@ -2834,6 +2846,8 @@ function Make($star) {
2834
2846
  let deployedInfo_version = info.version;
2835
2847
  let deployedInfo_deployedAt = new Date().toISOString();
2836
2848
  let deployedInfo_deploymentId = new Date().toISOString();
2849
+ let deployedInfo_commit = provenance.commit;
2850
+ let deployedInfo_dirty = provenance.dirty ? true : undefined;
2837
2851
  let deployedInfo_kind = info.kind;
2838
2852
  let deployedInfo_displayName = info.displayName;
2839
2853
  let deployedInfo_vendor = info.vendor;
@@ -2853,6 +2867,8 @@ function Make($star) {
2853
2867
  deployedAt: deployedInfo_deployedAt,
2854
2868
  actor: "local",
2855
2869
  deploymentId: deployedInfo_deploymentId,
2870
+ commit: deployedInfo_commit,
2871
+ dirty: deployedInfo_dirty,
2856
2872
  kind: deployedInfo_kind,
2857
2873
  displayName: deployedInfo_displayName,
2858
2874
  vendor: deployedInfo_vendor,
@@ -3102,7 +3118,7 @@ function Make($star) {
3102
3118
  connectPlugin(plugins$1);
3103
3119
  seedPluginStructuresStore(plugins$1);
3104
3120
  }
3105
- pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
3121
+ pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
3106
3122
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
3107
3123
  let parts = field.split("_");
3108
3124
  let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
@@ -3631,6 +3647,14 @@ function Make($star) {
3631
3647
  };
3632
3648
  }
3633
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
+
3634
3658
  export {
3635
3659
  log,
3636
3660
  platformGraphQLRef,
@@ -3638,6 +3662,10 @@ export {
3638
3662
  platformMCPRef,
3639
3663
  decodePluginEventEnvelope,
3640
3664
  decodeUiFragmentRegistryEventEnvelope,
3665
+ roleGroups,
3666
+ providedGroups,
3667
+ groupOf,
3668
+ adminGroup,
3641
3669
  MakeWithConfig,
3642
3670
  Make,
3643
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,
@@ -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
  }
@@ -18,6 +18,8 @@ let acceptedResultChannel = CommandTopic$ReventlessCore.acceptedResultChannel;
18
18
 
19
19
  let rejectedResultChannel = CommandTopic$ReventlessCore.rejectedResultChannel;
20
20
 
21
+ let ownershipRefusal = CommandTopic$ReventlessCore.ownershipRefusal;
22
+
21
23
  let reportAccepted = CommandTopic$ReventlessCore.reportAccepted;
22
24
 
23
25
  let reportRejected = CommandTopic$ReventlessCore.reportRejected;
@@ -51,6 +53,7 @@ export {
51
53
  fireCommandOutcome,
52
54
  acceptedResultChannel,
53
55
  rejectedResultChannel,
56
+ ownershipRefusal,
54
57
  reportAccepted,
55
58
  reportRejected,
56
59
  commandOutcomeToJson,
@@ -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();
@@ -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,