@enbox/agent 0.6.2 → 0.6.4

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.
@@ -200,6 +200,7 @@ function getRuleSetAtProtocolPath(
200
200
  function extractProtocolAwareDeps(
201
201
  message: GenericMessage,
202
202
  protocolDef: any,
203
+ isDelegateSession?: boolean,
203
204
  ): ClosureDependencyEdge[] {
204
205
  const desc = message.descriptor as Record<string, unknown>;
205
206
  if (desc.interface !== 'Records') { return []; }
@@ -246,15 +247,39 @@ function extractProtocolAwareDeps(
246
247
  identifierType : 'protocol',
247
248
  });
248
249
 
249
- // Level 2: key-delivery protocol record.
250
- // The key-delivery protocol must be installed locally.
251
- const keyDeliveryProtocol = 'https://identity.foundation/protocols/key-delivery';
252
- edges.push({
253
- dependencyClass : 5,
254
- label : 'keyDeliveryProtocol',
255
- identifier : keyDeliveryProtocol,
256
- identifierType : 'protocol',
257
- });
250
+ // Determine whether this record uses multi-party (ProtocolContext)
251
+ // encryption before emitting key-delivery edges.
252
+ const contextId = (message as any).contextId as string | undefined;
253
+ let isMultiParty = false;
254
+ if (contextId) {
255
+ const rootProtocolPath = protocolPath.split('/')[0];
256
+ isMultiParty = isMultiPartyContext(protocolDef, rootProtocolPath);
257
+ }
258
+
259
+ // Level 2: key-delivery protocol ProtocolsConfigure.
260
+ //
261
+ // For **owner** sessions this is always emitted — the DWN needs the
262
+ // protocol installed to authorize contextKey record access.
263
+ //
264
+ // For **delegate** sessions:
265
+ // - Single-party: the delegate decrypts via pre-derived
266
+ // `delegateDecryptionKeys` (ProtocolPath keys). No key-delivery
267
+ // records are involved. Edge is suppressed.
268
+ // - Multi-party: on in-memory cache miss the runtime falls back to
269
+ // `fetchCrossDeviceContextKey()` (dwn-encryption.ts:453,
270
+ // dwn-key-delivery.ts:268) which does a local RecordsQuery +
271
+ // RecordsRead against the owner's tenant. That authorization path
272
+ // requires the key-delivery ProtocolsConfigure. Edge is emitted.
273
+ const suppressKeyDelivery = isDelegateSession && !isMultiParty;
274
+ if (!suppressKeyDelivery) {
275
+ const keyDeliveryProtocol = 'https://identity.foundation/protocols/key-delivery';
276
+ edges.push({
277
+ dependencyClass : 5,
278
+ label : 'keyDeliveryProtocol',
279
+ identifier : keyDeliveryProtocol,
280
+ identifierType : 'protocol',
281
+ });
282
+ }
258
283
 
259
284
  // Level 3: Context key enforcement for multi-party contexts.
260
285
  // Uses isMultiPartyContext() from protocol-utils to determine whether the
@@ -264,21 +289,20 @@ function extractProtocolAwareDeps(
264
289
  //
265
290
  // Single-party contexts use ProtocolPath encryption (owner-only) and do
266
291
  // NOT need a context key — the edge is skipped entirely.
267
- const contextId = (message as any).contextId as string | undefined;
268
- if (contextId) {
269
- const rootProtocolPath = protocolPath.split('/')[0];
270
- const multiParty = isMultiPartyContext(protocolDef, rootProtocolPath);
271
- if (multiParty) {
272
- const rootContextId = contextId.split('/')[0];
273
- // Separator is '|' (not ':') because protocol URIs contain '://'
274
- // which would break indexOf(':') parsing in resolveDependency.
275
- edges.push({
276
- dependencyClass : 5,
277
- label : 'contextKeyRecord',
278
- identifier : `${desc.protocol}|${rootContextId}`,
279
- identifierType : 'messageCid',
280
- });
281
- }
292
+ //
293
+ // For delegates this edge is always emitted when applicable: the
294
+ // runtime's cross-device fallback still needs the contextKey record
295
+ // locally (on cache miss).
296
+ if (isMultiParty && contextId) {
297
+ const rootContextId = contextId.split('/')[0];
298
+ // Separator is '|' (not ':') because protocol URIs contain '://'
299
+ // which would break indexOf(':') parsing in resolveDependency.
300
+ edges.push({
301
+ dependencyClass : 5,
302
+ label : 'contextKeyRecord',
303
+ identifier : `${desc.protocol}|${rootContextId}`,
304
+ identifierType : 'messageCid',
305
+ });
282
306
  }
283
307
  }
284
308
  }
@@ -496,7 +520,7 @@ export async function evaluateClosure(
496
520
  const cachedProtocolMsg = context.protocolCache.get(currentProtocol);
497
521
  const protocolDef = (cachedProtocolMsg?.descriptor as any)?.definition;
498
522
  if (protocolDef) {
499
- const protoAwareEdges = extractProtocolAwareDeps(current, protocolDef);
523
+ const protoAwareEdges = extractProtocolAwareDeps(current, protocolDef, context.isDelegateSession);
500
524
  const protoResult = await resolveEdges(
501
525
  protoAwareEdges, allEdges, messageStore, context, visited, queue, rootCid, currentDepth
502
526
  );
@@ -122,19 +122,42 @@ export type ClosureEvaluationContext = {
122
122
  missingDeps: Set<string>;
123
123
  /** Maximum traversal depth. Default 32. */
124
124
  maxDepth: number;
125
+
126
+ /**
127
+ * When `true`, the sync link is operating as a delegated session.
128
+ *
129
+ * Affects class 5 (encryption) dependency extraction:
130
+ *
131
+ * - **Single-party** protocols: the delegate decrypts via pre-derived
132
+ * `delegateDecryptionKeys` (ProtocolPath keys). No key-delivery
133
+ * records are involved, so the `keyDeliveryProtocol` edge is
134
+ * suppressed entirely in `extractProtocolAwareDeps()`.
135
+ *
136
+ * - **Multi-party** protocols: on in-memory cache miss the runtime
137
+ * falls back to `fetchCrossDeviceContextKey()` (see
138
+ * `dwn-encryption.ts:453`, `dwn-key-delivery.ts:268`) which queries
139
+ * the local DWN. Both `keyDeliveryProtocol` and `contextKeyRecord`
140
+ * are emitted and resolved normally.
141
+ */
142
+ isDelegateSession?: boolean;
125
143
  };
126
144
 
127
145
  /**
128
146
  * Create a fresh evaluation context for a batch of closure evaluations.
129
147
  */
130
- export function createClosureContext(tenantDid: string, maxDepth?: number): ClosureEvaluationContext {
148
+ export function createClosureContext(
149
+ tenantDid: string,
150
+ maxDepth?: number,
151
+ options?: { isDelegateSession?: boolean },
152
+ ): ClosureEvaluationContext {
131
153
  return {
132
154
  tenantDid,
133
- protocolCache : new Map(),
134
- grantCache : new Map(),
135
- satisfiedDeps : new Set(),
136
- missingDeps : new Set(),
137
- maxDepth : maxDepth ?? 32,
155
+ protocolCache : new Map(),
156
+ grantCache : new Map(),
157
+ satisfiedDeps : new Set(),
158
+ missingDeps : new Set(),
159
+ maxDepth : maxDepth ?? 32,
160
+ isDelegateSession : options?.isDelegateSession,
138
161
  };
139
162
  }
140
163
 
@@ -1476,7 +1476,9 @@ export class SyncEngineLevel implements SyncEngine {
1476
1476
  const messageStore = this.agent.dwn.node.storage.messageStore;
1477
1477
  let closureCtx = this._closureContexts.get(did);
1478
1478
  if (!closureCtx) {
1479
- closureCtx = createClosureContext(did);
1479
+ closureCtx = createClosureContext(did, undefined, {
1480
+ isDelegateSession: !!delegateDid,
1481
+ });
1480
1482
  this._closureContexts.set(did, closureCtx);
1481
1483
  }
1482
1484
 
@@ -25,17 +25,32 @@ export type SyncMessageEntry = {
25
25
  * 202: message was successfully written to the remote DWN
26
26
  * 204: an initial write message was written without any data
27
27
  * 409: message was already present on the remote DWN
28
- * RecordsDelete + 404: the initial write was not found or already deleted
28
+ *
29
+ * When the *pushed* message is known (e.g. during push-sync), pass it as the
30
+ * second argument so that RecordsDelete + 404 ("initial write was not found or
31
+ * already deleted") can be detected. The DWN's 404 reply omits `entry`, so
32
+ * checking `reply.entry` alone is insufficient.
29
33
  */
30
- export function syncMessageReplyIsSuccessful(reply: UnionMessageReply): boolean {
31
- return reply.status.code === 202 ||
32
- reply.status.code === 204 ||
33
- reply.status.code === 409 ||
34
- (
35
- reply.entry?.message.descriptor.interface === DwnInterfaceName.Records &&
36
- reply.entry?.message.descriptor.method === DwnMethodName.Delete &&
37
- reply.status.code === 404
38
- );
34
+ export function syncMessageReplyIsSuccessful(reply: UnionMessageReply, pushedMessage?: GenericMessage): boolean {
35
+ if (reply.status.code === 202 || reply.status.code === 204 || reply.status.code === 409) {
36
+ return true;
37
+ }
38
+
39
+ if (reply.status.code === 404) {
40
+ // Check the pushed message first (always available during push-sync).
41
+ if (pushedMessage?.descriptor.interface === DwnInterfaceName.Records &&
42
+ pushedMessage?.descriptor.method === DwnMethodName.Delete) {
43
+ return true;
44
+ }
45
+
46
+ // Fallback: check the reply entry (for callers that don't pass the pushed message).
47
+ if (reply.entry?.message.descriptor.interface === DwnInterfaceName.Records &&
48
+ reply.entry?.message.descriptor.method === DwnMethodName.Delete) {
49
+ return true;
50
+ }
51
+ }
52
+
53
+ return false;
39
54
  }
40
55
 
41
56
  /**
@@ -364,7 +379,7 @@ export async function pushMessages({ did, dwnUrl, delegateDid, protocol, message
364
379
  message : entry.message
365
380
  });
366
381
 
367
- if (syncMessageReplyIsSuccessful(reply)) {
382
+ if (syncMessageReplyIsSuccessful(reply, entry.message)) {
368
383
  succeeded.push(cid);
369
384
  } else if (isPermanentPushFailure(reply)) {
370
385
  // Permanent failures (400/401/403) will never succeed — do NOT retry.