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

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,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.278 (2026-10-06)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-local
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.277 (2026-10-01)
15
+
16
+ ### Bug Fixes
17
+
18
+ * check a plugin's roles against the user pool's groups ([ddb3c42](https://github.com/ReventlessDev/reventless-core/commit/ddb3c422b7226a19800e781abd5f03af18867007))
19
+
20
+
6
21
  # 3.0.0-alpha.276 (2026-10-01)
7
22
 
8
23
  * feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.276",
3
+ "version": "3.0.0-alpha.278",
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",
38
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
39
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
40
+ "@reventlessdev/rescript-node": "2.0.0-alpha.21",
41
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
40
42
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
41
- "@reventlessdev/rescript-node": "2.0.0-alpha.20",
42
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
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
- "@reventlessdev/reventless-seed": "1.0.0-alpha.31",
49
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.150"
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.288",
44
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.136",
45
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.235",
46
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.152",
47
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.158",
48
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.186",
49
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.32"
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.96"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.97"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "^12.3.1"
package/src/Platform.res CHANGED
@@ -107,12 +107,9 @@ module MakeWithConfig = (
107
107
  let _ = Config.commandHandlerConfig
108
108
 
109
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
- )
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)
116
113
  // Activate Pulumi mock mode — must happen before any component creation.
117
114
  // Idempotent, so safe to call even if TestRunner.setup() was already called.
118
115
  let _ = TestRunner.setup()
@@ -2448,6 +2445,10 @@ module MakeWithConfig = (
2448
2445
  }}`,
2449
2446
  )
2450
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
+
2451
2452
  // Set the active deploy target so resolveTargetGraphQL/MCP() and QueryDb serverRef/relayRef
2452
2453
  // route registrations to the correct server. Mirrors AWS resolveTargetApi() pattern.
2453
2454
  currentDeployTarget.contents = apiTarget
@@ -26,12 +26,10 @@ import * as UiSlots$ReventlessLocal from "./UiSlots.res.mjs";
26
26
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
27
27
  import * as LocalBus$ReventlessLocal from "./adapter/LocalBus.res.mjs";
28
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";
30
29
  import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
31
30
  import * as UserStore$ReventlessLocal from "./adapter/Auth/UserStore.res.mjs";
32
31
  import * as TestRunner$ReventlessLocal from "./test/TestRunner.res.mjs";
33
32
  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";
35
33
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
36
34
  import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
37
35
  import * as ShellConfig$ReventlessLocal from "./ShellConfig.res.mjs";
@@ -143,7 +141,7 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
143
141
  }
144
142
 
145
143
  function MakeWithConfig(Config) {
146
- Role$Reventless.provideGroupsFrom(() => LocalAuth$ReventlessLocal.knownGroups().concat(AccountsManifest$Reventless.declaredGroups()));
144
+ Role$Reventless.provideGroupsFrom(UserStore$ReventlessLocal.providedGroups);
147
145
  TestRunner$ReventlessLocal.setup();
148
146
  let match = Config.backend;
149
147
  if (typeof match !== "object") {
@@ -1757,6 +1755,7 @@ function MakeWithConfig(Config) {
1757
1755
  let tmp;
1758
1756
  tmp = apiTarget === "Domain" ? "Domain" : "Platform";
1759
1757
  log.info("Platform", undefined, `deployPlugin target=` + tmp);
1758
+ UserStore$ReventlessLocal.pluginChecked.contents = true;
1760
1759
  currentDeployTarget.contents = apiTarget;
1761
1760
  StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
1762
1761
  let tmp$1;
@@ -1909,7 +1908,7 @@ function MakeWithConfig(Config) {
1909
1908
 
1910
1909
  function Make($star) {
1911
1910
  let backend = Backend$ReventlessLocal.fromEnv();
1912
- Role$Reventless.provideGroupsFrom(() => LocalAuth$ReventlessLocal.knownGroups().concat(AccountsManifest$Reventless.declaredGroups()));
1911
+ Role$Reventless.provideGroupsFrom(UserStore$ReventlessLocal.providedGroups);
1913
1912
  TestRunner$ReventlessLocal.setup();
1914
1913
  if (typeof backend !== "object") {
1915
1914
  BackendState$ReventlessLocal.setMemory();
@@ -3502,6 +3501,7 @@ function Make($star) {
3502
3501
  let tmp;
3503
3502
  tmp = apiTarget === "Domain" ? "Domain" : "Platform";
3504
3503
  log.info("Platform", undefined, `deployPlugin target=` + tmp);
3504
+ UserStore$ReventlessLocal.pluginChecked.contents = true;
3505
3505
  currentDeployTarget.contents = apiTarget;
3506
3506
  StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
3507
3507
  let tmp$1;
@@ -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
  }
@@ -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,