@enbox/agent 0.7.8 → 0.7.9
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 +6 -6
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/sync-closure-types.js +15 -0
- package/dist/esm/sync-closure-types.js.map +1 -1
- package/dist/esm/sync-engine-level.js +312 -64
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/types/sync-closure-types.d.ts.map +1 -1
- package/dist/types/sync-engine-level.d.ts +18 -7
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/sync-closure-types.ts +16 -0
- package/src/sync-engine-level.ts +392 -71
package/src/sync-engine-level.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AbstractLevel } from 'abstract-level';
|
|
2
2
|
|
|
3
3
|
import type { DwnSubscriptionHandler, ResubscribeFactory } from '@enbox/dwn-clients';
|
|
4
|
-
import type { GenericMessage, MessageEvent, MessagesSubscribeReply, MessagesSyncDependencyEntry, MessagesSyncDiffEntry, MessagesSyncReply, ProgressToken, ProtocolsConfigureMessage, RecordsProjectionScope, RecordsWriteMessage, StateIndex, SubscriptionMessage } from '@enbox/dwn-sdk-js';
|
|
4
|
+
import type { GenericMessage, MessageEvent, MessagesSubscribeReply, MessagesSyncDependencyEntry, MessagesSyncDiffEntry, MessagesSyncReply, ProgressToken, ProtocolsConfigureMessage, ProtocolsQueryReply, RecordsProjectionScope, RecordsWriteMessage, StateIndex, SubscriptionMessage } from '@enbox/dwn-sdk-js';
|
|
5
5
|
|
|
6
6
|
import ms from 'ms';
|
|
7
7
|
|
|
@@ -9,15 +9,15 @@ import { Level } from 'level';
|
|
|
9
9
|
import { sleep } from '@enbox/common';
|
|
10
10
|
import { authenticate, DwnInterfaceName, DwnMethodName, Encoder, hashToHex, initDefaultHashes, Message, ProtocolsConfigure, RECORDS_PROJECTION_ROOT_VERSION, RecordsProjection, RecordsWrite } from '@enbox/dwn-sdk-js';
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type { DwnMessageParams } from './types/dwn.js';
|
|
13
13
|
import type { EnboxPlatformAgent } from './types/agent.js';
|
|
14
14
|
import type { PermissionsApi } from './types/permissions.js';
|
|
15
15
|
import type { SyncMessageEntry } from './sync-messages.js';
|
|
16
16
|
import type { SyncScopeClassification } from './sync-scope-acceptance.js';
|
|
17
|
+
import type { ClosureEvaluationContext, ClosureResult } from './sync-closure-types.js';
|
|
17
18
|
import type { DeadLetterCategory, DeadLetterEntry, NonEmptyStringArray, PushResult, ReplicationLinkState, StartSyncParams, SyncAuthorization, SyncConnectivityState, SyncEngine, SyncEvent, SyncEventListener, SyncEventScope, SyncHealthSummary, SyncIdentityOptions, SyncMode, SyncScope } from './types/sync.js';
|
|
18
19
|
|
|
19
20
|
import { AgentPermissionsApi } from './permissions-api.js';
|
|
20
|
-
import type { DwnMessageParams } from './types/dwn.js';
|
|
21
21
|
|
|
22
22
|
import { buildLinkId } from './sync-link-id.js';
|
|
23
23
|
import { DwnInterface } from './types/dwn.js';
|
|
@@ -26,8 +26,8 @@ import { isRecordsWrite } from './utils.js';
|
|
|
26
26
|
import { ReplicationLedger } from './sync-replication-ledger.js';
|
|
27
27
|
import { topologicalSort } from './sync-topological-sort.js';
|
|
28
28
|
import { classifySyncEventScope, classifySyncMessageScope } from './sync-scope-acceptance.js';
|
|
29
|
+
import { ClosureFailureCode, createClosureContext, invalidateClosureCache, isTerminalClosureFailureCode } from './sync-closure-types.js';
|
|
29
30
|
import { computeAuthorizationEpoch, computeProjectionId, lexicographicalCompare, MAX_PENDING_TOKENS, protocolsForSyncScope, singleProtocolForSyncScope, syncScopeFromProtocols } from './types/sync.js';
|
|
30
|
-
import { createClosureContext, invalidateClosureCache, isTerminalClosureFailureCode } from './sync-closure-types.js';
|
|
31
31
|
import { fetchRemoteMessages, getMessageCid, pullMessages, pushMessages, SyncPullAbortedError } from './sync-messages.js';
|
|
32
32
|
import { partitionRemoteEntries, SyncLinkReconciler } from './sync-link-reconciler.js';
|
|
33
33
|
import { permissionGrantIdsFromEntries, resolveMessagesSyncScopes, toMessagesPermissionGrantIds, toSyncAuthorizationGrants } from './sync-permission-grants.js';
|
|
@@ -242,6 +242,13 @@ type PullDelivery = {
|
|
|
242
242
|
ordinal: number;
|
|
243
243
|
};
|
|
244
244
|
|
|
245
|
+
type LivePullDataStreamFactory = () => Promise<ReadableStream<Uint8Array> | undefined>;
|
|
246
|
+
|
|
247
|
+
type ApplyStatus = {
|
|
248
|
+
code: number;
|
|
249
|
+
detail?: string;
|
|
250
|
+
};
|
|
251
|
+
|
|
245
252
|
function syncEventScope(scope: SyncScope | undefined): SyncEventScope {
|
|
246
253
|
if (scope === undefined) {
|
|
247
254
|
return {};
|
|
@@ -353,6 +360,9 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
353
360
|
*/
|
|
354
361
|
private readonly _closureContexts: Map<string, ClosureEvaluationContext> = new Map();
|
|
355
362
|
|
|
363
|
+
/** Deduplicates concurrent live-sync repairs for the same tenant/protocol. */
|
|
364
|
+
private readonly _protocolMetadataRepairs: Map<string, Promise<boolean>> = new Map();
|
|
365
|
+
|
|
356
366
|
/** Maximum entries in the echo-loop suppression cache. */
|
|
357
367
|
private static readonly ECHO_SUPPRESS_MAX_ENTRIES = 10_000;
|
|
358
368
|
|
|
@@ -1533,6 +1543,7 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
1533
1543
|
|
|
1534
1544
|
// Clear closure evaluation contexts.
|
|
1535
1545
|
this._closureContexts.clear();
|
|
1546
|
+
this._protocolMetadataRepairs.clear();
|
|
1536
1547
|
this._recentlyPulledCids.clear();
|
|
1537
1548
|
|
|
1538
1549
|
// Clear the in-memory link and runtime state.
|
|
@@ -2163,42 +2174,132 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
2163
2174
|
}
|
|
2164
2175
|
|
|
2165
2176
|
private async processLivePullEvent(context: LivePullContext, event: MessageEvent): Promise<string | undefined> {
|
|
2166
|
-
const
|
|
2167
|
-
await this.
|
|
2177
|
+
const dataStreamFactory = await this.createLivePullDataStreamFactory(context, event);
|
|
2178
|
+
let applyStatus = await this.applyLivePullEvent(context, event, dataStreamFactory);
|
|
2168
2179
|
if (context.isStale()) { return undefined; }
|
|
2169
2180
|
|
|
2170
|
-
|
|
2181
|
+
let applied = SyncEngineLevel.isApplySuccess(applyStatus.code);
|
|
2182
|
+
if (applied) {
|
|
2183
|
+
this.invalidateClosureCacheForMessage(context.did, event.message);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2171
2186
|
if (!await this.ensureClosureComplete(context, event)) {
|
|
2172
2187
|
return undefined;
|
|
2173
2188
|
}
|
|
2174
2189
|
|
|
2190
|
+
if (!applied) {
|
|
2191
|
+
applyStatus = await this.applyLivePullEvent(context, event, dataStreamFactory);
|
|
2192
|
+
if (context.isStale()) { return undefined; }
|
|
2193
|
+
|
|
2194
|
+
applied = SyncEngineLevel.isApplySuccess(applyStatus.code);
|
|
2195
|
+
if (!applied) {
|
|
2196
|
+
throw await this.createLivePullApplyError(event, applyStatus);
|
|
2197
|
+
}
|
|
2198
|
+
this.invalidateClosureCacheForMessage(context.did, event.message);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2175
2201
|
// Squash convergence: processRawMessage triggers the DWN's built-in
|
|
2176
2202
|
// squash resumable task (performRecordsSquash), so no additional
|
|
2177
2203
|
// sync-engine side effect is needed here.
|
|
2178
2204
|
return Message.getCid(event.message);
|
|
2179
2205
|
}
|
|
2180
2206
|
|
|
2181
|
-
private async
|
|
2182
|
-
|
|
2207
|
+
private async applyLivePullEvent(
|
|
2208
|
+
context: LivePullContext,
|
|
2183
2209
|
event: MessageEvent,
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2210
|
+
dataStreamFactory: LivePullDataStreamFactory,
|
|
2211
|
+
): Promise<ApplyStatus> {
|
|
2212
|
+
const dataStream = await dataStreamFactory();
|
|
2213
|
+
const reply = await this.agent.dwn.processRawMessage(context.did, event.message, { dataStream });
|
|
2214
|
+
return reply.status;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
private async createLivePullDataStreamFactory(
|
|
2218
|
+
context: LivePullContext,
|
|
2219
|
+
event: MessageEvent,
|
|
2220
|
+
): Promise<LivePullDataStreamFactory> {
|
|
2221
|
+
if (!isRecordsWrite(event)) {
|
|
2222
|
+
return async () => undefined;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
const encodedData = (event.message as any).encodedData as string | undefined;
|
|
2226
|
+
if (encodedData) {
|
|
2227
|
+
delete (event.message as any).encodedData;
|
|
2228
|
+
const bytes = Encoder.base64UrlToBytes(encodedData);
|
|
2229
|
+
return async () => SyncEngineLevel.dataStreamFromBytes(bytes);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
const eventData = (event as any).data as ReadableStream<Uint8Array> | undefined;
|
|
2233
|
+
if (eventData) {
|
|
2234
|
+
const bytes = await SyncEngineLevel.readStreamBytes(eventData);
|
|
2235
|
+
return async () => SyncEngineLevel.dataStreamFromBytes(bytes);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
if (!(event.message.descriptor as any).dataCid) {
|
|
2239
|
+
return async () => undefined;
|
|
2188
2240
|
}
|
|
2189
2241
|
|
|
2190
2242
|
// For large RecordsWrite messages (no inline data), fetch the data from
|
|
2191
|
-
// the remote DWN via MessagesRead before
|
|
2243
|
+
// the remote DWN via MessagesRead before each store attempt. ReadableStream
|
|
2244
|
+
// instances are single-use, so a repair-triggered retry needs a fresh fetch.
|
|
2245
|
+
const { did, dwnUrl, delegateDid, permissionGrantIds } = context;
|
|
2192
2246
|
const messageCid = await Message.getCid(event.message);
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2247
|
+
return async () => {
|
|
2248
|
+
const fetched = await fetchRemoteMessages({
|
|
2249
|
+
did,
|
|
2250
|
+
dwnUrl,
|
|
2251
|
+
delegateDid,
|
|
2252
|
+
permissionGrantIds,
|
|
2253
|
+
messageCids : [messageCid],
|
|
2254
|
+
agent : this.agent,
|
|
2255
|
+
});
|
|
2256
|
+
return fetched[0]?.dataStream;
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
private async createLivePullApplyError(event: MessageEvent, status: ApplyStatus): Promise<Error> {
|
|
2261
|
+
const cid = await Message.getCid(event.message);
|
|
2262
|
+
return new Error(
|
|
2263
|
+
`SyncEngineLevel: live pull apply failed for ${cid}: ${status.code} ${status.detail ?? ''}`.trim()
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
private static dataStreamFromBytes(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
2268
|
+
return new ReadableStream<Uint8Array>({
|
|
2269
|
+
start(controller): void {
|
|
2270
|
+
controller.enqueue(bytes);
|
|
2271
|
+
controller.close();
|
|
2272
|
+
}
|
|
2200
2273
|
});
|
|
2201
|
-
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
private static async readStreamBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
|
|
2277
|
+
const reader = stream.getReader();
|
|
2278
|
+
const chunks: Uint8Array[] = [];
|
|
2279
|
+
let totalSize = 0;
|
|
2280
|
+
|
|
2281
|
+
try {
|
|
2282
|
+
for (;;) {
|
|
2283
|
+
const { done, value } = await reader.read();
|
|
2284
|
+
if (done) { break; }
|
|
2285
|
+
chunks.push(value);
|
|
2286
|
+
totalSize += value.byteLength;
|
|
2287
|
+
}
|
|
2288
|
+
} finally {
|
|
2289
|
+
reader.releaseLock();
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const bytes = new Uint8Array(totalSize);
|
|
2293
|
+
let offset = 0;
|
|
2294
|
+
for (const chunk of chunks) {
|
|
2295
|
+
bytes.set(chunk, offset);
|
|
2296
|
+
offset += chunk.byteLength;
|
|
2297
|
+
}
|
|
2298
|
+
return bytes;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
private static isApplySuccess(code: number): boolean {
|
|
2302
|
+
return (code >= 200 && code < 300) || code === 409;
|
|
2202
2303
|
}
|
|
2203
2304
|
|
|
2204
2305
|
private invalidateClosureCacheForMessage(did: string, message: GenericMessage): void {
|
|
@@ -2225,14 +2326,279 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
2225
2326
|
}
|
|
2226
2327
|
|
|
2227
2328
|
const messageStore = this.agent.dwn.node.storage.messageStore;
|
|
2228
|
-
|
|
2329
|
+
let closureResult = await evaluateClosure(event.message, messageStore, link.scope, closureCtx);
|
|
2229
2330
|
if (isStale()) { return false; }
|
|
2230
2331
|
if (closureResult.complete) { return true; }
|
|
2231
2332
|
|
|
2333
|
+
if (await this.tryRepairMissingProtocolMetadata(context, closureCtx, closureResult)) {
|
|
2334
|
+
if (isStale()) { return false; }
|
|
2335
|
+
|
|
2336
|
+
closureResult = await evaluateClosure(event.message, messageStore, link.scope, closureCtx);
|
|
2337
|
+
if (isStale()) { return false; }
|
|
2338
|
+
if (closureResult.complete) { return true; }
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2232
2341
|
await this.recordClosureFailure(context, event, closureResult.failure!.code, closureResult.failure!.detail);
|
|
2233
2342
|
return false;
|
|
2234
2343
|
}
|
|
2235
2344
|
|
|
2345
|
+
private async tryRepairMissingProtocolMetadata(
|
|
2346
|
+
context: LivePullContext,
|
|
2347
|
+
closureCtx: ClosureEvaluationContext,
|
|
2348
|
+
closureResult: ClosureResult,
|
|
2349
|
+
): Promise<boolean> {
|
|
2350
|
+
const failure = closureResult.failure;
|
|
2351
|
+
if (!SyncEngineLevel.isRepairableProtocolMetadataFailure(failure)) {
|
|
2352
|
+
return false;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
const { did } = context;
|
|
2356
|
+
const repairKey = `${did}|${failure.edge.identifier}`;
|
|
2357
|
+
const activeRepair = this._protocolMetadataRepairs.get(repairKey);
|
|
2358
|
+
if (activeRepair) {
|
|
2359
|
+
return activeRepair;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const repair = this.repairMissingProtocolMetadata(context, closureCtx, failure.edge.identifier);
|
|
2363
|
+
this._protocolMetadataRepairs.set(repairKey, repair);
|
|
2364
|
+
repair.finally(() => {
|
|
2365
|
+
if (this._protocolMetadataRepairs.get(repairKey) === repair) {
|
|
2366
|
+
this._protocolMetadataRepairs.delete(repairKey);
|
|
2367
|
+
}
|
|
2368
|
+
}).catch(() => { /* caller handles the repair result */ });
|
|
2369
|
+
return repair;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
private async repairMissingProtocolMetadata(
|
|
2373
|
+
{ did, dwnUrl, delegateDid, isStale }: LivePullContext,
|
|
2374
|
+
closureCtx: ClosureEvaluationContext,
|
|
2375
|
+
protocol: string,
|
|
2376
|
+
): Promise<boolean> {
|
|
2377
|
+
if (isStale()) {
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const configs = await this.fetchRemoteProtocolConfigClosure({
|
|
2382
|
+
authorDid : delegateDid ?? did,
|
|
2383
|
+
delegateDid,
|
|
2384
|
+
dwnUrl,
|
|
2385
|
+
protocol,
|
|
2386
|
+
tenantDid : did,
|
|
2387
|
+
});
|
|
2388
|
+
if (isStale() || configs.length === 0) {
|
|
2389
|
+
return false;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// Live subscriptions can deliver scoped records before the local replica
|
|
2393
|
+
// has the tenant's protocol metadata. Reuse the DWN ProtocolsQuery path and
|
|
2394
|
+
// only install configs that are signed by the tenant, including composed
|
|
2395
|
+
// protocol dependencies needed to authorize the record.
|
|
2396
|
+
let repaired = false;
|
|
2397
|
+
for (const config of configs) {
|
|
2398
|
+
if (isStale()) {
|
|
2399
|
+
return repaired;
|
|
2400
|
+
}
|
|
2401
|
+
const reply = await this.agent.dwn.processRawMessage(did, config);
|
|
2402
|
+
if (isStale()) {
|
|
2403
|
+
return repaired;
|
|
2404
|
+
}
|
|
2405
|
+
if (!SyncEngineLevel.protocolConfigApplySucceeded(reply.status.code)) {
|
|
2406
|
+
return repaired;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
invalidateClosureCache(closureCtx, config);
|
|
2410
|
+
repaired = true;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
return repaired;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
private async fetchRemoteProtocolConfigClosure({
|
|
2417
|
+
authorDid,
|
|
2418
|
+
delegateDid,
|
|
2419
|
+
dwnUrl,
|
|
2420
|
+
protocol,
|
|
2421
|
+
tenantDid,
|
|
2422
|
+
}: {
|
|
2423
|
+
authorDid: string;
|
|
2424
|
+
delegateDid?: string;
|
|
2425
|
+
dwnUrl: string;
|
|
2426
|
+
protocol: string;
|
|
2427
|
+
tenantDid: string;
|
|
2428
|
+
}): Promise<ProtocolsConfigureMessage[]> {
|
|
2429
|
+
const configsByProtocol = new Map<string, ProtocolsConfigureMessage>();
|
|
2430
|
+
const visiting = new Set<string>();
|
|
2431
|
+
|
|
2432
|
+
const visit = async (protocolUri: string): Promise<boolean> => {
|
|
2433
|
+
if (configsByProtocol.has(protocolUri)) {
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
if (visiting.has(protocolUri)) {
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
visiting.add(protocolUri);
|
|
2440
|
+
|
|
2441
|
+
const config = await this.fetchRemoteProtocolConfig({
|
|
2442
|
+
authorDid,
|
|
2443
|
+
delegateDid,
|
|
2444
|
+
dwnUrl,
|
|
2445
|
+
protocol: protocolUri,
|
|
2446
|
+
tenantDid,
|
|
2447
|
+
});
|
|
2448
|
+
if (config === undefined) {
|
|
2449
|
+
visiting.delete(protocolUri);
|
|
2450
|
+
return false;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
for (const usedProtocol of SyncEngineLevel.protocolsConfigureUses(config)) {
|
|
2454
|
+
if (!await visit(usedProtocol)) {
|
|
2455
|
+
visiting.delete(protocolUri);
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
configsByProtocol.set(protocolUri, config);
|
|
2461
|
+
visiting.delete(protocolUri);
|
|
2462
|
+
return true;
|
|
2463
|
+
};
|
|
2464
|
+
|
|
2465
|
+
return await visit(protocol) ? [...configsByProtocol.values()] : [];
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
private async fetchRemoteProtocolConfig({
|
|
2469
|
+
authorDid,
|
|
2470
|
+
delegateDid,
|
|
2471
|
+
dwnUrl,
|
|
2472
|
+
protocol,
|
|
2473
|
+
tenantDid,
|
|
2474
|
+
}: {
|
|
2475
|
+
authorDid: string;
|
|
2476
|
+
delegateDid?: string;
|
|
2477
|
+
dwnUrl: string;
|
|
2478
|
+
protocol: string;
|
|
2479
|
+
tenantDid: string;
|
|
2480
|
+
}): Promise<ProtocolsConfigureMessage | undefined> {
|
|
2481
|
+
try {
|
|
2482
|
+
const permissionGrantId = await this.getProtocolsQueryPermissionGrantId({
|
|
2483
|
+
delegateDid,
|
|
2484
|
+
protocol,
|
|
2485
|
+
tenantDid,
|
|
2486
|
+
});
|
|
2487
|
+
const { message } = await this.agent.processDwnRequest({
|
|
2488
|
+
author : authorDid,
|
|
2489
|
+
messageParams : {
|
|
2490
|
+
filter: { protocol },
|
|
2491
|
+
...(permissionGrantId === undefined ? {} : { permissionGrantId }),
|
|
2492
|
+
},
|
|
2493
|
+
messageType : DwnInterface.ProtocolsQuery,
|
|
2494
|
+
store : false,
|
|
2495
|
+
target : tenantDid,
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
const reply = await this.agent.rpc.sendDwnRequest({
|
|
2499
|
+
dwnUrl,
|
|
2500
|
+
message,
|
|
2501
|
+
targetDid: tenantDid,
|
|
2502
|
+
}) as ProtocolsQueryReply;
|
|
2503
|
+
if (reply.status.code !== 200 || reply.entries === undefined) {
|
|
2504
|
+
return undefined;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const candidates: ProtocolsConfigureMessage[] = [];
|
|
2508
|
+
for (const entry of reply.entries) {
|
|
2509
|
+
const config = await this.toAuthenticatedTenantProtocolConfig(tenantDid, entry);
|
|
2510
|
+
if (config?.descriptor.definition.protocol === protocol) {
|
|
2511
|
+
candidates.push(config);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
return SyncEngineLevel.newestProtocolConfig(candidates);
|
|
2516
|
+
} catch {
|
|
2517
|
+
return undefined;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
private async getProtocolsQueryPermissionGrantId({
|
|
2522
|
+
delegateDid,
|
|
2523
|
+
protocol,
|
|
2524
|
+
tenantDid,
|
|
2525
|
+
}: {
|
|
2526
|
+
delegateDid?: string;
|
|
2527
|
+
protocol: string;
|
|
2528
|
+
tenantDid: string;
|
|
2529
|
+
}): Promise<string | undefined> {
|
|
2530
|
+
if (delegateDid === undefined) {
|
|
2531
|
+
return undefined;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
try {
|
|
2535
|
+
const { grant } = await this._permissionsApi.getPermissionForRequest({
|
|
2536
|
+
connectedDid : tenantDid,
|
|
2537
|
+
delegateDid,
|
|
2538
|
+
protocol,
|
|
2539
|
+
cached : true,
|
|
2540
|
+
messageType : DwnInterface.ProtocolsQuery,
|
|
2541
|
+
});
|
|
2542
|
+
return grant.id;
|
|
2543
|
+
} catch {
|
|
2544
|
+
return undefined;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
private async toAuthenticatedTenantProtocolConfig(
|
|
2549
|
+
tenantDid: string,
|
|
2550
|
+
message: GenericMessage,
|
|
2551
|
+
): Promise<ProtocolsConfigureMessage | undefined> {
|
|
2552
|
+
if (!SyncEngineLevel.isProtocolsConfigureDefinitionMessage(message)) {
|
|
2553
|
+
return undefined;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
try {
|
|
2557
|
+
await ProtocolsConfigure.parse(message);
|
|
2558
|
+
if (Message.getAuthor(message) !== tenantDid) {
|
|
2559
|
+
return undefined;
|
|
2560
|
+
}
|
|
2561
|
+
await authenticate(message.authorization, this.agent.did);
|
|
2562
|
+
return message;
|
|
2563
|
+
} catch {
|
|
2564
|
+
return undefined;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
private static newestProtocolConfig(
|
|
2569
|
+
configs: ProtocolsConfigureMessage[],
|
|
2570
|
+
): ProtocolsConfigureMessage | undefined {
|
|
2571
|
+
let newest: ProtocolsConfigureMessage | undefined;
|
|
2572
|
+
for (const config of configs) {
|
|
2573
|
+
if (newest === undefined || SyncEngineLevel.isProtocolConfigNewer(config, newest)) {
|
|
2574
|
+
newest = config;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return newest;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
private static isProtocolConfigNewer(
|
|
2581
|
+
candidate: ProtocolsConfigureMessage,
|
|
2582
|
+
current: ProtocolsConfigureMessage,
|
|
2583
|
+
): boolean {
|
|
2584
|
+
return candidate.descriptor.messageTimestamp > current.descriptor.messageTimestamp;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
private static protocolConfigApplySucceeded(code: number): boolean {
|
|
2588
|
+
return (code >= 200 && code < 300) || code === 409;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
private static isRepairableProtocolMetadataFailure(
|
|
2592
|
+
failure: ClosureResult['failure'] | undefined,
|
|
2593
|
+
): failure is NonNullable<ClosureResult['failure']> {
|
|
2594
|
+
return failure?.edge.identifierType === 'protocol' &&
|
|
2595
|
+
(
|
|
2596
|
+
failure.code === ClosureFailureCode.ProtocolMetadataMissing ||
|
|
2597
|
+
failure.code === ClosureFailureCode.CrossProtocolReferenceMissing ||
|
|
2598
|
+
failure.code === ClosureFailureCode.EncryptionDependencyMissing
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2236
2602
|
private async recordClosureFailure(
|
|
2237
2603
|
{ did, dwnUrl, linkKey, link, isStale }: LivePullContext,
|
|
2238
2604
|
event: MessageEvent,
|
|
@@ -3263,20 +3629,12 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
3263
3629
|
tenantDid: string,
|
|
3264
3630
|
dependency: SyncDependencyEntryWithMessage,
|
|
3265
3631
|
): Promise<AuthenticatedProtocolConfigDependency | undefined> {
|
|
3266
|
-
|
|
3632
|
+
const config = await this.toAuthenticatedTenantProtocolConfig(tenantDid, dependency.message);
|
|
3633
|
+
if (config === undefined) {
|
|
3267
3634
|
return undefined;
|
|
3268
3635
|
}
|
|
3269
3636
|
|
|
3270
|
-
|
|
3271
|
-
await ProtocolsConfigure.parse(dependency.message);
|
|
3272
|
-
if (Message.getAuthor(dependency.message) !== tenantDid) {
|
|
3273
|
-
return undefined;
|
|
3274
|
-
}
|
|
3275
|
-
await authenticate(dependency.message.authorization, this.agent.did);
|
|
3276
|
-
return { ...dependency, message: dependency.message };
|
|
3277
|
-
} catch {
|
|
3278
|
-
return undefined;
|
|
3279
|
-
}
|
|
3637
|
+
return { ...dependency, message: config };
|
|
3280
3638
|
}
|
|
3281
3639
|
|
|
3282
3640
|
private static async projectedDependencyCidsMatch({
|
|
@@ -3775,43 +4133,6 @@ export class SyncEngineLevel implements SyncEngine {
|
|
|
3775
4133
|
return buildLinkId(did, dwnUrl, projectionId, authorizationEpoch);
|
|
3776
4134
|
}
|
|
3777
4135
|
|
|
3778
|
-
// ---------------------------------------------------------------------------
|
|
3779
|
-
// Utility helpers
|
|
3780
|
-
// ---------------------------------------------------------------------------
|
|
3781
|
-
|
|
3782
|
-
/**
|
|
3783
|
-
* Extracts a ReadableStream from a MessageEvent if it contains a
|
|
3784
|
-
* RecordsWrite with data — either as an inline `encodedData` field
|
|
3785
|
-
* (for records <= 30 KB) or as a pre-existing data stream.
|
|
3786
|
-
*/
|
|
3787
|
-
private extractDataStream(event: MessageEvent): ReadableStream<Uint8Array> | undefined {
|
|
3788
|
-
if (!isRecordsWrite(event)) {
|
|
3789
|
-
return undefined;
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
|
-
// Check for inline base64url-encoded data (small records from EventLog).
|
|
3793
|
-
// Delete the transport-level field so the DWN schema validator does not
|
|
3794
|
-
// reject the message for having unevaluated properties.
|
|
3795
|
-
const encodedData = (event.message as any).encodedData as string | undefined;
|
|
3796
|
-
if (encodedData) {
|
|
3797
|
-
delete (event.message as any).encodedData;
|
|
3798
|
-
const bytes = Encoder.base64UrlToBytes(encodedData);
|
|
3799
|
-
return new ReadableStream<Uint8Array>({
|
|
3800
|
-
start(controller): void {
|
|
3801
|
-
controller.enqueue(bytes);
|
|
3802
|
-
controller.close();
|
|
3803
|
-
}
|
|
3804
|
-
});
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3807
|
-
// Check for a pre-existing data stream (e.g. from a direct message read).
|
|
3808
|
-
if ((event as any).data) {
|
|
3809
|
-
return (event as any).data;
|
|
3810
|
-
}
|
|
3811
|
-
|
|
3812
|
-
return undefined;
|
|
3813
|
-
}
|
|
3814
|
-
|
|
3815
4136
|
// ---------------------------------------------------------------------------
|
|
3816
4137
|
// Default Hash Cache
|
|
3817
4138
|
// ---------------------------------------------------------------------------
|