@reventlessdev/reventless-local 3.0.0-alpha.217 → 3.0.0-alpha.218
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 +12 -0
- package/package.json +11 -11
- package/src/BakedManifest.res +63 -51
- package/src/BakedManifest.res.mjs +44 -28
- package/src/HostShellDist.res +29 -0
- package/src/HostShellDist.res.mjs +20 -0
- package/src/Platform.res +30 -11
- package/src/Platform.res.mjs +6 -0
- package/src/ShellConfig.res +178 -0
- package/src/ShellConfig.res.mjs +114 -0
- package/src/UiHints.res +107 -0
- package/src/UiHints.res.mjs +61 -0
- package/src/adapter/Auth/LocalAuth.res +136 -6
- package/src/adapter/Auth/LocalAuth.res.mjs +82 -3
- package/src/adapter/DomainGraphQL_Server.res +67 -4
- package/src/adapter/DomainGraphQL_Server.res.mjs +49 -2
- package/src/adapter/GraphQL_Server.res.mjs +3 -0
- package/tests/BakedManifestFilesTest.res +68 -0
- package/tests/BakedManifestFilesTest.res.mjs +115 -0
- package/tests/ShellConfigTest.res +221 -0
- package/tests/ShellConfigTest.res.mjs +203 -0
- package/tests/UiHintsTest.res +94 -0
- package/tests/UiHintsTest.res.mjs +99 -0
- package/tests/adapter/LocalAuthLoginTest.res +214 -0
- package/tests/adapter/LocalAuthLoginTest.res.mjs +191 -8
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
4
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
4
6
|
import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
|
|
5
7
|
|
|
6
8
|
let alice_groups = ["Editor"];
|
|
@@ -36,7 +38,7 @@ function resetAll() {
|
|
|
36
38
|
globalThis.test("issue returns Ok(token) for valid credentials", async () => {
|
|
37
39
|
resetAll();
|
|
38
40
|
LocalAuth$ReventlessLocal.Login.setCredentials("alice", "alice-pw", alice);
|
|
39
|
-
let result = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw");
|
|
41
|
+
let result = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw", undefined);
|
|
40
42
|
if (result.TAG !== "Ok") {
|
|
41
43
|
return Stdlib_JsError.throwWithMessage("unexpected Error: " + result._0);
|
|
42
44
|
}
|
|
@@ -46,7 +48,7 @@ globalThis.test("issue returns Ok(token) for valid credentials", async () => {
|
|
|
46
48
|
|
|
47
49
|
globalThis.test("issue returns Error for unknown username", async () => {
|
|
48
50
|
resetAll();
|
|
49
|
-
let result = await LocalAuth$ReventlessLocal.Login.issue("ghost", "x");
|
|
51
|
+
let result = await LocalAuth$ReventlessLocal.Login.issue("ghost", "x", undefined);
|
|
50
52
|
if (result.TAG === "Ok") {
|
|
51
53
|
return Stdlib_JsError.throwWithMessage("expected Error");
|
|
52
54
|
}
|
|
@@ -55,7 +57,7 @@ globalThis.test("issue returns Error for unknown username", async () => {
|
|
|
55
57
|
globalThis.test("issue returns Error for wrong password", async () => {
|
|
56
58
|
resetAll();
|
|
57
59
|
LocalAuth$ReventlessLocal.Login.setCredentials("alice", "alice-pw", alice);
|
|
58
|
-
let result = await LocalAuth$ReventlessLocal.Login.issue("alice", "WRONG");
|
|
60
|
+
let result = await LocalAuth$ReventlessLocal.Login.issue("alice", "WRONG", undefined);
|
|
59
61
|
if (result.TAG === "Ok") {
|
|
60
62
|
return Stdlib_JsError.throwWithMessage("expected Error");
|
|
61
63
|
}
|
|
@@ -64,7 +66,7 @@ globalThis.test("issue returns Error for wrong password", async () => {
|
|
|
64
66
|
globalThis.test("verifyAndDecode round-trips the identity for a fresh token", async () => {
|
|
65
67
|
resetAll();
|
|
66
68
|
LocalAuth$ReventlessLocal.Login.setCredentials("alice", "alice-pw", alice);
|
|
67
|
-
let t = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw");
|
|
69
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw", undefined);
|
|
68
70
|
let token;
|
|
69
71
|
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage("issue failed: " + t._0);
|
|
70
72
|
let identity = LocalAuth$ReventlessLocal.Login.verifyAndDecode(token);
|
|
@@ -81,7 +83,7 @@ globalThis.test("verifyAndDecode round-trips the identity for a fresh token", as
|
|
|
81
83
|
globalThis.test("verifyAndDecode rejects a tampered payload", async () => {
|
|
82
84
|
resetAll();
|
|
83
85
|
LocalAuth$ReventlessLocal.Login.setCredentials("alice", "alice-pw", alice);
|
|
84
|
-
let t = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw");
|
|
86
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw", undefined);
|
|
85
87
|
let token;
|
|
86
88
|
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage("issue failed: " + t._0);
|
|
87
89
|
let parts = token.split(".");
|
|
@@ -100,7 +102,7 @@ globalThis.test("verifyAndDecode rejects a tampered payload", async () => {
|
|
|
100
102
|
globalThis.test("verifyAndDecode rejects a tampered signature", async () => {
|
|
101
103
|
resetAll();
|
|
102
104
|
LocalAuth$ReventlessLocal.Login.setCredentials("alice", "alice-pw", alice);
|
|
103
|
-
let t = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw");
|
|
105
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw", undefined);
|
|
104
106
|
let token;
|
|
105
107
|
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage("issue failed: " + t._0);
|
|
106
108
|
let match = LocalAuth$ReventlessLocal.Login.verifyAndDecode(token + "x");
|
|
@@ -120,7 +122,7 @@ globalThis.test("verifyAndDecode rejects a malformed token (no dot)", async () =
|
|
|
120
122
|
globalThis.test("authenticate accepts a valid Bearer token", async () => {
|
|
121
123
|
resetAll();
|
|
122
124
|
LocalAuth$ReventlessLocal.Login.setCredentials("bob", "bob-pw", bob);
|
|
123
|
-
let t = await LocalAuth$ReventlessLocal.Login.issue("bob", "bob-pw");
|
|
125
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("bob", "bob-pw", undefined);
|
|
124
126
|
let token;
|
|
125
127
|
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage("issue failed: " + t._0);
|
|
126
128
|
let result = await LocalAuth$ReventlessLocal.authenticate(buildContext([[
|
|
@@ -158,7 +160,7 @@ globalThis.test("authenticate rejects invalid Bearer even when X-User is present
|
|
|
158
160
|
globalThis.test("Bearer outranks X-User when both present and Bearer is valid", async () => {
|
|
159
161
|
resetAll();
|
|
160
162
|
LocalAuth$ReventlessLocal.Login.setCredentials("bob", "bob-pw", bob);
|
|
161
|
-
let t = await LocalAuth$ReventlessLocal.Login.issue("bob", "bob-pw");
|
|
163
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("bob", "bob-pw", undefined);
|
|
162
164
|
let token;
|
|
163
165
|
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage("issue failed: " + t._0);
|
|
164
166
|
let result = await LocalAuth$ReventlessLocal.authenticate(buildContext([
|
|
@@ -198,10 +200,191 @@ globalThis.test("setCredentials mirrors identity into the X-User registry", asyn
|
|
|
198
200
|
globalThis.expect(identity.groups).toEqual(["Editor"]);
|
|
199
201
|
});
|
|
200
202
|
|
|
203
|
+
let multiRole_groups = [
|
|
204
|
+
"Fulfilment",
|
|
205
|
+
"Shopper"
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
let multiRole = {
|
|
209
|
+
userId: "u-carol",
|
|
210
|
+
username: "carol",
|
|
211
|
+
groups: multiRole_groups,
|
|
212
|
+
provider: "InMemory"
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
function decodeOrThrow(token) {
|
|
216
|
+
let i = LocalAuth$ReventlessLocal.Login.verifyAndDecode(token);
|
|
217
|
+
if (i !== undefined) {
|
|
218
|
+
return i;
|
|
219
|
+
} else {
|
|
220
|
+
return Stdlib_JsError.throwWithMessage("expected a verifiable token");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function carolLoggedIn() {
|
|
225
|
+
resetAll();
|
|
226
|
+
LocalAuth$ReventlessLocal.Login.setCredentials("carol", "carol-pw", multiRole);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
globalThis.test("a login naming no role mints exactly what it minted before", async () => {
|
|
230
|
+
carolLoggedIn();
|
|
231
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", undefined);
|
|
232
|
+
let plain;
|
|
233
|
+
plain = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
234
|
+
let identity = decodeOrThrow(plain);
|
|
235
|
+
globalThis.expect(identity.groups).toEqual([
|
|
236
|
+
"Fulfilment",
|
|
237
|
+
"Shopper"
|
|
238
|
+
]);
|
|
239
|
+
globalThis.expect(identity.claims).toEqual(undefined);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
globalThis.test("a login naming a held role mints that role alone", async () => {
|
|
243
|
+
carolLoggedIn();
|
|
244
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Shopper");
|
|
245
|
+
let token;
|
|
246
|
+
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
247
|
+
let identity = decodeOrThrow(token);
|
|
248
|
+
globalThis.expect(identity.groups).toEqual(["Shopper"]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
globalThis.test("a narrowed token remembers the choice and what it gave up", async () => {
|
|
252
|
+
carolLoggedIn();
|
|
253
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Shopper");
|
|
254
|
+
let token;
|
|
255
|
+
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
256
|
+
let identity = decodeOrThrow(token);
|
|
257
|
+
globalThis.expect([
|
|
258
|
+
Identity$Reventless.getClaim(identity, "activeRole"),
|
|
259
|
+
Identity$Reventless.getClaim(identity, "availableRoles")
|
|
260
|
+
]).toEqual([
|
|
261
|
+
"Shopper",
|
|
262
|
+
"Fulfilment,Shopper"
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
globalThis.test("a login naming a role the user does not hold is REFUSED", async () => {
|
|
267
|
+
carolLoggedIn();
|
|
268
|
+
let msg = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Admin");
|
|
269
|
+
if (msg.TAG === "Ok") {
|
|
270
|
+
return Stdlib_JsError.throwWithMessage("expected a request to widen to be refused");
|
|
271
|
+
}
|
|
272
|
+
globalThis.expect(msg._0.includes("Admin")).toEqual(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
globalThis.test("narrowing cannot smuggle a group in through the claims bag", async () => {
|
|
276
|
+
resetAll();
|
|
277
|
+
let newrecord = {...multiRole};
|
|
278
|
+
newrecord.claims = Object.fromEntries([[
|
|
279
|
+
"availableRoles",
|
|
280
|
+
"Admin,Fulfilment,Shopper"
|
|
281
|
+
]]);
|
|
282
|
+
LocalAuth$ReventlessLocal.Login.setCredentials("carol", "carol-pw", newrecord);
|
|
283
|
+
let match = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Admin");
|
|
284
|
+
if (match.TAG === "Ok") {
|
|
285
|
+
return Stdlib_JsError.throwWithMessage("expected membership to be judged by groups, not by a claim");
|
|
286
|
+
}
|
|
287
|
+
globalThis.expect(true).toEqual(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
globalThis.test("a narrowed token authenticates as the narrowed identity", async () => {
|
|
291
|
+
carolLoggedIn();
|
|
292
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Shopper");
|
|
293
|
+
let token;
|
|
294
|
+
token = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
295
|
+
let result = await LocalAuth$ReventlessLocal.authenticate(buildContext([[
|
|
296
|
+
"authorization",
|
|
297
|
+
"Bearer " + token
|
|
298
|
+
]]));
|
|
299
|
+
if (typeof result !== "object") {
|
|
300
|
+
return Stdlib_JsError.throwWithMessage("expected the narrowed token to authenticate");
|
|
301
|
+
}
|
|
302
|
+
if (result.TAG !== "Authenticated") {
|
|
303
|
+
return Stdlib_JsError.throwWithMessage("expected the narrowed token to authenticate");
|
|
304
|
+
}
|
|
305
|
+
globalThis.expect(result._0.groups).toEqual(["Shopper"]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
globalThis.test("the minted identity matches the token, not the stored user", async () => {
|
|
309
|
+
carolLoggedIn();
|
|
310
|
+
let minted = LocalAuth$ReventlessLocal.Login.mintedIdentity("carol", "Shopper");
|
|
311
|
+
let stored = LocalAuth$ReventlessLocal.lookupUser("carol");
|
|
312
|
+
globalThis.expect([
|
|
313
|
+
Stdlib_Option.mapOr(minted, [], i => i.groups),
|
|
314
|
+
Stdlib_Option.mapOr(stored, [], i => i.groups)
|
|
315
|
+
]).toEqual([
|
|
316
|
+
["Shopper"],
|
|
317
|
+
[
|
|
318
|
+
"Fulfilment",
|
|
319
|
+
"Shopper"
|
|
320
|
+
]
|
|
321
|
+
]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
globalThis.test("a session can be re-minted as one of its roles", async () => {
|
|
325
|
+
carolLoggedIn();
|
|
326
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", undefined);
|
|
327
|
+
let wide;
|
|
328
|
+
wide = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
329
|
+
let t$1 = LocalAuth$ReventlessLocal.Login.reissue(wide, "Shopper");
|
|
330
|
+
if (t$1.TAG !== "Ok") {
|
|
331
|
+
return Stdlib_JsError.throwWithMessage(t$1._0);
|
|
332
|
+
}
|
|
333
|
+
globalThis.expect(decodeOrThrow(t$1._0).groups).toEqual(["Shopper"]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
globalThis.test("a narrowed session can widen back to its full membership", async () => {
|
|
337
|
+
carolLoggedIn();
|
|
338
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Shopper");
|
|
339
|
+
let narrow;
|
|
340
|
+
narrow = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
341
|
+
let t$1 = LocalAuth$ReventlessLocal.Login.reissue(narrow, undefined);
|
|
342
|
+
if (t$1.TAG !== "Ok") {
|
|
343
|
+
return Stdlib_JsError.throwWithMessage(t$1._0);
|
|
344
|
+
}
|
|
345
|
+
let identity = decodeOrThrow(t$1._0);
|
|
346
|
+
globalThis.expect([
|
|
347
|
+
identity.groups,
|
|
348
|
+
identity.claims
|
|
349
|
+
]).toEqual([
|
|
350
|
+
[
|
|
351
|
+
"Fulfilment",
|
|
352
|
+
"Shopper"
|
|
353
|
+
],
|
|
354
|
+
undefined
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
globalThis.test("switching judges membership from the store, not the token's claim", async () => {
|
|
359
|
+
carolLoggedIn();
|
|
360
|
+
let t = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", "Shopper");
|
|
361
|
+
let narrow;
|
|
362
|
+
narrow = t.TAG === "Ok" ? t._0 : Stdlib_JsError.throwWithMessage(t._0);
|
|
363
|
+
let newrecord = {...multiRole};
|
|
364
|
+
LocalAuth$ReventlessLocal.Login.setCredentials("carol", "carol-pw", (newrecord.groups = ["Shopper"], newrecord));
|
|
365
|
+
let match = LocalAuth$ReventlessLocal.Login.reissue(narrow, "Fulfilment");
|
|
366
|
+
if (match.TAG === "Ok") {
|
|
367
|
+
return Stdlib_JsError.throwWithMessage("expected a revoked role to be unreachable via the token's claim");
|
|
368
|
+
}
|
|
369
|
+
globalThis.expect(true).toEqual(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
globalThis.test("switching refuses a token this server did not sign", async () => {
|
|
373
|
+
carolLoggedIn();
|
|
374
|
+
let msg = LocalAuth$ReventlessLocal.Login.reissue("not.a-real-token", "Shopper");
|
|
375
|
+
if (msg.TAG === "Ok") {
|
|
376
|
+
return Stdlib_JsError.throwWithMessage("expected an unverifiable token to be refused");
|
|
377
|
+
}
|
|
378
|
+
globalThis.expect(msg._0).toEqual("Invalid token");
|
|
379
|
+
});
|
|
380
|
+
|
|
201
381
|
export {
|
|
202
382
|
alice,
|
|
203
383
|
bob,
|
|
204
384
|
buildContext,
|
|
205
385
|
resetAll,
|
|
386
|
+
multiRole,
|
|
387
|
+
decodeOrThrow,
|
|
388
|
+
carolLoggedIn,
|
|
206
389
|
}
|
|
207
390
|
/* Not a pure module */
|
|
@@ -81,7 +81,7 @@ globalThis.test("load(~users) registers credentials and returns InlineUsers", as
|
|
|
81
81
|
} else {
|
|
82
82
|
Stdlib_JsError.throwWithMessage("load failed: " + msg._0);
|
|
83
83
|
}
|
|
84
|
-
let result = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw");
|
|
84
|
+
let result = await LocalAuth$ReventlessLocal.Login.issue("alice", "alice-pw", undefined);
|
|
85
85
|
if (result.TAG !== "Ok") {
|
|
86
86
|
Stdlib_JsError.throwWithMessage("issue rejected loaded credentials: " + result._0);
|
|
87
87
|
}
|
|
@@ -112,7 +112,7 @@ globalThis.test("load(~usersFile) reads a YAML file from disk", async () => {
|
|
|
112
112
|
} else {
|
|
113
113
|
Stdlib_JsError.throwWithMessage("load failed: " + msg._0);
|
|
114
114
|
}
|
|
115
|
-
let result = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw");
|
|
115
|
+
let result = await LocalAuth$ReventlessLocal.Login.issue("carol", "carol-pw", undefined);
|
|
116
116
|
if (result.TAG !== "Ok") {
|
|
117
117
|
Stdlib_JsError.throwWithMessage("issue rejected file-loaded credentials: " + result._0);
|
|
118
118
|
}
|