@nxgt/janus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/auth/config.d.ts +201 -0
  4. package/dist/auth/config.d.ts.map +1 -0
  5. package/dist/auth/context.d.ts +87 -0
  6. package/dist/auth/context.d.ts.map +1 -0
  7. package/dist/auth/hashers.d.ts +35 -0
  8. package/dist/auth/hashers.d.ts.map +1 -0
  9. package/dist/auth/index.d.ts +9 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/janus.d.ts +38 -0
  12. package/dist/auth/janus.d.ts.map +1 -0
  13. package/dist/auth/outage.d.ts +61 -0
  14. package/dist/auth/outage.d.ts.map +1 -0
  15. package/dist/auth/port/assert-stores.d.ts +16 -0
  16. package/dist/auth/port/assert-stores.d.ts.map +1 -0
  17. package/dist/auth/port/memory.d.ts +25 -0
  18. package/dist/auth/port/memory.d.ts.map +1 -0
  19. package/dist/auth/port/types.d.ts +393 -0
  20. package/dist/auth/port/types.d.ts.map +1 -0
  21. package/dist/auth/secrets.d.ts +17 -0
  22. package/dist/auth/secrets.d.ts.map +1 -0
  23. package/dist/auth/sessions.d.ts +31 -0
  24. package/dist/auth/sessions.d.ts.map +1 -0
  25. package/dist/auth/standard-schema.d.ts +41 -0
  26. package/dist/auth/standard-schema.d.ts.map +1 -0
  27. package/dist/auth/types.d.ts +353 -0
  28. package/dist/auth/types.d.ts.map +1 -0
  29. package/dist/auth/users.d.ts +9 -0
  30. package/dist/auth/users.d.ts.map +1 -0
  31. package/dist/chunks/index-658b6mr2.js +269 -0
  32. package/dist/chunks/index-658b6mr2.js.map +11 -0
  33. package/dist/chunks/index-6dytvy3h.js +93 -0
  34. package/dist/chunks/index-6dytvy3h.js.map +11 -0
  35. package/dist/chunks/index-6p56fpbe.js +147 -0
  36. package/dist/chunks/index-6p56fpbe.js.map +12 -0
  37. package/dist/chunks/index-fgb3t64y.js +77 -0
  38. package/dist/chunks/index-fgb3t64y.js.map +10 -0
  39. package/dist/conformance/assert.d.ts +36 -0
  40. package/dist/conformance/assert.d.ts.map +1 -0
  41. package/dist/conformance/cases/outage.d.ts +3 -0
  42. package/dist/conformance/cases/outage.d.ts.map +1 -0
  43. package/dist/conformance/cases/sessions.d.ts +3 -0
  44. package/dist/conformance/cases/sessions.d.ts.map +1 -0
  45. package/dist/conformance/cases/tokens.d.ts +3 -0
  46. package/dist/conformance/cases/tokens.d.ts.map +1 -0
  47. package/dist/conformance/cases/users.d.ts +3 -0
  48. package/dist/conformance/cases/users.d.ts.map +1 -0
  49. package/dist/conformance/describe.d.ts +74 -0
  50. package/dist/conformance/describe.d.ts.map +1 -0
  51. package/dist/conformance/fixtures.d.ts +11 -0
  52. package/dist/conformance/fixtures.d.ts.map +1 -0
  53. package/dist/conformance/index.d.ts +33 -0
  54. package/dist/conformance/index.d.ts.map +1 -0
  55. package/dist/conformance/index.js +1113 -0
  56. package/dist/conformance/index.js.map +18 -0
  57. package/dist/conformance/reference.d.ts +13 -0
  58. package/dist/conformance/reference.d.ts.map +1 -0
  59. package/dist/conformance/relations.d.ts +67 -0
  60. package/dist/conformance/relations.d.ts.map +1 -0
  61. package/dist/conformance/types.d.ts +71 -0
  62. package/dist/conformance/types.d.ts.map +1 -0
  63. package/dist/errors/janus-error.d.ts +259 -0
  64. package/dist/errors/janus-error.d.ts.map +1 -0
  65. package/dist/ids/id.d.ts +55 -0
  66. package/dist/ids/id.d.ts.map +1 -0
  67. package/dist/index.d.ts +30 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +962 -0
  70. package/dist/index.js.map +19 -0
  71. package/dist/pagination/cursor-page.d.ts +53 -0
  72. package/dist/pagination/cursor-page.d.ts.map +1 -0
  73. package/dist/permissions/engine.d.ts +46 -0
  74. package/dist/permissions/engine.d.ts.map +1 -0
  75. package/dist/permissions/index.d.ts +24 -0
  76. package/dist/permissions/index.d.ts.map +1 -0
  77. package/dist/permissions/index.js +685 -0
  78. package/dist/permissions/index.js.map +15 -0
  79. package/dist/permissions/input.d.ts +29 -0
  80. package/dist/permissions/input.d.ts.map +1 -0
  81. package/dist/permissions/model.d.ts +368 -0
  82. package/dist/permissions/model.d.ts.map +1 -0
  83. package/dist/permissions/port/memory.d.ts +17 -0
  84. package/dist/permissions/port/memory.d.ts.map +1 -0
  85. package/dist/permissions/port/types.d.ts +84 -0
  86. package/dist/permissions/port/types.d.ts.map +1 -0
  87. package/dist/permissions/resolve.d.ts +59 -0
  88. package/dist/permissions/resolve.d.ts.map +1 -0
  89. package/dist/permissions/reverse.d.ts +53 -0
  90. package/dist/permissions/reverse.d.ts.map +1 -0
  91. package/dist/permissions/walk.d.ts +29 -0
  92. package/dist/permissions/walk.d.ts.map +1 -0
  93. package/dist/subjects/notation.d.ts +35 -0
  94. package/dist/subjects/notation.d.ts.map +1 -0
  95. package/dist/subjects/subject.d.ts +75 -0
  96. package/dist/subjects/subject.d.ts.map +1 -0
  97. package/dist/time/clock.d.ts +31 -0
  98. package/dist/time/clock.d.ts.map +1 -0
  99. package/dist/time/duration.d.ts +23 -0
  100. package/dist/time/duration.d.ts.map +1 -0
  101. package/docs/README.md +17 -0
  102. package/docs/guide/adapters.md +296 -0
  103. package/docs/guide/email-flows.md +146 -0
  104. package/docs/guide/errors.md +146 -0
  105. package/docs/guide/passwords.md +136 -0
  106. package/docs/guide/permissions.md +347 -0
  107. package/docs/guide/sessions.md +211 -0
  108. package/docs/guide/users.md +277 -0
  109. package/docs/guide/vocabulary.md +163 -0
  110. package/docs/roadmap.md +93 -0
  111. package/docs/troubleshooting.md +648 -0
  112. package/package.json +70 -0
