@xnetjs/data 0.0.2 → 0.0.3

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 (137) hide show
  1. package/README.md +112 -0
  2. package/dist/account-BZHETMC4.js +12 -0
  3. package/dist/auth/index.d.ts +413 -0
  4. package/dist/auth/index.js +102 -0
  5. package/dist/budget-WR2Q4YH3.js +12 -0
  6. package/dist/canvas-HFMMVQLD.js +10 -0
  7. package/dist/channel-DXDYJ3ZH.js +12 -0
  8. package/dist/chat-message-QUUQGXEW.js +10 -0
  9. package/dist/chunk-2DSDH2ZH.js +52 -0
  10. package/dist/chunk-337E3ZOB.js +54 -0
  11. package/dist/chunk-37267RSF.js +41 -0
  12. package/dist/chunk-3JKW6TDE.js +66 -0
  13. package/dist/chunk-3RB2FMMO.js +148 -0
  14. package/dist/{chunk-BQBPA5HS.js → chunk-4U4KBCDN.js} +16 -2
  15. package/dist/chunk-4UQTURCD.js +722 -0
  16. package/dist/chunk-54Y7QRZN.js +109 -0
  17. package/dist/chunk-5FHJE7RC.js +46 -0
  18. package/dist/chunk-64AM6JPM.js +40 -0
  19. package/dist/chunk-6CN53PON.js +143 -0
  20. package/dist/chunk-6GB7PBCR.js +40 -0
  21. package/dist/chunk-6OPZJ7MG.js +78 -0
  22. package/dist/{chunk-IDMBCRUC.js → chunk-77UDLVQY.js} +1 -1
  23. package/dist/chunk-7GMJOAPF.js +52 -0
  24. package/dist/chunk-7PDRDSFH.js +69 -0
  25. package/dist/chunk-C5IRSEEZ.js +69 -0
  26. package/dist/chunk-CHZUSBSB.js +45 -0
  27. package/dist/chunk-DOZJXPQ6.js +232 -0
  28. package/dist/chunk-DTPSTMUD.js +105 -0
  29. package/dist/chunk-E6S557NC.js +402 -0
  30. package/dist/chunk-EIQX7INF.js +45 -0
  31. package/dist/chunk-ENTHVNOC.js +57 -0
  32. package/dist/chunk-EZA5UT4Y.js +5543 -0
  33. package/dist/chunk-GR3NDMBY.js +76 -0
  34. package/dist/chunk-GWYVY2K4.js +63 -0
  35. package/dist/chunk-IJ675GJR.js +42 -0
  36. package/dist/chunk-IP4BEVVD.js +57 -0
  37. package/dist/chunk-J4IVJA5M.js +102 -0
  38. package/dist/chunk-J6EDPPH4.js +84 -0
  39. package/dist/{chunk-SZC345Z2.js → chunk-JESSJ5CZ.js} +1015 -1110
  40. package/dist/chunk-JO6Y4E5V.js +73 -0
  41. package/dist/chunk-KPVBKKZS.js +47 -0
  42. package/dist/chunk-KTJLFJEQ.js +43 -0
  43. package/dist/chunk-L66WXPTW.js +32 -0
  44. package/dist/chunk-LGD72FVD.js +48 -0
  45. package/dist/chunk-O64EK5FK.js +887 -0
  46. package/dist/chunk-PMUQACPY.js +33 -0
  47. package/dist/chunk-PSUJEKVV.js +1722 -0
  48. package/dist/chunk-QVZXXJSP.js +6122 -0
  49. package/dist/chunk-QWFTRZQT.js +142 -0
  50. package/dist/chunk-RSSZXCLL.js +51 -0
  51. package/dist/chunk-S4B5OPZU.js +415 -0
  52. package/dist/{chunk-VYR5GPJP.js → chunk-SPFKSTPT.js} +9 -3
  53. package/dist/chunk-U6CBYDVZ.js +50 -0
  54. package/dist/chunk-UDUPQKRB.js +49 -0
  55. package/dist/chunk-V6HZ3LF3.js +380 -0
  56. package/dist/chunk-WMSCEBFH.js +64 -0
  57. package/dist/chunk-WUR3WWQM.js +65 -0
  58. package/dist/chunk-XHEA5UER.js +75 -0
  59. package/dist/chunk-XTUGZSYQ.js +75 -0
  60. package/dist/chunk-XWA3CJQ6.js +45 -0
  61. package/dist/chunk-YEQ6LLUY.js +49 -0
  62. package/dist/chunk-YWN27MLD.js +47 -0
  63. package/dist/chunk-YXZGYLKV.js +65 -0
  64. package/dist/chunk-ZICIXDEE.js +54 -0
  65. package/dist/chunk-ZMCO3O2Z.js +70 -0
  66. package/dist/clone-DpbVKLUu.d.ts +2205 -0
  67. package/dist/comment-Y2Y3QYYI.js +10 -0
  68. package/dist/crm-AAJXYIJY.js +68 -0
  69. package/dist/dashboard-3DC6B4BJ.js +10 -0
  70. package/dist/database/index.d.ts +882 -0
  71. package/dist/database/index.js +523 -0
  72. package/dist/database-DRGL5UYS.js +10 -0
  73. package/dist/database-field-F47FY3XX.js +10 -0
  74. package/dist/database-row-77OXW2NF.js +10 -0
  75. package/dist/database-select-option-6S3TTA7A.js +10 -0
  76. package/dist/database-view-YW7DQG5X.js +10 -0
  77. package/dist/experiment-TJOAL3OH.js +10 -0
  78. package/dist/external-item-D654PKLI.js +14 -0
  79. package/dist/external-reference-K5SUJIQN.js +8 -0
  80. package/dist/feed-L23XTNDI.js +12 -0
  81. package/dist/feed-item-GSO242J6.js +12 -0
  82. package/dist/folder-ONKSWHUT.js +21 -0
  83. package/dist/game-BF7DP5JU.js +50 -0
  84. package/dist/grant-NNKMWXPX.js +8 -0
  85. package/dist/grant-expiration-cleaner-DBiNTWbD.d.ts +249 -0
  86. package/dist/import-batch-JMESZ4VI.js +12 -0
  87. package/dist/inbox-state-AN6FPCIY.js +12 -0
  88. package/dist/index.d.ts +224 -4548
  89. package/dist/index.js +2206 -4754
  90. package/dist/map-AUV65YS3.js +10 -0
  91. package/dist/media-asset-4I5YPR4R.js +8 -0
  92. package/dist/memory-NV6EVJBE.js +12 -0
  93. package/dist/metric-X7W55DLC.js +10 -0
  94. package/dist/milestone-JJGOGK4R.js +12 -0
  95. package/dist/moderation-QSUVBCNS.js +30 -0
  96. package/dist/observation-UZSXXGBC.js +10 -0
  97. package/dist/page-NI7XODS6.js +10 -0
  98. package/dist/posting-FZOYVGH2.js +12 -0
  99. package/dist/profile-42A43GES.js +8 -0
  100. package/dist/project-G2CTLDXY.js +10 -0
  101. package/dist/query-ast-CWo6_30h.d.ts +226 -0
  102. package/dist/reaction-VAQBVDUD.js +10 -0
  103. package/dist/registry-CQoN_p8U.d.ts +99 -0
  104. package/dist/saved-view-AOQHGHNM.js +10 -0
  105. package/dist/schema/index.d.ts +6182 -0
  106. package/dist/schema/index.js +631 -0
  107. package/dist/schema-extension-5BUXRDPB.js +18 -0
  108. package/dist/space-PZ737CIS.js +42 -0
  109. package/dist/space-membership-N35NRX6F.js +14 -0
  110. package/dist/store/index.d.ts +391 -0
  111. package/dist/store/index.js +135 -0
  112. package/dist/store-CvgiydME.d.ts +338 -0
  113. package/dist/sync/awareness.d.ts +69 -0
  114. package/dist/sync/awareness.js +20 -0
  115. package/dist/system-Q5I6STDC.js +38 -0
  116. package/dist/tag-C677GKQW.js +16 -0
  117. package/dist/task-WQLMPN5U.js +16 -0
  118. package/dist/task-view-T5S32V4L.js +10 -0
  119. package/dist/transaction-U3ITRGJP.js +12 -0
  120. package/dist/transcription-F3T23LLO.js +12 -0
  121. package/dist/types-Def-5Nka.d.ts +1525 -0
  122. package/dist/updates.d.ts +44 -0
  123. package/dist/updates.js +16 -0
  124. package/dist/user-widget-4SJJFPG5.js +8 -0
  125. package/dist/view-types-DSde-uT_.d.ts +316 -0
  126. package/package.json +32 -9
  127. package/dist/canvas-HSIKIQFK.js +0 -7
  128. package/dist/chunk-2L5ZUGG5.js +0 -53
  129. package/dist/chunk-4MTS5KAQ.js +0 -22
  130. package/dist/chunk-GZHARFKC.js +0 -25
  131. package/dist/chunk-WKIKJTI2.js +0 -45
  132. package/dist/comment-277JD7DV.js +0 -7
  133. package/dist/database-W3KEHLI3.js +0 -7
  134. package/dist/database-row-3O2QSNZN.js +0 -7
  135. package/dist/grant-QVZ454YC.js +0 -7
  136. package/dist/page-O4WTOIEO.js +0 -7
  137. package/dist/task-SEKAYJH7.js +0 -7
