@xnetjs/data 0.0.2
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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/canvas-HSIKIQFK.js +7 -0
- package/dist/chunk-2L5ZUGG5.js +53 -0
- package/dist/chunk-4MTS5KAQ.js +22 -0
- package/dist/chunk-BQBPA5HS.js +76 -0
- package/dist/chunk-GZHARFKC.js +25 -0
- package/dist/chunk-IDMBCRUC.js +29 -0
- package/dist/chunk-SZC345Z2.js +2347 -0
- package/dist/chunk-VYR5GPJP.js +39 -0
- package/dist/chunk-WKIKJTI2.js +45 -0
- package/dist/comment-277JD7DV.js +7 -0
- package/dist/database-W3KEHLI3.js +7 -0
- package/dist/database-row-3O2QSNZN.js +7 -0
- package/dist/grant-QVZ454YC.js +7 -0
- package/dist/index.d.ts +4598 -0
- package/dist/index.js +5103 -0
- package/dist/page-O4WTOIEO.js +7 -0
- package/dist/task-SEKAYJH7.js +7 -0
- package/dist/ts-plugin/index.cjs +240 -0
- package/dist/ts-plugin/index.d.cts +46 -0
- package/dist/ts-plugin/index.d.ts +46 -0
- package/dist/ts-plugin/index.js +213 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2347 @@
|
|
|
1
|
+
// src/schema/node.ts
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
var DEFAULT_SCHEMA_VERSION = "1.0.0";
|
|
4
|
+
function parseSchemaIRI(iri) {
|
|
5
|
+
const match = iri.match(/^(xnet:\/\/[^/]+\/)([^@]+)(?:@(.+))?$/);
|
|
6
|
+
if (!match) {
|
|
7
|
+
return {
|
|
8
|
+
iri,
|
|
9
|
+
baseIRI: iri,
|
|
10
|
+
namespace: "",
|
|
11
|
+
name: iri,
|
|
12
|
+
version: DEFAULT_SCHEMA_VERSION,
|
|
13
|
+
hasExplicitVersion: false
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const [, namespace, name, version] = match;
|
|
17
|
+
const hasExplicitVersion = version !== void 0;
|
|
18
|
+
const resolvedVersion = version ?? DEFAULT_SCHEMA_VERSION;
|
|
19
|
+
const baseIRI = `${namespace}${name}`;
|
|
20
|
+
return {
|
|
21
|
+
iri: hasExplicitVersion ? iri : `${baseIRI}@${resolvedVersion}`,
|
|
22
|
+
baseIRI,
|
|
23
|
+
namespace,
|
|
24
|
+
name,
|
|
25
|
+
version: resolvedVersion,
|
|
26
|
+
hasExplicitVersion
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function normalizeSchemaIRI(iri) {
|
|
30
|
+
const parsed = parseSchemaIRI(iri);
|
|
31
|
+
return parsed.iri;
|
|
32
|
+
}
|
|
33
|
+
function getBaseSchemaIRI(iri) {
|
|
34
|
+
return parseSchemaIRI(iri).baseIRI;
|
|
35
|
+
}
|
|
36
|
+
function isNode(value) {
|
|
37
|
+
if (typeof value !== "object" || value === null) return false;
|
|
38
|
+
const obj = value;
|
|
39
|
+
return typeof obj.id === "string" && typeof obj.schemaId === "string" && obj.schemaId.startsWith("xnet://") && typeof obj.createdAt === "number" && typeof obj.createdBy === "string" && obj.createdBy.startsWith("did:key:");
|
|
40
|
+
}
|
|
41
|
+
function createNodeId(length) {
|
|
42
|
+
return nanoid(length);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/auth/grants.ts
|
|
46
|
+
var GRANT_SCHEMA_IRI = "xnet://xnet.fyi/Grant";
|
|
47
|
+
function isGrantActive(grant, now = Date.now()) {
|
|
48
|
+
const revokedAt = asNumber(grant.properties.revokedAt);
|
|
49
|
+
if (revokedAt > 0) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const expiresAt = asNumber(grant.properties.expiresAt);
|
|
53
|
+
if (expiresAt > 0 && expiresAt <= now) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
var GrantIndex = class {
|
|
59
|
+
constructor(store, options = {}) {
|
|
60
|
+
this.store = store;
|
|
61
|
+
this.schemaId = options.schemaId ?? GRANT_SCHEMA_IRI;
|
|
62
|
+
this.clock = options.clock ?? Date.now;
|
|
63
|
+
}
|
|
64
|
+
byResourceAndGrantee = /* @__PURE__ */ new Map();
|
|
65
|
+
byResource = /* @__PURE__ */ new Map();
|
|
66
|
+
grantsById = /* @__PURE__ */ new Map();
|
|
67
|
+
unsubscribe = null;
|
|
68
|
+
schemaId;
|
|
69
|
+
clock;
|
|
70
|
+
async initialize() {
|
|
71
|
+
const grants = await this.loadGrantNodes();
|
|
72
|
+
for (const grant of grants) {
|
|
73
|
+
this.indexGrant(grant);
|
|
74
|
+
}
|
|
75
|
+
this.unsubscribe = this.store.subscribe((event) => {
|
|
76
|
+
const node = event.node;
|
|
77
|
+
if (!node || !this.isGrantSchema(node.schemaId)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.removeGrant(node.id);
|
|
81
|
+
if (!node.deleted) {
|
|
82
|
+
this.indexGrant(node);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
findGrants(resource, grantee) {
|
|
87
|
+
const grantsByGrantee = this.byResourceAndGrantee.get(resource);
|
|
88
|
+
if (!grantsByGrantee) return [];
|
|
89
|
+
const grantIds = grantsByGrantee.get(grantee);
|
|
90
|
+
if (!grantIds) return [];
|
|
91
|
+
const now = this.clock();
|
|
92
|
+
const grants = [];
|
|
93
|
+
for (const grantId of grantIds) {
|
|
94
|
+
const grant = this.grantsById.get(grantId);
|
|
95
|
+
if (grant && isGrantActive(grant, now)) {
|
|
96
|
+
grants.push(grant);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return grants;
|
|
100
|
+
}
|
|
101
|
+
findGrantsForResource(resource) {
|
|
102
|
+
const grantIds = this.byResource.get(resource);
|
|
103
|
+
if (!grantIds) return [];
|
|
104
|
+
const now = this.clock();
|
|
105
|
+
const grants = [];
|
|
106
|
+
for (const grantId of grantIds) {
|
|
107
|
+
const grant = this.grantsById.get(grantId);
|
|
108
|
+
if (grant && isGrantActive(grant, now)) {
|
|
109
|
+
grants.push(grant);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return grants;
|
|
113
|
+
}
|
|
114
|
+
findAllGrantsForResource(resource) {
|
|
115
|
+
const grantIds = this.byResource.get(resource);
|
|
116
|
+
if (!grantIds) return [];
|
|
117
|
+
const grants = [];
|
|
118
|
+
for (const grantId of grantIds) {
|
|
119
|
+
const grant = this.grantsById.get(grantId);
|
|
120
|
+
if (grant) {
|
|
121
|
+
grants.push(grant);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return grants;
|
|
125
|
+
}
|
|
126
|
+
findGrantsForGrantee(grantee) {
|
|
127
|
+
const grants = [];
|
|
128
|
+
const now = this.clock();
|
|
129
|
+
for (const grantsByGrantee of this.byResourceAndGrantee.values()) {
|
|
130
|
+
const grantIds = grantsByGrantee.get(grantee);
|
|
131
|
+
if (!grantIds) continue;
|
|
132
|
+
for (const grantId of grantIds) {
|
|
133
|
+
const grant = this.grantsById.get(grantId);
|
|
134
|
+
if (grant && isGrantActive(grant, now)) {
|
|
135
|
+
grants.push(grant);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return grants;
|
|
140
|
+
}
|
|
141
|
+
dispose() {
|
|
142
|
+
this.unsubscribe?.();
|
|
143
|
+
this.unsubscribe = null;
|
|
144
|
+
this.byResourceAndGrantee.clear();
|
|
145
|
+
this.byResource.clear();
|
|
146
|
+
this.grantsById.clear();
|
|
147
|
+
}
|
|
148
|
+
async loadGrantNodes() {
|
|
149
|
+
const directMatches = await this.store.list({ schemaId: this.schemaId, includeDeleted: false });
|
|
150
|
+
if (directMatches.length > 0 || this.schemaId.includes("@")) {
|
|
151
|
+
return directMatches;
|
|
152
|
+
}
|
|
153
|
+
const allNodes = await this.store.list({ includeDeleted: false });
|
|
154
|
+
return allNodes.filter((node) => this.isGrantSchema(node.schemaId));
|
|
155
|
+
}
|
|
156
|
+
isGrantSchema(schemaId) {
|
|
157
|
+
return schemaId === this.schemaId || schemaId.startsWith(`${this.schemaId}@`);
|
|
158
|
+
}
|
|
159
|
+
indexGrant(node) {
|
|
160
|
+
const resource = asString(node.properties.resource);
|
|
161
|
+
const grantee = asDid(node.properties.grantee);
|
|
162
|
+
if (!resource || !grantee) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const grant = {
|
|
166
|
+
id: node.id,
|
|
167
|
+
properties: node.properties,
|
|
168
|
+
deleted: node.deleted
|
|
169
|
+
};
|
|
170
|
+
this.grantsById.set(node.id, grant);
|
|
171
|
+
let byGrantee = this.byResourceAndGrantee.get(resource);
|
|
172
|
+
if (!byGrantee) {
|
|
173
|
+
byGrantee = /* @__PURE__ */ new Map();
|
|
174
|
+
this.byResourceAndGrantee.set(resource, byGrantee);
|
|
175
|
+
}
|
|
176
|
+
let grantIdsForGrantee = byGrantee.get(grantee);
|
|
177
|
+
if (!grantIdsForGrantee) {
|
|
178
|
+
grantIdsForGrantee = /* @__PURE__ */ new Set();
|
|
179
|
+
byGrantee.set(grantee, grantIdsForGrantee);
|
|
180
|
+
}
|
|
181
|
+
grantIdsForGrantee.add(node.id);
|
|
182
|
+
let grantIdsForResource = this.byResource.get(resource);
|
|
183
|
+
if (!grantIdsForResource) {
|
|
184
|
+
grantIdsForResource = /* @__PURE__ */ new Set();
|
|
185
|
+
this.byResource.set(resource, grantIdsForResource);
|
|
186
|
+
}
|
|
187
|
+
grantIdsForResource.add(node.id);
|
|
188
|
+
}
|
|
189
|
+
removeGrant(grantId) {
|
|
190
|
+
const existing = this.grantsById.get(grantId);
|
|
191
|
+
if (!existing) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const resource = asString(existing.properties.resource);
|
|
195
|
+
const grantee = asDid(existing.properties.grantee);
|
|
196
|
+
this.grantsById.delete(grantId);
|
|
197
|
+
if (!resource || !grantee) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const byGrantee = this.byResourceAndGrantee.get(resource);
|
|
201
|
+
if (byGrantee) {
|
|
202
|
+
const grantIds = byGrantee.get(grantee);
|
|
203
|
+
if (grantIds) {
|
|
204
|
+
grantIds.delete(grantId);
|
|
205
|
+
if (grantIds.size === 0) {
|
|
206
|
+
byGrantee.delete(grantee);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (byGrantee.size === 0) {
|
|
210
|
+
this.byResourceAndGrantee.delete(resource);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const byResource = this.byResource.get(resource);
|
|
214
|
+
if (byResource) {
|
|
215
|
+
byResource.delete(grantId);
|
|
216
|
+
if (byResource.size === 0) {
|
|
217
|
+
this.byResource.delete(resource);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
function asDid(value) {
|
|
223
|
+
if (typeof value === "string" && value.startsWith("did:key:")) {
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function asString(value) {
|
|
229
|
+
return typeof value === "string" ? value : null;
|
|
230
|
+
}
|
|
231
|
+
function asNumber(value) {
|
|
232
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/auth/offline-policy.ts
|
|
236
|
+
var DEFAULT_OFFLINE_POLICY = {
|
|
237
|
+
decisionCacheTTL: 5 * 60 * 1e3,
|
|
238
|
+
maxStaleness: 60 * 60 * 1e3,
|
|
239
|
+
revalidation: "hybrid",
|
|
240
|
+
allowOfflineGrants: true
|
|
241
|
+
};
|
|
242
|
+
function mergeOfflinePolicy(current, patch) {
|
|
243
|
+
return {
|
|
244
|
+
...current,
|
|
245
|
+
...patch
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/auth/mode.ts
|
|
250
|
+
function getAuthMode(schema) {
|
|
251
|
+
if (!schema.authorization) return "legacy";
|
|
252
|
+
return "enforce";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/auth/serialize.ts
|
|
256
|
+
function serializeAuthExpression(expr) {
|
|
257
|
+
switch (expr._tag) {
|
|
258
|
+
case "allow":
|
|
259
|
+
return { _tag: "allow", roles: [...expr.roles] };
|
|
260
|
+
case "deny":
|
|
261
|
+
return { _tag: "deny", roles: [...expr.roles] };
|
|
262
|
+
case "and":
|
|
263
|
+
return { _tag: "and", exprs: expr.exprs.map(serializeAuthExpression) };
|
|
264
|
+
case "or":
|
|
265
|
+
return { _tag: "or", exprs: expr.exprs.map(serializeAuthExpression) };
|
|
266
|
+
case "not":
|
|
267
|
+
return { _tag: "not", expr: serializeAuthExpression(expr.expr) };
|
|
268
|
+
case "roleRef":
|
|
269
|
+
return { _tag: "roleRef", role: expr.role };
|
|
270
|
+
case "public":
|
|
271
|
+
return { _tag: "public" };
|
|
272
|
+
case "authenticated":
|
|
273
|
+
return { _tag: "authenticated" };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function deserializeAuthExpression(serialized) {
|
|
277
|
+
switch (serialized._tag) {
|
|
278
|
+
case "allow":
|
|
279
|
+
return { _tag: "allow", roles: serialized.roles };
|
|
280
|
+
case "deny":
|
|
281
|
+
return { _tag: "deny", roles: serialized.roles };
|
|
282
|
+
case "and":
|
|
283
|
+
return { _tag: "and", exprs: serialized.exprs.map(deserializeAuthExpression) };
|
|
284
|
+
case "or":
|
|
285
|
+
return { _tag: "or", exprs: serialized.exprs.map(deserializeAuthExpression) };
|
|
286
|
+
case "not":
|
|
287
|
+
return { _tag: "not", expr: deserializeAuthExpression(serialized.expr) };
|
|
288
|
+
case "roleRef":
|
|
289
|
+
return { _tag: "roleRef", role: serialized.role };
|
|
290
|
+
case "public":
|
|
291
|
+
return { _tag: "public" };
|
|
292
|
+
case "authenticated":
|
|
293
|
+
return { _tag: "authenticated" };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function serializeRoleResolver(resolver) {
|
|
297
|
+
switch (resolver._tag) {
|
|
298
|
+
case "creator":
|
|
299
|
+
return { _tag: "creator" };
|
|
300
|
+
case "property":
|
|
301
|
+
return { _tag: "property", propertyName: resolver.propertyName };
|
|
302
|
+
case "relation":
|
|
303
|
+
return {
|
|
304
|
+
_tag: "relation",
|
|
305
|
+
relationName: resolver.relationName,
|
|
306
|
+
targetRole: resolver.targetRole
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function deserializeRoleResolver(serialized) {
|
|
311
|
+
switch (serialized._tag) {
|
|
312
|
+
case "creator":
|
|
313
|
+
return { _tag: "creator" };
|
|
314
|
+
case "property":
|
|
315
|
+
return { _tag: "property", propertyName: serialized.propertyName };
|
|
316
|
+
case "relation":
|
|
317
|
+
return {
|
|
318
|
+
_tag: "relation",
|
|
319
|
+
relationName: serialized.relationName,
|
|
320
|
+
targetRole: serialized.targetRole
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function serializeAuthorization(auth) {
|
|
325
|
+
const serialized = {
|
|
326
|
+
roles: {},
|
|
327
|
+
actions: {}
|
|
328
|
+
};
|
|
329
|
+
for (const [name, resolver] of Object.entries(auth.roles)) {
|
|
330
|
+
serialized.roles[name] = serializeRoleResolver(resolver);
|
|
331
|
+
}
|
|
332
|
+
for (const [name, expr] of Object.entries(auth.actions)) {
|
|
333
|
+
serialized.actions[name] = serializeAuthExpression(expr);
|
|
334
|
+
}
|
|
335
|
+
if (auth.publicProps) {
|
|
336
|
+
serialized.publicProps = [...auth.publicProps];
|
|
337
|
+
}
|
|
338
|
+
if (auth.fieldRules) {
|
|
339
|
+
serialized.fieldRules = {};
|
|
340
|
+
for (const [fieldName, rule] of Object.entries(auth.fieldRules)) {
|
|
341
|
+
serialized.fieldRules[fieldName] = {
|
|
342
|
+
allow: serializeAuthExpression(rule.allow),
|
|
343
|
+
deny: rule.deny ? serializeAuthExpression(rule.deny) : void 0
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (auth.nodePolicy) {
|
|
348
|
+
serialized.nodePolicy = {
|
|
349
|
+
mode: auth.nodePolicy.mode,
|
|
350
|
+
allow: [...auth.nodePolicy.allow]
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return serialized;
|
|
354
|
+
}
|
|
355
|
+
function deserializeAuthorization(serialized) {
|
|
356
|
+
const auth = {
|
|
357
|
+
roles: {},
|
|
358
|
+
actions: {}
|
|
359
|
+
};
|
|
360
|
+
for (const [name, resolver] of Object.entries(serialized.roles)) {
|
|
361
|
+
auth.roles[name] = deserializeRoleResolver(resolver);
|
|
362
|
+
}
|
|
363
|
+
for (const [name, expr] of Object.entries(serialized.actions)) {
|
|
364
|
+
auth.actions[name] = deserializeAuthExpression(expr);
|
|
365
|
+
}
|
|
366
|
+
if (serialized.publicProps) {
|
|
367
|
+
auth.publicProps = [...serialized.publicProps];
|
|
368
|
+
}
|
|
369
|
+
if (serialized.fieldRules) {
|
|
370
|
+
auth.fieldRules = {};
|
|
371
|
+
for (const [fieldName, rule] of Object.entries(serialized.fieldRules)) {
|
|
372
|
+
auth.fieldRules[fieldName] = {
|
|
373
|
+
allow: deserializeAuthExpression(rule.allow),
|
|
374
|
+
deny: rule.deny ? deserializeAuthExpression(rule.deny) : void 0
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (serialized.nodePolicy) {
|
|
379
|
+
auth.nodePolicy = {
|
|
380
|
+
mode: serialized.nodePolicy.mode,
|
|
381
|
+
allow: serialized.nodePolicy.allow
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return auth;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/auth/validate.ts
|
|
388
|
+
var BUILTIN_ROLES = ["owner"];
|
|
389
|
+
function extractRoleRefs(expr) {
|
|
390
|
+
const roles = [];
|
|
391
|
+
function visit(e) {
|
|
392
|
+
switch (e._tag) {
|
|
393
|
+
case "allow":
|
|
394
|
+
case "deny":
|
|
395
|
+
roles.push(...e.roles);
|
|
396
|
+
break;
|
|
397
|
+
case "and":
|
|
398
|
+
case "or":
|
|
399
|
+
for (const sub of e.exprs) visit(sub);
|
|
400
|
+
break;
|
|
401
|
+
case "not":
|
|
402
|
+
visit(e.expr);
|
|
403
|
+
break;
|
|
404
|
+
case "roleRef":
|
|
405
|
+
roles.push(e.role);
|
|
406
|
+
break;
|
|
407
|
+
case "public":
|
|
408
|
+
case "authenticated":
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
visit(expr);
|
|
413
|
+
return roles;
|
|
414
|
+
}
|
|
415
|
+
function countExpressionNodes(expr) {
|
|
416
|
+
let count = 1;
|
|
417
|
+
switch (expr._tag) {
|
|
418
|
+
case "and":
|
|
419
|
+
case "or":
|
|
420
|
+
for (const sub of expr.exprs) {
|
|
421
|
+
count += countExpressionNodes(sub);
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
case "not":
|
|
425
|
+
count += countExpressionNodes(expr.expr);
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
return count;
|
|
429
|
+
}
|
|
430
|
+
function hasPublicAccess(expr) {
|
|
431
|
+
if (expr._tag === "public") return true;
|
|
432
|
+
if (expr._tag === "or") return expr.exprs.some(hasPublicAccess);
|
|
433
|
+
if (expr._tag === "and") return expr.exprs.every(hasPublicAccess);
|
|
434
|
+
if (expr._tag === "not") return false;
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
var MAX_EXPRESSION_NODES = 50;
|
|
438
|
+
function validateAuthorization(auth, properties) {
|
|
439
|
+
const errors = [];
|
|
440
|
+
for (const [actionName, expr] of Object.entries(auth.actions)) {
|
|
441
|
+
const referencedRoles = extractRoleRefs(expr);
|
|
442
|
+
for (const ref of referencedRoles) {
|
|
443
|
+
if (!(ref in auth.roles) && !BUILTIN_ROLES.includes(ref)) {
|
|
444
|
+
errors.push({
|
|
445
|
+
code: "AUTH_SCHEMA_INVALID_ROLE_REF",
|
|
446
|
+
message: `Action '${actionName}' references unknown role '${ref}'`,
|
|
447
|
+
path: `authorization.actions.${actionName}`
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const [roleName, resolver] of Object.entries(auth.roles)) {
|
|
453
|
+
if (resolver._tag === "property") {
|
|
454
|
+
const propDef = properties[resolver.propertyName];
|
|
455
|
+
if (!propDef) {
|
|
456
|
+
errors.push({
|
|
457
|
+
code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
|
|
458
|
+
message: `Role '${roleName}' references non-existent property '${resolver.propertyName}'`,
|
|
459
|
+
path: `authorization.roles.${roleName}`
|
|
460
|
+
});
|
|
461
|
+
} else if (propDef.type !== "person") {
|
|
462
|
+
errors.push({
|
|
463
|
+
code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
|
|
464
|
+
message: `Role '${roleName}' references property '${resolver.propertyName}' which is not a person type (got '${propDef.type}')`,
|
|
465
|
+
path: `authorization.roles.${roleName}`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (resolver._tag === "relation") {
|
|
470
|
+
const propDef = properties[resolver.relationName];
|
|
471
|
+
if (!propDef) {
|
|
472
|
+
errors.push({
|
|
473
|
+
code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
|
|
474
|
+
message: `Role '${roleName}' references non-existent relation '${resolver.relationName}'`,
|
|
475
|
+
path: `authorization.roles.${roleName}`
|
|
476
|
+
});
|
|
477
|
+
} else if (propDef.type !== "relation") {
|
|
478
|
+
errors.push({
|
|
479
|
+
code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
|
|
480
|
+
message: `Role '${roleName}' references property '${resolver.relationName}' which is not a relation type (got '${propDef.type}')`,
|
|
481
|
+
path: `authorization.roles.${roleName}`
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (auth.publicProps) {
|
|
487
|
+
for (const prop of auth.publicProps) {
|
|
488
|
+
if (!(prop in properties)) {
|
|
489
|
+
errors.push({
|
|
490
|
+
code: "AUTH_SCHEMA_INVALID_PUBLIC_PROP",
|
|
491
|
+
message: `publicProps references non-existent property '${prop}'`,
|
|
492
|
+
path: `authorization.publicProps`
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const [actionName, expr] of Object.entries(auth.actions)) {
|
|
498
|
+
const nodeCount = countExpressionNodes(expr);
|
|
499
|
+
if (nodeCount > MAX_EXPRESSION_NODES) {
|
|
500
|
+
errors.push({
|
|
501
|
+
code: "AUTH_SCHEMA_EXPR_LIMIT_EXCEEDED",
|
|
502
|
+
message: `Action '${actionName}' expression has ${nodeCount} nodes (max ${MAX_EXPRESSION_NODES})`,
|
|
503
|
+
path: `authorization.actions.${actionName}`
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (auth.fieldRules) {
|
|
508
|
+
for (const fieldName of Object.keys(auth.fieldRules)) {
|
|
509
|
+
if (!(fieldName in properties)) {
|
|
510
|
+
errors.push({
|
|
511
|
+
code: "AUTH_SCHEMA_INVALID_FIELD_REF",
|
|
512
|
+
message: `fieldRules references non-existent property '${fieldName}'`,
|
|
513
|
+
path: `authorization.fieldRules.${fieldName}`
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const mutationActions = ["write", "delete", "share"];
|
|
519
|
+
for (const actionName of mutationActions) {
|
|
520
|
+
const expr = auth.actions[actionName];
|
|
521
|
+
if (expr && hasPublicAccess(expr)) {
|
|
522
|
+
errors.push({
|
|
523
|
+
code: "AUTH_SCHEMA_UNSAFE_PUBLIC_MUTATION",
|
|
524
|
+
message: `Action '${actionName}' allows PUBLIC access, which is unsafe for mutation operations`,
|
|
525
|
+
path: `authorization.actions.${actionName}`
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return { valid: errors.length === 0, errors };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/auth/evaluator.ts
|
|
533
|
+
var DecisionCache = class {
|
|
534
|
+
cache = /* @__PURE__ */ new Map();
|
|
535
|
+
ttlMs;
|
|
536
|
+
maxSize;
|
|
537
|
+
now;
|
|
538
|
+
constructor(options = {}) {
|
|
539
|
+
this.ttlMs = options.ttlMs ?? 3e4;
|
|
540
|
+
this.maxSize = options.maxSize ?? 1e4;
|
|
541
|
+
this.now = options.now ?? Date.now;
|
|
542
|
+
}
|
|
543
|
+
get(subject, action, nodeId) {
|
|
544
|
+
const key = this.key(subject, action, nodeId);
|
|
545
|
+
const entry = this.cache.get(key);
|
|
546
|
+
if (!entry) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
if (entry.expiresAt <= this.now()) {
|
|
550
|
+
this.cache.delete(key);
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
this.cache.delete(key);
|
|
554
|
+
this.cache.set(key, entry);
|
|
555
|
+
return entry.decision;
|
|
556
|
+
}
|
|
557
|
+
set(subject, action, nodeId, decision) {
|
|
558
|
+
const key = this.key(subject, action, nodeId);
|
|
559
|
+
this.cache.delete(key);
|
|
560
|
+
this.cache.set(key, {
|
|
561
|
+
decision,
|
|
562
|
+
expiresAt: this.now() + this.ttlMs
|
|
563
|
+
});
|
|
564
|
+
if (this.cache.size > this.maxSize) {
|
|
565
|
+
const oldestKey = this.cache.keys().next().value;
|
|
566
|
+
if (oldestKey) {
|
|
567
|
+
this.cache.delete(oldestKey);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
invalidateNode(nodeId) {
|
|
572
|
+
for (const key of this.cache.keys()) {
|
|
573
|
+
if (key.endsWith(`:${nodeId}`)) {
|
|
574
|
+
this.cache.delete(key);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
invalidateSubject(did) {
|
|
579
|
+
for (const key of this.cache.keys()) {
|
|
580
|
+
if (key.startsWith(`${did}:`)) {
|
|
581
|
+
this.cache.delete(key);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
clear() {
|
|
586
|
+
this.cache.clear();
|
|
587
|
+
}
|
|
588
|
+
setTTL(ttlMs) {
|
|
589
|
+
this.ttlMs = Math.max(0, ttlMs);
|
|
590
|
+
}
|
|
591
|
+
key(subject, action, nodeId) {
|
|
592
|
+
return `${subject}:${action}:${nodeId}`;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
var DefaultRoleResolver = class {
|
|
596
|
+
constructor(store, schemaRegistry, options = {}) {
|
|
597
|
+
this.store = store;
|
|
598
|
+
this.schemaRegistry = schemaRegistry;
|
|
599
|
+
this.maxDepth = options.maxDepth ?? 3;
|
|
600
|
+
this.maxNodes = options.maxNodes ?? 100;
|
|
601
|
+
}
|
|
602
|
+
maxDepth;
|
|
603
|
+
maxNodes;
|
|
604
|
+
async resolveRoles(did, node, schema) {
|
|
605
|
+
const roles = /* @__PURE__ */ new Set();
|
|
606
|
+
if (!schema.authorization) {
|
|
607
|
+
return roles;
|
|
608
|
+
}
|
|
609
|
+
const auth = deserializeAuthorization(schema.authorization);
|
|
610
|
+
for (const [roleName, resolver] of Object.entries(auth.roles)) {
|
|
611
|
+
const visited = /* @__PURE__ */ new Set();
|
|
612
|
+
const hasRole = await this.checkRole(did, resolver, node, 0, visited);
|
|
613
|
+
if (hasRole) {
|
|
614
|
+
roles.add(roleName);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return roles;
|
|
618
|
+
}
|
|
619
|
+
async resolveRoleMembers(resolver, node) {
|
|
620
|
+
return this.resolveRoleMembersInner(resolver, node, 0, /* @__PURE__ */ new Set());
|
|
621
|
+
}
|
|
622
|
+
async resolveRoleMembersInner(resolver, node, depth, visited) {
|
|
623
|
+
if (depth > this.maxDepth || visited.size >= this.maxNodes) {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
const visitKey = `${node.id}:${resolver._tag}`;
|
|
627
|
+
if (visited.has(visitKey)) {
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
visited.add(visitKey);
|
|
631
|
+
switch (resolver._tag) {
|
|
632
|
+
case "creator":
|
|
633
|
+
return [node.createdBy];
|
|
634
|
+
case "property":
|
|
635
|
+
return readDidProperty(node.properties[resolver.propertyName]);
|
|
636
|
+
case "relation": {
|
|
637
|
+
const relatedNodes = await this.loadRelatedNodes(node, resolver.relationName);
|
|
638
|
+
if (relatedNodes.length === 0) {
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
const members = /* @__PURE__ */ new Set();
|
|
642
|
+
for (const relatedNode of relatedNodes) {
|
|
643
|
+
const relatedSchema = await this.schemaRegistry.get(relatedNode.schemaId);
|
|
644
|
+
const relatedAuth = relatedSchema?.schema.authorization;
|
|
645
|
+
if (!relatedAuth) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const targetResolver = deserializeAuthorization(relatedAuth).roles[resolver.targetRole];
|
|
649
|
+
if (!targetResolver) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const targetMembers = await this.resolveRoleMembersInner(
|
|
653
|
+
targetResolver,
|
|
654
|
+
relatedNode,
|
|
655
|
+
depth + 1,
|
|
656
|
+
visited
|
|
657
|
+
);
|
|
658
|
+
for (const member of targetMembers) {
|
|
659
|
+
members.add(member);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return [...members];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async checkRole(did, resolver, node, depth, visited) {
|
|
667
|
+
if (depth > this.maxDepth || visited.size >= this.maxNodes) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
const visitKey = `${node.id}:${resolver._tag}:${depth}`;
|
|
671
|
+
if (visited.has(visitKey)) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
visited.add(visitKey);
|
|
675
|
+
switch (resolver._tag) {
|
|
676
|
+
case "creator":
|
|
677
|
+
return did === node.createdBy;
|
|
678
|
+
case "property":
|
|
679
|
+
return readDidProperty(node.properties[resolver.propertyName]).includes(did);
|
|
680
|
+
case "relation": {
|
|
681
|
+
const relatedNodes = await this.loadRelatedNodes(node, resolver.relationName);
|
|
682
|
+
for (const relatedNode of relatedNodes) {
|
|
683
|
+
const relatedSchema = await this.schemaRegistry.get(relatedNode.schemaId);
|
|
684
|
+
const relatedAuth = relatedSchema?.schema.authorization;
|
|
685
|
+
if (!relatedAuth) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const targetResolver = deserializeAuthorization(relatedAuth).roles[resolver.targetRole];
|
|
689
|
+
if (!targetResolver) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const hasRole = await this.checkRole(did, targetResolver, relatedNode, depth + 1, visited);
|
|
693
|
+
if (hasRole) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async loadRelatedNodes(node, relationName) {
|
|
702
|
+
const value = node.properties[relationName];
|
|
703
|
+
const relationIds = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : typeof value === "string" ? [value] : [];
|
|
704
|
+
const nodes = [];
|
|
705
|
+
for (const relationId of relationIds) {
|
|
706
|
+
const relatedNode = await this.store.get(relationId);
|
|
707
|
+
if (relatedNode) {
|
|
708
|
+
nodes.push(relatedNode);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return nodes;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
function evaluateExpression(expr, roles, isAuthenticated) {
|
|
715
|
+
switch (expr._tag) {
|
|
716
|
+
case "allow":
|
|
717
|
+
return expr.roles.some((role2) => roles.has(role2));
|
|
718
|
+
case "deny":
|
|
719
|
+
return expr.roles.some((role2) => roles.has(role2));
|
|
720
|
+
case "and":
|
|
721
|
+
return expr.exprs.every((entry) => evaluateExpression(entry, roles, isAuthenticated));
|
|
722
|
+
case "or":
|
|
723
|
+
return expr.exprs.some((entry) => evaluateExpression(entry, roles, isAuthenticated));
|
|
724
|
+
case "not":
|
|
725
|
+
return !evaluateExpression(expr.expr, roles, isAuthenticated);
|
|
726
|
+
case "roleRef":
|
|
727
|
+
return roles.has(expr.role);
|
|
728
|
+
case "public":
|
|
729
|
+
return true;
|
|
730
|
+
case "authenticated":
|
|
731
|
+
return isAuthenticated;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
var DefaultPolicyEvaluator = class {
|
|
735
|
+
constructor(options) {
|
|
736
|
+
this.options = options;
|
|
737
|
+
this.grantIndex = options.grantIndex;
|
|
738
|
+
this.cache = options.cache ?? new DecisionCache();
|
|
739
|
+
this.roleResolver = options.roleResolver ?? new DefaultRoleResolver(options.store, options.schemaRegistry, {
|
|
740
|
+
maxDepth: options.maxDepth,
|
|
741
|
+
maxNodes: options.maxNodes
|
|
742
|
+
});
|
|
743
|
+
this.now = options.now ?? Date.now;
|
|
744
|
+
this.offlinePolicy = mergeOfflinePolicy(DEFAULT_OFFLINE_POLICY, options.offlinePolicy ?? {});
|
|
745
|
+
this.getLastSyncedAt = options.getLastSyncedAt;
|
|
746
|
+
this.isOnline = options.isOnline ?? (() => true);
|
|
747
|
+
this.onRevalidation = options.onRevalidation;
|
|
748
|
+
this.onDecision = options.onDecision;
|
|
749
|
+
this.lastConnectivityOnline = this.isOnline();
|
|
750
|
+
}
|
|
751
|
+
grantIndex;
|
|
752
|
+
cache;
|
|
753
|
+
roleResolver;
|
|
754
|
+
now;
|
|
755
|
+
offlinePolicy;
|
|
756
|
+
getLastSyncedAt;
|
|
757
|
+
isOnline;
|
|
758
|
+
onRevalidation;
|
|
759
|
+
onDecision;
|
|
760
|
+
lastConnectivityOnline;
|
|
761
|
+
async can(input) {
|
|
762
|
+
const start = performance.now();
|
|
763
|
+
this.cache.setTTL(this.offlinePolicy.decisionCacheTTL);
|
|
764
|
+
if (this.isAuthStateStale()) {
|
|
765
|
+
const decision2 = this.deny(input, ["DENY_STALE_OFFLINE"], start);
|
|
766
|
+
this.emitDecision(decision2);
|
|
767
|
+
return decision2;
|
|
768
|
+
}
|
|
769
|
+
const cached = this.cache.get(input.subject, input.action, input.nodeId);
|
|
770
|
+
if (cached) {
|
|
771
|
+
const decision2 = {
|
|
772
|
+
...cached,
|
|
773
|
+
cached: true,
|
|
774
|
+
duration: performance.now() - start
|
|
775
|
+
};
|
|
776
|
+
this.emitDecision(decision2);
|
|
777
|
+
return decision2;
|
|
778
|
+
}
|
|
779
|
+
const node = input.node ? await this.loadNodeFromInput(input) : await this.options.store.get(input.nodeId);
|
|
780
|
+
if (!node) {
|
|
781
|
+
const decision2 = this.deny(input, ["DENY_NOT_AUTHENTICATED"], start);
|
|
782
|
+
this.emitDecision(decision2);
|
|
783
|
+
return decision2;
|
|
784
|
+
}
|
|
785
|
+
const schema = await this.options.schemaRegistry.get(node.schemaId);
|
|
786
|
+
if (!schema) {
|
|
787
|
+
const decision2 = this.deny(input, ["DENY_NOT_AUTHENTICATED"], start);
|
|
788
|
+
this.emitDecision(decision2);
|
|
789
|
+
return decision2;
|
|
790
|
+
}
|
|
791
|
+
const mode = getAuthMode(schema.schema);
|
|
792
|
+
if (mode === "legacy") {
|
|
793
|
+
const allowed = node.createdBy === input.subject;
|
|
794
|
+
const decision2 = this.decision(input, allowed, allowed ? ["owner"] : [], start);
|
|
795
|
+
this.cache.set(input.subject, input.action, input.nodeId, decision2);
|
|
796
|
+
this.emitDecision(decision2);
|
|
797
|
+
return decision2;
|
|
798
|
+
}
|
|
799
|
+
if (!schema.schema.authorization) {
|
|
800
|
+
const decision2 = this.deny(input, ["DENY_NO_ROLE_MATCH"], start);
|
|
801
|
+
this.emitDecision(decision2);
|
|
802
|
+
return decision2;
|
|
803
|
+
}
|
|
804
|
+
const auth = deserializeAuthorization(schema.schema.authorization);
|
|
805
|
+
const roles = await this.roleResolver.resolveRoles(input.subject, node, schema.schema);
|
|
806
|
+
const fieldRuleDenied = this.checkFieldRules(input, auth.fieldRules, roles);
|
|
807
|
+
if (fieldRuleDenied) {
|
|
808
|
+
const decision2 = this.deny(input, ["DENY_FIELD_RESTRICTED"], start, [...roles]);
|
|
809
|
+
this.emitDecision(decision2);
|
|
810
|
+
return decision2;
|
|
811
|
+
}
|
|
812
|
+
const actionExpr = auth.actions[input.action];
|
|
813
|
+
if (!actionExpr) {
|
|
814
|
+
const decision2 = this.deny(input, ["DENY_NO_ROLE_MATCH"], start, [...roles]);
|
|
815
|
+
this.emitDecision(decision2);
|
|
816
|
+
return decision2;
|
|
817
|
+
}
|
|
818
|
+
if (matchesDenyExpression(actionExpr, roles, true)) {
|
|
819
|
+
const decision2 = this.deny(input, ["DENY_NODE_POLICY"], start, [...roles]);
|
|
820
|
+
this.emitDecision(decision2);
|
|
821
|
+
return decision2;
|
|
822
|
+
}
|
|
823
|
+
const allowedByRole = evaluateExpression(actionExpr, roles, true);
|
|
824
|
+
if (allowedByRole) {
|
|
825
|
+
const decision2 = this.decision(input, true, [...roles], start);
|
|
826
|
+
this.cache.set(input.subject, input.action, input.nodeId, decision2);
|
|
827
|
+
this.emitDecision(decision2);
|
|
828
|
+
return decision2;
|
|
829
|
+
}
|
|
830
|
+
const grant = this.findMatchingGrant(
|
|
831
|
+
input,
|
|
832
|
+
this.grantIndex?.findGrants(input.nodeId, input.subject) ?? []
|
|
833
|
+
);
|
|
834
|
+
if (grant) {
|
|
835
|
+
const decision2 = this.decision(input, true, [...roles], start, [grant.id]);
|
|
836
|
+
this.cache.set(input.subject, input.action, input.nodeId, decision2);
|
|
837
|
+
this.emitDecision(decision2);
|
|
838
|
+
return decision2;
|
|
839
|
+
}
|
|
840
|
+
const reasons = roles.size > 0 ? ["DENY_NO_ROLE_MATCH"] : ["DENY_NO_ROLE_MATCH", "DENY_NO_GRANT"];
|
|
841
|
+
const decision = this.deny(input, reasons, start, [...roles]);
|
|
842
|
+
this.emitDecision(decision);
|
|
843
|
+
return decision;
|
|
844
|
+
}
|
|
845
|
+
async explain(input) {
|
|
846
|
+
const steps = [];
|
|
847
|
+
const start = performance.now();
|
|
848
|
+
const nodeStart = performance.now();
|
|
849
|
+
const node = input.node ? await this.loadNodeFromInput(input) : await this.options.store.get(input.nodeId);
|
|
850
|
+
steps.push({
|
|
851
|
+
phase: "node-deny",
|
|
852
|
+
input: { nodeId: input.nodeId },
|
|
853
|
+
output: { nodeFound: Boolean(node) },
|
|
854
|
+
duration: performance.now() - nodeStart
|
|
855
|
+
});
|
|
856
|
+
let roles = /* @__PURE__ */ new Set();
|
|
857
|
+
let actionExpr;
|
|
858
|
+
if (node) {
|
|
859
|
+
const schema = await this.options.schemaRegistry.get(node.schemaId);
|
|
860
|
+
if (schema?.schema.authorization) {
|
|
861
|
+
const roleStart = performance.now();
|
|
862
|
+
roles = await this.roleResolver.resolveRoles(input.subject, node, schema.schema);
|
|
863
|
+
steps.push({
|
|
864
|
+
phase: "role-resolve",
|
|
865
|
+
input: { subject: input.subject, nodeId: node.id },
|
|
866
|
+
output: { roles: [...roles] },
|
|
867
|
+
duration: performance.now() - roleStart
|
|
868
|
+
});
|
|
869
|
+
const auth = deserializeAuthorization(schema.schema.authorization);
|
|
870
|
+
actionExpr = auth.actions[input.action];
|
|
871
|
+
const evalStart = performance.now();
|
|
872
|
+
const requiredRoles = actionExpr ? extractRoleRefs(actionExpr) : [];
|
|
873
|
+
const allowed = actionExpr ? evaluateExpression(actionExpr, roles, true) : false;
|
|
874
|
+
steps.push({
|
|
875
|
+
phase: "schema-eval",
|
|
876
|
+
input: {
|
|
877
|
+
action: input.action,
|
|
878
|
+
requiredRoles
|
|
879
|
+
},
|
|
880
|
+
output: {
|
|
881
|
+
allowed,
|
|
882
|
+
userRoles: [...roles]
|
|
883
|
+
},
|
|
884
|
+
duration: performance.now() - evalStart
|
|
885
|
+
});
|
|
886
|
+
const grantStart = performance.now();
|
|
887
|
+
const grants = this.grantIndex?.findGrants(input.nodeId, input.subject) ?? [];
|
|
888
|
+
steps.push({
|
|
889
|
+
phase: "grant-check",
|
|
890
|
+
input: {
|
|
891
|
+
subject: input.subject,
|
|
892
|
+
action: input.action,
|
|
893
|
+
nodeId: input.nodeId
|
|
894
|
+
},
|
|
895
|
+
output: {
|
|
896
|
+
grantCount: grants.length,
|
|
897
|
+
grantIds: grants.map((grant) => grant.id)
|
|
898
|
+
},
|
|
899
|
+
duration: performance.now() - grantStart
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const decision = await this.can(input);
|
|
904
|
+
return {
|
|
905
|
+
...decision,
|
|
906
|
+
steps,
|
|
907
|
+
duration: performance.now() - start
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
invalidate(nodeId) {
|
|
911
|
+
this.cache.invalidateNode(nodeId);
|
|
912
|
+
}
|
|
913
|
+
invalidateSubject(did) {
|
|
914
|
+
this.cache.invalidateSubject(did);
|
|
915
|
+
}
|
|
916
|
+
getOfflinePolicy() {
|
|
917
|
+
return { ...this.offlinePolicy };
|
|
918
|
+
}
|
|
919
|
+
setOfflinePolicy(patch) {
|
|
920
|
+
this.offlinePolicy = mergeOfflinePolicy(this.offlinePolicy, patch);
|
|
921
|
+
}
|
|
922
|
+
handleConnectivityChange(isOnline) {
|
|
923
|
+
const cameOnline = !this.lastConnectivityOnline && isOnline;
|
|
924
|
+
this.lastConnectivityOnline = isOnline;
|
|
925
|
+
if (!cameOnline) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (this.offlinePolicy.revalidation === "hybrid") {
|
|
929
|
+
this.cache.clear();
|
|
930
|
+
this.onRevalidation?.({
|
|
931
|
+
type: "hybrid-revalidation",
|
|
932
|
+
at: this.now(),
|
|
933
|
+
invalidated: "all"
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
findMatchingGrant(input, grants) {
|
|
938
|
+
const now = this.now();
|
|
939
|
+
for (const grant of grants) {
|
|
940
|
+
if (!isGrantActive(grant, now)) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const actions = parseActions(grant.properties.actions);
|
|
944
|
+
if (actions.includes(input.action)) {
|
|
945
|
+
return grant;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
checkFieldRules(input, fieldRules, roles) {
|
|
951
|
+
if (input.action !== "write" || !input.patch || !fieldRules) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
for (const fieldName of Object.keys(input.patch)) {
|
|
955
|
+
const fieldRule = fieldRules[fieldName];
|
|
956
|
+
if (!fieldRule) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const denied = fieldRule.deny ? evaluateExpression(fieldRule.deny, roles, true) : false;
|
|
960
|
+
const allowed = evaluateExpression(fieldRule.allow, roles, true);
|
|
961
|
+
if (denied || !allowed) {
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
decision(input, allowed, roles, start, grants = []) {
|
|
968
|
+
return {
|
|
969
|
+
allowed,
|
|
970
|
+
action: input.action,
|
|
971
|
+
subject: input.subject,
|
|
972
|
+
resource: input.nodeId,
|
|
973
|
+
roles,
|
|
974
|
+
grants,
|
|
975
|
+
reasons: [],
|
|
976
|
+
cached: false,
|
|
977
|
+
evaluatedAt: this.now(),
|
|
978
|
+
duration: performance.now() - start
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
deny(input, reasons, start, roles = []) {
|
|
982
|
+
return {
|
|
983
|
+
allowed: false,
|
|
984
|
+
action: input.action,
|
|
985
|
+
subject: input.subject,
|
|
986
|
+
resource: input.nodeId,
|
|
987
|
+
roles,
|
|
988
|
+
grants: [],
|
|
989
|
+
reasons,
|
|
990
|
+
cached: false,
|
|
991
|
+
evaluatedAt: this.now(),
|
|
992
|
+
duration: performance.now() - start
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
async loadNodeFromInput(input) {
|
|
996
|
+
if (!input.node) {
|
|
997
|
+
return this.options.store.get(input.nodeId);
|
|
998
|
+
}
|
|
999
|
+
const existing = await this.options.store.get(input.nodeId);
|
|
1000
|
+
if (existing) {
|
|
1001
|
+
return existing;
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
id: input.nodeId,
|
|
1005
|
+
schemaId: input.node.schemaId,
|
|
1006
|
+
properties: input.node.properties ?? {},
|
|
1007
|
+
timestamps: {},
|
|
1008
|
+
deleted: false,
|
|
1009
|
+
createdAt: this.now(),
|
|
1010
|
+
createdBy: input.node.createdBy,
|
|
1011
|
+
updatedAt: this.now(),
|
|
1012
|
+
updatedBy: input.node.createdBy
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
isAuthStateStale() {
|
|
1016
|
+
if (!this.getLastSyncedAt || !this.isOnline()) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
const lastSyncedAt = this.getLastSyncedAt();
|
|
1020
|
+
if (!Number.isFinite(lastSyncedAt) || lastSyncedAt <= 0) {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
return this.now() - lastSyncedAt > this.offlinePolicy.maxStaleness;
|
|
1024
|
+
}
|
|
1025
|
+
emitDecision(decision) {
|
|
1026
|
+
this.onDecision?.({
|
|
1027
|
+
type: "auth:decision",
|
|
1028
|
+
timestamp: this.now(),
|
|
1029
|
+
subject: decision.subject,
|
|
1030
|
+
action: decision.action,
|
|
1031
|
+
resource: decision.resource,
|
|
1032
|
+
allowed: decision.allowed,
|
|
1033
|
+
cached: decision.cached,
|
|
1034
|
+
roles: decision.roles,
|
|
1035
|
+
reasons: decision.reasons,
|
|
1036
|
+
duration: decision.duration
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
function matchesDenyExpression(expr, roles, isAuthenticated) {
|
|
1041
|
+
switch (expr._tag) {
|
|
1042
|
+
case "deny":
|
|
1043
|
+
return evaluateExpression(expr, roles, isAuthenticated);
|
|
1044
|
+
case "and":
|
|
1045
|
+
case "or":
|
|
1046
|
+
return expr.exprs.some((entry) => matchesDenyExpression(entry, roles, isAuthenticated));
|
|
1047
|
+
case "not":
|
|
1048
|
+
return matchesDenyExpression(expr.expr, roles, isAuthenticated);
|
|
1049
|
+
default:
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function readDidProperty(value) {
|
|
1054
|
+
if (Array.isArray(value)) {
|
|
1055
|
+
return value.filter(isDid);
|
|
1056
|
+
}
|
|
1057
|
+
if (isDid(value)) {
|
|
1058
|
+
return [value];
|
|
1059
|
+
}
|
|
1060
|
+
return [];
|
|
1061
|
+
}
|
|
1062
|
+
function isDid(value) {
|
|
1063
|
+
return typeof value === "string" && value.startsWith("did:key:");
|
|
1064
|
+
}
|
|
1065
|
+
function parseActions(value) {
|
|
1066
|
+
if (Array.isArray(value)) {
|
|
1067
|
+
return value.filter((entry) => typeof entry === "string");
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof value !== "string") {
|
|
1070
|
+
return [];
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const parsed = JSON.parse(value);
|
|
1074
|
+
if (!Array.isArray(parsed)) {
|
|
1075
|
+
return [];
|
|
1076
|
+
}
|
|
1077
|
+
return parsed.filter((entry) => typeof entry === "string");
|
|
1078
|
+
} catch {
|
|
1079
|
+
return [];
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/auth/grant-expiration-cleaner.ts
|
|
1084
|
+
var GrantExpirationCleaner = class _GrantExpirationCleaner {
|
|
1085
|
+
constructor(store, options = {}) {
|
|
1086
|
+
this.store = store;
|
|
1087
|
+
this.clock = options.clock ?? Date.now;
|
|
1088
|
+
this.cleanupIntervalMs = options.cleanupIntervalMs ?? _GrantExpirationCleaner.DEFAULT_CLEANUP_INTERVAL_MS;
|
|
1089
|
+
this.clockSkewToleranceMs = options.clockSkewToleranceMs ?? _GrantExpirationCleaner.DEFAULT_CLOCK_SKEW_TOLERANCE_MS;
|
|
1090
|
+
this.systemRevoker = options.systemRevoker ?? "SYSTEM";
|
|
1091
|
+
}
|
|
1092
|
+
static DEFAULT_CLOCK_SKEW_TOLERANCE_MS = 6e4;
|
|
1093
|
+
static DEFAULT_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
1094
|
+
clock;
|
|
1095
|
+
cleanupIntervalMs;
|
|
1096
|
+
clockSkewToleranceMs;
|
|
1097
|
+
systemRevoker;
|
|
1098
|
+
timer = null;
|
|
1099
|
+
start() {
|
|
1100
|
+
if (this.timer) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
this.timer = setInterval(() => {
|
|
1104
|
+
void this.cleanup();
|
|
1105
|
+
}, this.cleanupIntervalMs);
|
|
1106
|
+
}
|
|
1107
|
+
stop() {
|
|
1108
|
+
if (!this.timer) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
clearInterval(this.timer);
|
|
1112
|
+
this.timer = null;
|
|
1113
|
+
}
|
|
1114
|
+
async cleanup() {
|
|
1115
|
+
const now = this.clock() - this.clockSkewToleranceMs;
|
|
1116
|
+
const grants = await this.store.list({
|
|
1117
|
+
schemaId: GRANT_SCHEMA_IRI,
|
|
1118
|
+
includeDeleted: false
|
|
1119
|
+
});
|
|
1120
|
+
let pruned = 0;
|
|
1121
|
+
for (const grant of grants) {
|
|
1122
|
+
const expiresAt = asNumber2(grant.properties.expiresAt);
|
|
1123
|
+
const revokedAt = asNumber2(grant.properties.revokedAt);
|
|
1124
|
+
if (revokedAt > 0) {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (expiresAt > 0 && expiresAt < now) {
|
|
1128
|
+
await this.store.update(grant.id, {
|
|
1129
|
+
properties: {
|
|
1130
|
+
revokedAt: now,
|
|
1131
|
+
revokedBy: this.systemRevoker
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
pruned += 1;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return { pruned };
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
function asNumber2(value) {
|
|
1141
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/auth/grant-rate-limit.ts
|
|
1145
|
+
var GrantRateLimiter = class {
|
|
1146
|
+
limitPerMinute;
|
|
1147
|
+
windowMs;
|
|
1148
|
+
now;
|
|
1149
|
+
attemptsByPeer = /* @__PURE__ */ new Map();
|
|
1150
|
+
constructor(options = {}) {
|
|
1151
|
+
this.limitPerMinute = options.limitPerMinute ?? 10;
|
|
1152
|
+
this.windowMs = options.windowMs ?? 6e4;
|
|
1153
|
+
this.now = options.now ?? Date.now;
|
|
1154
|
+
}
|
|
1155
|
+
allow(peerDid) {
|
|
1156
|
+
const cutoff = this.now() - this.windowMs;
|
|
1157
|
+
const attempts = this.attemptsByPeer.get(peerDid) ?? [];
|
|
1158
|
+
const recent = attempts.filter((timestamp) => timestamp > cutoff);
|
|
1159
|
+
if (recent.length >= this.limitPerMinute) {
|
|
1160
|
+
this.attemptsByPeer.set(peerDid, recent);
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
recent.push(this.now());
|
|
1164
|
+
this.attemptsByPeer.set(peerDid, recent);
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
reset(peerDid) {
|
|
1168
|
+
if (peerDid) {
|
|
1169
|
+
this.attemptsByPeer.delete(peerDid);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
this.attemptsByPeer.clear();
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
// src/auth/store-auth.ts
|
|
1177
|
+
import { createUCAN } from "@xnetjs/identity";
|
|
1178
|
+
var MAX_PROOF_DEPTH = 4;
|
|
1179
|
+
var StoreAuthError = class extends Error {
|
|
1180
|
+
code;
|
|
1181
|
+
constructor(code, message) {
|
|
1182
|
+
super(message);
|
|
1183
|
+
this.name = "StoreAuthError";
|
|
1184
|
+
this.code = code;
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
var StoreAuth = class {
|
|
1188
|
+
constructor(options) {
|
|
1189
|
+
this.options = options;
|
|
1190
|
+
this.rateLimiter = options.rateLimiter ?? new GrantRateLimiter();
|
|
1191
|
+
this.now = options.now ?? Date.now;
|
|
1192
|
+
this.maxProofDepth = options.maxProofDepth ?? MAX_PROOF_DEPTH;
|
|
1193
|
+
this.offlinePolicy = { ...DEFAULT_OFFLINE_POLICY };
|
|
1194
|
+
}
|
|
1195
|
+
rateLimiter;
|
|
1196
|
+
now;
|
|
1197
|
+
maxProofDepth;
|
|
1198
|
+
offlinePolicy;
|
|
1199
|
+
async can(input) {
|
|
1200
|
+
return this.options.evaluator.can({
|
|
1201
|
+
subject: this.options.actorDid,
|
|
1202
|
+
action: input.action,
|
|
1203
|
+
nodeId: input.nodeId,
|
|
1204
|
+
patch: input.patch
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
async explain(input) {
|
|
1208
|
+
return this.options.evaluator.explain({
|
|
1209
|
+
subject: this.options.actorDid,
|
|
1210
|
+
action: input.action,
|
|
1211
|
+
nodeId: input.nodeId
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
async grant(input) {
|
|
1215
|
+
if (input.to === this.options.actorDid) {
|
|
1216
|
+
throw new Error("Cannot grant access to yourself");
|
|
1217
|
+
}
|
|
1218
|
+
if (!this.rateLimiter.allow(this.options.actorDid)) {
|
|
1219
|
+
throw new StoreAuthError("AUTH_RATE_LIMIT_EXCEEDED", "Grant rate limit exceeded");
|
|
1220
|
+
}
|
|
1221
|
+
const canShare = await this.options.evaluator.can({
|
|
1222
|
+
subject: this.options.actorDid,
|
|
1223
|
+
action: "share",
|
|
1224
|
+
nodeId: input.resource
|
|
1225
|
+
});
|
|
1226
|
+
if (!canShare.allowed) {
|
|
1227
|
+
throw new StoreAuthError(
|
|
1228
|
+
"AUTH_PERMISSION_DENIED",
|
|
1229
|
+
"Permission denied: cannot share this resource"
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
for (const action of input.actions) {
|
|
1233
|
+
const canAction = await this.options.evaluator.can({
|
|
1234
|
+
subject: this.options.actorDid,
|
|
1235
|
+
action,
|
|
1236
|
+
nodeId: input.resource
|
|
1237
|
+
});
|
|
1238
|
+
if (!canAction.allowed) {
|
|
1239
|
+
throw new StoreAuthError(
|
|
1240
|
+
"AUTH_PERMISSION_DENIED",
|
|
1241
|
+
`Permission denied: cannot delegate action '${action}'`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const resourceNode = await this.options.store.get(input.resource);
|
|
1246
|
+
if (!resourceNode) {
|
|
1247
|
+
throw new Error(`Resource not found: ${input.resource}`);
|
|
1248
|
+
}
|
|
1249
|
+
const parentGrant = input.parentGrantId ? await this.getGrantNodeOrThrow(input.parentGrantId) : null;
|
|
1250
|
+
const proofDepth = this.computeProofDepth(parentGrant);
|
|
1251
|
+
if (proofDepth > this.maxProofDepth) {
|
|
1252
|
+
throw new StoreAuthError(
|
|
1253
|
+
"AUTH_DELEGATION_DEPTH_EXCEEDED",
|
|
1254
|
+
`Delegation proof depth exceeds max ${this.maxProofDepth}`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const expiresAt = this.computeExpiration(input.expiresIn);
|
|
1258
|
+
this.assertDelegationAttenuation(input, parentGrant, expiresAt);
|
|
1259
|
+
const ucanToken = this.createDelegationUCAN({
|
|
1260
|
+
...input,
|
|
1261
|
+
expiresAt,
|
|
1262
|
+
parentUcanToken: parseString(parentGrant?.properties.ucanToken) ?? void 0
|
|
1263
|
+
});
|
|
1264
|
+
const created2 = await this.options.store.create({
|
|
1265
|
+
schemaId: GRANT_SCHEMA_IRI,
|
|
1266
|
+
properties: {
|
|
1267
|
+
issuer: this.options.actorDid,
|
|
1268
|
+
grantee: input.to,
|
|
1269
|
+
resource: input.resource,
|
|
1270
|
+
resourceSchema: resourceNode.schemaId,
|
|
1271
|
+
actions: JSON.stringify(input.actions),
|
|
1272
|
+
expiresAt,
|
|
1273
|
+
revokedAt: 0,
|
|
1274
|
+
revokedBy: "",
|
|
1275
|
+
ucanToken,
|
|
1276
|
+
proofDepth,
|
|
1277
|
+
parentGrantId: parentGrant?.id ?? ""
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
if (this.options.keyManager && this.options.publicKeyResolver) {
|
|
1281
|
+
const contentKey = await this.options.keyManager.getContentKey(input.resource);
|
|
1282
|
+
const granteePublicKey = await this.options.publicKeyResolver.resolve(input.to);
|
|
1283
|
+
if (!granteePublicKey) {
|
|
1284
|
+
throw new Error(`Cannot resolve public key for ${input.to}`);
|
|
1285
|
+
}
|
|
1286
|
+
await this.options.keyManager.addRecipient({
|
|
1287
|
+
resourceId: input.resource,
|
|
1288
|
+
recipient: input.to,
|
|
1289
|
+
contentKey,
|
|
1290
|
+
recipientPublicKey: granteePublicKey
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
this.options.evaluator.invalidateSubject(input.to);
|
|
1294
|
+
this.options.evaluator.invalidate(input.resource);
|
|
1295
|
+
const node = await this.getGrantNodeOrThrow(created2.id);
|
|
1296
|
+
return toGrant(node);
|
|
1297
|
+
}
|
|
1298
|
+
async revoke(input) {
|
|
1299
|
+
const grantNode = await this.getGrantNodeOrThrow(input.grantId);
|
|
1300
|
+
const grant = toGrant(grantNode);
|
|
1301
|
+
const canRevoke = grant.issuer === this.options.actorDid || (await this.options.evaluator.can({
|
|
1302
|
+
subject: this.options.actorDid,
|
|
1303
|
+
action: "share",
|
|
1304
|
+
nodeId: grant.resource
|
|
1305
|
+
})).allowed;
|
|
1306
|
+
if (!canRevoke) {
|
|
1307
|
+
throw new Error("Permission denied: cannot revoke this grant");
|
|
1308
|
+
}
|
|
1309
|
+
await this.validateRevocation(grantNode);
|
|
1310
|
+
const revokedAt = this.now();
|
|
1311
|
+
await this.options.store.update(input.grantId, {
|
|
1312
|
+
properties: {
|
|
1313
|
+
revokedAt,
|
|
1314
|
+
revokedBy: this.options.actorDid
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
await this.cascadeRevocation(input.grantId);
|
|
1318
|
+
if (this.options.keyManager) {
|
|
1319
|
+
await this.options.keyManager.rotateContentKey(grant.resource, grant.grantee);
|
|
1320
|
+
}
|
|
1321
|
+
this.options.evaluator.invalidateSubject(grant.grantee);
|
|
1322
|
+
this.options.evaluator.invalidate(grant.resource);
|
|
1323
|
+
}
|
|
1324
|
+
async listGrants(input) {
|
|
1325
|
+
const status = input.status ?? "all";
|
|
1326
|
+
const grants = await this.loadGrantNodes();
|
|
1327
|
+
return grants.filter((grant) => parseString(grant.properties.resource) === input.nodeId).map(toGrant).filter((grant) => this.matchesStatus(grant, status));
|
|
1328
|
+
}
|
|
1329
|
+
async listIssuedGrants() {
|
|
1330
|
+
const grants = await this.loadGrantNodes();
|
|
1331
|
+
return grants.map(toGrant).filter((grant) => grant.issuer === this.options.actorDid);
|
|
1332
|
+
}
|
|
1333
|
+
async listReceivedGrants() {
|
|
1334
|
+
const grants = await this.loadGrantNodes();
|
|
1335
|
+
return grants.map(toGrant).filter((grant) => grant.grantee === this.options.actorDid);
|
|
1336
|
+
}
|
|
1337
|
+
getOfflinePolicy() {
|
|
1338
|
+
return { ...this.offlinePolicy };
|
|
1339
|
+
}
|
|
1340
|
+
setOfflinePolicy(policy) {
|
|
1341
|
+
this.offlinePolicy = mergeOfflinePolicy(this.offlinePolicy, policy);
|
|
1342
|
+
const evaluator = this.options.evaluator;
|
|
1343
|
+
evaluator.setOfflinePolicy?.(policy);
|
|
1344
|
+
}
|
|
1345
|
+
matchesStatus(grant, status) {
|
|
1346
|
+
if (status === "all") {
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
const now = this.now();
|
|
1350
|
+
const revoked = grant.revokedAt > 0;
|
|
1351
|
+
const expired = !revoked && grant.expiresAt > 0 && grant.expiresAt <= now;
|
|
1352
|
+
if (status === "active") {
|
|
1353
|
+
return !revoked && !expired;
|
|
1354
|
+
}
|
|
1355
|
+
if (status === "expired") {
|
|
1356
|
+
return expired;
|
|
1357
|
+
}
|
|
1358
|
+
return revoked;
|
|
1359
|
+
}
|
|
1360
|
+
async validateRevocation(grant) {
|
|
1361
|
+
const resource = parseString(grant.properties.resource);
|
|
1362
|
+
const grantee = parseDid(grant.properties.grantee);
|
|
1363
|
+
if (!resource || !grantee) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const resourceNode = await this.options.store.get(resource);
|
|
1367
|
+
const shareHolders = /* @__PURE__ */ new Set();
|
|
1368
|
+
if (resourceNode) {
|
|
1369
|
+
shareHolders.add(resourceNode.createdBy);
|
|
1370
|
+
}
|
|
1371
|
+
const grants = this.options.grantIndex ? this.options.grantIndex.findAllGrantsForResource(resource) : (await this.loadGrantNodes()).filter(
|
|
1372
|
+
(entry) => parseString(entry.properties.resource) === resource
|
|
1373
|
+
);
|
|
1374
|
+
for (const entry of grants) {
|
|
1375
|
+
if (entry.id === grant.id) {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const grantActions = parseActions2(entry.properties.actions);
|
|
1379
|
+
if (!grantActions.includes("share") || !isGrantActive(entry, this.now())) {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const holderDid = parseDid(entry.properties.grantee);
|
|
1383
|
+
if (holderDid) {
|
|
1384
|
+
shareHolders.add(holderDid);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
shareHolders.delete(grantee);
|
|
1388
|
+
if (shareHolders.size === 0) {
|
|
1389
|
+
throw new Error(
|
|
1390
|
+
`Cannot revoke: this would leave zero users with 'share' access on ${resource}.`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
assertDelegationAttenuation(input, parentGrant, expiresAt) {
|
|
1395
|
+
if (!parentGrant) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const parentActions = parseActions2(parentGrant.properties.actions);
|
|
1399
|
+
for (const action of input.actions) {
|
|
1400
|
+
if (!parentActions.includes(action)) {
|
|
1401
|
+
throw new StoreAuthError(
|
|
1402
|
+
"AUTH_DELEGATION_ESCALATION",
|
|
1403
|
+
`Delegation escalation blocked for action '${action}'`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const parentExpiresAt = parseNumber(parentGrant.properties.expiresAt);
|
|
1408
|
+
if (parentExpiresAt > 0 && expiresAt > 0 && expiresAt > parentExpiresAt) {
|
|
1409
|
+
throw new StoreAuthError(
|
|
1410
|
+
"AUTH_DELEGATION_ESCALATION",
|
|
1411
|
+
"Delegation expiration exceeds parent grant expiration"
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
computeProofDepth(parentGrant) {
|
|
1416
|
+
if (!parentGrant) {
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
return parseNumber(parentGrant.properties.proofDepth) + 1;
|
|
1420
|
+
}
|
|
1421
|
+
async cascadeRevocation(revokedGrantId) {
|
|
1422
|
+
const now = this.now();
|
|
1423
|
+
const grants = await this.loadGrantNodes();
|
|
1424
|
+
const queue = [revokedGrantId];
|
|
1425
|
+
const visited = new Set(queue);
|
|
1426
|
+
while (queue.length > 0) {
|
|
1427
|
+
const parentId = queue.shift();
|
|
1428
|
+
if (!parentId) {
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
for (const grant of grants) {
|
|
1432
|
+
const parentGrantId = parseString(grant.properties.parentGrantId);
|
|
1433
|
+
if (!parentGrantId || parentGrantId !== parentId) {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (!isGrantActive(grant, now) || visited.has(grant.id)) {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
await this.options.store.update(grant.id, {
|
|
1440
|
+
properties: {
|
|
1441
|
+
revokedAt: now,
|
|
1442
|
+
revokedBy: this.options.actorDid
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
const childGrantee = parseDid(grant.properties.grantee);
|
|
1446
|
+
const childResource = parseString(grant.properties.resource);
|
|
1447
|
+
if (childGrantee && childResource) {
|
|
1448
|
+
this.options.evaluator.invalidateSubject(childGrantee);
|
|
1449
|
+
this.options.evaluator.invalidate(childResource);
|
|
1450
|
+
}
|
|
1451
|
+
queue.push(grant.id);
|
|
1452
|
+
visited.add(grant.id);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
async getGrantNodeOrThrow(grantId) {
|
|
1457
|
+
const node = await this.options.store.get(grantId);
|
|
1458
|
+
if (!node || !isGrantSchema(node.schemaId)) {
|
|
1459
|
+
throw new Error(`Grant not found: ${grantId}`);
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
id: node.id,
|
|
1463
|
+
properties: node.properties,
|
|
1464
|
+
deleted: false
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
async loadGrantNodes() {
|
|
1468
|
+
const nodes = await this.options.store.list({
|
|
1469
|
+
schemaId: GRANT_SCHEMA_IRI,
|
|
1470
|
+
includeDeleted: false
|
|
1471
|
+
});
|
|
1472
|
+
return nodes.filter((node) => isGrantSchema(node.schemaId)).map((node) => ({
|
|
1473
|
+
id: node.id,
|
|
1474
|
+
properties: node.properties,
|
|
1475
|
+
deleted: false
|
|
1476
|
+
}));
|
|
1477
|
+
}
|
|
1478
|
+
createDelegationUCAN(input) {
|
|
1479
|
+
return createUCAN({
|
|
1480
|
+
issuer: this.options.actorDid,
|
|
1481
|
+
issuerKey: this.options.signingKey,
|
|
1482
|
+
audience: input.to,
|
|
1483
|
+
capabilities: input.actions.map((action) => ({
|
|
1484
|
+
with: `xnet://${this.options.actorDid}/node/${input.resource}`,
|
|
1485
|
+
can: `xnet/${action}`
|
|
1486
|
+
})),
|
|
1487
|
+
expiration: Math.floor(input.expiresAt / 1e3),
|
|
1488
|
+
proofs: input.parentUcanToken ? [input.parentUcanToken] : []
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
computeExpiration(expiresIn) {
|
|
1492
|
+
if (typeof expiresIn === "number") {
|
|
1493
|
+
return expiresIn;
|
|
1494
|
+
}
|
|
1495
|
+
if (typeof expiresIn === "string") {
|
|
1496
|
+
const parsed = parseDuration(expiresIn);
|
|
1497
|
+
if (parsed !== null) {
|
|
1498
|
+
return this.now() + parsed;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return this.now() + 7 * 24 * 60 * 60 * 1e3;
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
function toGrant(node) {
|
|
1505
|
+
return {
|
|
1506
|
+
id: node.id,
|
|
1507
|
+
issuer: parseDid(node.properties.issuer) ?? "did:key:unknown",
|
|
1508
|
+
grantee: parseDid(node.properties.grantee) ?? "did:key:unknown",
|
|
1509
|
+
resource: parseString(node.properties.resource) ?? "",
|
|
1510
|
+
resourceSchema: parseString(node.properties.resourceSchema) ?? "",
|
|
1511
|
+
actions: parseActions2(node.properties.actions),
|
|
1512
|
+
expiresAt: parseNumber(node.properties.expiresAt),
|
|
1513
|
+
revokedAt: parseNumber(node.properties.revokedAt),
|
|
1514
|
+
revokedBy: parseDid(node.properties.revokedBy) ?? void 0,
|
|
1515
|
+
ucanToken: parseString(node.properties.ucanToken) ?? void 0,
|
|
1516
|
+
proofDepth: parseNumber(node.properties.proofDepth),
|
|
1517
|
+
parentGrantId: parseString(node.properties.parentGrantId) ?? void 0
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function isGrantSchema(schemaId) {
|
|
1521
|
+
return schemaId === GRANT_SCHEMA_IRI || schemaId.startsWith(`${GRANT_SCHEMA_IRI}@`);
|
|
1522
|
+
}
|
|
1523
|
+
function parseActions2(value) {
|
|
1524
|
+
if (Array.isArray(value)) {
|
|
1525
|
+
return value.filter(isAuthAction);
|
|
1526
|
+
}
|
|
1527
|
+
if (typeof value !== "string") {
|
|
1528
|
+
return [];
|
|
1529
|
+
}
|
|
1530
|
+
try {
|
|
1531
|
+
const parsed = JSON.parse(value);
|
|
1532
|
+
if (!Array.isArray(parsed)) {
|
|
1533
|
+
return [];
|
|
1534
|
+
}
|
|
1535
|
+
return parsed.filter(isAuthAction);
|
|
1536
|
+
} catch {
|
|
1537
|
+
return [];
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function isAuthAction(value) {
|
|
1541
|
+
return value === "read" || value === "write" || value === "delete" || value === "share" || value === "admin";
|
|
1542
|
+
}
|
|
1543
|
+
function parseDid(value) {
|
|
1544
|
+
if (typeof value === "string" && value.startsWith("did:key:")) {
|
|
1545
|
+
return value;
|
|
1546
|
+
}
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
function parseString(value) {
|
|
1550
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1551
|
+
}
|
|
1552
|
+
function parseNumber(value) {
|
|
1553
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1554
|
+
}
|
|
1555
|
+
function parseDuration(value) {
|
|
1556
|
+
const match = value.match(/^(\d+)([smhdw])$/);
|
|
1557
|
+
if (!match) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
const amount = Number(match[1]);
|
|
1561
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
const unit = match[2];
|
|
1565
|
+
switch (unit) {
|
|
1566
|
+
case "s":
|
|
1567
|
+
return amount * 1e3;
|
|
1568
|
+
case "m":
|
|
1569
|
+
return amount * 60 * 1e3;
|
|
1570
|
+
case "h":
|
|
1571
|
+
return amount * 60 * 60 * 1e3;
|
|
1572
|
+
case "d":
|
|
1573
|
+
return amount * 24 * 60 * 60 * 1e3;
|
|
1574
|
+
case "w":
|
|
1575
|
+
return amount * 7 * 24 * 60 * 60 * 1e3;
|
|
1576
|
+
default:
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/auth/index.ts
|
|
1582
|
+
import { AUTH_ACTIONS } from "@xnetjs/core";
|
|
1583
|
+
|
|
1584
|
+
// src/auth/builders.ts
|
|
1585
|
+
var role = {
|
|
1586
|
+
/**
|
|
1587
|
+
* Role held by the node's creator.
|
|
1588
|
+
* The DID in `createdBy` holds this role.
|
|
1589
|
+
*/
|
|
1590
|
+
creator() {
|
|
1591
|
+
return { _tag: "creator" };
|
|
1592
|
+
},
|
|
1593
|
+
/**
|
|
1594
|
+
* Role determined by a person property on the node.
|
|
1595
|
+
* The DID(s) in that property hold this role.
|
|
1596
|
+
*
|
|
1597
|
+
* @param propertyName - Name of the person/person[] property
|
|
1598
|
+
*/
|
|
1599
|
+
property(propertyName) {
|
|
1600
|
+
return { _tag: "property", propertyName };
|
|
1601
|
+
},
|
|
1602
|
+
/**
|
|
1603
|
+
* Role inherited from a related node.
|
|
1604
|
+
* Users who hold `targetRole` on the related node hold this role.
|
|
1605
|
+
*
|
|
1606
|
+
* @param relationName - Name of the relation property
|
|
1607
|
+
* @param targetRole - Role to check on the related node
|
|
1608
|
+
*/
|
|
1609
|
+
relation(relationName, targetRole) {
|
|
1610
|
+
return { _tag: "relation", relationName, targetRole };
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var relation = role.relation;
|
|
1614
|
+
|
|
1615
|
+
// src/auth/recipients.ts
|
|
1616
|
+
var PUBLIC_CONTENT_KEY = new Uint8Array(32);
|
|
1617
|
+
|
|
1618
|
+
// src/schema/define.ts
|
|
1619
|
+
var DEFAULT_SCHEMA_VERSION2 = "1.0.0";
|
|
1620
|
+
function defineSchema(options) {
|
|
1621
|
+
const version = options.version ?? DEFAULT_SCHEMA_VERSION2;
|
|
1622
|
+
const schemaId = `${options.namespace}${options.name}@${version}`;
|
|
1623
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1624
|
+
const REF_NAME_PATTERN = /(?:^target$|^inReplyTo$|^replyTo[A-Z].*(?:Id|Node)$|(?:parent|node|comment|task|page|database)(?:Id|Ref)$)/;
|
|
1625
|
+
for (const [name, builder] of Object.entries(options.properties)) {
|
|
1626
|
+
if (builder.definition.type === "text" && REF_NAME_PATTERN.test(name)) {
|
|
1627
|
+
console.warn(
|
|
1628
|
+
`[xNet Schema] "${options.name}.${name}" is a text() property whose name suggests it stores a reference. Consider using relation() for node references or person() for DIDs.`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const properties = Object.entries(options.properties).map(
|
|
1634
|
+
([name, builder]) => ({
|
|
1635
|
+
"@id": `${schemaId}#${name}`,
|
|
1636
|
+
name,
|
|
1637
|
+
...builder.definition
|
|
1638
|
+
})
|
|
1639
|
+
);
|
|
1640
|
+
const propertyMap = Object.fromEntries(properties.map((property) => [property.name, property]));
|
|
1641
|
+
if (options.authorization) {
|
|
1642
|
+
const authResult = validateAuthorization(options.authorization, propertyMap);
|
|
1643
|
+
if (!authResult.valid) {
|
|
1644
|
+
const message = authResult.errors.map((error) => `${error.path}: [${error.code}] ${error.message}`).join(", ");
|
|
1645
|
+
throw new Error(`Invalid authorization in schema '${options.name}': ${message}`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const schema = {
|
|
1649
|
+
"@id": schemaId,
|
|
1650
|
+
"@type": "xnet://xnet.fyi/Schema",
|
|
1651
|
+
name: options.name,
|
|
1652
|
+
namespace: options.namespace,
|
|
1653
|
+
version,
|
|
1654
|
+
migrateFrom: options.migrateFrom,
|
|
1655
|
+
properties,
|
|
1656
|
+
extends: options.extends?.schema["@id"],
|
|
1657
|
+
document: options.document,
|
|
1658
|
+
authorization: options.authorization ? serializeAuthorization(options.authorization) : void 0
|
|
1659
|
+
};
|
|
1660
|
+
function validate(node) {
|
|
1661
|
+
const errors = [];
|
|
1662
|
+
if (typeof node !== "object" || node === null) {
|
|
1663
|
+
return { valid: false, errors: [{ path: "", message: "Node must be an object" }] };
|
|
1664
|
+
}
|
|
1665
|
+
const obj = node;
|
|
1666
|
+
if (typeof obj.id !== "string") {
|
|
1667
|
+
errors.push({ path: "id", message: "id is required and must be a string" });
|
|
1668
|
+
}
|
|
1669
|
+
if (obj.schemaId !== schemaId) {
|
|
1670
|
+
errors.push({
|
|
1671
|
+
path: "schemaId",
|
|
1672
|
+
message: `schemaId must be '${schemaId}'`,
|
|
1673
|
+
value: obj.schemaId
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
if (typeof obj.createdAt !== "number") {
|
|
1677
|
+
errors.push({ path: "createdAt", message: "createdAt is required and must be a number" });
|
|
1678
|
+
}
|
|
1679
|
+
if (typeof obj.createdBy !== "string" || !obj.createdBy.startsWith("did:key:")) {
|
|
1680
|
+
errors.push({ path: "createdBy", message: "createdBy must be a valid DID" });
|
|
1681
|
+
}
|
|
1682
|
+
for (const [name, builder] of Object.entries(options.properties)) {
|
|
1683
|
+
const value = obj[name];
|
|
1684
|
+
const isRequired = builder.definition.required;
|
|
1685
|
+
if (value === void 0 || value === null) {
|
|
1686
|
+
if (isRequired) {
|
|
1687
|
+
errors.push({ path: name, message: `${name} is required` });
|
|
1688
|
+
}
|
|
1689
|
+
} else if (!builder.validate(value)) {
|
|
1690
|
+
errors.push({ path: name, message: `${name} has invalid value`, value });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return { valid: errors.length === 0, errors };
|
|
1694
|
+
}
|
|
1695
|
+
function create(props, createOptions) {
|
|
1696
|
+
const now = createOptions.createdAt ?? Date.now();
|
|
1697
|
+
const id = createOptions.id ?? createNodeId();
|
|
1698
|
+
const node = {
|
|
1699
|
+
id,
|
|
1700
|
+
schemaId,
|
|
1701
|
+
createdAt: now,
|
|
1702
|
+
createdBy: createOptions.createdBy
|
|
1703
|
+
};
|
|
1704
|
+
for (const [name, builder] of Object.entries(options.properties)) {
|
|
1705
|
+
const rawValue = props[name];
|
|
1706
|
+
const coerced = builder.coerce(rawValue);
|
|
1707
|
+
if (coerced !== null) {
|
|
1708
|
+
node[name] = coerced;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return node;
|
|
1712
|
+
}
|
|
1713
|
+
function is(node) {
|
|
1714
|
+
return node.schemaId === schemaId;
|
|
1715
|
+
}
|
|
1716
|
+
return {
|
|
1717
|
+
schema,
|
|
1718
|
+
validate,
|
|
1719
|
+
create,
|
|
1720
|
+
is,
|
|
1721
|
+
_schemaId: schemaId,
|
|
1722
|
+
_properties: options.properties
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/schema/properties/text.ts
|
|
1727
|
+
function text(options = {}) {
|
|
1728
|
+
return {
|
|
1729
|
+
definition: {
|
|
1730
|
+
type: "text",
|
|
1731
|
+
required: options.required ?? false,
|
|
1732
|
+
config: {
|
|
1733
|
+
minLength: options.minLength,
|
|
1734
|
+
maxLength: options.maxLength,
|
|
1735
|
+
pattern: options.pattern?.source,
|
|
1736
|
+
placeholder: options.placeholder
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
validate(value) {
|
|
1740
|
+
if (value === null || value === void 0) {
|
|
1741
|
+
return !options.required;
|
|
1742
|
+
}
|
|
1743
|
+
if (typeof value !== "string") return false;
|
|
1744
|
+
if (options.minLength !== void 0 && value.length < options.minLength) return false;
|
|
1745
|
+
if (options.maxLength !== void 0 && value.length > options.maxLength) return false;
|
|
1746
|
+
if (options.pattern && !options.pattern.test(value)) return false;
|
|
1747
|
+
return true;
|
|
1748
|
+
},
|
|
1749
|
+
coerce(value) {
|
|
1750
|
+
if (value === null || value === void 0) return null;
|
|
1751
|
+
return String(value);
|
|
1752
|
+
},
|
|
1753
|
+
_type: ""
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/schema/properties/number.ts
|
|
1758
|
+
function number(options = {}) {
|
|
1759
|
+
return {
|
|
1760
|
+
definition: {
|
|
1761
|
+
type: "number",
|
|
1762
|
+
required: options.required ?? false,
|
|
1763
|
+
config: {
|
|
1764
|
+
min: options.min,
|
|
1765
|
+
max: options.max,
|
|
1766
|
+
integer: options.integer
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
validate(value) {
|
|
1770
|
+
if (value === null || value === void 0) {
|
|
1771
|
+
return !options.required;
|
|
1772
|
+
}
|
|
1773
|
+
if (typeof value !== "number" || isNaN(value)) return false;
|
|
1774
|
+
if (options.min !== void 0 && value < options.min) return false;
|
|
1775
|
+
if (options.max !== void 0 && value > options.max) return false;
|
|
1776
|
+
if (options.integer && !Number.isInteger(value)) return false;
|
|
1777
|
+
return true;
|
|
1778
|
+
},
|
|
1779
|
+
coerce(value) {
|
|
1780
|
+
if (value === null || value === void 0) return null;
|
|
1781
|
+
const num = Number(value);
|
|
1782
|
+
if (isNaN(num)) return null;
|
|
1783
|
+
if (options.integer) return Math.round(num);
|
|
1784
|
+
return num;
|
|
1785
|
+
},
|
|
1786
|
+
_type: 0
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/schema/properties/checkbox.ts
|
|
1791
|
+
function checkbox(options = {}) {
|
|
1792
|
+
return {
|
|
1793
|
+
definition: {
|
|
1794
|
+
type: "checkbox",
|
|
1795
|
+
required: options.required ?? false,
|
|
1796
|
+
config: {
|
|
1797
|
+
default: options.default
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
validate(value) {
|
|
1801
|
+
if (value === null || value === void 0) {
|
|
1802
|
+
return !options.required;
|
|
1803
|
+
}
|
|
1804
|
+
return typeof value === "boolean";
|
|
1805
|
+
},
|
|
1806
|
+
coerce(value) {
|
|
1807
|
+
if (value === null || value === void 0) {
|
|
1808
|
+
return options.default ?? null;
|
|
1809
|
+
}
|
|
1810
|
+
if (typeof value === "boolean") return value;
|
|
1811
|
+
if (value === "true" || value === 1) return true;
|
|
1812
|
+
if (value === "false" || value === 0) return false;
|
|
1813
|
+
return Boolean(value);
|
|
1814
|
+
},
|
|
1815
|
+
_type: false
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/schema/properties/date.ts
|
|
1820
|
+
function date(options = {}) {
|
|
1821
|
+
return {
|
|
1822
|
+
definition: {
|
|
1823
|
+
type: "date",
|
|
1824
|
+
required: options.required ?? false,
|
|
1825
|
+
config: {
|
|
1826
|
+
includeTime: options.includeTime ?? false
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
validate(value) {
|
|
1830
|
+
if (value === null || value === void 0) {
|
|
1831
|
+
return !options.required;
|
|
1832
|
+
}
|
|
1833
|
+
if (typeof value !== "number") return false;
|
|
1834
|
+
return value >= 0 && value < 3250368e7;
|
|
1835
|
+
},
|
|
1836
|
+
coerce(value) {
|
|
1837
|
+
if (value === null || value === void 0) return null;
|
|
1838
|
+
if (typeof value === "number") {
|
|
1839
|
+
return value;
|
|
1840
|
+
}
|
|
1841
|
+
if (value instanceof Date) {
|
|
1842
|
+
return value.getTime();
|
|
1843
|
+
}
|
|
1844
|
+
if (typeof value === "string") {
|
|
1845
|
+
const parsed = Date.parse(value);
|
|
1846
|
+
if (!isNaN(parsed)) return parsed;
|
|
1847
|
+
}
|
|
1848
|
+
return null;
|
|
1849
|
+
},
|
|
1850
|
+
_type: 0
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/schema/properties/dateRange.ts
|
|
1855
|
+
function isValidDateRange(value) {
|
|
1856
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1857
|
+
const obj = value;
|
|
1858
|
+
if (typeof obj.start !== "string") return false;
|
|
1859
|
+
if (!isValidDateString(obj.start)) return false;
|
|
1860
|
+
if (obj.end !== void 0) {
|
|
1861
|
+
if (typeof obj.end !== "string") return false;
|
|
1862
|
+
if (!isValidDateString(obj.end)) return false;
|
|
1863
|
+
}
|
|
1864
|
+
return true;
|
|
1865
|
+
}
|
|
1866
|
+
function isValidDateString(str) {
|
|
1867
|
+
const d = new Date(str);
|
|
1868
|
+
return !isNaN(d.getTime());
|
|
1869
|
+
}
|
|
1870
|
+
function dateRange(options = {}) {
|
|
1871
|
+
return {
|
|
1872
|
+
definition: {
|
|
1873
|
+
type: "dateRange",
|
|
1874
|
+
required: options.required ?? false,
|
|
1875
|
+
config: {
|
|
1876
|
+
includeTime: options.includeTime ?? false
|
|
1877
|
+
}
|
|
1878
|
+
},
|
|
1879
|
+
validate(value) {
|
|
1880
|
+
if (value === null || value === void 0) {
|
|
1881
|
+
return !options.required;
|
|
1882
|
+
}
|
|
1883
|
+
return isValidDateRange(value);
|
|
1884
|
+
},
|
|
1885
|
+
coerce(value) {
|
|
1886
|
+
if (value === null || value === void 0) return null;
|
|
1887
|
+
if (isValidDateRange(value)) {
|
|
1888
|
+
return value;
|
|
1889
|
+
}
|
|
1890
|
+
if (typeof value === "object" && value !== null) {
|
|
1891
|
+
const obj = value;
|
|
1892
|
+
if (obj.start) {
|
|
1893
|
+
const start = new Date(obj.start);
|
|
1894
|
+
if (!isNaN(start.getTime())) {
|
|
1895
|
+
const result = { start: start.toISOString() };
|
|
1896
|
+
if (obj.end) {
|
|
1897
|
+
const end = new Date(obj.end);
|
|
1898
|
+
if (!isNaN(end.getTime())) {
|
|
1899
|
+
result.end = end.toISOString();
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return result;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return null;
|
|
1907
|
+
},
|
|
1908
|
+
_type: {}
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/schema/properties/select.ts
|
|
1913
|
+
function select(options) {
|
|
1914
|
+
const validIds = new Set(options.options.map((o) => o.id));
|
|
1915
|
+
return {
|
|
1916
|
+
definition: {
|
|
1917
|
+
type: "select",
|
|
1918
|
+
required: options.required ?? false,
|
|
1919
|
+
config: {
|
|
1920
|
+
options: options.options,
|
|
1921
|
+
default: options.default
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
validate(value) {
|
|
1925
|
+
if (value === null || value === void 0) {
|
|
1926
|
+
return !options.required;
|
|
1927
|
+
}
|
|
1928
|
+
return typeof value === "string" && validIds.has(value);
|
|
1929
|
+
},
|
|
1930
|
+
coerce(value) {
|
|
1931
|
+
if (value === null || value === void 0) {
|
|
1932
|
+
return options.default ?? null;
|
|
1933
|
+
}
|
|
1934
|
+
if (typeof value === "string" && validIds.has(value)) {
|
|
1935
|
+
return value;
|
|
1936
|
+
}
|
|
1937
|
+
const byName = options.options.find(
|
|
1938
|
+
(o) => o.name.toLowerCase() === String(value).toLowerCase()
|
|
1939
|
+
);
|
|
1940
|
+
if (byName) return byName.id;
|
|
1941
|
+
return null;
|
|
1942
|
+
},
|
|
1943
|
+
_type: ""
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// src/schema/properties/multiSelect.ts
|
|
1948
|
+
function multiSelect(options) {
|
|
1949
|
+
const validIds = new Set(options.options.map((o) => o.id));
|
|
1950
|
+
return {
|
|
1951
|
+
definition: {
|
|
1952
|
+
type: "multiSelect",
|
|
1953
|
+
required: options.required ?? false,
|
|
1954
|
+
config: {
|
|
1955
|
+
options: options.options,
|
|
1956
|
+
default: options.default
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
validate(value) {
|
|
1960
|
+
if (value === null || value === void 0) {
|
|
1961
|
+
return !options.required;
|
|
1962
|
+
}
|
|
1963
|
+
if (!Array.isArray(value)) return false;
|
|
1964
|
+
return value.every((v) => typeof v === "string" && validIds.has(v));
|
|
1965
|
+
},
|
|
1966
|
+
coerce(value) {
|
|
1967
|
+
if (value === null || value === void 0) {
|
|
1968
|
+
return options.default ?? [];
|
|
1969
|
+
}
|
|
1970
|
+
if (!Array.isArray(value)) {
|
|
1971
|
+
if (typeof value === "string" && validIds.has(value)) {
|
|
1972
|
+
return [value];
|
|
1973
|
+
}
|
|
1974
|
+
return null;
|
|
1975
|
+
}
|
|
1976
|
+
const result = [];
|
|
1977
|
+
for (const v of value) {
|
|
1978
|
+
if (typeof v === "string" && validIds.has(v)) {
|
|
1979
|
+
result.push(v);
|
|
1980
|
+
} else {
|
|
1981
|
+
const byName = options.options.find(
|
|
1982
|
+
(o) => o.name.toLowerCase() === String(v).toLowerCase()
|
|
1983
|
+
);
|
|
1984
|
+
if (byName) result.push(byName.id);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return result;
|
|
1988
|
+
},
|
|
1989
|
+
_type: []
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/schema/properties/person.ts
|
|
1994
|
+
var DID_PATTERN = /^did:[a-z]+:[a-zA-Z0-9._:-]+$/;
|
|
1995
|
+
function person(options = {}) {
|
|
1996
|
+
const isMultiple = options.multiple ?? false;
|
|
1997
|
+
return {
|
|
1998
|
+
definition: {
|
|
1999
|
+
type: "person",
|
|
2000
|
+
required: options.required ?? false,
|
|
2001
|
+
config: {
|
|
2002
|
+
multiple: isMultiple
|
|
2003
|
+
}
|
|
2004
|
+
},
|
|
2005
|
+
validate(value) {
|
|
2006
|
+
if (value === null || value === void 0) {
|
|
2007
|
+
return !options.required;
|
|
2008
|
+
}
|
|
2009
|
+
if (isMultiple) {
|
|
2010
|
+
if (!Array.isArray(value)) return false;
|
|
2011
|
+
return value.every((v) => typeof v === "string" && DID_PATTERN.test(v));
|
|
2012
|
+
} else {
|
|
2013
|
+
return typeof value === "string" && DID_PATTERN.test(value);
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
coerce(value) {
|
|
2017
|
+
if (value === null || value === void 0) {
|
|
2018
|
+
return isMultiple ? [] : null;
|
|
2019
|
+
}
|
|
2020
|
+
if (isMultiple) {
|
|
2021
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
2022
|
+
return arr.filter((v) => typeof v === "string" && DID_PATTERN.test(v));
|
|
2023
|
+
} else {
|
|
2024
|
+
if (typeof value === "string" && DID_PATTERN.test(value)) {
|
|
2025
|
+
return value;
|
|
2026
|
+
}
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
_type: isMultiple ? [] : ""
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// src/schema/properties/relation.ts
|
|
2035
|
+
function relation2(options = {}) {
|
|
2036
|
+
const isMultiple = options.multiple ?? false;
|
|
2037
|
+
return {
|
|
2038
|
+
definition: {
|
|
2039
|
+
type: "relation",
|
|
2040
|
+
required: options.required ?? false,
|
|
2041
|
+
config: {
|
|
2042
|
+
...options.target !== void 0 && { target: options.target },
|
|
2043
|
+
multiple: isMultiple
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
validate(value) {
|
|
2047
|
+
if (value === null || value === void 0) {
|
|
2048
|
+
return !options.required;
|
|
2049
|
+
}
|
|
2050
|
+
if (isMultiple) {
|
|
2051
|
+
if (!Array.isArray(value)) return false;
|
|
2052
|
+
return value.every((v) => typeof v === "string" && v.length > 0);
|
|
2053
|
+
} else {
|
|
2054
|
+
return typeof value === "string" && value.length > 0;
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
coerce(value) {
|
|
2058
|
+
if (value === null || value === void 0) {
|
|
2059
|
+
return isMultiple ? [] : null;
|
|
2060
|
+
}
|
|
2061
|
+
if (isMultiple) {
|
|
2062
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
2063
|
+
return arr.filter((v) => typeof v === "string" && v.length > 0);
|
|
2064
|
+
} else {
|
|
2065
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2066
|
+
return value;
|
|
2067
|
+
}
|
|
2068
|
+
return null;
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
_type: isMultiple ? [] : ""
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/schema/properties/url.ts
|
|
2076
|
+
var URL_PATTERN = /^https?:\/\/.+/i;
|
|
2077
|
+
function url(options = {}) {
|
|
2078
|
+
return {
|
|
2079
|
+
definition: {
|
|
2080
|
+
type: "url",
|
|
2081
|
+
required: options.required ?? false,
|
|
2082
|
+
config: {
|
|
2083
|
+
placeholder: options.placeholder
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
validate(value) {
|
|
2087
|
+
if (value === null || value === void 0 || value === "") {
|
|
2088
|
+
return !options.required;
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof value !== "string") return false;
|
|
2091
|
+
return URL_PATTERN.test(value);
|
|
2092
|
+
},
|
|
2093
|
+
coerce(value) {
|
|
2094
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
2095
|
+
const str = String(value).trim();
|
|
2096
|
+
if (str && !str.match(/^https?:\/\//i)) {
|
|
2097
|
+
return `https://${str}`;
|
|
2098
|
+
}
|
|
2099
|
+
return str || null;
|
|
2100
|
+
},
|
|
2101
|
+
_type: ""
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// src/schema/properties/email.ts
|
|
2106
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2107
|
+
function email(options = {}) {
|
|
2108
|
+
return {
|
|
2109
|
+
definition: {
|
|
2110
|
+
type: "email",
|
|
2111
|
+
required: options.required ?? false,
|
|
2112
|
+
config: {
|
|
2113
|
+
placeholder: options.placeholder
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
validate(value) {
|
|
2117
|
+
if (value === null || value === void 0 || value === "") {
|
|
2118
|
+
return !options.required;
|
|
2119
|
+
}
|
|
2120
|
+
if (typeof value !== "string") return false;
|
|
2121
|
+
return EMAIL_PATTERN.test(value);
|
|
2122
|
+
},
|
|
2123
|
+
coerce(value) {
|
|
2124
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
2125
|
+
const str = String(value).trim().toLowerCase();
|
|
2126
|
+
return str || null;
|
|
2127
|
+
},
|
|
2128
|
+
_type: ""
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/schema/properties/phone.ts
|
|
2133
|
+
var PHONE_PATTERN = /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/;
|
|
2134
|
+
function phone(options = {}) {
|
|
2135
|
+
return {
|
|
2136
|
+
definition: {
|
|
2137
|
+
type: "phone",
|
|
2138
|
+
required: options.required ?? false,
|
|
2139
|
+
config: {
|
|
2140
|
+
placeholder: options.placeholder
|
|
2141
|
+
}
|
|
2142
|
+
},
|
|
2143
|
+
validate(value) {
|
|
2144
|
+
if (value === null || value === void 0 || value === "") {
|
|
2145
|
+
return !options.required;
|
|
2146
|
+
}
|
|
2147
|
+
if (typeof value !== "string") return false;
|
|
2148
|
+
const digitCount = value.replace(/\D/g, "").length;
|
|
2149
|
+
return PHONE_PATTERN.test(value) && digitCount >= 7;
|
|
2150
|
+
},
|
|
2151
|
+
coerce(value) {
|
|
2152
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
2153
|
+
const str = String(value).trim().replace(/\s+/g, " ");
|
|
2154
|
+
return str || null;
|
|
2155
|
+
},
|
|
2156
|
+
_type: ""
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/schema/properties/file.ts
|
|
2161
|
+
function isValidFileRef(value) {
|
|
2162
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2163
|
+
const obj = value;
|
|
2164
|
+
return typeof obj.cid === "string" && typeof obj.name === "string" && typeof obj.mimeType === "string" && typeof obj.size === "number";
|
|
2165
|
+
}
|
|
2166
|
+
function file(options = {}) {
|
|
2167
|
+
const isMultiple = options.multiple ?? false;
|
|
2168
|
+
return {
|
|
2169
|
+
definition: {
|
|
2170
|
+
type: "file",
|
|
2171
|
+
required: options.required ?? false,
|
|
2172
|
+
config: {
|
|
2173
|
+
multiple: isMultiple,
|
|
2174
|
+
accept: options.accept,
|
|
2175
|
+
maxSize: options.maxSize
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
validate(value) {
|
|
2179
|
+
if (value === null || value === void 0) {
|
|
2180
|
+
return !options.required;
|
|
2181
|
+
}
|
|
2182
|
+
if (isMultiple) {
|
|
2183
|
+
if (!Array.isArray(value)) return false;
|
|
2184
|
+
return value.every(isValidFileRef);
|
|
2185
|
+
} else {
|
|
2186
|
+
return isValidFileRef(value);
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
coerce(value) {
|
|
2190
|
+
if (value === null || value === void 0) {
|
|
2191
|
+
return isMultiple ? [] : null;
|
|
2192
|
+
}
|
|
2193
|
+
if (isMultiple) {
|
|
2194
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
2195
|
+
return arr.filter(isValidFileRef);
|
|
2196
|
+
} else {
|
|
2197
|
+
if (isValidFileRef(value)) {
|
|
2198
|
+
return value;
|
|
2199
|
+
}
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
},
|
|
2203
|
+
_type: isMultiple ? [] : {}
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/schema/properties/created.ts
|
|
2208
|
+
function created(options = {}) {
|
|
2209
|
+
return {
|
|
2210
|
+
definition: {
|
|
2211
|
+
type: "created",
|
|
2212
|
+
required: false,
|
|
2213
|
+
// Auto-populated, not user-required
|
|
2214
|
+
config: {
|
|
2215
|
+
label: options.label,
|
|
2216
|
+
auto: true,
|
|
2217
|
+
readonly: true
|
|
2218
|
+
}
|
|
2219
|
+
},
|
|
2220
|
+
validate(value) {
|
|
2221
|
+
if (value === null || value === void 0) {
|
|
2222
|
+
return true;
|
|
2223
|
+
}
|
|
2224
|
+
return typeof value === "number" && value > 0 && Number.isFinite(value);
|
|
2225
|
+
},
|
|
2226
|
+
coerce(value) {
|
|
2227
|
+
if (value === null || value === void 0) {
|
|
2228
|
+
return Date.now();
|
|
2229
|
+
}
|
|
2230
|
+
if (typeof value === "number" && value > 0) {
|
|
2231
|
+
return value;
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof value === "string") {
|
|
2234
|
+
const parsed = Date.parse(value);
|
|
2235
|
+
if (!isNaN(parsed)) return parsed;
|
|
2236
|
+
}
|
|
2237
|
+
return Date.now();
|
|
2238
|
+
},
|
|
2239
|
+
_type: 0
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/schema/properties/updated.ts
|
|
2244
|
+
function updated(options = {}) {
|
|
2245
|
+
return {
|
|
2246
|
+
definition: {
|
|
2247
|
+
type: "updated",
|
|
2248
|
+
required: false,
|
|
2249
|
+
// Auto-populated, not user-required
|
|
2250
|
+
config: {
|
|
2251
|
+
label: options.label,
|
|
2252
|
+
auto: true,
|
|
2253
|
+
readonly: true
|
|
2254
|
+
}
|
|
2255
|
+
},
|
|
2256
|
+
validate(value) {
|
|
2257
|
+
if (value === null || value === void 0) {
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2260
|
+
return typeof value === "number" && value > 0 && Number.isFinite(value);
|
|
2261
|
+
},
|
|
2262
|
+
coerce(value) {
|
|
2263
|
+
if (value === null || value === void 0) {
|
|
2264
|
+
return Date.now();
|
|
2265
|
+
}
|
|
2266
|
+
if (typeof value === "number" && value > 0) {
|
|
2267
|
+
return value;
|
|
2268
|
+
}
|
|
2269
|
+
if (typeof value === "string") {
|
|
2270
|
+
const parsed = Date.parse(value);
|
|
2271
|
+
if (!isNaN(parsed)) return parsed;
|
|
2272
|
+
}
|
|
2273
|
+
return Date.now();
|
|
2274
|
+
},
|
|
2275
|
+
_type: 0
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/schema/properties/createdBy.ts
|
|
2280
|
+
var DID_PATTERN2 = /^did:[a-z]+:[a-zA-Z0-9._:-]+$/;
|
|
2281
|
+
function createdBy(options = {}) {
|
|
2282
|
+
return {
|
|
2283
|
+
definition: {
|
|
2284
|
+
type: "createdBy",
|
|
2285
|
+
required: false,
|
|
2286
|
+
// Auto-populated, not user-required
|
|
2287
|
+
config: {
|
|
2288
|
+
label: options.label,
|
|
2289
|
+
auto: true,
|
|
2290
|
+
readonly: true
|
|
2291
|
+
}
|
|
2292
|
+
},
|
|
2293
|
+
validate(value) {
|
|
2294
|
+
if (value === null || value === void 0) {
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
return typeof value === "string" && DID_PATTERN2.test(value);
|
|
2298
|
+
},
|
|
2299
|
+
coerce(value) {
|
|
2300
|
+
if (value === null || value === void 0) {
|
|
2301
|
+
return null;
|
|
2302
|
+
}
|
|
2303
|
+
if (typeof value === "string" && DID_PATTERN2.test(value)) {
|
|
2304
|
+
return value;
|
|
2305
|
+
}
|
|
2306
|
+
return null;
|
|
2307
|
+
},
|
|
2308
|
+
_type: ""
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
export {
|
|
2313
|
+
parseSchemaIRI,
|
|
2314
|
+
normalizeSchemaIRI,
|
|
2315
|
+
getBaseSchemaIRI,
|
|
2316
|
+
isNode,
|
|
2317
|
+
createNodeId,
|
|
2318
|
+
GRANT_SCHEMA_IRI,
|
|
2319
|
+
isGrantActive,
|
|
2320
|
+
GrantIndex,
|
|
2321
|
+
DEFAULT_OFFLINE_POLICY,
|
|
2322
|
+
mergeOfflinePolicy,
|
|
2323
|
+
DecisionCache,
|
|
2324
|
+
DefaultRoleResolver,
|
|
2325
|
+
DefaultPolicyEvaluator,
|
|
2326
|
+
GrantExpirationCleaner,
|
|
2327
|
+
GrantRateLimiter,
|
|
2328
|
+
StoreAuthError,
|
|
2329
|
+
StoreAuth,
|
|
2330
|
+
defineSchema,
|
|
2331
|
+
text,
|
|
2332
|
+
number,
|
|
2333
|
+
checkbox,
|
|
2334
|
+
date,
|
|
2335
|
+
dateRange,
|
|
2336
|
+
select,
|
|
2337
|
+
multiSelect,
|
|
2338
|
+
person,
|
|
2339
|
+
relation2 as relation,
|
|
2340
|
+
url,
|
|
2341
|
+
email,
|
|
2342
|
+
phone,
|
|
2343
|
+
file,
|
|
2344
|
+
created,
|
|
2345
|
+
updated,
|
|
2346
|
+
createdBy
|
|
2347
|
+
};
|