@@ -0,0 +1,685 @@
1
+ import {
2
+ guardRelations,
3
+ MAX_PAGE_SIZE2,
4
+ pageLimit2
5
+ } from "../chunks/index-6dytvy3h.js";
6
+ import {
7
+ PermissionDepthError2,
8
+ formatEntity2
9
+ } from "../chunks/index-6p56fpbe.js";
10
+ import {
11
+ createMemoryRelations2
12
+ } from "../chunks/index-fgb3t64y.js";
13
+
14
+ // src/permissions/resolve.ts
15
+ function admits(holders, subject) {
16
+ return holders.some((holder) => holder.kind === "type" ? !("relation" in subject) && holder.type === subject.type : ("relation" in subject) && holder.type === subject.type && holder.relation === subject.relation);
17
+ }
18
+ var NAME = /^[a-z][A-Za-z0-9]*$/;
19
+ var FIELD = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
20
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21
+ function resolveModel(config, where) {
22
+ const refuse = (message) => new TypeError(`${where}: ${message}`);
23
+ if (!isRecord(config))
24
+ throw refuse("pass { subjects, types }");
25
+ if (!Array.isArray(config.subjects)) {
26
+ throw refuse("subjects must be an array of user types — pass auth.types from janus()");
27
+ }
28
+ for (const subject of config.subjects) {
29
+ if (typeof subject !== "string" || !NAME.test(subject)) {
30
+ throw refuse(`the subject type "${String(subject)}" must be a camelCase name`);
31
+ }
32
+ }
33
+ const subjects = new Set(config.subjects);
34
+ if (!isRecord(config.types) || Object.keys(config.types).length === 0) {
35
+ throw refuse("types declares no object type");
36
+ }
37
+ const typeNames = new Set(Object.keys(config.types));
38
+ for (const name of typeNames) {
39
+ if (!NAME.test(name)) {
40
+ throw refuse(`the object type "${name}" must be a camelCase name — letters and digits, starting with a lowercase letter`);
41
+ }
42
+ if (subjects.has(name)) {
43
+ throw refuse(`"${name}" names a user type and an object type; a subject of type "${name}" would be ambiguous`);
44
+ }
45
+ }
46
+ const relationNames = new Map;
47
+ const permissionNames = new Map;
48
+ for (const [name, def] of Object.entries(config.types)) {
49
+ const at = `types.${name}`;
50
+ if (!isRecord(def))
51
+ throw refuse(`${at} must be an object`);
52
+ for (const key of Object.keys(def)) {
53
+ if (key !== "relations" && key !== "permissions") {
54
+ throw refuse(`${at}.${key} is not a key of an object type`);
55
+ }
56
+ }
57
+ relationNames.set(name, namesIn(def.relations, `${at}.relations`, refuse));
58
+ const permissions = namesIn(def.permissions, `${at}.permissions`, refuse);
59
+ for (const permission of permissions) {
60
+ if (relationNames.get(name)?.has(permission)) {
61
+ throw refuse(`${at}: "${permission}" names a relation and a permission; rename one`);
62
+ }
63
+ }
64
+ permissionNames.set(name, permissions);
65
+ }
66
+ const isSubjectType = (type) => subjects.has(type) || typeNames.has(type);
67
+ const namesOf = (type) => new Set([
68
+ ...relationNames.get(type) ?? [],
69
+ ...permissionNames.get(type) ?? []
70
+ ]);
71
+ const types = new Map;
72
+ for (const [name, def] of Object.entries(config.types)) {
73
+ const at = `types.${name}`;
74
+ const relations = new Map;
75
+ for (const [relation, holders] of Object.entries(def.relations ?? {})) {
76
+ const here = `${at}.relations.${relation}`;
77
+ relations.set(relation, resolveRelation(holders, here, {
78
+ refuse,
79
+ isSubjectType,
80
+ relationNames
81
+ }));
82
+ }
83
+ const permissions = new Map;
84
+ for (const [permission, rules] of Object.entries(def.permissions ?? {})) {
85
+ const here = `${at}.permissions.${permission}`;
86
+ if (!Array.isArray(rules) || rules.length === 0) {
87
+ throw refuse(`${here} must be a non-empty array of rules`);
88
+ }
89
+ permissions.set(permission, rules.map((rule, index) => resolveRule(rule, `${here}[${index}]`, {
90
+ refuse,
91
+ own: namesOf(name),
92
+ relations,
93
+ typeNames,
94
+ namesOf,
95
+ self: name
96
+ })));
97
+ }
98
+ types.set(name, { name, relations, permissions });
99
+ }
100
+ refuseLoops(types, refuse);
101
+ refuseDataBeyondRoot(types, refuse);
102
+ return { subjects, types };
103
+ }
104
+ function namesIn(value, at, refuse) {
105
+ if (value === undefined)
106
+ return new Set;
107
+ if (!isRecord(value))
108
+ throw refuse(`${at} must be an object`);
109
+ for (const name of Object.keys(value)) {
110
+ if (!NAME.test(name)) {
111
+ throw refuse(`${at}: "${name}" must be a camelCase name — letters and digits, starting with a lowercase letter`);
112
+ }
113
+ }
114
+ return new Set(Object.keys(value));
115
+ }
116
+ function resolveRelation(value, here, {
117
+ refuse,
118
+ isSubjectType,
119
+ relationNames
120
+ }) {
121
+ if (isRecord(value) && value.kind === "fromField") {
122
+ const { field, subject, lookup } = value;
123
+ if (typeof field !== "string" || !FIELD.test(field)) {
124
+ throw refuse(`${here}: fromField must name a top-level field`);
125
+ }
126
+ if (typeof subject !== "string" || !isSubjectType(subject)) {
127
+ throw refuse(`${here}: fromField names "${String(subject)}", which is not a subject type`);
128
+ }
129
+ if (lookup !== undefined && typeof lookup !== "function") {
130
+ throw refuse(`${here}: fromField's lookup must be a function`);
131
+ }
132
+ return lookup === undefined ? { kind: "fromField", field, subject } : {
133
+ kind: "fromField",
134
+ field,
135
+ subject,
136
+ lookup
137
+ };
138
+ }
139
+ if (!Array.isArray(value) || value.length === 0) {
140
+ throw refuse(`${here} must be a non-empty array of subject types, or fromField()`);
141
+ }
142
+ const holders = value.map((holder) => {
143
+ if (typeof holder !== "string") {
144
+ throw refuse(`${here}: a holder must be a string`);
145
+ }
146
+ const hash = holder.indexOf("#");
147
+ if (hash < 0) {
148
+ if (!isSubjectType(holder)) {
149
+ throw refuse(`${here}: "${holder}" is not a subject type`);
150
+ }
151
+ return { kind: "type", type: holder };
152
+ }
153
+ const type = holder.slice(0, hash);
154
+ const relation = holder.slice(hash + 1);
155
+ if (!relationNames.get(type)?.has(relation)) {
156
+ throw refuse(`${here}: "${holder}" is not a subject set — it must name an object type and one of its relations`);
157
+ }
158
+ return { kind: "set", type, relation };
159
+ });
160
+ return { kind: "stored", holders };
161
+ }
162
+ function resolveRule(value, here, context) {
163
+ const { refuse } = context;
164
+ if (isRecord(value) && value.kind === "when") {
165
+ if (typeof value.test !== "function") {
166
+ throw refuse(`${here}: when() takes a function as its test`);
167
+ }
168
+ const inner = resolveRule(value.rule, here, context);
169
+ return { ...inner, test: value.test };
170
+ }
171
+ if (typeof value !== "string") {
172
+ throw refuse(`${here}: a rule is a name, an arrow, or when()`);
173
+ }
174
+ const arrow = value.indexOf("->");
175
+ if (arrow < 0) {
176
+ if (!context.own.has(value)) {
177
+ throw refuse(`${here}: "${value}" is not a relation or a permission of ${context.self}`);
178
+ }
179
+ return { kind: "name", name: value };
180
+ }
181
+ const relation = value.slice(0, arrow);
182
+ const target = value.slice(arrow + 2);
183
+ const through = context.relations.get(relation);
184
+ if (through === undefined) {
185
+ throw refuse(`${here}: "${value}" goes through "${relation}", which is not a relation of ${context.self}`);
186
+ }
187
+ const targets = through.kind === "fromField" ? [through.subject] : through.holders.map((holder) => holder.kind === "type" ? holder.type : null);
188
+ for (const type of targets) {
189
+ if (type === null || !context.typeNames.has(type)) {
190
+ throw refuse(`${here}: "${value}" goes through "${relation}", which can hold ${type === null ? "a subject set" : `a ${type}`}; an arrow follows object types only`);
191
+ }
192
+ if (!context.namesOf(type).has(target)) {
193
+ throw refuse(`${here}: "${value}" names "${target}", which ${type} does not declare`);
194
+ }
195
+ }
196
+ return { kind: "arrow", relation, target };
197
+ }
198
+ function refuseLoops(types, refuse) {
199
+ for (const type of types.values()) {
200
+ const visiting = [];
201
+ const done = new Set;
202
+ const visit = (permission) => {
203
+ if (done.has(permission))
204
+ return;
205
+ const loop = visiting.indexOf(permission);
206
+ if (loop >= 0) {
207
+ throw refuse(`types.${type.name}.permissions: ${[...visiting.slice(loop), permission].join(" → ")} is a loop no relation ends`);
208
+ }
209
+ visiting.push(permission);
210
+ for (const rule of type.permissions.get(permission) ?? []) {
211
+ if (rule.kind === "name" && type.permissions.has(rule.name)) {
212
+ visit(rule.name);
213
+ }
214
+ }
215
+ visiting.pop();
216
+ done.add(permission);
217
+ };
218
+ for (const permission of type.permissions.keys())
219
+ visit(permission);
220
+ }
221
+ }
222
+ function refuseDataBeyondRoot(types, refuse) {
223
+ const fieldRead = (type, name) => {
224
+ const relation = type.relations.get(name);
225
+ if (relation !== undefined) {
226
+ return relation.kind === "fromField" ? relation.field : null;
227
+ }
228
+ for (const rule of type.permissions.get(name) ?? []) {
229
+ const field = rule.kind === "name" ? fieldRead(type, rule.name) : fieldRead(type, rule.relation);
230
+ if (field !== null)
231
+ return field;
232
+ }
233
+ return null;
234
+ };
235
+ for (const type of types.values()) {
236
+ for (const [relation, def] of type.relations) {
237
+ if (def.kind !== "stored")
238
+ continue;
239
+ for (const holder of def.holders) {
240
+ if (holder.kind !== "set")
241
+ continue;
242
+ const target = types.get(holder.type);
243
+ const field = target === undefined ? null : fieldRead(target, holder.relation);
244
+ if (field !== null) {
245
+ throw refuse(`types.${type.name}.relations.${relation}: "${holder.type}#${holder.relation}" reads ${holder.type}.${field}, and a subject set reaches ${holder.type}s nobody passed to can() — store that relation instead of reading it`);
246
+ }
247
+ }
248
+ }
249
+ for (const [permission, rules] of type.permissions) {
250
+ for (const rule of rules) {
251
+ if (rule.kind !== "arrow")
252
+ continue;
253
+ const through = type.relations.get(rule.relation);
254
+ const targets = through?.kind === "fromField" ? [through.subject] : (through?.holders ?? []).map((holder) => holder.type);
255
+ for (const name of targets) {
256
+ const target = types.get(name);
257
+ const field = target === undefined ? null : fieldRead(target, rule.target);
258
+ if (field !== null) {
259
+ throw refuse(`types.${type.name}.permissions.${permission}: "${rule.relation}->${rule.target}" reaches ${name}.${rule.target}, which reads ${name}.${field}, and only the object passed to can() carries its data — store that relation instead of reading it`);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ // src/permissions/input.ts
268
+ var RESERVED = /[@#()]/;
269
+ var isRecord2 = (value) => typeof value === "object" && value !== null;
270
+ function idOf(value, what, where) {
271
+ if (typeof value !== "string" || value === "" || RESERVED.test(value)) {
272
+ throw new TypeError(`${where}: ${what} must be a non-empty string without @, # or parentheses`);
273
+ }
274
+ return value;
275
+ }
276
+ function typeOf(model, name, where) {
277
+ const type = model.types.get(name);
278
+ if (type === undefined) {
279
+ throw new TypeError(`${where}: "${name}" is not an object type of the model`);
280
+ }
281
+ return type;
282
+ }
283
+ function objectOf(model, value, where) {
284
+ if (!isRecord2(value) || typeof value.type !== "string") {
285
+ throw new TypeError(`${where}: the object must be { type, id, …its fields }`);
286
+ }
287
+ typeOf(model, value.type, where);
288
+ return {
289
+ type: value.type,
290
+ id: idOf(value.id, "the object id", where),
291
+ data: value
292
+ };
293
+ }
294
+ function subjectOf(model, value, where) {
295
+ if (!isRecord2(value) || typeof value.type !== "string") {
296
+ throw new TypeError(`${where}: the subject must be a user, or { type, id }`);
297
+ }
298
+ const id = idOf(value.id, "the subject id", where);
299
+ if (model.subjects.has(value.type))
300
+ return { type: value.type, id };
301
+ const type = typeOf(model, value.type, where);
302
+ if (typeof value.relation !== "string")
303
+ return { type: type.name, id };
304
+ if (!type.relations.has(value.relation)) {
305
+ throw new TypeError(`${where}: "${value.relation}" is not a relation of ${type.name}, so ${type.name}:${id}#${value.relation} is no subject set`);
306
+ }
307
+ return { type: type.name, id, relation: value.relation };
308
+ }
309
+ function tupleOf(model, object, relation, subject, where) {
310
+ const target = objectOf(model, object, where);
311
+ const type = typeOf(model, target.type, where);
312
+ const def = type.relations.get(relation);
313
+ if (def === undefined) {
314
+ throw new TypeError(`${where}: "${relation}" is not a relation of ${type.name}`);
315
+ }
316
+ if (def.kind === "fromField") {
317
+ throw new TypeError(`${where}: ${type.name}.${relation} is read from ${def.field}; there is nothing to store — change the ${type.name} instead`);
318
+ }
319
+ const who = subjectOf(model, subject, where);
320
+ if (!admits(def.holders, who)) {
321
+ throw new TypeError(`${where}: ${type.name}.${relation} is not held by ${"relation" in who ? `${who.type}#${who.relation}` : who.type}`);
322
+ }
323
+ return {
324
+ object: { type: target.type, id: target.id },
325
+ relation,
326
+ subject: who
327
+ };
328
+ }
329
+
330
+ // src/permissions/model.ts
331
+ function fromField(field, subject, options) {
332
+ return Object.freeze(options === undefined ? { kind: "fromField", field, subject } : { kind: "fromField", field, subject, lookup: options.lookup });
333
+ }
334
+ function when(rule, test) {
335
+ return Object.freeze({ kind: "when", rule, test });
336
+ }
337
+ var RESOLVED = new WeakMap;
338
+ function defineModel(config) {
339
+ const resolved = resolveModel(config, "defineModel");
340
+ const model = Object.freeze({
341
+ subjects: [...resolved.subjects],
342
+ types: [...resolved.types.keys()],
343
+ definition: config
344
+ });
345
+ RESOLVED.set(model, resolved);
346
+ return model;
347
+ }
348
+ function resolvedOf(model) {
349
+ const resolved = RESOLVED.get(model);
350
+ if (resolved === undefined) {
351
+ throw new TypeError("permissions: this model was not made by defineModel() — pass what defineModel() answered");
352
+ }
353
+ return resolved;
354
+ }
355
+
356
+ // src/permissions/reverse.ts
357
+ class Reverse {
358
+ constructor(model, store, maxDepth, ctx, subject, question) {
359
+ this.model = model;
360
+ this.store = store;
361
+ this.maxDepth = maxDepth;
362
+ this.ctx = ctx;
363
+ this.subject = subject;
364
+ this.question = question;
365
+ this.previous = new Map;
366
+ this.current = new Map;
367
+ this.path = new Set;
368
+ this.looped = false;
369
+ }
370
+ async objects(type, name) {
371
+ for (let round = 0;round <= this.maxDepth; round += 1) {
372
+ this.current = new Map;
373
+ this.looped = false;
374
+ const found = await this.reach(type, name);
375
+ if (!this.looped || this.same())
376
+ return found;
377
+ this.previous = this.current;
378
+ }
379
+ throw new PermissionDepthError2(`list: listing ${this.question} crossed more than ${this.maxDepth} relations without an answer`, { permission: this.question, maxDepth: this.maxDepth });
380
+ }
381
+ same() {
382
+ for (const [key, ids] of this.current) {
383
+ if ((this.previous.get(key)?.size ?? 0) !== ids.size)
384
+ return false;
385
+ }
386
+ return true;
387
+ }
388
+ async reach(typeName, name) {
389
+ const key = `${typeName}#${name}`;
390
+ const done = this.current.get(key);
391
+ if (done !== undefined)
392
+ return done;
393
+ if (this.path.has(key)) {
394
+ this.looped = true;
395
+ return this.previous.get(key) ?? new Set;
396
+ }
397
+ const type = this.model.types.get(typeName);
398
+ if (type === undefined)
399
+ return new Set;
400
+ this.path.add(key);
401
+ try {
402
+ const found = type.permissions.has(name) ? await this.permission(type, name) : await this.relation(type, name);
403
+ this.current.set(key, found);
404
+ return found;
405
+ } finally {
406
+ this.path.delete(key);
407
+ }
408
+ }
409
+ async relation(type, name) {
410
+ const relation = type.relations.get(name);
411
+ if (relation === undefined)
412
+ return new Set;
413
+ if (relation.kind === "fromField") {
414
+ const subject = this.subject;
415
+ return "relation" in subject || subject.type !== relation.subject ? new Set : new Set(await this.lookup(type, name, subject.id));
416
+ }
417
+ const found = new Set(admits(relation.holders, this.subject) ? await this.holding(type.name, name, this.subject) : []);
418
+ for (const holder of relation.holders) {
419
+ if (holder.kind !== "set")
420
+ continue;
421
+ for (const id of await this.reach(holder.type, holder.relation)) {
422
+ const set = { type: holder.type, id, relation: holder.relation };
423
+ for (const object of await this.holding(type.name, name, set)) {
424
+ found.add(object);
425
+ }
426
+ }
427
+ }
428
+ return found;
429
+ }
430
+ async permission(type, name) {
431
+ const found = new Set;
432
+ for (const rule of type.permissions.get(name) ?? []) {
433
+ if (rule.test !== undefined && !this.passes(rule, type, name))
434
+ continue;
435
+ if (rule.kind === "name") {
436
+ for (const id of await this.reach(type.name, rule.name))
437
+ found.add(id);
438
+ continue;
439
+ }
440
+ for (const id of await this.arrow(type, rule.relation, rule.target)) {
441
+ found.add(id);
442
+ }
443
+ }
444
+ return found;
445
+ }
446
+ async arrow(type, relation, target) {
447
+ const through = type.relations.get(relation);
448
+ const found = new Set;
449
+ if (through === undefined)
450
+ return found;
451
+ if (through.kind === "fromField") {
452
+ for (const id of await this.reach(through.subject, target)) {
453
+ for (const object of await this.lookup(type, relation, id)) {
454
+ found.add(object);
455
+ }
456
+ }
457
+ return found;
458
+ }
459
+ for (const holder of through.holders) {
460
+ if (holder.kind !== "type" || !this.model.types.has(holder.type)) {
461
+ continue;
462
+ }
463
+ for (const id of await this.reach(holder.type, target)) {
464
+ const entity = { type: holder.type, id };
465
+ for (const object of await this.holding(type.name, relation, entity)) {
466
+ found.add(object);
467
+ }
468
+ }
469
+ }
470
+ return found;
471
+ }
472
+ async holding(type, relation, subject) {
473
+ const ids = [];
474
+ let after = null;
475
+ do {
476
+ const page = await this.store.findObjects({
477
+ type,
478
+ relation,
479
+ subject,
480
+ after,
481
+ limit: MAX_PAGE_SIZE2
482
+ });
483
+ ids.push(...page.items);
484
+ after = page.nextCursor;
485
+ } while (after !== null);
486
+ return ids;
487
+ }
488
+ async lookup(type, relation, id) {
489
+ const def = type.relations.get(relation);
490
+ if (def?.kind !== "fromField" || def.lookup === undefined) {
491
+ throw new TypeError(`list: ${type.name}.${relation} is read from a field, and has no lookup to find the ${type.name}s naming ${id} — fromField('${def?.kind === "fromField" ? def.field : relation}', '${def?.kind === "fromField" ? def.subject : ""}', { lookup })`);
492
+ }
493
+ const ids = await def.lookup(id);
494
+ if (!Array.isArray(ids) || ids.some((each) => typeof each !== "string")) {
495
+ throw new TypeError(`list: the lookup of ${type.name}.${relation} must answer an array of ids`);
496
+ }
497
+ return ids;
498
+ }
499
+ passes(rule, type, name) {
500
+ if (this.ctx === undefined) {
501
+ throw new TypeError(`list: ${type.name}.${name} reaches a condition, and no ctx was passed — pass { ctx }`);
502
+ }
503
+ return rule.test(this.ctx) === true;
504
+ }
505
+ }
506
+
507
+ // src/permissions/walk.ts
508
+ class Walk {
509
+ constructor(model, store, maxDepth, ctx, question) {
510
+ this.model = model;
511
+ this.store = store;
512
+ this.maxDepth = maxDepth;
513
+ this.ctx = ctx;
514
+ this.question = question;
515
+ this.path = new Set;
516
+ }
517
+ async holds(subject, node, name, depth) {
518
+ if (depth > this.maxDepth) {
519
+ throw new PermissionDepthError2(`can: checking ${this.question} crossed more than ${this.maxDepth} relations without an answer`, { permission: this.question, maxDepth: this.maxDepth });
520
+ }
521
+ const key = `${formatEntity2(node.entity)}#${name}`;
522
+ if (this.path.has(key))
523
+ return false;
524
+ const type = this.model.types.get(node.entity.type);
525
+ if (type === undefined)
526
+ return false;
527
+ this.path.add(key);
528
+ try {
529
+ return type.permissions.has(name) ? await this.permission(subject, type, node, name, depth) : await this.relation(subject, type, node, name, depth);
530
+ } finally {
531
+ this.path.delete(key);
532
+ }
533
+ }
534
+ async relation(subject, type, node, name, depth) {
535
+ const relation = type.relations.get(name);
536
+ if (relation === undefined)
537
+ return false;
538
+ if (relation.kind === "fromField") {
539
+ const id = fieldOf(node, relation.field, `${type.name}.${name}`);
540
+ return id !== null && !("relation" in subject) && subject.type === relation.subject && subject.id === id;
541
+ }
542
+ if (admits(relation.holders, subject) && await this.store.has({ object: node.entity, relation: name, subject })) {
543
+ return true;
544
+ }
545
+ for (const set of await this.store.findSubjectSets(node.entity, name)) {
546
+ if (!admits(relation.holders, set))
547
+ continue;
548
+ const through = { entity: { type: set.type, id: set.id }, data: null };
549
+ if (await this.holds(subject, through, set.relation, depth + 1)) {
550
+ return true;
551
+ }
552
+ }
553
+ return false;
554
+ }
555
+ async permission(subject, type, node, name, depth) {
556
+ for (const rule of type.permissions.get(name) ?? []) {
557
+ if (rule.test !== undefined && !this.passes(rule, type, name))
558
+ continue;
559
+ if (rule.kind === "name") {
560
+ if (await this.holds(subject, node, rule.name, depth))
561
+ return true;
562
+ continue;
563
+ }
564
+ for (const target of await this.targets(type, node, rule.relation)) {
565
+ if (await this.holds(subject, target, rule.target, depth + 1)) {
566
+ return true;
567
+ }
568
+ }
569
+ }
570
+ return false;
571
+ }
572
+ passes(rule, type, name) {
573
+ if (this.ctx === undefined) {
574
+ throw new TypeError(`can: ${type.name}.${name} reaches a condition, and no ctx was passed — pass { ctx }`);
575
+ }
576
+ return rule.test(this.ctx) === true;
577
+ }
578
+ async targets(type, node, relation) {
579
+ const through = type.relations.get(relation);
580
+ if (through === undefined)
581
+ return [];
582
+ if (through.kind === "fromField") {
583
+ const id = fieldOf(node, through.field, `${type.name}.${relation}`);
584
+ return id === null ? [] : [{ entity: { type: through.subject, id }, data: null }];
585
+ }
586
+ return (await this.store.findEntities(node.entity, relation)).filter((entity) => admits(through.holders, entity)).map((entity) => ({
587
+ entity: { type: entity.type, id: entity.id },
588
+ data: null
589
+ }));
590
+ }
591
+ }
592
+ function fieldOf(node, field, what) {
593
+ if (node.data === null) {
594
+ throw new TypeError(`can: ${what} reads ${field} from an object nobody passed`);
595
+ }
596
+ const value = node.data[field];
597
+ if (value === null)
598
+ return null;
599
+ if (typeof value !== "string") {
600
+ throw new TypeError(`can: ${what} reads ${field}, which the object ${value === undefined ? "does not carry — pass the loaded object, spread" : "holds as something other than a string id"}`);
601
+ }
602
+ return value;
603
+ }
604
+
605
+ // src/permissions/engine.ts
606
+ var DEFAULT_MAX_DEPTH = 25;
607
+ var REQUIRED = [
608
+ "write",
609
+ "has",
610
+ "findSubjectSets",
611
+ "findEntities",
612
+ "findObjects",
613
+ "deleteEntity"
614
+ ];
615
+ function permissions(options) {
616
+ const where = "permissions";
617
+ const model = resolvedOf(options.model);
618
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
619
+ if (!Number.isInteger(maxDepth) || maxDepth < 1) {
620
+ throw new TypeError(`${where}: maxDepth must be a positive integer`);
621
+ }
622
+ const raw = options.store;
623
+ for (const method of REQUIRED) {
624
+ if (typeof raw?.[method] !== "function") {
625
+ throw new TypeError(`${where}: store.${method} is missing`);
626
+ }
627
+ }
628
+ const store = guardRelations(options.store);
629
+ const check = async (subject, permission, object, options) => {
630
+ if (subject === null || subject === undefined)
631
+ return false;
632
+ const root = objectOf(model, object, "can");
633
+ const type = typeOf(model, root.type, "can");
634
+ if (!type.relations.has(permission) && !type.permissions.has(permission)) {
635
+ throw new TypeError(`can: "${permission}" is not a relation or a permission of ${type.name}`);
636
+ }
637
+ return new Walk(model, store, maxDepth, options?.ctx, `${type.name}#${permission}`).holds(subjectOf(model, subject, "can"), { entity: { type: root.type, id: root.id }, data: root.data }, permission, 0);
638
+ };
639
+ const list = async (subject, permission, typeName, options) => {
640
+ const limit = pageLimit2(options?.limit, "list");
641
+ const after = options?.after ?? null;
642
+ if (after !== null && typeof after !== "string") {
643
+ throw new TypeError("list: after must be the nextCursor of a page, or null");
644
+ }
645
+ if (subject === null || subject === undefined) {
646
+ return { items: [], nextCursor: null };
647
+ }
648
+ if (typeof typeName !== "string") {
649
+ throw new TypeError("list: the type must be an object type of the model");
650
+ }
651
+ const type = typeOf(model, typeName, "list");
652
+ if (!type.relations.has(permission) && !type.permissions.has(permission)) {
653
+ throw new TypeError(`list: "${permission}" is not a relation or a permission of ${type.name}`);
654
+ }
655
+ const ids = await new Reverse(model, store, maxDepth, options?.ctx, subjectOf(model, subject, "list"), `${type.name}#${permission}`).objects(type.name, permission);
656
+ const rest = [...ids].sort().filter((id) => after === null || id > after);
657
+ const items = rest.slice(0, limit);
658
+ const last = items.at(-1);
659
+ return {
660
+ items,
661
+ nextCursor: rest.length > limit && last !== undefined ? last : null
662
+ };
663
+ };
664
+ const change = (operation) => async (object, relation, subject) => {
665
+ const tuple = tupleOf(model, object, relation, subject, operation);
666
+ await store.write(operation === "grant" ? { add: [tuple] } : { remove: [tuple] });
667
+ };
668
+ return Object.freeze({
669
+ model: options.model,
670
+ can: check,
671
+ list,
672
+ grant: change("grant"),
673
+ revoke: change("revoke")
674
+ });
675
+ }
676
+ export {
677
+ createMemoryRelations2 as createMemoryRelations,
678
+ defineModel,
679
+ fromField,
680
+ permissions,
681
+ when
682
+ };
683
+
684
+ //# debugId=42583EA285837CAD64756E2164756E21
685
+ //# sourceMappingURL=index.js.map