@enbox/agent 0.5.10 → 0.5.12
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/dist/browser.mjs +9 -9
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-record-upgrade.js +1 -1
- package/dist/esm/dwn-record-upgrade.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/sync-closure-resolver.js +855 -0
- package/dist/esm/sync-closure-resolver.js.map +1 -0
- package/dist/esm/sync-closure-types.js +189 -0
- package/dist/esm/sync-closure-types.js.map +1 -0
- package/dist/esm/sync-engine-level.js +956 -37
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +42 -5
- package/dist/esm/sync-messages.js.map +1 -1
- package/dist/esm/sync-replication-ledger.js +220 -0
- package/dist/esm/sync-replication-ledger.js.map +1 -0
- package/dist/esm/types/sync.js +54 -1
- package/dist/esm/types/sync.js.map +1 -1
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/sync-closure-resolver.d.ts +19 -0
- package/dist/types/sync-closure-resolver.d.ts.map +1 -0
- package/dist/types/sync-closure-types.d.ts +122 -0
- package/dist/types/sync-closure-types.d.ts.map +1 -0
- package/dist/types/sync-engine-level.d.ts +137 -2
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +15 -1
- package/dist/types/sync-messages.d.ts.map +1 -1
- package/dist/types/sync-replication-ledger.d.ts +72 -0
- package/dist/types/sync-replication-ledger.d.ts.map +1 -0
- package/dist/types/types/sync.d.ts +190 -0
- package/dist/types/types/sync.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/dwn-api.ts +2 -1
- package/src/dwn-record-upgrade.ts +1 -1
- package/src/index.ts +5 -0
- package/src/sync-closure-resolver.ts +919 -0
- package/src/sync-closure-types.ts +270 -0
- package/src/sync-engine-level.ts +1041 -45
- package/src/sync-messages.ts +44 -6
- package/src/sync-replication-ledger.ts +197 -0
- package/src/types/sync.ts +204 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { Message } from '@enbox/dwn-sdk-js';
|
|
11
|
+
import { isMultiPartyContext } from './protocol-utils.js';
|
|
12
|
+
import { ClosureFailureCode, createClosureContext } from './sync-closure-types.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Dependency extraction helpers (one per dependency class)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Class 1: Protocol metadata closure.
|
|
18
|
+
* Extract the protocol URI from the message descriptor. The ProtocolsConfigure
|
|
19
|
+
* for that protocol must be present locally.
|
|
20
|
+
*/
|
|
21
|
+
function extractProtocolDeps(message) {
|
|
22
|
+
const desc = message.descriptor;
|
|
23
|
+
const protocol = desc.protocol;
|
|
24
|
+
if (!protocol) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return [{
|
|
28
|
+
dependencyClass: 1,
|
|
29
|
+
label: 'protocolsConfigure',
|
|
30
|
+
identifier: protocol,
|
|
31
|
+
identifierType: 'protocol',
|
|
32
|
+
}];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Class 2: Record ancestry closure.
|
|
36
|
+
* - initialWrite (for non-initial writes)
|
|
37
|
+
* - parentId chain
|
|
38
|
+
*/
|
|
39
|
+
function extractAncestryDeps(message) {
|
|
40
|
+
const desc = message.descriptor;
|
|
41
|
+
const edges = [];
|
|
42
|
+
// Only Records interface messages have ancestry dependencies.
|
|
43
|
+
if (desc.interface !== 'Records') {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const recordId = message.recordId;
|
|
47
|
+
// parentId dependency — the parent record must be present.
|
|
48
|
+
const parentId = desc.parentId;
|
|
49
|
+
if (parentId) {
|
|
50
|
+
edges.push({
|
|
51
|
+
dependencyClass: 2,
|
|
52
|
+
label: 'parentRecord',
|
|
53
|
+
identifier: parentId,
|
|
54
|
+
identifierType: 'recordId',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// contextId dependency — if the record has a contextId that differs from
|
|
58
|
+
// its own recordId, the context root record must be present. The contextId
|
|
59
|
+
// is a hierarchical path of recordIds (e.g., "rootId/childId/grandchildId").
|
|
60
|
+
// The context root is the first segment.
|
|
61
|
+
const contextId = message.contextId;
|
|
62
|
+
if (contextId && recordId && contextId !== recordId) {
|
|
63
|
+
const contextRootId = contextId.split('/')[0];
|
|
64
|
+
edges.push({
|
|
65
|
+
dependencyClass: 2,
|
|
66
|
+
label: 'contextRoot',
|
|
67
|
+
identifier: contextRootId,
|
|
68
|
+
identifierType: 'recordId',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// initialWrite dependency — non-initial writes need their initialWrite.
|
|
72
|
+
// An initial write has entryId === recordId, but we can't compute entryId here
|
|
73
|
+
// without the full CID computation. Instead, check dateCreated vs messageTimestamp
|
|
74
|
+
// as a heuristic, then the resolver will verify via the message store.
|
|
75
|
+
if (recordId && desc.method === 'Write') {
|
|
76
|
+
const dateCreated = desc.dateCreated;
|
|
77
|
+
const messageTimestamp = desc.messageTimestamp;
|
|
78
|
+
if (dateCreated && messageTimestamp && dateCreated !== messageTimestamp) {
|
|
79
|
+
// Non-initial write — needs the initialWrite.
|
|
80
|
+
edges.push({
|
|
81
|
+
dependencyClass: 2,
|
|
82
|
+
label: 'initialWrite',
|
|
83
|
+
identifier: recordId,
|
|
84
|
+
identifierType: 'recordId',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// RecordsDelete also needs the initialWrite for authorization and index construction.
|
|
89
|
+
if (recordId && desc.method === 'Delete') {
|
|
90
|
+
edges.push({
|
|
91
|
+
dependencyClass: 2,
|
|
92
|
+
label: 'initialWrite',
|
|
93
|
+
identifier: recordId,
|
|
94
|
+
identifierType: 'recordId',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return edges;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Class 3: Authorization closure.
|
|
101
|
+
* If the message uses a permissionGrantId, the grant record must be present.
|
|
102
|
+
*/
|
|
103
|
+
function extractAuthorizationDeps(message) {
|
|
104
|
+
var _a;
|
|
105
|
+
const edges = [];
|
|
106
|
+
const auth = message.authorization;
|
|
107
|
+
if (!auth) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
// The signature payload is at authorization.signature.payload (GeneralJws).
|
|
111
|
+
// This is the base64url-encoded JSON containing permissionGrantId, protocolRole, etc.
|
|
112
|
+
const payload = (_a = auth.signature) === null || _a === void 0 ? void 0 : _a.payload;
|
|
113
|
+
if (payload) {
|
|
114
|
+
try {
|
|
115
|
+
// Payload is base64url-encoded JSON.
|
|
116
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
|
|
117
|
+
if (decoded.permissionGrantId) {
|
|
118
|
+
edges.push({
|
|
119
|
+
dependencyClass: 3,
|
|
120
|
+
label: 'permissionGrant',
|
|
121
|
+
identifier: decoded.permissionGrantId,
|
|
122
|
+
identifierType: 'grantId',
|
|
123
|
+
});
|
|
124
|
+
// Also require the grant's revocation state to be resolvable.
|
|
125
|
+
// The revocation is a child record at protocolPath 'grant/revocation'
|
|
126
|
+
// with parentId === grantId. We add it as a separate edge so the
|
|
127
|
+
// resolver can check for its presence (or confirmed absence).
|
|
128
|
+
edges.push({
|
|
129
|
+
dependencyClass: 3,
|
|
130
|
+
label: 'grantRevocation',
|
|
131
|
+
identifier: decoded.permissionGrantId,
|
|
132
|
+
identifierType: 'grantId',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (_b) {
|
|
137
|
+
// If we can't decode, skip — authorization will fail at apply time.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return edges;
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Protocol-definition-aware dependency extraction (classes 4, 5, 6)
|
|
144
|
+
//
|
|
145
|
+
// These classes require the ProtocolDefinition to be available (fetched by
|
|
146
|
+
// class 1 resolution). They are called during BFS traversal, not as static
|
|
147
|
+
// extractors from the message alone.
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
/**
|
|
150
|
+
* Look up the ProtocolRuleSet for a given protocolPath within a protocol
|
|
151
|
+
* structure tree. Returns undefined if the path doesn't exist.
|
|
152
|
+
*
|
|
153
|
+
* Mirrors `getRuleSetAtPath` from `@enbox/dwn-sdk-js/src/utils/protocols.ts`
|
|
154
|
+
* (not in public API, reimplemented here to avoid internal path coupling).
|
|
155
|
+
*/
|
|
156
|
+
function getRuleSetAtProtocolPath(structure, protocolPath) {
|
|
157
|
+
if (!structure || !protocolPath) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const segments = protocolPath.split('/');
|
|
161
|
+
let currentLevel = structure;
|
|
162
|
+
let current;
|
|
163
|
+
for (const segment of segments) {
|
|
164
|
+
if (!Object.hasOwn(currentLevel, segment)) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
current = currentLevel[segment];
|
|
168
|
+
currentLevel = current;
|
|
169
|
+
}
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Extract protocol-definition-aware dependencies for classes 4, 5, and 6.
|
|
174
|
+
* Called during BFS traversal after the ProtocolDefinition has been fetched
|
|
175
|
+
* and cached (class 1 resolution ensures this).
|
|
176
|
+
*
|
|
177
|
+
* @param message - The message being evaluated.
|
|
178
|
+
* @param protocolDef - The cached ProtocolDefinition (from class 1 resolution).
|
|
179
|
+
* @param context - The evaluation context (for accessing `uses` map resolution).
|
|
180
|
+
*/
|
|
181
|
+
function extractProtocolAwareDeps(message, protocolDef) {
|
|
182
|
+
var _a, _b, _c;
|
|
183
|
+
const desc = message.descriptor;
|
|
184
|
+
if (desc.interface !== 'Records') {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const protocolPath = desc.protocolPath;
|
|
188
|
+
if (!protocolPath) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const edges = [];
|
|
192
|
+
const ruleSet = getRuleSetAtProtocolPath(protocolDef === null || protocolDef === void 0 ? void 0 : protocolDef.structure, protocolPath);
|
|
193
|
+
// --- Class 4: Squash / visibility floor ---
|
|
194
|
+
// The runtime's squash backstop derives scope entirely from the incoming
|
|
195
|
+
// message's own contextId (via Records.getParentContextFromOfContextId).
|
|
196
|
+
// It does NOT fetch any additional record to determine scope — the contextId
|
|
197
|
+
// is part of the message, and the $squash rule is in the ProtocolsConfigure
|
|
198
|
+
// (already a class 1 dependency). So no additional closure dependency is
|
|
199
|
+
// needed for squash. processRawMessage triggers the DWN's built-in squash
|
|
200
|
+
// resumable task which handles purging internally.
|
|
201
|
+
//
|
|
202
|
+
// Future work: if subset-specific squash side-effects are needed (where the
|
|
203
|
+
// consumer has records that the source purged), that belongs in the engine's
|
|
204
|
+
// post-apply logic, not in closure dependency extraction.
|
|
205
|
+
// --- Class 5: Encryption / key-delivery closure ---
|
|
206
|
+
// Two levels of dependency:
|
|
207
|
+
//
|
|
208
|
+
// Level 1 (protocol-definition): The ProtocolsConfigure with injected $encryption
|
|
209
|
+
// key material must be present. This is satisfied by class 1.
|
|
210
|
+
//
|
|
211
|
+
// Level 2 (key-delivery records): For multi-party contexts (where records use
|
|
212
|
+
// ProtocolContext encryption), the context key record in the key-delivery protocol
|
|
213
|
+
// must be locally present for the consumer to decrypt the record. Context keys are
|
|
214
|
+
// stored at protocolPath 'contextKey' in the key-delivery protocol, tagged with
|
|
215
|
+
// the source protocol URI and the root contextId.
|
|
216
|
+
if (ruleSet === null || ruleSet === void 0 ? void 0 : ruleSet.$encryption) {
|
|
217
|
+
const typeName = protocolPath.split('/').pop();
|
|
218
|
+
const typeDef = (_a = protocolDef === null || protocolDef === void 0 ? void 0 : protocolDef.types) === null || _a === void 0 ? void 0 : _a[typeName !== null && typeName !== void 0 ? typeName : ''];
|
|
219
|
+
if ((typeDef === null || typeDef === void 0 ? void 0 : typeDef.encryptionRequired) === true) {
|
|
220
|
+
// Level 1: protocol definition edge (already satisfied by class 1 ProtocolsConfigure).
|
|
221
|
+
edges.push({
|
|
222
|
+
dependencyClass: 5,
|
|
223
|
+
label: 'encryptionKeyMaterial',
|
|
224
|
+
identifier: desc.protocol,
|
|
225
|
+
identifierType: 'protocol',
|
|
226
|
+
});
|
|
227
|
+
// Level 2: key-delivery protocol record.
|
|
228
|
+
// The key-delivery protocol must be installed locally.
|
|
229
|
+
const keyDeliveryProtocol = 'https://identity.foundation/protocols/key-delivery';
|
|
230
|
+
edges.push({
|
|
231
|
+
dependencyClass: 5,
|
|
232
|
+
label: 'keyDeliveryProtocol',
|
|
233
|
+
identifier: keyDeliveryProtocol,
|
|
234
|
+
identifierType: 'protocol',
|
|
235
|
+
});
|
|
236
|
+
// Level 3: Context key enforcement for multi-party contexts.
|
|
237
|
+
// Uses isMultiPartyContext() from protocol-utils to determine whether the
|
|
238
|
+
// record's root protocol path has $role descendants or relational who/of
|
|
239
|
+
// read rules — these indicate multi-party access where ProtocolContext
|
|
240
|
+
// encryption is used and a contextKey record is required for decryption.
|
|
241
|
+
//
|
|
242
|
+
// Single-party contexts use ProtocolPath encryption (owner-only) and do
|
|
243
|
+
// NOT need a context key — the edge is skipped entirely.
|
|
244
|
+
const contextId = message.contextId;
|
|
245
|
+
if (contextId) {
|
|
246
|
+
const rootProtocolPath = protocolPath.split('/')[0];
|
|
247
|
+
const multiParty = isMultiPartyContext(protocolDef, rootProtocolPath);
|
|
248
|
+
if (multiParty) {
|
|
249
|
+
const rootContextId = contextId.split('/')[0];
|
|
250
|
+
// Separator is '|' (not ':') because protocol URIs contain '://'
|
|
251
|
+
// which would break indexOf(':') parsing in resolveDependency.
|
|
252
|
+
edges.push({
|
|
253
|
+
dependencyClass: 5,
|
|
254
|
+
label: 'contextKeyRecord',
|
|
255
|
+
identifier: `${desc.protocol}|${rootContextId}`,
|
|
256
|
+
identifierType: 'messageCid',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// --- Class 6: Cross-protocol $ref closure ---
|
|
263
|
+
// Three levels of dependency when a record is at or below a $ref node:
|
|
264
|
+
//
|
|
265
|
+
// Level 1 (protocol config): The referenced protocol's ProtocolsConfigure must be present.
|
|
266
|
+
// Level 2 (parent record): If the record's path has exactly 2 segments (e.g., thread/comment),
|
|
267
|
+
// the parent record lives in the REFERENCED protocol, not the composing protocol.
|
|
268
|
+
// Level 3 (cross-protocol role): If the message's authorization invokes a cross-protocol role
|
|
269
|
+
// (format "alias:path"), the role record in the referenced protocol must be locally present.
|
|
270
|
+
const firstSegment = protocolPath.split('/')[0];
|
|
271
|
+
const rootRuleSet = (_b = protocolDef === null || protocolDef === void 0 ? void 0 : protocolDef.structure) === null || _b === void 0 ? void 0 : _b[firstSegment];
|
|
272
|
+
if (rootRuleSet === null || rootRuleSet === void 0 ? void 0 : rootRuleSet.$ref) {
|
|
273
|
+
const colonIdx = rootRuleSet.$ref.indexOf(':');
|
|
274
|
+
if (colonIdx > 0) {
|
|
275
|
+
const alias = rootRuleSet.$ref.substring(0, colonIdx);
|
|
276
|
+
const usesMap = protocolDef === null || protocolDef === void 0 ? void 0 : protocolDef.uses;
|
|
277
|
+
const referencedProtocol = usesMap === null || usesMap === void 0 ? void 0 : usesMap[alias];
|
|
278
|
+
if (referencedProtocol) {
|
|
279
|
+
// Level 1: referenced protocol ProtocolsConfigure.
|
|
280
|
+
edges.push({
|
|
281
|
+
dependencyClass: 6,
|
|
282
|
+
label: 'crossProtocolConfig',
|
|
283
|
+
identifier: referencedProtocol,
|
|
284
|
+
identifierType: 'protocol',
|
|
285
|
+
});
|
|
286
|
+
// Level 2: $ref parent record in the referenced protocol.
|
|
287
|
+
// When path depth is 2 (e.g., "thread/comment"), the parent is the $ref node
|
|
288
|
+
// itself, which lives in the referenced protocol. The parent's recordId is
|
|
289
|
+
// the message's parentId. The query MUST be scoped to the referenced
|
|
290
|
+
// protocol URI to prevent false matches from the composing protocol.
|
|
291
|
+
// Identifier format: "referencedProtocol|parentId"
|
|
292
|
+
const pathSegments = protocolPath.split('/');
|
|
293
|
+
const parentId = desc.parentId;
|
|
294
|
+
if (pathSegments.length === 2 && parentId) {
|
|
295
|
+
edges.push({
|
|
296
|
+
dependencyClass: 6,
|
|
297
|
+
label: 'crossProtocolParent',
|
|
298
|
+
identifier: `${referencedProtocol}|${parentId}`,
|
|
299
|
+
identifierType: 'recordId',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Level 3: Cross-protocol role records.
|
|
306
|
+
// If the message's authorization invokes a role in "alias:path" format,
|
|
307
|
+
// the role record must be locally present in the referenced protocol.
|
|
308
|
+
// This is extracted from the authorization payload regardless of $ref presence.
|
|
309
|
+
const auth = message.authorization;
|
|
310
|
+
if (auth && (protocolDef === null || protocolDef === void 0 ? void 0 : protocolDef.uses)) {
|
|
311
|
+
// Same payload path as class 3: authorization.signature.payload
|
|
312
|
+
const payload = (_c = auth.signature) === null || _c === void 0 ? void 0 : _c.payload;
|
|
313
|
+
if (payload) {
|
|
314
|
+
try {
|
|
315
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
|
|
316
|
+
const protocolRole = decoded.protocolRole;
|
|
317
|
+
if (protocolRole && protocolRole.includes(':')) {
|
|
318
|
+
// Cross-protocol role: "alias:protocolPath"
|
|
319
|
+
const roleColonIdx = protocolRole.indexOf(':');
|
|
320
|
+
const roleAlias = protocolRole.substring(0, roleColonIdx);
|
|
321
|
+
const roleProtocol = protocolDef.uses[roleAlias];
|
|
322
|
+
if (roleProtocol) {
|
|
323
|
+
// The referenced protocol's ProtocolsConfigure (may already be added above).
|
|
324
|
+
edges.push({
|
|
325
|
+
dependencyClass: 6,
|
|
326
|
+
label: 'crossProtocolRoleConfig',
|
|
327
|
+
identifier: roleProtocol,
|
|
328
|
+
identifierType: 'protocol',
|
|
329
|
+
});
|
|
330
|
+
// The actual role record in the referenced protocol.
|
|
331
|
+
// Mirrors verifyInvokedRole() from protocol-authorization-action.ts:
|
|
332
|
+
// queries by protocol + protocolPath + recipient + contextId prefix.
|
|
333
|
+
const roleProtocolPath = protocolRole.substring(roleColonIdx + 1);
|
|
334
|
+
// Derive author from the message's JWS signature, not from the
|
|
335
|
+
// payload. The payload does NOT contain an author field — the
|
|
336
|
+
// DWN SDK extracts it from the signature's `kid` header via
|
|
337
|
+
// Message.getAuthor().
|
|
338
|
+
const messageAuthor = Message.getAuthor(message);
|
|
339
|
+
const messageContextId = message.contextId;
|
|
340
|
+
if (messageAuthor) {
|
|
341
|
+
// Build the role record query filter.
|
|
342
|
+
const roleFilter = {
|
|
343
|
+
interface: 'Records',
|
|
344
|
+
method: 'Write',
|
|
345
|
+
protocol: roleProtocol,
|
|
346
|
+
protocolPath: roleProtocolPath,
|
|
347
|
+
recipient: messageAuthor,
|
|
348
|
+
isLatestBaseState: true,
|
|
349
|
+
};
|
|
350
|
+
// Context scoping: the role record must be within the same context.
|
|
351
|
+
// The ancestor segment count determines how many contextId segments
|
|
352
|
+
// to use as the prefix filter (matching verifyInvokedRole logic).
|
|
353
|
+
if (messageContextId) {
|
|
354
|
+
const ancestorCount = roleProtocolPath.split('/').length - 1;
|
|
355
|
+
if (ancestorCount > 0) {
|
|
356
|
+
const contextSegments = messageContextId.split('/');
|
|
357
|
+
const contextPrefix = contextSegments.slice(0, ancestorCount).join('/');
|
|
358
|
+
if (contextPrefix) {
|
|
359
|
+
roleFilter.contextId = { gte: contextPrefix, lt: contextPrefix + '\uffff' };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Cache key must include the context prefix to prevent false
|
|
364
|
+
// positives across different contexts (e.g., different threads).
|
|
365
|
+
const contextPrefix = roleFilter.contextId
|
|
366
|
+
? JSON.stringify(roleFilter.contextId) : 'no-context';
|
|
367
|
+
edges.push({
|
|
368
|
+
dependencyClass: 6,
|
|
369
|
+
label: 'crossProtocolRoleRecord',
|
|
370
|
+
identifier: `${roleProtocol}|${roleProtocolPath}|${messageAuthor}|${contextPrefix}`,
|
|
371
|
+
identifierType: 'filter',
|
|
372
|
+
filter: roleFilter,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (_d) {
|
|
379
|
+
// If we can't decode the payload, skip.
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return edges;
|
|
384
|
+
}
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Closure resolver
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
/**
|
|
389
|
+
* Evaluates closure completeness for a single operation (closure root).
|
|
390
|
+
*
|
|
391
|
+
* Uses BFS traversal with deduplication and depth limiting. Shared caching
|
|
392
|
+
* across evaluation batches via {@link ClosureEvaluationContext}.
|
|
393
|
+
*
|
|
394
|
+
* The resolver queries the local MessageStore directly (bypassing auth)
|
|
395
|
+
* because it needs to verify dependency presence, not access-control the
|
|
396
|
+
* syncing agent's own local store.
|
|
397
|
+
*/
|
|
398
|
+
export function evaluateClosure(message, messageStore, scope, context) {
|
|
399
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
400
|
+
var _a;
|
|
401
|
+
// Full-tenant scope bypasses closure evaluation entirely.
|
|
402
|
+
if (scope.kind === 'full') {
|
|
403
|
+
const cid = yield Message.getCid(message);
|
|
404
|
+
return {
|
|
405
|
+
complete: true,
|
|
406
|
+
rootMessageCid: cid,
|
|
407
|
+
edges: [],
|
|
408
|
+
depth: 0,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const rootCid = yield Message.getCid(message);
|
|
412
|
+
const allEdges = [];
|
|
413
|
+
const visited = new Set();
|
|
414
|
+
let currentDepth = 0;
|
|
415
|
+
// BFS queue: each item is a message to evaluate for dependencies.
|
|
416
|
+
const queue = [message];
|
|
417
|
+
visited.add(rootCid);
|
|
418
|
+
while (queue.length > 0) {
|
|
419
|
+
if (currentDepth >= context.maxDepth) {
|
|
420
|
+
return {
|
|
421
|
+
complete: false,
|
|
422
|
+
rootMessageCid: rootCid,
|
|
423
|
+
edges: allEdges,
|
|
424
|
+
failure: {
|
|
425
|
+
code: ClosureFailureCode.DepthExceeded,
|
|
426
|
+
edge: { dependencyClass: 1, label: 'depth', identifier: String(currentDepth), identifierType: 'messageCid' },
|
|
427
|
+
detail: `closure traversal exceeded max depth of ${context.maxDepth}`,
|
|
428
|
+
},
|
|
429
|
+
depth: currentDepth,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const batchSize = queue.length;
|
|
433
|
+
for (let i = 0; i < batchSize; i++) {
|
|
434
|
+
const current = queue.shift();
|
|
435
|
+
// Phase 1: Extract and resolve static dependency edges (classes 1-3).
|
|
436
|
+
// This populates the protocolCache when class 1 (ProtocolsConfigure)
|
|
437
|
+
// is resolved, which is needed by classes 4-6.
|
|
438
|
+
const staticEdges = [
|
|
439
|
+
...extractProtocolDeps(current),
|
|
440
|
+
...extractAncestryDeps(current),
|
|
441
|
+
...extractAuthorizationDeps(current),
|
|
442
|
+
];
|
|
443
|
+
const resolveResult = yield resolveEdges(staticEdges, allEdges, messageStore, context, visited, queue, rootCid, currentDepth);
|
|
444
|
+
if (resolveResult) {
|
|
445
|
+
return resolveResult;
|
|
446
|
+
} // Early failure.
|
|
447
|
+
// Phase 2: Extract protocol-definition-aware edges (classes 4-6).
|
|
448
|
+
// Runs AFTER static resolution so the ProtocolDefinition is in the cache.
|
|
449
|
+
const currentDesc = current.descriptor;
|
|
450
|
+
const currentProtocol = currentDesc.protocol;
|
|
451
|
+
if (currentProtocol) {
|
|
452
|
+
const cachedProtocolMsg = context.protocolCache.get(currentProtocol);
|
|
453
|
+
const protocolDef = (_a = cachedProtocolMsg === null || cachedProtocolMsg === void 0 ? void 0 : cachedProtocolMsg.descriptor) === null || _a === void 0 ? void 0 : _a.definition;
|
|
454
|
+
if (protocolDef) {
|
|
455
|
+
const protoAwareEdges = extractProtocolAwareDeps(current, protocolDef);
|
|
456
|
+
const protoResult = yield resolveEdges(protoAwareEdges, allEdges, messageStore, context, visited, queue, rootCid, currentDepth);
|
|
457
|
+
if (protoResult) {
|
|
458
|
+
return protoResult;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Phase 3: Validate grant temporal ordering (causal grant check).
|
|
463
|
+
// After all edges are resolved, if this message uses a grant, verify
|
|
464
|
+
// that the grant is temporally valid at the message's commit point:
|
|
465
|
+
// - grant.dateGranted <= message.messageTimestamp < grant.dateExpires
|
|
466
|
+
// - no revocation exists with revocation.messageTimestamp <= message.messageTimestamp
|
|
467
|
+
// This runs only for the root message (currentDepth === 0) to avoid
|
|
468
|
+
// redundant checks on transitive dependencies.
|
|
469
|
+
if (currentDepth === 0) {
|
|
470
|
+
const grantCheckResult = validateGrantTemporal(current, allEdges, context, rootCid, currentDepth);
|
|
471
|
+
if (grantCheckResult) {
|
|
472
|
+
return grantCheckResult;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
currentDepth++;
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
complete: true,
|
|
480
|
+
rootMessageCid: rootCid,
|
|
481
|
+
edges: allEdges,
|
|
482
|
+
depth: currentDepth,
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Evaluate closure for a batch of messages. Shares caching across all roots.
|
|
488
|
+
*/
|
|
489
|
+
export function evaluateClosureBatch(messages, messageStore, scope, tenantDid, maxDepth) {
|
|
490
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
491
|
+
const context = createClosureContext(tenantDid, maxDepth);
|
|
492
|
+
const results = [];
|
|
493
|
+
for (const msg of messages) {
|
|
494
|
+
const result = yield evaluateClosure(msg, messageStore, scope, context);
|
|
495
|
+
results.push(result);
|
|
496
|
+
}
|
|
497
|
+
return results;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Causal grant ordering validation
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
/**
|
|
504
|
+
* Validates that any permission grant used by the message is temporally valid
|
|
505
|
+
* at the message's commit point. Returns a failure result if the grant is
|
|
506
|
+
* expired or revoked at the message's timestamp, null if valid or no grant used.
|
|
507
|
+
*
|
|
508
|
+
* Temporal model (from grant-authorization.ts):
|
|
509
|
+
* grant.dateGranted <= message.messageTimestamp < grant.dateExpires
|
|
510
|
+
* AND (no revocation OR revocation.messageTimestamp > message.messageTimestamp)
|
|
511
|
+
*/
|
|
512
|
+
function validateGrantTemporal(message, allEdges, context, rootCid, currentDepth) {
|
|
513
|
+
var _a;
|
|
514
|
+
// Check if this message has a grant dependency.
|
|
515
|
+
const grantEdge = allEdges.find(e => e.label === 'permissionGrant');
|
|
516
|
+
if (!grantEdge) {
|
|
517
|
+
return null;
|
|
518
|
+
} // No grant used.
|
|
519
|
+
const messageTimestamp = message.descriptor.messageTimestamp;
|
|
520
|
+
if (!messageTimestamp) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
// Look up the resolved grant record from the cache.
|
|
524
|
+
const grantRecord = context.grantCache.get(grantEdge.identifier);
|
|
525
|
+
if (!grantRecord) {
|
|
526
|
+
return null;
|
|
527
|
+
} // Grant wasn't resolved (would have failed earlier).
|
|
528
|
+
// Extract grant temporal bounds.
|
|
529
|
+
// dateGranted = descriptor.dateCreated (set by PermissionGrant constructor)
|
|
530
|
+
const dateGranted = grantRecord.descriptor.dateCreated;
|
|
531
|
+
// dateExpires is in the encoded data payload (base64url JSON).
|
|
532
|
+
let dateExpires;
|
|
533
|
+
const encodedData = grantRecord.encodedData;
|
|
534
|
+
if (encodedData) {
|
|
535
|
+
try {
|
|
536
|
+
const grantData = JSON.parse(Buffer.from(encodedData, 'base64url').toString('utf-8'));
|
|
537
|
+
dateExpires = grantData.dateExpires;
|
|
538
|
+
}
|
|
539
|
+
catch ( /* ignore parse errors */_b) { /* ignore parse errors */ }
|
|
540
|
+
}
|
|
541
|
+
// Check: grant not yet active.
|
|
542
|
+
if (dateGranted && messageTimestamp < dateGranted) {
|
|
543
|
+
return {
|
|
544
|
+
complete: false,
|
|
545
|
+
rootMessageCid: rootCid,
|
|
546
|
+
edges: allEdges,
|
|
547
|
+
failure: {
|
|
548
|
+
code: ClosureFailureCode.GrantNotYetActive,
|
|
549
|
+
edge: grantEdge,
|
|
550
|
+
detail: `grant '${grantEdge.identifier}' is not yet active at message timestamp ${messageTimestamp} (dateGranted: ${dateGranted})`,
|
|
551
|
+
},
|
|
552
|
+
depth: currentDepth,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
// Check: grant expired.
|
|
556
|
+
if (dateExpires && messageTimestamp >= dateExpires) {
|
|
557
|
+
return {
|
|
558
|
+
complete: false,
|
|
559
|
+
rootMessageCid: rootCid,
|
|
560
|
+
edges: allEdges,
|
|
561
|
+
failure: {
|
|
562
|
+
code: ClosureFailureCode.GrantExpired,
|
|
563
|
+
edge: grantEdge,
|
|
564
|
+
detail: `grant '${grantEdge.identifier}' expired at ${dateExpires}, message timestamp is ${messageTimestamp}`,
|
|
565
|
+
},
|
|
566
|
+
depth: currentDepth,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
// Check: grant revoked at or before message timestamp.
|
|
570
|
+
const revocationCacheKey = `revocation:${grantEdge.identifier}`;
|
|
571
|
+
const revocationRecord = context.grantCache.get(revocationCacheKey);
|
|
572
|
+
if (revocationRecord && revocationRecord.descriptor.interface !== 'Synthetic') {
|
|
573
|
+
const revocationTimestamp = revocationRecord.descriptor.messageTimestamp;
|
|
574
|
+
if (revocationTimestamp && revocationTimestamp <= messageTimestamp) {
|
|
575
|
+
return {
|
|
576
|
+
complete: false,
|
|
577
|
+
rootMessageCid: rootCid,
|
|
578
|
+
edges: allEdges,
|
|
579
|
+
failure: {
|
|
580
|
+
code: ClosureFailureCode.GrantRevocationMissing,
|
|
581
|
+
edge: (_a = allEdges.find(e => e.label === 'grantRevocation')) !== null && _a !== void 0 ? _a : grantEdge,
|
|
582
|
+
detail: `grant '${grantEdge.identifier}' was revoked at ${revocationTimestamp}, before message timestamp ${messageTimestamp}`,
|
|
583
|
+
},
|
|
584
|
+
depth: currentDepth,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return null; // Grant is temporally valid.
|
|
589
|
+
}
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Internal helpers
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
/**
|
|
594
|
+
* Resolve a list of dependency edges. Returns a ClosureResult on failure,
|
|
595
|
+
* or null if all edges were resolved successfully.
|
|
596
|
+
*/
|
|
597
|
+
function resolveEdges(edges, allEdges, messageStore, context, visited, queue, rootCid, currentDepth) {
|
|
598
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
599
|
+
for (const edge of edges) {
|
|
600
|
+
allEdges.push(edge);
|
|
601
|
+
// Include label in dep key to distinguish edges with the same identifier
|
|
602
|
+
// but different semantics (e.g., permissionGrant vs grantRevocation both
|
|
603
|
+
// use identifierType:'grantId' with the same grantId).
|
|
604
|
+
const depKey = `${edge.label}:${edge.identifierType}:${edge.identifier}`;
|
|
605
|
+
if (context.satisfiedDeps.has(depKey)) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (context.missingDeps.has(depKey)) {
|
|
609
|
+
return {
|
|
610
|
+
complete: false,
|
|
611
|
+
rootMessageCid: rootCid,
|
|
612
|
+
edges: allEdges,
|
|
613
|
+
failure: {
|
|
614
|
+
code: mapEdgeToFailureCode(edge),
|
|
615
|
+
edge,
|
|
616
|
+
detail: `dependency '${edge.label}' (${edge.identifier}) is missing`,
|
|
617
|
+
},
|
|
618
|
+
depth: currentDepth,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const resolved = yield resolveDependency(edge, messageStore, context);
|
|
622
|
+
if (!resolved) {
|
|
623
|
+
context.missingDeps.add(depKey);
|
|
624
|
+
return {
|
|
625
|
+
complete: false,
|
|
626
|
+
rootMessageCid: rootCid,
|
|
627
|
+
edges: allEdges,
|
|
628
|
+
failure: {
|
|
629
|
+
code: mapEdgeToFailureCode(edge),
|
|
630
|
+
edge,
|
|
631
|
+
detail: `dependency '${edge.label}' (${edge.identifier}) not found locally`,
|
|
632
|
+
},
|
|
633
|
+
depth: currentDepth,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
context.satisfiedDeps.add(depKey);
|
|
637
|
+
// Add resolved record to the BFS queue for transitive dependency evaluation,
|
|
638
|
+
// but skip leaf dependency types that don't have their own closure requirements:
|
|
639
|
+
// - grantId: grant/revocation records are leaf nodes
|
|
640
|
+
// - filter: filter-resolved records (e.g., role records) are leaf nodes
|
|
641
|
+
// - messageCid with contextKeyRecord label: key-delivery records are leaf nodes
|
|
642
|
+
const isLeafDep = edge.identifierType === 'grantId' ||
|
|
643
|
+
edge.identifierType === 'filter' ||
|
|
644
|
+
edge.label === 'contextKeyRecord';
|
|
645
|
+
if (!isLeafDep) {
|
|
646
|
+
const resolvedCid = yield Message.getCid(resolved);
|
|
647
|
+
if (!visited.has(resolvedCid)) {
|
|
648
|
+
visited.add(resolvedCid);
|
|
649
|
+
queue.push(resolved);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return null; // All edges resolved successfully.
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Resolve a dependency edge by querying the local MessageStore.
|
|
658
|
+
* Returns the resolved message or null if not found.
|
|
659
|
+
*/
|
|
660
|
+
function resolveDependency(edge, messageStore, context) {
|
|
661
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
662
|
+
var _a, _b, _c, _d;
|
|
663
|
+
switch (edge.identifierType) {
|
|
664
|
+
case 'protocol': {
|
|
665
|
+
// Check cache first.
|
|
666
|
+
if (context.protocolCache.has(edge.identifier)) {
|
|
667
|
+
return (_a = context.protocolCache.get(edge.identifier)) !== null && _a !== void 0 ? _a : null;
|
|
668
|
+
}
|
|
669
|
+
// Query for ProtocolsConfigure.
|
|
670
|
+
const { messages } = yield messageStore.query(context.tenantDid, [{
|
|
671
|
+
interface: 'Protocols',
|
|
672
|
+
method: 'Configure',
|
|
673
|
+
protocol: edge.identifier,
|
|
674
|
+
isLatestBaseState: true,
|
|
675
|
+
}]);
|
|
676
|
+
const found = messages.length > 0 ? messages[0] : null;
|
|
677
|
+
context.protocolCache.set(edge.identifier, found);
|
|
678
|
+
return found;
|
|
679
|
+
}
|
|
680
|
+
case 'recordId': {
|
|
681
|
+
if (edge.label === 'initialWrite') {
|
|
682
|
+
// Query specifically for the initial write: entryId === recordId,
|
|
683
|
+
// stored with isLatestBaseState: false after subsequent writes.
|
|
684
|
+
// Try entryId first (canonical), fall back to recordId + non-latest.
|
|
685
|
+
const { messages: byEntryId } = yield messageStore.query(context.tenantDid, [{
|
|
686
|
+
entryId: edge.identifier,
|
|
687
|
+
}]);
|
|
688
|
+
if (byEntryId.length > 0) {
|
|
689
|
+
return byEntryId[0];
|
|
690
|
+
}
|
|
691
|
+
// Fallback: query by recordId with isLatestBaseState: false
|
|
692
|
+
// (initial writes are re-stored with this flag after updates).
|
|
693
|
+
const { messages: byRecordId } = yield messageStore.query(context.tenantDid, [{
|
|
694
|
+
interface: 'Records',
|
|
695
|
+
method: 'Write',
|
|
696
|
+
recordId: edge.identifier,
|
|
697
|
+
isLatestBaseState: false,
|
|
698
|
+
}]);
|
|
699
|
+
return byRecordId.length > 0 ? byRecordId[0] : null;
|
|
700
|
+
}
|
|
701
|
+
// crossProtocolParent: identifier is "referencedProtocol|parentId"
|
|
702
|
+
// Query MUST be scoped to the referenced protocol URI.
|
|
703
|
+
if (edge.label === 'crossProtocolParent') {
|
|
704
|
+
const pipeIdx = edge.identifier.indexOf('|');
|
|
705
|
+
if (pipeIdx > 0) {
|
|
706
|
+
const refProtocol = edge.identifier.substring(0, pipeIdx);
|
|
707
|
+
const refParentId = edge.identifier.substring(pipeIdx + 1);
|
|
708
|
+
const { messages: refParents } = yield messageStore.query(context.tenantDid, [{
|
|
709
|
+
interface: 'Records',
|
|
710
|
+
method: 'Write',
|
|
711
|
+
protocol: refProtocol,
|
|
712
|
+
recordId: refParentId,
|
|
713
|
+
isLatestBaseState: true,
|
|
714
|
+
}]);
|
|
715
|
+
return refParents.length > 0 ? refParents[0] : null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// For parentRecord, contextRoot, or other recordId lookups, query latest state.
|
|
719
|
+
const { messages } = yield messageStore.query(context.tenantDid, [{
|
|
720
|
+
interface: 'Records',
|
|
721
|
+
recordId: edge.identifier,
|
|
722
|
+
isLatestBaseState: true,
|
|
723
|
+
}]);
|
|
724
|
+
return messages.length > 0 ? messages[0] : null;
|
|
725
|
+
}
|
|
726
|
+
case 'grantId': {
|
|
727
|
+
if (edge.label === 'grantRevocation') {
|
|
728
|
+
// Query for revocation records: child records at 'grant/revocation'
|
|
729
|
+
// protocolPath with parentId === grantId. The absence of a revocation
|
|
730
|
+
// is a valid result (grant is not revoked) — return a synthetic
|
|
731
|
+
// "no revocation" marker so the dependency is considered satisfied.
|
|
732
|
+
const cacheKey = `revocation:${edge.identifier}`;
|
|
733
|
+
if (context.grantCache.has(cacheKey)) {
|
|
734
|
+
return (_b = context.grantCache.get(cacheKey)) !== null && _b !== void 0 ? _b : null;
|
|
735
|
+
}
|
|
736
|
+
const { messages: revocations } = yield messageStore.query(context.tenantDid, [{
|
|
737
|
+
interface: 'Records',
|
|
738
|
+
method: 'Write',
|
|
739
|
+
protocol: 'https://identity.foundation/dwn/permissions',
|
|
740
|
+
parentId: edge.identifier,
|
|
741
|
+
protocolPath: 'grant/revocation',
|
|
742
|
+
isLatestBaseState: true,
|
|
743
|
+
}]);
|
|
744
|
+
// Store the result. If no revocation exists, store an explicit synthetic
|
|
745
|
+
// sentinel — never use the grant record as a sentinel, because
|
|
746
|
+
// validateGrantTemporal checks descriptor.interface !== 'Synthetic' to
|
|
747
|
+
// distinguish real revocations from "no revocation" markers.
|
|
748
|
+
// Select the oldest revocation (matching SDK's Message.getOldestMessage logic).
|
|
749
|
+
// The oldest revocation determines the temporal boundary — messages at or after
|
|
750
|
+
// its timestamp are rejected.
|
|
751
|
+
let oldestRevocation;
|
|
752
|
+
for (const rev of revocations) {
|
|
753
|
+
const revTs = rev.descriptor.messageTimestamp;
|
|
754
|
+
if (!oldestRevocation || revTs < oldestRevocation.descriptor.messageTimestamp) {
|
|
755
|
+
oldestRevocation = rev;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const noRevocationSentinel = { descriptor: { interface: 'Synthetic', method: 'NoRevocation' } };
|
|
759
|
+
const result = oldestRevocation !== null && oldestRevocation !== void 0 ? oldestRevocation : noRevocationSentinel;
|
|
760
|
+
context.grantCache.set(cacheKey, result);
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
// Grant record lookup.
|
|
764
|
+
if (context.grantCache.has(edge.identifier)) {
|
|
765
|
+
return (_c = context.grantCache.get(edge.identifier)) !== null && _c !== void 0 ? _c : null;
|
|
766
|
+
}
|
|
767
|
+
const { messages } = yield messageStore.query(context.tenantDid, [{
|
|
768
|
+
interface: 'Records',
|
|
769
|
+
method: 'Write',
|
|
770
|
+
protocol: 'https://identity.foundation/dwn/permissions',
|
|
771
|
+
protocolPath: 'grant',
|
|
772
|
+
recordId: edge.identifier,
|
|
773
|
+
isLatestBaseState: true,
|
|
774
|
+
}]);
|
|
775
|
+
const found = messages.length > 0 ? messages[0] : null;
|
|
776
|
+
context.grantCache.set(edge.identifier, found);
|
|
777
|
+
return found;
|
|
778
|
+
}
|
|
779
|
+
case 'messageCid': {
|
|
780
|
+
// Special case: contextKeyRecord uses messageCid type but requires a
|
|
781
|
+
// tag-based query against the key-delivery protocol, not a CID lookup.
|
|
782
|
+
// Identifier format is "sourceProtocol|rootContextId".
|
|
783
|
+
if (edge.label === 'contextKeyRecord') {
|
|
784
|
+
const separatorIdx = edge.identifier.indexOf('|');
|
|
785
|
+
if (separatorIdx > 0) {
|
|
786
|
+
const sourceProtocol = edge.identifier.substring(0, separatorIdx);
|
|
787
|
+
const rootContextId = edge.identifier.substring(separatorIdx + 1);
|
|
788
|
+
const { messages: contextKeys } = yield messageStore.query(context.tenantDid, [{
|
|
789
|
+
interface: 'Records',
|
|
790
|
+
method: 'Write',
|
|
791
|
+
protocol: 'https://identity.foundation/protocols/key-delivery',
|
|
792
|
+
protocolPath: 'contextKey',
|
|
793
|
+
'tag.protocol': sourceProtocol,
|
|
794
|
+
'tag.contextId': rootContextId,
|
|
795
|
+
}]);
|
|
796
|
+
if (contextKeys.length > 0) {
|
|
797
|
+
return contextKeys[0];
|
|
798
|
+
}
|
|
799
|
+
// No context key found. Since the edge is only emitted for multi-party
|
|
800
|
+
// contexts (isMultiPartyContext returned true), the absence of a context
|
|
801
|
+
// key means the consumer cannot decrypt this record. Return null to
|
|
802
|
+
// fail closure — the link will transition to repairing.
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return (_d = yield messageStore.get(context.tenantDid, edge.identifier)) !== null && _d !== void 0 ? _d : null;
|
|
807
|
+
}
|
|
808
|
+
case 'filter': {
|
|
809
|
+
// Filter-based resolution: query the MessageStore with the structured
|
|
810
|
+
// filter carried in edge.filter. Used for dependencies that require
|
|
811
|
+
// multi-field queries (e.g., cross-protocol role records).
|
|
812
|
+
if (!edge.filter) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
const { messages: filterResults } = yield messageStore.query(context.tenantDid, [edge.filter]);
|
|
816
|
+
return filterResults.length > 0 ? filterResults[0] : null;
|
|
817
|
+
}
|
|
818
|
+
default:
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Map a dependency edge to the appropriate failure code.
|
|
825
|
+
*/
|
|
826
|
+
function mapEdgeToFailureCode(edge) {
|
|
827
|
+
switch (edge.dependencyClass) {
|
|
828
|
+
case 1: return ClosureFailureCode.ProtocolMetadataMissing;
|
|
829
|
+
case 2:
|
|
830
|
+
if (edge.label === 'initialWrite') {
|
|
831
|
+
return ClosureFailureCode.InitialWriteMissing;
|
|
832
|
+
}
|
|
833
|
+
if (edge.label === 'parentRecord') {
|
|
834
|
+
return ClosureFailureCode.ParentChainMissing;
|
|
835
|
+
}
|
|
836
|
+
if (edge.label === 'contextRoot') {
|
|
837
|
+
return ClosureFailureCode.ContextChainMissing;
|
|
838
|
+
}
|
|
839
|
+
return ClosureFailureCode.ParentChainMissing;
|
|
840
|
+
case 3:
|
|
841
|
+
if (edge.label === 'permissionGrant') {
|
|
842
|
+
return ClosureFailureCode.GrantMissing;
|
|
843
|
+
}
|
|
844
|
+
return ClosureFailureCode.GrantRevocationMissing;
|
|
845
|
+
case 4:
|
|
846
|
+
if (edge.label === 'squashContextRoot') {
|
|
847
|
+
return ClosureFailureCode.VisibilityFloorMissing;
|
|
848
|
+
}
|
|
849
|
+
return ClosureFailureCode.VisibilityFloorMissing;
|
|
850
|
+
case 5: return ClosureFailureCode.EncryptionDependencyMissing;
|
|
851
|
+
case 6: return ClosureFailureCode.CrossProtocolReferenceMissing;
|
|
852
|
+
default: return ClosureFailureCode.DependencyForbidden;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
//# sourceMappingURL=sync-closure-resolver.js.map
|