@enbox/agent 0.5.10 → 0.5.11
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 +952 -37
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +19 -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 +6 -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 +188 -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 +1035 -45
- package/src/sync-messages.ts +21 -6
- package/src/sync-replication-ledger.ts +197 -0
- package/src/types/sync.ts +202 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { GenericMessage } from '@enbox/dwn-sdk-js';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Closure failure codes
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Typed failure codes for closure resolution. Each code maps to a specific
|
|
9
|
+
* dependency class from the closure RFC.
|
|
10
|
+
*/
|
|
11
|
+
export enum ClosureFailureCode {
|
|
12
|
+
/** Class 1: The ProtocolsConfigure for the protocol could not be found. */
|
|
13
|
+
ProtocolMetadataMissing = 'ClosureProtocolMetadataMissing',
|
|
14
|
+
/** Class 2: The initialWrite for a non-initial RecordsWrite is missing. */
|
|
15
|
+
InitialWriteMissing = 'ClosureInitialWriteMissing',
|
|
16
|
+
/** Class 2: A parent record in the parentId chain is missing. */
|
|
17
|
+
ParentChainMissing = 'ClosureParentChainMissing',
|
|
18
|
+
/** Class 2: A context ancestor record is missing. */
|
|
19
|
+
ContextChainMissing = 'ClosureContextChainMissing',
|
|
20
|
+
/** Class 3: A permission grant referenced by permissionGrantId is missing. */
|
|
21
|
+
GrantMissing = 'ClosureGrantMissing',
|
|
22
|
+
/** Class 3: The grant exists but is not yet active at the message's timestamp. */
|
|
23
|
+
GrantNotYetActive = 'ClosureGrantNotYetActive',
|
|
24
|
+
/** Class 3: The grant exists but has expired at the message's timestamp. */
|
|
25
|
+
GrantExpired = 'ClosureGrantExpired',
|
|
26
|
+
/** Class 3: A revocation record that affects a referenced grant is missing. */
|
|
27
|
+
GrantRevocationMissing = 'ClosureGrantRevocationMissing',
|
|
28
|
+
/** Class 4: A squash floor or visibility-floor record is missing. */
|
|
29
|
+
VisibilityFloorMissing = 'ClosureVisibilityFloorMissing',
|
|
30
|
+
/** Class 5: An encryption/key-delivery dependency is missing. */
|
|
31
|
+
EncryptionDependencyMissing = 'ClosureEncryptionDependencyMissing',
|
|
32
|
+
/** Class 6: A cross-protocol $ref dependency is missing. */
|
|
33
|
+
CrossProtocolReferenceMissing = 'ClosureCrossProtocolReferenceMissing',
|
|
34
|
+
/** A dependency exists but the syncing principal is not authorized to fetch it. */
|
|
35
|
+
DependencyForbidden = 'ClosureDependencyForbidden',
|
|
36
|
+
/** Traversal depth exceeded the configured maximum (default 32). */
|
|
37
|
+
DepthExceeded = 'ClosureDepthExceeded',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Closure dependency edge
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A single dependency edge in the closure graph — identifies what is needed
|
|
46
|
+
* and why. Used for diagnostics, deduplication, and fetch queue management.
|
|
47
|
+
*/
|
|
48
|
+
export type ClosureDependencyEdge = {
|
|
49
|
+
/** The dependency class that produced this edge. */
|
|
50
|
+
dependencyClass: 1 | 2 | 3 | 4 | 5 | 6;
|
|
51
|
+
/** Human-readable label for the dependency (e.g., "initialWrite", "grant", "protocolsConfigure"). */
|
|
52
|
+
label: string;
|
|
53
|
+
/**
|
|
54
|
+
* The identifier used to look up this dependency. Typically a `messageCid`
|
|
55
|
+
* or `recordId` depending on the dependency class.
|
|
56
|
+
*/
|
|
57
|
+
identifier: string;
|
|
58
|
+
/** The type of identifier — determines the fetch strategy. */
|
|
59
|
+
identifierType: 'messageCid' | 'recordId' | 'protocol' | 'grantId' | 'filter';
|
|
60
|
+
/**
|
|
61
|
+
* When `identifierType` is `'filter'`, this carries the full query filter
|
|
62
|
+
* as a structured object. Used for dependencies that require multi-field
|
|
63
|
+
* queries (e.g., cross-protocol role records queried by protocol +
|
|
64
|
+
* protocolPath + recipient + contextId prefix).
|
|
65
|
+
*/
|
|
66
|
+
filter?: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Closure result
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Result of closure evaluation for a single operation (closure root).
|
|
75
|
+
*/
|
|
76
|
+
export type ClosureResult = {
|
|
77
|
+
/** Whether all hard dependencies are satisfied. */
|
|
78
|
+
complete: boolean;
|
|
79
|
+
/** The closure root's messageCid. */
|
|
80
|
+
rootMessageCid: string;
|
|
81
|
+
/** All dependency edges that were evaluated. */
|
|
82
|
+
edges: ClosureDependencyEdge[];
|
|
83
|
+
/**
|
|
84
|
+
* If incomplete, the first unsatisfied dependency. Used for diagnostics
|
|
85
|
+
* and determines the failure code for repair transitions.
|
|
86
|
+
*/
|
|
87
|
+
failure?: {
|
|
88
|
+
code: ClosureFailureCode;
|
|
89
|
+
edge: ClosureDependencyEdge;
|
|
90
|
+
detail: string;
|
|
91
|
+
};
|
|
92
|
+
/** Total number of dependency hops traversed. */
|
|
93
|
+
depth: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Closure evaluation context (per-batch caching)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Shared context for a batch of closure evaluations. Caches protocol
|
|
102
|
+
* definitions, grant records, and previously resolved operations to avoid
|
|
103
|
+
* redundant queries across closure roots in the same evaluation pass.
|
|
104
|
+
*/
|
|
105
|
+
export type ClosureEvaluationContext = {
|
|
106
|
+
/** Tenant DID for this evaluation batch. */
|
|
107
|
+
tenantDid: string;
|
|
108
|
+
/** Cached ProtocolsConfigure definitions keyed by protocol URI. */
|
|
109
|
+
protocolCache: Map<string, any>;
|
|
110
|
+
/** Cached grant records keyed by grantId. */
|
|
111
|
+
grantCache: Map<string, GenericMessage | null>;
|
|
112
|
+
/**
|
|
113
|
+
* Set of dependency identifiers already known to be locally present.
|
|
114
|
+
* Keyed by `${identifierType}:${identifier}` to prevent cross-namespace
|
|
115
|
+
* collisions (e.g., a recordId and a grantId with the same string value).
|
|
116
|
+
*/
|
|
117
|
+
satisfiedDeps: Set<string>;
|
|
118
|
+
/**
|
|
119
|
+
* Set of dependency identifiers already known to be missing/unfetchable.
|
|
120
|
+
* Same composite key format as `satisfiedDeps`.
|
|
121
|
+
*/
|
|
122
|
+
missingDeps: Set<string>;
|
|
123
|
+
/** Maximum traversal depth. Default 32. */
|
|
124
|
+
maxDepth: number;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a fresh evaluation context for a batch of closure evaluations.
|
|
129
|
+
*/
|
|
130
|
+
export function createClosureContext(tenantDid: string, maxDepth?: number): ClosureEvaluationContext {
|
|
131
|
+
return {
|
|
132
|
+
tenantDid,
|
|
133
|
+
protocolCache : new Map(),
|
|
134
|
+
grantCache : new Map(),
|
|
135
|
+
satisfiedDeps : new Set(),
|
|
136
|
+
missingDeps : new Set(),
|
|
137
|
+
maxDepth : maxDepth ?? 32,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Invalidate cached entries that may be affected by a newly processed message.
|
|
143
|
+
* Called after `processRawMessage` succeeds to ensure subsequent closure
|
|
144
|
+
* evaluations see the updated state.
|
|
145
|
+
*
|
|
146
|
+
* - `ProtocolsConfigure` → clears protocolCache for that protocol URI
|
|
147
|
+
* - Permissions grant write → clears grantCache for that recordId
|
|
148
|
+
* - Permissions revocation → clears grantCache for the parent grantId
|
|
149
|
+
* - Any Records message → clears satisfiedDeps/missingDeps for that recordId
|
|
150
|
+
* (the message may have changed what's locally present)
|
|
151
|
+
*/
|
|
152
|
+
export function invalidateClosureCache(
|
|
153
|
+
context: ClosureEvaluationContext,
|
|
154
|
+
message: GenericMessage,
|
|
155
|
+
): void {
|
|
156
|
+
const desc = message.descriptor as Record<string, unknown>;
|
|
157
|
+
|
|
158
|
+
// ProtocolsConfigure update → invalidate protocol cache.
|
|
159
|
+
if (desc.interface === 'Protocols' && desc.method === 'Configure') {
|
|
160
|
+
const protocol = desc.definition
|
|
161
|
+
? (desc.definition as any).protocol as string | undefined
|
|
162
|
+
: undefined;
|
|
163
|
+
if (protocol) {
|
|
164
|
+
context.protocolCache.delete(protocol);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Permissions protocol messages → invalidate grant/revocation cache.
|
|
169
|
+
if (desc.protocol === 'https://identity.foundation/dwn/permissions') {
|
|
170
|
+
const recordId = (message as any).recordId as string | undefined;
|
|
171
|
+
const protocolPath = desc.protocolPath as string | undefined;
|
|
172
|
+
|
|
173
|
+
if (protocolPath === 'grant' && recordId) {
|
|
174
|
+
// Grant write → invalidate the grant cache and dep keys.
|
|
175
|
+
context.grantCache.delete(recordId);
|
|
176
|
+
context.satisfiedDeps.delete(`permissionGrant:grantId:${recordId}`);
|
|
177
|
+
context.missingDeps.delete(`permissionGrant:grantId:${recordId}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (protocolPath === 'grant/revocation') {
|
|
181
|
+
// Revocation write → invalidate revocation cache and both dep keys.
|
|
182
|
+
const parentId = desc.parentId as string | undefined;
|
|
183
|
+
if (parentId) {
|
|
184
|
+
context.grantCache.delete(`revocation:${parentId}`);
|
|
185
|
+
context.satisfiedDeps.delete(`grantRevocation:grantId:${parentId}`);
|
|
186
|
+
context.missingDeps.delete(`grantRevocation:grantId:${parentId}`);
|
|
187
|
+
// Also invalidate the grant itself since its revocation state changed.
|
|
188
|
+
context.satisfiedDeps.delete(`permissionGrant:grantId:${parentId}`);
|
|
189
|
+
context.missingDeps.delete(`permissionGrant:grantId:${parentId}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Any Records message → invalidate dep keys that could be affected.
|
|
195
|
+
// Handles multiple key formats:
|
|
196
|
+
// - plain recordId entries (class 2 parent/context/initialWrite)
|
|
197
|
+
// - composite protocol|recordId entries (class 6 crossProtocolParent)
|
|
198
|
+
// - filter-based entries keyed by protocol+protocolPath (class 6 role records)
|
|
199
|
+
// - contextKeyRecord entries keyed by source protocol from tags (class 5)
|
|
200
|
+
// Uses boundary-aware matching for recordId to avoid substring collisions,
|
|
201
|
+
// prefix matching for filter/contextKey entries, and tag-based source
|
|
202
|
+
// protocol extraction for key-delivery records.
|
|
203
|
+
if (desc.interface === 'Records') {
|
|
204
|
+
const recordId = (message as any).recordId as string | undefined;
|
|
205
|
+
const protocol = desc.protocol as string | undefined;
|
|
206
|
+
const protocolPath = desc.protocolPath as string | undefined;
|
|
207
|
+
|
|
208
|
+
if (recordId) {
|
|
209
|
+
// Invalidate dep keys that reference this exact recordId.
|
|
210
|
+
// Check for recordId at a key boundary (preceded by ':' or '|')
|
|
211
|
+
// to avoid substring false matches (e.g., 'thread-1' matching 'thread-10').
|
|
212
|
+
const matchesRecordId = (key: string): boolean => {
|
|
213
|
+
const idx = key.indexOf(recordId);
|
|
214
|
+
if (idx === -1) { return false; }
|
|
215
|
+
const charBefore = idx > 0 ? key[idx - 1] : ':';
|
|
216
|
+
const charAfter = idx + recordId.length < key.length ? key[idx + recordId.length] : '|';
|
|
217
|
+
const validBefore = charBefore === ':' || charBefore === '|';
|
|
218
|
+
const validAfter = charAfter === '|' || charAfter === undefined;
|
|
219
|
+
return validBefore && (idx + recordId.length === key.length || validAfter);
|
|
220
|
+
};
|
|
221
|
+
for (const key of context.satisfiedDeps) {
|
|
222
|
+
if (matchesRecordId(key)) { context.satisfiedDeps.delete(key); }
|
|
223
|
+
}
|
|
224
|
+
for (const key of context.missingDeps) {
|
|
225
|
+
if (matchesRecordId(key)) { context.missingDeps.delete(key); }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Invalidate filter-based role record deps that match this protocol + protocolPath.
|
|
230
|
+
// Dep keys are "label:filter:protocol|protocolPath|author|context".
|
|
231
|
+
// We match any key containing "filter:protocol|protocolPath".
|
|
232
|
+
if (protocol && protocolPath) {
|
|
233
|
+
const filterFragment = `filter:${protocol}|${protocolPath}`;
|
|
234
|
+
for (const key of context.satisfiedDeps) {
|
|
235
|
+
if (key.includes(filterFragment)) { context.satisfiedDeps.delete(key); }
|
|
236
|
+
}
|
|
237
|
+
for (const key of context.missingDeps) {
|
|
238
|
+
if (key.includes(filterFragment)) { context.missingDeps.delete(key); }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Invalidate contextKeyRecord deps.
|
|
243
|
+
// Context key records are in the key-delivery protocol, but their closure
|
|
244
|
+
// cache keys use the SOURCE protocol (from tags.protocol), not the
|
|
245
|
+
// key-delivery protocol URI. So when a key-delivery record arrives, we
|
|
246
|
+
// must extract the source protocol from the record's tags.
|
|
247
|
+
if (protocol === 'https://identity.foundation/protocols/key-delivery') {
|
|
248
|
+
// Real key-delivery writes have tags: { protocol: sourceProtocol, contextId: ... }
|
|
249
|
+
const tags = (message as any).descriptor?.tags as Record<string, string> | undefined;
|
|
250
|
+
const sourceProtocol = tags?.protocol;
|
|
251
|
+
if (sourceProtocol) {
|
|
252
|
+
const ctxKeyFragment = `messageCid:${sourceProtocol}|`;
|
|
253
|
+
for (const key of context.satisfiedDeps) {
|
|
254
|
+
if (key.includes(ctxKeyFragment)) { context.satisfiedDeps.delete(key); }
|
|
255
|
+
}
|
|
256
|
+
for (const key of context.missingDeps) {
|
|
257
|
+
if (key.includes(ctxKeyFragment)) { context.missingDeps.delete(key); }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else if (protocol) {
|
|
261
|
+
const ctxKeyFragment = `messageCid:${protocol}|`;
|
|
262
|
+
for (const key of context.satisfiedDeps) {
|
|
263
|
+
if (key.includes(ctxKeyFragment)) { context.satisfiedDeps.delete(key); }
|
|
264
|
+
}
|
|
265
|
+
for (const key of context.missingDeps) {
|
|
266
|
+
if (key.includes(ctxKeyFragment)) { context.missingDeps.delete(key); }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|