@@ -1,255 +1,236 @@
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
- };
1
+ // src/auth/index.ts
2
+ import { AUTH_ACTIONS as AUTH_ACTIONS2 } from "@xnetjs/core";
3
+
4
+ // src/auth/builders.ts
5
+ function allow(...roles) {
6
+ return { _tag: "allow", roles };
28
7
  }
29
- function normalizeSchemaIRI(iri) {
30
- const parsed = parseSchemaIRI(iri);
31
- return parsed.iri;
8
+ function deny(...roles) {
9
+ return { _tag: "deny", roles };
32
10
  }
33
- function getBaseSchemaIRI(iri) {
34
- return parseSchemaIRI(iri).baseIRI;
11
+ function and(...exprs) {
12
+ return { _tag: "and", exprs };
35
13
  }
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:");
14
+ function or(...exprs) {
15
+ return { _tag: "or", exprs };
40
16
  }
41
- function createNodeId(length) {
42
- return nanoid(length);
17
+ function not(expr) {
18
+ return { _tag: "not", expr };
43
19
  }
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;
20
+ var PUBLIC = { _tag: "public" };
21
+ var AUTHENTICATED = { _tag: "authenticated" };
22
+ var role = {
23
+ /**
24
+ * Role held by the node's creator.
25
+ * The DID in `createdBy` holds this role.
26
+ */
27
+ creator() {
28
+ return { _tag: "creator" };
29
+ },
30
+ /**
31
+ * Role determined by a person property on the node.
32
+ * The DID(s) in that property hold this role.
33
+ *
34
+ * @param propertyName - Name of the person/person[] property
35
+ */
36
+ property(propertyName) {
37
+ return { _tag: "property", propertyName };
38
+ },
39
+ /**
40
+ * Role inherited from a related node.
41
+ * Users who hold `targetRole` on the related node hold this role.
42
+ *
43
+ * @param relationName - Name of the relation property
44
+ * @param targetRole - Role to check on the related node
45
+ */
46
+ relation(relationName, targetRole) {
47
+ return { _tag: "relation", relationName, targetRole };
48
+ },
49
+ /**
50
+ * Role determined by membership edges that reference THIS node.
51
+ *
52
+ * Unlike `relation` (which follows a forward relation) this is a reverse-edge
53
+ * lookup: the subject holds the role when a `config.edgeSchema` edge points at
54
+ * this node (or, when `parentProp` is set, any ancestor) with the subject as
55
+ * `memberProp` and a `roleProp` rank `>= minRole`. This is how Space
56
+ * membership cascades down a nested container tree.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * spaceMember: role.members({
61
+ * edgeSchema: SPACE_MEMBERSHIP_SCHEMA_IRI, containerProp: 'space',
62
+ * memberProp: 'member', roleProp: 'role', minRole: 'member',
63
+ * roleOrder: SPACE_ROLES, parentProp: 'parent'
64
+ * })
65
+ * ```
66
+ */
67
+ members(config) {
68
+ return {
69
+ _tag: "membership",
70
+ edgeSchema: config.edgeSchema,
71
+ containerProp: config.containerProp,
72
+ memberProp: config.memberProp,
73
+ roleProp: config.roleProp,
74
+ minRole: config.minRole,
75
+ roleOrder: config.roleOrder,
76
+ parentProp: config.parentProp
77
+ };
51
78
  }
52
- const expiresAt = asNumber(grant.properties.expiresAt);
53
- if (expiresAt > 0 && expiresAt <= now) {
54
- return false;
79
+ };
80
+ var relation = role.relation;
81
+
82
+ // src/auth/validate.ts
83
+ var BUILTIN_ROLES = ["owner"];
84
+ function extractRoleRefs(expr) {
85
+ const roles = [];
86
+ function visit(e) {
87
+ switch (e._tag) {
88
+ case "allow":
89
+ case "deny":
90
+ roles.push(...e.roles);
91
+ break;
92
+ case "and":
93
+ case "or":
94
+ for (const sub of e.exprs) visit(sub);
95
+ break;
96
+ case "not":
97
+ visit(e.expr);
98
+ break;
99
+ case "roleRef":
100
+ roles.push(e.role);
101
+ break;
102
+ case "public":
103
+ case "authenticated":
104
+ break;
105
+ }
55
106
  }
56
- return true;
107
+ visit(expr);
108
+ return roles;
57
109
  }
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);
110
+ function countExpressionNodes(expr) {
111
+ let count = 1;
112
+ switch (expr._tag) {
113
+ case "and":
114
+ case "or":
115
+ for (const sub of expr.exprs) {
116
+ count += countExpressionNodes(sub);
83
117
  }
84
- });
118
+ break;
119
+ case "not":
120
+ count += countExpressionNodes(expr.expr);
121
+ break;
85
122
  }
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);
123
+ return count;
124
+ }
125
+ function hasPublicAccess(expr) {
126
+ if (expr._tag === "public") return true;
127
+ if (expr._tag === "or") return expr.exprs.some(hasPublicAccess);
128
+ if (expr._tag === "and") return expr.exprs.every(hasPublicAccess);
129
+ if (expr._tag === "not") return false;
130
+ return false;
131
+ }
132
+ var MAX_EXPRESSION_NODES = 50;
133
+ function validateAuthorization(auth, properties) {
134
+ const errors = [];
135
+ for (const [actionName, expr] of Object.entries(auth.actions)) {
136
+ const referencedRoles = extractRoleRefs(expr);
137
+ for (const ref of referencedRoles) {
138
+ if (!(ref in auth.roles) && !BUILTIN_ROLES.includes(ref)) {
139
+ errors.push({
140
+ code: "AUTH_SCHEMA_INVALID_ROLE_REF",
141
+ message: `Action '${actionName}' references unknown role '${ref}'`,
142
+ path: `authorization.actions.${actionName}`
143
+ });
97
144
  }
98
145
  }
99
- return grants;
100
146
  }
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);
147
+ for (const [roleName, resolver] of Object.entries(auth.roles)) {
148
+ if (resolver._tag === "property") {
149
+ const propDef = properties[resolver.propertyName];
150
+ if (!propDef) {
151
+ errors.push({
152
+ code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
153
+ message: `Role '${roleName}' references non-existent property '${resolver.propertyName}'`,
154
+ path: `authorization.roles.${roleName}`
155
+ });
156
+ } else if (propDef.type !== "person") {
157
+ errors.push({
158
+ code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
159
+ message: `Role '${roleName}' references property '${resolver.propertyName}' which is not a person type (got '${propDef.type}')`,
160
+ path: `authorization.roles.${roleName}`
161
+ });
110
162
  }
111
163
  }
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);
164
+ if (resolver._tag === "relation") {
165
+ const propDef = properties[resolver.relationName];
166
+ if (!propDef) {
167
+ errors.push({
168
+ code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
169
+ message: `Role '${roleName}' references non-existent relation '${resolver.relationName}'`,
170
+ path: `authorization.roles.${roleName}`
171
+ });
172
+ } else if (propDef.type !== "relation") {
173
+ errors.push({
174
+ code: "AUTH_SCHEMA_INVALID_RELATION_PATH",
175
+ message: `Role '${roleName}' references property '${resolver.relationName}' which is not a relation type (got '${propDef.type}')`,
176
+ path: `authorization.roles.${roleName}`
177
+ });
122
178
  }
123
179
  }
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
- }
180
+ if (resolver._tag === "membership") {
181
+ if (!resolver.roleOrder.includes(resolver.minRole)) {
182
+ errors.push({
183
+ code: "AUTH_SCHEMA_INVALID_ROLE_REF",
184
+ message: `Role '${roleName}' membership minRole '${resolver.minRole}' is not in its roleOrder`,
185
+ path: `authorization.roles.${roleName}`
186
+ });
137
187
  }
138
188
  }
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
189
  }
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;
190
+ if (auth.publicProps) {
191
+ for (const prop of auth.publicProps) {
192
+ if (!(prop in properties)) {
193
+ errors.push({
194
+ code: "AUTH_SCHEMA_INVALID_PUBLIC_PROP",
195
+ message: `publicProps references non-existent property '${prop}'`,
196
+ path: `authorization.publicProps`
197
+ });
198
+ }
152
199
  }
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
200
  }
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);
201
+ for (const [actionName, expr] of Object.entries(auth.actions)) {
202
+ const nodeCount = countExpressionNodes(expr);
203
+ if (nodeCount > MAX_EXPRESSION_NODES) {
204
+ errors.push({
205
+ code: "AUTH_SCHEMA_EXPR_LIMIT_EXCEEDED",
206
+ message: `Action '${actionName}' expression has ${nodeCount} nodes (max ${MAX_EXPRESSION_NODES})`,
207
+ path: `authorization.actions.${actionName}`
208
+ });
186
209
  }
187
- grantIdsForResource.add(node.id);
188
210
  }
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);
211
+ if (auth.fieldRules) {
212
+ for (const fieldName of Object.keys(auth.fieldRules)) {
213
+ if (!(fieldName in properties)) {
214
+ errors.push({
215
+ code: "AUTH_SCHEMA_INVALID_FIELD_REF",
216
+ message: `fieldRules references non-existent property '${fieldName}'`,
217
+ path: `authorization.fieldRules.${fieldName}`
218
+ });
218
219
  }
219
220
  }
220
221
  }
221
- };
222
- function asDid(value) {
223
- if (typeof value === "string" && value.startsWith("did:key:")) {
224
- return value;
222
+ const mutationActions = ["write", "delete", "share"];
223
+ for (const actionName of mutationActions) {
224
+ const expr = auth.actions[actionName];
225
+ if (expr && hasPublicAccess(expr)) {
226
+ errors.push({
227
+ code: "AUTH_SCHEMA_UNSAFE_PUBLIC_MUTATION",
228
+ message: `Action '${actionName}' allows PUBLIC access, which is unsafe for mutation operations`,
229
+ path: `authorization.actions.${actionName}`
230
+ });
231
+ }
225
232
  }
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";
233
+ return { valid: errors.length === 0, errors };
253
234
  }
254
235
 
255
236
  // src/auth/serialize.ts
@@ -305,6 +286,17 @@ function serializeRoleResolver(resolver) {
305
286
  relationName: resolver.relationName,
306
287
  targetRole: resolver.targetRole
307
288
  };
289
+ case "membership":
290
+ return {
291
+ _tag: "membership",
292
+ edgeSchema: resolver.edgeSchema,
293
+ containerProp: resolver.containerProp,
294
+ memberProp: resolver.memberProp,
295
+ roleProp: resolver.roleProp,
296
+ minRole: resolver.minRole,
297
+ roleOrder: [...resolver.roleOrder],
298
+ parentProp: resolver.parentProp
299
+ };
308
300
  }
309
301
  }
310
302
  function deserializeRoleResolver(serialized) {
@@ -319,6 +311,17 @@ function deserializeRoleResolver(serialized) {
319
311
  relationName: serialized.relationName,
320
312
  targetRole: serialized.targetRole
321
313
  };
314
+ case "membership":
315
+ return {
316
+ _tag: "membership",
317
+ edgeSchema: serialized.edgeSchema,
318
+ containerProp: serialized.containerProp,
319
+ memberProp: serialized.memberProp,
320
+ roleProp: serialized.roleProp,
321
+ minRole: serialized.minRole,
322
+ roleOrder: [...serialized.roleOrder],
323
+ parentProp: serialized.parentProp
324
+ };
322
325
  }
323
326
  }
324
327
  function serializeAuthorization(auth) {
@@ -384,152 +387,622 @@ function deserializeAuthorization(serialized) {
384
387
  return auth;
385
388
  }
386
389
 
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;
390
+ // src/auth/presets.ts
391
+ var presets = {
392
+ /**
393
+ * Private - only the creator can access.
394
+ *
395
+ * Use for personal notes, private documents, etc.
396
+ */
397
+ private: () => ({
398
+ roles: {
399
+ owner: role.creator()
400
+ },
401
+ actions: {
402
+ read: allow("owner"),
403
+ write: allow("owner"),
404
+ delete: allow("owner"),
405
+ share: allow("owner")
410
406
  }
411
- }
412
- visit(expr);
413
- return roles;
414
- }
415
- function countExpressionNodes(expr) {
416
- let count = 1;
407
+ }),
408
+ /**
409
+ * Public read - anyone can read, only creator can write.
410
+ *
411
+ * Use for published content, blog posts, etc.
412
+ */
413
+ publicRead: () => ({
414
+ roles: {
415
+ owner: role.creator()
416
+ },
417
+ actions: {
418
+ read: PUBLIC,
419
+ write: allow("owner"),
420
+ delete: allow("owner"),
421
+ share: allow("owner")
422
+ }
423
+ }),
424
+ /**
425
+ * Collaborative - roles inherited from a parent relation.
426
+ *
427
+ * Use for items within a workspace/project that inherits permissions.
428
+ *
429
+ * @param parentRelation - Name of the relation property to the parent node
430
+ */
431
+ collaborative: (parentRelation) => ({
432
+ roles: {
433
+ owner: role.creator(),
434
+ admin: role.relation(parentRelation, "admin"),
435
+ editor: role.relation(parentRelation, "editor"),
436
+ viewer: role.relation(parentRelation, "viewer")
437
+ },
438
+ actions: {
439
+ read: allow("viewer", "editor", "admin", "owner"),
440
+ write: allow("editor", "admin", "owner"),
441
+ delete: allow("admin", "owner"),
442
+ share: allow("admin", "owner")
443
+ }
444
+ }),
445
+ /**
446
+ * Open - any authenticated user can read/write, only creator can delete.
447
+ *
448
+ * Use for wiki-style collaborative content.
449
+ */
450
+ open: () => ({
451
+ roles: {
452
+ owner: role.creator()
453
+ },
454
+ actions: {
455
+ read: AUTHENTICATED,
456
+ write: AUTHENTICATED,
457
+ delete: allow("owner"),
458
+ share: allow("owner")
459
+ }
460
+ }),
461
+ /**
462
+ * Team-based - permissions based on a team/editors property.
463
+ *
464
+ * @param editorsProperty - Name of the person[] property containing editors
465
+ */
466
+ team: (editorsProperty) => ({
467
+ roles: {
468
+ owner: role.creator(),
469
+ editor: role.property(editorsProperty)
470
+ },
471
+ actions: {
472
+ read: allow("editor", "owner"),
473
+ write: allow("editor", "owner"),
474
+ delete: allow("owner"),
475
+ share: allow("owner")
476
+ }
477
+ })
478
+ };
479
+
480
+ // src/auth/permission-matrix.ts
481
+ import { AUTH_ACTIONS } from "@xnetjs/core";
482
+ function collect(expr, polarity, acc) {
417
483
  switch (expr._tag) {
484
+ case "allow":
485
+ for (const role2 of expr.roles) (polarity ? acc.allow : acc.deny).add(role2);
486
+ return;
487
+ case "roleRef": {
488
+ const bucket = polarity ? acc.allow : acc.deny;
489
+ bucket.add(expr.role);
490
+ return;
491
+ }
492
+ case "deny":
493
+ for (const role2 of expr.roles) (polarity ? acc.deny : acc.allow).add(role2);
494
+ return;
495
+ case "public":
496
+ if (polarity) acc.public = true;
497
+ return;
498
+ case "authenticated":
499
+ if (polarity) acc.authenticated = true;
500
+ return;
418
501
  case "and":
419
502
  case "or":
420
- for (const sub of expr.exprs) {
421
- count += countExpressionNodes(sub);
503
+ for (const sub of expr.exprs) collect(sub, polarity, acc);
504
+ return;
505
+ case "not":
506
+ collect(expr.expr, !polarity, acc);
507
+ return;
508
+ }
509
+ }
510
+ function summarizeAction(action, expr) {
511
+ const acc = { allow: /* @__PURE__ */ new Set(), deny: /* @__PURE__ */ new Set(), public: false, authenticated: false };
512
+ if (expr) collect(expr, true, acc);
513
+ return {
514
+ action,
515
+ roles: [...acc.allow],
516
+ denied: [...acc.deny],
517
+ public: acc.public,
518
+ authenticated: acc.authenticated
519
+ };
520
+ }
521
+ function describeRoleResolver(resolver) {
522
+ switch (resolver._tag) {
523
+ case "creator":
524
+ return "Node creator";
525
+ case "property":
526
+ return `Listed in "${resolver.propertyName}"`;
527
+ case "relation":
528
+ return `Inherited from "${resolver.relationName}" (as ${resolver.targetRole})`;
529
+ case "membership":
530
+ return `Member of "${resolver.containerProp}" (\u2265 ${resolver.minRole})`;
531
+ }
532
+ }
533
+ function buildPermissionMatrix(authorization) {
534
+ if (!authorization) {
535
+ return {
536
+ roles: [],
537
+ actions: AUTH_ACTIONS.map((action) => ({
538
+ action,
539
+ roles: [],
540
+ denied: [],
541
+ public: true,
542
+ authenticated: false
543
+ }))
544
+ };
545
+ }
546
+ const roles = Object.entries(authorization.roles).map(([role2, resolver]) => ({
547
+ role: role2,
548
+ kind: resolver._tag,
549
+ provenance: describeRoleResolver(resolver)
550
+ }));
551
+ const actionKeys = [
552
+ ...AUTH_ACTIONS.filter((action) => action in authorization.actions),
553
+ ...Object.keys(authorization.actions).filter(
554
+ (action) => !AUTH_ACTIONS.includes(action)
555
+ )
556
+ ];
557
+ return {
558
+ roles,
559
+ actions: actionKeys.map((action) => summarizeAction(action, authorization.actions[action]))
560
+ };
561
+ }
562
+
563
+ // src/auth/mode.ts
564
+ function getAuthMode(schema) {
565
+ if (!schema.authorization) return "legacy";
566
+ return "enforce";
567
+ }
568
+ function warnLegacySchema(schema) {
569
+ if (!schema.authorization) {
570
+ console.warn(
571
+ `[xnet:auth] Schema '${schema.name}' has no authorization block - data will be owner-only in legacy mode. Add an 'authorization' block to enable explicit access control.`
572
+ );
573
+ }
574
+ }
575
+
576
+ // src/auth/hub-policy.ts
577
+ var SCHEMA_ACTION_TO_HUB = {
578
+ read: "read",
579
+ write: "write",
580
+ share: "share",
581
+ delete: "admin"
582
+ };
583
+ function schemaToHubPolicy(schema) {
584
+ if (!schema.authorization) {
585
+ return { roleActions: {}, public: false };
586
+ }
587
+ const auth = deserializeAuthorization(schema.authorization);
588
+ const roleActions = {};
589
+ let isPublic = false;
590
+ for (const [action, expr] of Object.entries(auth.actions)) {
591
+ if (!expr) continue;
592
+ if (action === "read" && hasPublicAccess(expr)) {
593
+ isPublic = true;
594
+ }
595
+ const hubAction = SCHEMA_ACTION_TO_HUB[action];
596
+ if (!hubAction) continue;
597
+ for (const roleName of extractRoleRefs(expr)) {
598
+ const actions = roleActions[roleName] ??= /* @__PURE__ */ new Set();
599
+ actions.add(hubAction);
600
+ }
601
+ }
602
+ return {
603
+ roleActions: Object.fromEntries(
604
+ Object.entries(roleActions).map(([role2, actions]) => [role2, [...actions].sort()])
605
+ ),
606
+ public: isPublic
607
+ };
608
+ }
609
+ function capitalize(value) {
610
+ return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
611
+ }
612
+ function hubActionsForSpaceRole(schema, spaceRole) {
613
+ const cascadeRole = `space${capitalize(spaceRole)}`;
614
+ return schemaToHubPolicy(schema).roleActions[cascadeRole] ?? [];
615
+ }
616
+
617
+ // src/auth/recipients.ts
618
+ var PUBLIC_CONTENT_KEY = new Uint8Array(32);
619
+ var PUBLIC_RECIPIENT = "PUBLIC";
620
+ var MAX_CONTAINER_DEPTH = 32;
621
+ async function computeRecipients(schema, node, dependencies) {
622
+ const recipients = /* @__PURE__ */ new Set();
623
+ recipients.add(node.createdBy);
624
+ const addGrantRecipients = () => {
625
+ const grants = dependencies.grantIndex?.findGrantsForResource(node.id) ?? [];
626
+ for (const grant of grants) {
627
+ const actions = parseGrantActions(grant.properties.actions);
628
+ if (actions.includes("read") || actions.includes("write")) {
629
+ const grantee = grant.properties.grantee;
630
+ if (typeof grantee === "string" && grantee.startsWith("did:key:")) {
631
+ recipients.add(grantee);
632
+ }
633
+ }
634
+ }
635
+ };
636
+ if (!schema.authorization) {
637
+ addGrantRecipients();
638
+ return [...recipients];
639
+ }
640
+ const auth = deserializeAuthorization(schema.authorization);
641
+ const readExpr = auth.actions.read;
642
+ if (!readExpr) {
643
+ addGrantRecipients();
644
+ return [...recipients];
645
+ }
646
+ if (hasPublicAccess(readExpr)) {
647
+ return [PUBLIC_RECIPIENT];
648
+ }
649
+ const readRoles = extractRoleRefs(readExpr);
650
+ for (const roleName of readRoles) {
651
+ const resolver = auth.roles[roleName];
652
+ if (!resolver) continue;
653
+ const members = await resolveRoleMembers(resolver, node, schema, dependencies, 0);
654
+ for (const did of members) {
655
+ recipients.add(did);
656
+ }
657
+ }
658
+ addGrantRecipients();
659
+ return [...recipients];
660
+ }
661
+ async function resolveRoleMembers(resolver, node, _schema, dependencies, depth) {
662
+ if (depth > (dependencies.maxDepth ?? 3)) {
663
+ return [];
664
+ }
665
+ switch (resolver._tag) {
666
+ case "creator":
667
+ return [node.createdBy];
668
+ case "property":
669
+ return readDidProperty(node.properties[resolver.propertyName]);
670
+ case "relation": {
671
+ const targetId = node.properties[resolver.relationName];
672
+ if (typeof targetId !== "string") {
673
+ return [];
674
+ }
675
+ const targetNode = await dependencies.getNode(targetId);
676
+ if (!targetNode || !dependencies.getSchema) {
677
+ return [];
678
+ }
679
+ const targetSchema = await dependencies.getSchema(targetNode.schemaId);
680
+ if (!targetSchema?.authorization) {
681
+ return [];
682
+ }
683
+ const targetAuth = deserializeAuthorization(targetSchema.authorization);
684
+ const targetResolver = targetAuth.roles[resolver.targetRole];
685
+ if (!targetResolver) {
686
+ return [];
687
+ }
688
+ return resolveRoleMembers(targetResolver, targetNode, targetSchema, dependencies, depth + 1);
689
+ }
690
+ case "membership": {
691
+ if (!dependencies.listNodes) {
692
+ return [];
693
+ }
694
+ const containerIds = await collectContainerIds(node, resolver.parentProp, dependencies);
695
+ const edges = await dependencies.listNodes(resolver.edgeSchema);
696
+ const members = /* @__PURE__ */ new Set();
697
+ for (const edge of edges) {
698
+ if (edge.deleted) continue;
699
+ const container = edge.properties[resolver.containerProp];
700
+ if (typeof container !== "string" || !containerIds.has(container)) continue;
701
+ const edgeRole = edge.properties[resolver.roleProp];
702
+ if (typeof edgeRole !== "string") continue;
703
+ const have = resolver.roleOrder.indexOf(edgeRole);
704
+ const need = resolver.roleOrder.indexOf(resolver.minRole);
705
+ if (have < 0 || need < 0 || have < need) continue;
706
+ for (const did of readDidProperty(edge.properties[resolver.memberProp])) {
707
+ members.add(did);
708
+ }
709
+ }
710
+ return [...members];
711
+ }
712
+ }
713
+ }
714
+ async function collectContainerIds(node, parentProp, dependencies) {
715
+ const ids = /* @__PURE__ */ new Set([node.id]);
716
+ if (!parentProp) {
717
+ return ids;
718
+ }
719
+ let current = node;
720
+ let depth = 0;
721
+ while (current && depth < MAX_CONTAINER_DEPTH) {
722
+ const parentId = current.properties[parentProp];
723
+ if (typeof parentId !== "string" || ids.has(parentId)) {
724
+ break;
725
+ }
726
+ ids.add(parentId);
727
+ current = await dependencies.getNode(parentId);
728
+ depth += 1;
729
+ }
730
+ return ids;
731
+ }
732
+ function readDidProperty(value) {
733
+ if (Array.isArray(value)) {
734
+ return value.filter(isDid);
735
+ }
736
+ if (isDid(value)) {
737
+ return [value];
738
+ }
739
+ return [];
740
+ }
741
+ function isDid(value) {
742
+ return typeof value === "string" && value.startsWith("did:key:");
743
+ }
744
+ function parseGrantActions(value) {
745
+ if (Array.isArray(value)) {
746
+ return value.filter((entry) => typeof entry === "string");
747
+ }
748
+ if (typeof value !== "string") {
749
+ return [];
750
+ }
751
+ try {
752
+ const parsed = JSON.parse(value);
753
+ if (!Array.isArray(parsed)) {
754
+ return [];
755
+ }
756
+ return parsed.filter((entry) => typeof entry === "string");
757
+ } catch {
758
+ return [];
759
+ }
760
+ }
761
+ function hasRecipientsChanged(previous, next) {
762
+ if (previous.length !== next.length) {
763
+ return true;
764
+ }
765
+ const nextSet = new Set(next);
766
+ return previous.some((recipient) => !nextSet.has(recipient));
767
+ }
768
+ function canTransitionToPublic(readExpr) {
769
+ if (!readExpr) {
770
+ return false;
771
+ }
772
+ return hasPublicAccess(readExpr);
773
+ }
774
+
775
+ // src/auth/migration.ts
776
+ async function handleAuthMigration(oldSchema, newSchema, node, dependencies) {
777
+ const oldAuth = oldSchema.authorization;
778
+ const newAuth = newSchema.authorization;
779
+ if (!oldAuth && newAuth) {
780
+ await dependencies.encryptExistingNode(node, newSchema);
781
+ return;
782
+ }
783
+ if (oldAuth && !newAuth) {
784
+ console.warn(
785
+ `[xnet:auth] Schema '${newSchema.name}' removed authorization block. Existing nodes remain encrypted until manually migrated.`
786
+ );
787
+ return;
788
+ }
789
+ if (!oldAuth || !newAuth) {
790
+ return;
791
+ }
792
+ const oldRecipients = await computeRecipients(oldSchema, node, dependencies);
793
+ const newRecipients = await computeRecipients(newSchema, node, dependencies);
794
+ if (!hasRecipientsChanged(oldRecipients, newRecipients)) {
795
+ return;
796
+ }
797
+ await dependencies.rotateContentKeyForNode(node.id, newRecipients);
798
+ }
799
+
800
+ // src/auth/grants.ts
801
+ var GRANT_SCHEMA_IRI = "xnet://xnet.fyi/Grant";
802
+ function isGrantActive(grant, now = Date.now()) {
803
+ const revokedAt = asNumber(grant.properties.revokedAt);
804
+ if (revokedAt > 0) {
805
+ return false;
806
+ }
807
+ const expiresAt = asNumber(grant.properties.expiresAt);
808
+ if (expiresAt > 0 && expiresAt <= now) {
809
+ return false;
810
+ }
811
+ return true;
812
+ }
813
+ var GrantIndex = class {
814
+ constructor(store, options = {}) {
815
+ this.store = store;
816
+ this.schemaId = options.schemaId ?? GRANT_SCHEMA_IRI;
817
+ this.clock = options.clock ?? Date.now;
818
+ }
819
+ byResourceAndGrantee = /* @__PURE__ */ new Map();
820
+ byResource = /* @__PURE__ */ new Map();
821
+ grantsById = /* @__PURE__ */ new Map();
822
+ unsubscribe = null;
823
+ schemaId;
824
+ clock;
825
+ async initialize() {
826
+ const grants = await this.loadGrantNodes();
827
+ for (const grant of grants) {
828
+ this.indexGrant(grant);
829
+ }
830
+ this.unsubscribe = this.store.subscribe((event) => {
831
+ const node = event.node;
832
+ if (!node || !this.isGrantSchema(node.schemaId)) {
833
+ return;
834
+ }
835
+ this.removeGrant(node.id);
836
+ if (!node.deleted) {
837
+ this.indexGrant(node);
838
+ }
839
+ });
840
+ }
841
+ findGrants(resource, grantee) {
842
+ const grantsByGrantee = this.byResourceAndGrantee.get(resource);
843
+ if (!grantsByGrantee) return [];
844
+ const grantIds = grantsByGrantee.get(grantee);
845
+ if (!grantIds) return [];
846
+ const now = this.clock();
847
+ const grants = [];
848
+ for (const grantId of grantIds) {
849
+ const grant = this.grantsById.get(grantId);
850
+ if (grant && isGrantActive(grant, now)) {
851
+ grants.push(grant);
422
852
  }
423
- break;
424
- case "not":
425
- count += countExpressionNodes(expr.expr);
426
- break;
853
+ }
854
+ return grants;
427
855
  }
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
- });
856
+ findGrantsForResource(resource) {
857
+ const grantIds = this.byResource.get(resource);
858
+ if (!grantIds) return [];
859
+ const now = this.clock();
860
+ const grants = [];
861
+ for (const grantId of grantIds) {
862
+ const grant = this.grantsById.get(grantId);
863
+ if (grant && isGrantActive(grant, now)) {
864
+ grants.push(grant);
449
865
  }
450
866
  }
867
+ return grants;
451
868
  }
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
- });
869
+ findAllGrantsForResource(resource) {
870
+ const grantIds = this.byResource.get(resource);
871
+ if (!grantIds) return [];
872
+ const grants = [];
873
+ for (const grantId of grantIds) {
874
+ const grant = this.grantsById.get(grantId);
875
+ if (grant) {
876
+ grants.push(grant);
467
877
  }
468
878
  }
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
- });
879
+ return grants;
880
+ }
881
+ findGrantsForGrantee(grantee) {
882
+ const grants = [];
883
+ const now = this.clock();
884
+ for (const grantsByGrantee of this.byResourceAndGrantee.values()) {
885
+ const grantIds = grantsByGrantee.get(grantee);
886
+ if (!grantIds) continue;
887
+ for (const grantId of grantIds) {
888
+ const grant = this.grantsById.get(grantId);
889
+ if (grant && isGrantActive(grant, now)) {
890
+ grants.push(grant);
891
+ }
483
892
  }
484
893
  }
894
+ return grants;
485
895
  }
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
- }
896
+ dispose() {
897
+ this.unsubscribe?.();
898
+ this.unsubscribe = null;
899
+ this.byResourceAndGrantee.clear();
900
+ this.byResource.clear();
901
+ this.grantsById.clear();
902
+ }
903
+ async loadGrantNodes() {
904
+ const directMatches = await this.store.list({ schemaId: this.schemaId, includeDeleted: false });
905
+ if (directMatches.length > 0 || this.schemaId.includes("@")) {
906
+ return directMatches;
495
907
  }
908
+ const allNodes = await this.store.list({ includeDeleted: false });
909
+ return allNodes.filter((node) => this.isGrantSchema(node.schemaId));
496
910
  }
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
- });
911
+ isGrantSchema(schemaId) {
912
+ return schemaId === this.schemaId || schemaId.startsWith(`${this.schemaId}@`);
913
+ }
914
+ indexGrant(node) {
915
+ const resource = asString(node.properties.resource);
916
+ const grantee = asDid(node.properties.grantee);
917
+ if (!resource || !grantee) {
918
+ return;
919
+ }
920
+ const grant = {
921
+ id: node.id,
922
+ properties: node.properties,
923
+ deleted: node.deleted
924
+ };
925
+ this.grantsById.set(node.id, grant);
926
+ let byGrantee = this.byResourceAndGrantee.get(resource);
927
+ if (!byGrantee) {
928
+ byGrantee = /* @__PURE__ */ new Map();
929
+ this.byResourceAndGrantee.set(resource, byGrantee);
930
+ }
931
+ let grantIdsForGrantee = byGrantee.get(grantee);
932
+ if (!grantIdsForGrantee) {
933
+ grantIdsForGrantee = /* @__PURE__ */ new Set();
934
+ byGrantee.set(grantee, grantIdsForGrantee);
935
+ }
936
+ grantIdsForGrantee.add(node.id);
937
+ let grantIdsForResource = this.byResource.get(resource);
938
+ if (!grantIdsForResource) {
939
+ grantIdsForResource = /* @__PURE__ */ new Set();
940
+ this.byResource.set(resource, grantIdsForResource);
505
941
  }
942
+ grantIdsForResource.add(node.id);
506
943
  }
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
- });
944
+ removeGrant(grantId) {
945
+ const existing = this.grantsById.get(grantId);
946
+ if (!existing) {
947
+ return;
948
+ }
949
+ const resource = asString(existing.properties.resource);
950
+ const grantee = asDid(existing.properties.grantee);
951
+ this.grantsById.delete(grantId);
952
+ if (!resource || !grantee) {
953
+ return;
954
+ }
955
+ const byGrantee = this.byResourceAndGrantee.get(resource);
956
+ if (byGrantee) {
957
+ const grantIds = byGrantee.get(grantee);
958
+ if (grantIds) {
959
+ grantIds.delete(grantId);
960
+ if (grantIds.size === 0) {
961
+ byGrantee.delete(grantee);
962
+ }
963
+ }
964
+ if (byGrantee.size === 0) {
965
+ this.byResourceAndGrantee.delete(resource);
515
966
  }
516
967
  }
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
- });
968
+ const byResource = this.byResource.get(resource);
969
+ if (byResource) {
970
+ byResource.delete(grantId);
971
+ if (byResource.size === 0) {
972
+ this.byResource.delete(resource);
973
+ }
527
974
  }
528
975
  }
529
- return { valid: errors.length === 0, errors };
976
+ };
977
+ function asDid(value) {
978
+ if (typeof value === "string" && value.startsWith("did:key:")) {
979
+ return value;
980
+ }
981
+ return null;
982
+ }
983
+ function asString(value) {
984
+ return typeof value === "string" ? value : null;
985
+ }
986
+ function asNumber(value) {
987
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
988
+ }
989
+
990
+ // src/auth/offline-policy.ts
991
+ var DEFAULT_OFFLINE_POLICY = {
992
+ decisionCacheTTL: 5 * 60 * 1e3,
993
+ maxStaleness: 60 * 60 * 1e3,
994
+ revalidation: "hybrid",
995
+ allowOfflineGrants: true
996
+ };
997
+ function mergeOfflinePolicy(current, patch) {
998
+ return {
999
+ ...current,
1000
+ ...patch
1001
+ };
530
1002
  }
531
1003
 
532
1004
  // src/auth/evaluator.ts
1005
+ var MAX_CONTAINER_DEPTH2 = 32;
533
1006
  var DecisionCache = class {
534
1007
  cache = /* @__PURE__ */ new Map();
535
1008
  ttlMs;
@@ -632,7 +1105,7 @@ var DefaultRoleResolver = class {
632
1105
  case "creator":
633
1106
  return [node.createdBy];
634
1107
  case "property":
635
- return readDidProperty(node.properties[resolver.propertyName]);
1108
+ return readDidProperty2(node.properties[resolver.propertyName]);
636
1109
  case "relation": {
637
1110
  const relatedNodes = await this.loadRelatedNodes(node, resolver.relationName);
638
1111
  if (relatedNodes.length === 0) {
@@ -661,6 +1134,20 @@ var DefaultRoleResolver = class {
661
1134
  }
662
1135
  return [...members];
663
1136
  }
1137
+ case "membership": {
1138
+ const containerIds = await this.collectContainerIds(node, resolver.parentProp);
1139
+ const edges = await this.store.list({ schemaId: resolver.edgeSchema });
1140
+ const members = /* @__PURE__ */ new Set();
1141
+ for (const edge of edges) {
1142
+ if (edge.deleted) continue;
1143
+ if (!this.membershipMatchesContainer(edge, resolver, containerIds)) continue;
1144
+ if (!membershipRoleAtLeast(edge.properties[resolver.roleProp], resolver)) continue;
1145
+ for (const did of readDidProperty2(edge.properties[resolver.memberProp])) {
1146
+ members.add(did);
1147
+ }
1148
+ }
1149
+ return [...members];
1150
+ }
664
1151
  }
665
1152
  }
666
1153
  async checkRole(did, resolver, node, depth, visited) {
@@ -676,7 +1163,7 @@ var DefaultRoleResolver = class {
676
1163
  case "creator":
677
1164
  return did === node.createdBy;
678
1165
  case "property":
679
- return readDidProperty(node.properties[resolver.propertyName]).includes(did);
1166
+ return readDidProperty2(node.properties[resolver.propertyName]).includes(did);
680
1167
  case "relation": {
681
1168
  const relatedNodes = await this.loadRelatedNodes(node, resolver.relationName);
682
1169
  for (const relatedNode of relatedNodes) {
@@ -696,7 +1183,47 @@ var DefaultRoleResolver = class {
696
1183
  }
697
1184
  return false;
698
1185
  }
1186
+ case "membership": {
1187
+ const containerIds = await this.collectContainerIds(node, resolver.parentProp);
1188
+ const edges = await this.store.list({ schemaId: resolver.edgeSchema });
1189
+ for (const edge of edges) {
1190
+ if (edge.deleted) continue;
1191
+ if (!this.membershipMatchesContainer(edge, resolver, containerIds)) continue;
1192
+ if (!readDidProperty2(edge.properties[resolver.memberProp]).includes(did)) continue;
1193
+ if (membershipRoleAtLeast(edge.properties[resolver.roleProp], resolver)) {
1194
+ return true;
1195
+ }
1196
+ }
1197
+ return false;
1198
+ }
1199
+ }
1200
+ }
1201
+ membershipMatchesContainer(edge, resolver, containerIds) {
1202
+ const value = edge.properties[resolver.containerProp];
1203
+ return typeof value === "string" && containerIds.has(value);
1204
+ }
1205
+ /**
1206
+ * The node's own id plus the ids of every ancestor reachable by walking
1207
+ * `parentProp`. Cycle- and depth-bounded. When `parentProp` is undefined the
1208
+ * set is just the node itself (no inheritance).
1209
+ */
1210
+ async collectContainerIds(node, parentProp) {
1211
+ const ids = /* @__PURE__ */ new Set([node.id]);
1212
+ if (!parentProp) {
1213
+ return ids;
1214
+ }
1215
+ let current = node;
1216
+ let depth = 0;
1217
+ while (current && depth < MAX_CONTAINER_DEPTH2) {
1218
+ const parentId = current.properties[parentProp];
1219
+ if (typeof parentId !== "string" || ids.has(parentId)) {
1220
+ break;
1221
+ }
1222
+ ids.add(parentId);
1223
+ current = await this.store.get(parentId);
1224
+ depth += 1;
699
1225
  }
1226
+ return ids;
700
1227
  }
701
1228
  async loadRelatedNodes(node, relationName) {
702
1229
  const value = node.properties[relationName];
@@ -766,7 +1293,8 @@ var DefaultPolicyEvaluator = class {
766
1293
  this.emitDecision(decision2);
767
1294
  return decision2;
768
1295
  }
769
- const cached = this.cache.get(input.subject, input.action, input.nodeId);
1296
+ const useCache = !input.patch;
1297
+ const cached = useCache ? this.cache.get(input.subject, input.action, input.nodeId) : void 0;
770
1298
  if (cached) {
771
1299
  const decision2 = {
772
1300
  ...cached,
@@ -789,15 +1317,24 @@ var DefaultPolicyEvaluator = class {
789
1317
  return decision2;
790
1318
  }
791
1319
  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);
1320
+ if (mode === "legacy" || !schema.schema.authorization) {
1321
+ if (node.createdBy === input.subject) {
1322
+ const decision3 = this.decision(input, true, ["owner"], start);
1323
+ if (useCache) this.cache.set(input.subject, input.action, input.nodeId, decision3);
1324
+ this.emitDecision(decision3);
1325
+ return decision3;
1326
+ }
1327
+ const grant2 = this.findMatchingGrant(
1328
+ input,
1329
+ this.grantIndex?.findGrants(input.nodeId, input.subject) ?? []
1330
+ );
1331
+ if (grant2) {
1332
+ const decision3 = this.decision(input, true, [], start, [grant2.id]);
1333
+ if (useCache) this.cache.set(input.subject, input.action, input.nodeId, decision3);
1334
+ this.emitDecision(decision3);
1335
+ return decision3;
1336
+ }
1337
+ const decision2 = this.deny(input, ["DENY_NO_ROLE_MATCH", "DENY_NO_GRANT"], start);
801
1338
  this.emitDecision(decision2);
802
1339
  return decision2;
803
1340
  }
@@ -823,7 +1360,7 @@ var DefaultPolicyEvaluator = class {
823
1360
  const allowedByRole = evaluateExpression(actionExpr, roles, true);
824
1361
  if (allowedByRole) {
825
1362
  const decision2 = this.decision(input, true, [...roles], start);
826
- this.cache.set(input.subject, input.action, input.nodeId, decision2);
1363
+ if (useCache) this.cache.set(input.subject, input.action, input.nodeId, decision2);
827
1364
  this.emitDecision(decision2);
828
1365
  return decision2;
829
1366
  }
@@ -833,7 +1370,7 @@ var DefaultPolicyEvaluator = class {
833
1370
  );
834
1371
  if (grant) {
835
1372
  const decision2 = this.decision(input, true, [...roles], start, [grant.id]);
836
- this.cache.set(input.subject, input.action, input.nodeId, decision2);
1373
+ if (useCache) this.cache.set(input.subject, input.action, input.nodeId, decision2);
837
1374
  this.emitDecision(decision2);
838
1375
  return decision2;
839
1376
  }
@@ -1050,16 +1587,24 @@ function matchesDenyExpression(expr, roles, isAuthenticated) {
1050
1587
  return false;
1051
1588
  }
1052
1589
  }
1053
- function readDidProperty(value) {
1590
+ function readDidProperty2(value) {
1054
1591
  if (Array.isArray(value)) {
1055
- return value.filter(isDid);
1592
+ return value.filter(isDid2);
1056
1593
  }
1057
- if (isDid(value)) {
1594
+ if (isDid2(value)) {
1058
1595
  return [value];
1059
1596
  }
1060
1597
  return [];
1061
1598
  }
1062
- function isDid(value) {
1599
+ function membershipRoleAtLeast(edgeRole, resolver) {
1600
+ if (typeof edgeRole !== "string") {
1601
+ return false;
1602
+ }
1603
+ const have = resolver.roleOrder.indexOf(edgeRole);
1604
+ const need = resolver.roleOrder.indexOf(resolver.minRole);
1605
+ return have >= 0 && need >= 0 && have >= need;
1606
+ }
1607
+ function isDid2(value) {
1063
1608
  return typeof value === "string" && value.startsWith("did:key:");
1064
1609
  }
1065
1610
  function parseActions(value) {
@@ -1079,6 +1624,20 @@ function parseActions(value) {
1079
1624
  return [];
1080
1625
  }
1081
1626
  }
1627
+ async function createPolicyEvaluator(options) {
1628
+ const grantIndex = new GrantIndex(options.store, {
1629
+ schemaId: options.grantSchemaId
1630
+ });
1631
+ await grantIndex.initialize();
1632
+ return new DefaultPolicyEvaluator({
1633
+ store: options.store,
1634
+ schemaRegistry: options.schemaRegistry,
1635
+ grantIndex,
1636
+ cache: options.cache,
1637
+ maxDepth: options.maxDepth,
1638
+ maxNodes: options.maxNodes
1639
+ });
1640
+ }
1082
1641
 
1083
1642
  // src/auth/grant-expiration-cleaner.ts
1084
1643
  var GrantExpirationCleaner = class _GrantExpirationCleaner {
@@ -1261,7 +1820,7 @@ var StoreAuth = class {
1261
1820
  expiresAt,
1262
1821
  parentUcanToken: parseString(parentGrant?.properties.ucanToken) ?? void 0
1263
1822
  });
1264
- const created2 = await this.options.store.create({
1823
+ const created = await this.options.store.create({
1265
1824
  schemaId: GRANT_SCHEMA_IRI,
1266
1825
  properties: {
1267
1826
  issuer: this.options.actorDid,
@@ -1292,7 +1851,7 @@ var StoreAuth = class {
1292
1851
  }
1293
1852
  this.options.evaluator.invalidateSubject(input.to);
1294
1853
  this.options.evaluator.invalidate(input.resource);
1295
- const node = await this.getGrantNodeOrThrow(created2.id);
1854
+ const node = await this.getGrantNodeOrThrow(created.id);
1296
1855
  return toGrant(node);
1297
1856
  }
1298
1857
  async revoke(input) {
@@ -1578,743 +2137,101 @@ function parseDuration(value) {
1578
2137
  }
1579
2138
  }
1580
2139
 
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) {
2140
+ // src/auth/auth-migrator.ts
2141
+ var AuthMigrator = class {
2142
+ constructor(store, schemaRegistry, publicKeyResolver, grantIndex, encryptionLayer) {
2143
+ this.store = store;
2144
+ this.schemaRegistry = schemaRegistry;
2145
+ this.publicKeyResolver = publicKeyResolver;
2146
+ this.grantIndex = grantIndex;
2147
+ this.encryptionLayer = encryptionLayer;
2148
+ }
2149
+ async migrateSchema(schemaIri, options = {}) {
2150
+ const schema = await this.schemaRegistry.get(schemaIri);
2151
+ if (!schema?.schema.authorization) {
2152
+ throw new Error(`Schema ${schemaIri} has no authorization block`);
2153
+ }
2154
+ const nodes = await this.store.list({ schemaId: schemaIri, includeDeleted: false });
2155
+ const total = nodes.length;
2156
+ const batchSize = Math.max(1, options.batchSize ?? 100);
2157
+ let migrated = 0;
2158
+ let failed = 0;
1661
2159
  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` });
2160
+ for (let index = 0; index < nodes.length; index += batchSize) {
2161
+ const batch = nodes.slice(index, index + batchSize);
2162
+ for (const node of batch) {
2163
+ try {
2164
+ const recipients = await computeRecipients(schema.schema, node, {
2165
+ getNode: (nodeId) => this.store.get(nodeId),
2166
+ getSchema: (schemaId) => this.resolveSchema(schemaId),
2167
+ grantIndex: this.grantIndex
2168
+ });
2169
+ const didRecipients = recipients.filter(
2170
+ (recipient) => recipient !== "PUBLIC"
2171
+ );
2172
+ const recipientKeys = await this.publicKeyResolver.resolveBatch(didRecipients);
2173
+ await this.encryptionLayer.encryptAndStoreNode(node, recipientKeys);
2174
+ migrated++;
2175
+ } catch (error) {
2176
+ failed++;
2177
+ errors.push({
2178
+ nodeId: node.id,
2179
+ error: error instanceof Error ? error.message : String(error)
2180
+ });
1688
2181
  }
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
2182
  }
2183
+ options.onProgress?.(migrated + failed, total);
1710
2184
  }
1711
- return node;
2185
+ return { total, migrated, failed, errors };
1712
2186
  }
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;
2187
+ async resolveSchema(schemaId) {
2188
+ const schema = await this.schemaRegistry.get(schemaId);
2189
+ return schema?.schema;
1863
2190
  }
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
- }
2191
+ };
2278
2192
 
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
- }
2193
+ // src/auth/rollout.ts
2194
+ var AUTH_FEATURE_FLAGS = {
2195
+ enforceLocal: false,
2196
+ enforceRemote: false,
2197
+ enforceHub: false,
2198
+ enforceEncryption: false,
2199
+ logDecisions: true
2200
+ };
2311
2201
 
2312
2202
  export {
2313
- parseSchemaIRI,
2314
- normalizeSchemaIRI,
2315
- getBaseSchemaIRI,
2316
- isNode,
2317
- createNodeId,
2203
+ allow,
2204
+ deny,
2205
+ and,
2206
+ or,
2207
+ not,
2208
+ PUBLIC,
2209
+ AUTHENTICATED,
2210
+ role,
2211
+ relation,
2212
+ BUILTIN_ROLES,
2213
+ extractRoleRefs,
2214
+ countExpressionNodes,
2215
+ hasPublicAccess,
2216
+ MAX_EXPRESSION_NODES,
2217
+ validateAuthorization,
2218
+ serializeAuthExpression,
2219
+ deserializeAuthExpression,
2220
+ serializeAuthorization,
2221
+ deserializeAuthorization,
2222
+ presets,
2223
+ describeRoleResolver,
2224
+ buildPermissionMatrix,
2225
+ getAuthMode,
2226
+ warnLegacySchema,
2227
+ schemaToHubPolicy,
2228
+ hubActionsForSpaceRole,
2229
+ PUBLIC_CONTENT_KEY,
2230
+ PUBLIC_RECIPIENT,
2231
+ computeRecipients,
2232
+ hasRecipientsChanged,
2233
+ canTransitionToPublic,
2234
+ handleAuthMigration,
2318
2235
  GRANT_SCHEMA_IRI,
2319
2236
  isGrantActive,
2320
2237
  GrantIndex,
@@ -2322,26 +2239,14 @@ export {
2322
2239
  mergeOfflinePolicy,
2323
2240
  DecisionCache,
2324
2241
  DefaultRoleResolver,
2242
+ evaluateExpression,
2325
2243
  DefaultPolicyEvaluator,
2244
+ createPolicyEvaluator,
2326
2245
  GrantExpirationCleaner,
2327
2246
  GrantRateLimiter,
2328
2247
  StoreAuthError,
2329
2248
  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
2249
+ AuthMigrator,
2250
+ AUTH_FEATURE_FLAGS,
2251
+ AUTH_ACTIONS2 as AUTH_ACTIONS
2347
2252
  };