@metalabel/dfos-web-relay 0.8.1 → 0.10.0
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/README.md +2 -2
- package/dist/index.d.ts +49 -7
- package/dist/index.js +375 -151
- package/dist/serve.js +28 -1
- package/openapi.yaml +67 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -49,14 +49,14 @@ serve({ port: 4444 });
|
|
|
49
49
|
| `GET` | `/countersignatures/:cid` | Get countersignatures for an operation |
|
|
50
50
|
| `GET` | `/operations/:cid/countersignatures` | Same as above (alias) |
|
|
51
51
|
| `PUT` | `/content/:contentId/blob/:operationCID` | Upload blob (auth required) |
|
|
52
|
-
| `GET` | `/content/:contentId/blob` | Download blob at head (auth + credential)
|
|
52
|
+
| `GET` | `/content/:contentId/blob` | Download blob at head (standing auth, or auth + credential) |
|
|
53
53
|
| `GET` | `/content/:contentId/blob/:ref` | Download blob at specific operation ref |
|
|
54
54
|
|
|
55
55
|
## Blob Authorization
|
|
56
56
|
|
|
57
57
|
**Upload**: Auth token required. Caller must be the chain creator or the signer of the referenced operation (enables delegated upload).
|
|
58
58
|
|
|
59
|
-
**Download**:
|
|
59
|
+
**Download**: If a public credential (`aud: *`) exists as a standing authorization, the blob is served without authentication. Otherwise, auth token required — chain creator can download directly, other identities must present a DFOS read credential (issued by the creator) in the `X-Credential` header.
|
|
60
60
|
|
|
61
61
|
## Peering
|
|
62
62
|
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ interface RelayOptions {
|
|
|
21
21
|
peers?: PeerConfig[];
|
|
22
22
|
/** Injected peer client — if omitted, a default HTTP implementation is used */
|
|
23
23
|
peerClient?: PeerClient;
|
|
24
|
+
/**
|
|
25
|
+
* Max lifetime (exp-iat, seconds) honored on a self-signed auth token.
|
|
26
|
+
* Default 86400 (24h); a value <= 0 disables the ceiling. Applies only to auth
|
|
27
|
+
* tokens, never to DFOS credentials.
|
|
28
|
+
*/
|
|
29
|
+
maxAuthTokenTTLSeconds?: number;
|
|
24
30
|
}
|
|
25
31
|
interface PeerConfig {
|
|
26
32
|
url: string;
|
|
@@ -236,6 +242,14 @@ interface IngestionResult {
|
|
|
236
242
|
kind?: OperationKind;
|
|
237
243
|
/** Chain identifier if applicable */
|
|
238
244
|
chainId?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Structured dependency-failure signal. When true, the rejection is due to a
|
|
247
|
+
* missing dependency that may arrive later via sync or gossip, so the
|
|
248
|
+
* sequencer must keep the op pending (retryable) rather than durably reject
|
|
249
|
+
* it. This is the discriminator the sequencer branches on — NOT substring
|
|
250
|
+
* matching of the human-readable `error` string.
|
|
251
|
+
*/
|
|
252
|
+
dependencyMissing?: boolean;
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
/**
|
|
@@ -247,6 +261,20 @@ interface IngestionResult {
|
|
|
247
261
|
* they are available via the relay's proof plane routes.
|
|
248
262
|
*/
|
|
249
263
|
declare const bootstrapRelayIdentity: (store: RelayStore) => Promise<RelayIdentity>;
|
|
264
|
+
/**
|
|
265
|
+
* Bootstrap a relay identity from an EXISTING key + key ID, with optional pinned
|
|
266
|
+
* timestamps and profile name. Used for deterministic bootstrap — e.g. the
|
|
267
|
+
* dual-relay parity harness pins one key + one createdAt across both twins so
|
|
268
|
+
* the relay's own genesis + profile log entries are byte-identical, and durable
|
|
269
|
+
* relays that reload their key from storage. Mirrors the Go twin's
|
|
270
|
+
* BootstrapRelayIdentityFromKey.
|
|
271
|
+
*/
|
|
272
|
+
declare const bootstrapRelayIdentityFromKey: (store: RelayStore, params: {
|
|
273
|
+
privateKey: Uint8Array;
|
|
274
|
+
keyId: string;
|
|
275
|
+
name?: string;
|
|
276
|
+
createdAt?: string;
|
|
277
|
+
}) => Promise<RelayIdentity>;
|
|
250
278
|
|
|
251
279
|
/**
|
|
252
280
|
* Create an HTTP-based PeerClient.
|
|
@@ -265,6 +293,13 @@ interface CreatedRelay {
|
|
|
265
293
|
/** Sync operations from all configured sync peers (call on a schedule) */
|
|
266
294
|
syncFromPeers: () => Promise<void>;
|
|
267
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Split items into batches of at most `size`, preserving order with no loss.
|
|
298
|
+
* gossip() uses this to stay within the receiver's per-batch cap; exported so
|
|
299
|
+
* the split behavior is directly testable (mirrors Go's maxGossipBatch chunking,
|
|
300
|
+
* whose TestGossipChunksLargeBatches drives the split directly).
|
|
301
|
+
*/
|
|
302
|
+
declare const chunkOps: <T>(items: T[], size: number) => T[][];
|
|
268
303
|
/**
|
|
269
304
|
* Create a DFOS web relay Hono application
|
|
270
305
|
*
|
|
@@ -346,6 +381,13 @@ declare class MemoryRelayStore implements RelayStore {
|
|
|
346
381
|
resetSequencer(): Promise<void>;
|
|
347
382
|
}
|
|
348
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Derive the operation CID from a JWS token by re-encoding the decoded payload.
|
|
386
|
+
* Returns the empty string for an undecodable token. Used at verify-failure
|
|
387
|
+
* sites so a rejection still carries a CID and can be durably rejected by the
|
|
388
|
+
* sequencer (instead of being skipped forever by `if (!res.cid) continue`).
|
|
389
|
+
*/
|
|
390
|
+
declare const computeOpCID: (jwsToken: string) => Promise<string>;
|
|
349
391
|
/**
|
|
350
392
|
* Create a key resolver that looks up Ed25519 public keys from identity chains
|
|
351
393
|
* in the store. Used for chain re-verification during ingestion.
|
|
@@ -411,13 +453,13 @@ declare const ingestOperations: (tokens: string[], store: RelayStore, options?:
|
|
|
411
453
|
}) => Promise<IngestionResult[]>;
|
|
412
454
|
|
|
413
455
|
/**
|
|
414
|
-
* Returns true if
|
|
415
|
-
* arrive later via sync or gossip.
|
|
416
|
-
*
|
|
456
|
+
* Returns true if a rejection is retryable (a missing dependency that may
|
|
457
|
+
* arrive later via sync or gossip). The sequencer branches on the STRUCTURED
|
|
458
|
+
* `dependencyMissing` flag set by the ingest producer — not on substring
|
|
459
|
+
* matching of the human-readable `error` string. Mirrors the Go twin's
|
|
460
|
+
* structured discriminator.
|
|
417
461
|
*/
|
|
418
|
-
declare const isDependencyFailure: (
|
|
419
|
-
/** Derive the operation CID from a JWS token */
|
|
420
|
-
declare const computeOpCID: (jwsToken: string) => Promise<string | undefined>;
|
|
462
|
+
declare const isDependencyFailure: (res: Pick<IngestionResult, "dependencyMissing">) => boolean;
|
|
421
463
|
/**
|
|
422
464
|
* Process unsequenced raw ops in a fixed-point loop until no more progress
|
|
423
465
|
* is made. Returns the JWS tokens of newly sequenced ops and aggregate stats.
|
|
@@ -427,4 +469,4 @@ declare const sequenceOps: (store: RelayStore) => Promise<{
|
|
|
427
469
|
result: SequenceResult;
|
|
428
470
|
}>;
|
|
429
471
|
|
|
430
|
-
export { type BlobKey, type CreatedRelay, type IngestionResult, type LogEntry, MemoryRelayStore, type OperationKind, type PeerClient, type PeerConfig, type PeerLogEntry, type RelayIdentity, type RelayOptions, type RelayStore, type SequenceResult, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, computeOpCID, createCurrentKeyResolver, createHistoricalIdentityResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations, isDependencyFailure, sequenceOps };
|
|
472
|
+
export { type BlobKey, type CreatedRelay, type IngestionResult, type LogEntry, MemoryRelayStore, type OperationKind, type PeerClient, type PeerConfig, type PeerLogEntry, type RelayIdentity, type RelayOptions, type RelayStore, type SequenceResult, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, bootstrapRelayIdentityFromKey, chunkOps, computeOpCID, createCurrentKeyResolver, createHistoricalIdentityResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations, isDependencyFailure, sequenceOps };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createNewEd25519Keypair,
|
|
9
9
|
generateId,
|
|
10
|
+
importEd25519Keypair,
|
|
10
11
|
signPayloadEd25519
|
|
11
12
|
} from "@metalabel/dfos-protocol/crypto";
|
|
12
13
|
|
|
@@ -26,6 +27,16 @@ import {
|
|
|
26
27
|
verifyDFOSCredential
|
|
27
28
|
} from "@metalabel/dfos-protocol/credentials";
|
|
28
29
|
import { dagCborCanonicalEncode, decodeJwsUnsafe } from "@metalabel/dfos-protocol/crypto";
|
|
30
|
+
var FORK_POINT_STATE_ERROR_PREFIX = "failed to compute state at fork point: ";
|
|
31
|
+
var DEPENDENCY_FAILURE_SUBSTRINGS = ["unknown identity:", "unknown key "];
|
|
32
|
+
var isKeyResolutionFailure = (message) => DEPENDENCY_FAILURE_SUBSTRINGS.some((s) => message.includes(s));
|
|
33
|
+
var isCredentialDependencyFailure = (message) => message.includes("issuer identity not found:") || message.includes(" not found on identity ");
|
|
34
|
+
var computeOpCID = async (jwsToken) => {
|
|
35
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
36
|
+
if (!decoded) return "";
|
|
37
|
+
const encoded = await dagCborCanonicalEncode(decoded.payload);
|
|
38
|
+
return encoded.cid.toString();
|
|
39
|
+
};
|
|
29
40
|
var MAX_FUTURE_TIMESTAMP_MS = 24 * 60 * 60 * 1e3;
|
|
30
41
|
var isFutureTimestamp = (createdAt) => {
|
|
31
42
|
const ts = new Date(createdAt).getTime();
|
|
@@ -231,7 +242,13 @@ var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
|
|
|
231
242
|
const opType = payload["type"];
|
|
232
243
|
const isGenesis = opType === "create";
|
|
233
244
|
if (isGenesis) {
|
|
234
|
-
|
|
245
|
+
let identity;
|
|
246
|
+
try {
|
|
247
|
+
identity = await verifyIdentityChain({ didPrefix: "did:dfos", log: [jwsToken] });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const message = err instanceof Error ? err.message : "verification failed";
|
|
250
|
+
return { cid, status: "rejected", error: message };
|
|
251
|
+
}
|
|
235
252
|
const createdAt = payload["createdAt"];
|
|
236
253
|
const chain2 = {
|
|
237
254
|
did: identity.did,
|
|
@@ -252,15 +269,27 @@ var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
|
|
|
252
269
|
if (hashIdx < 0) return { cid, status: "rejected", error: "non-genesis kid must be a DID URL" };
|
|
253
270
|
const did = kid.substring(0, hashIdx);
|
|
254
271
|
const chain = await store.getIdentityChain(did);
|
|
255
|
-
if (!chain)
|
|
272
|
+
if (!chain)
|
|
273
|
+
return {
|
|
274
|
+
cid,
|
|
275
|
+
status: "rejected",
|
|
276
|
+
error: `unknown identity: ${did}`,
|
|
277
|
+
dependencyMissing: true
|
|
278
|
+
};
|
|
256
279
|
const previousCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
257
280
|
if (previousCID === chain.headCID) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
281
|
+
let extResult2;
|
|
282
|
+
try {
|
|
283
|
+
extResult2 = await verifyIdentityExtensionFromTrustedState({
|
|
284
|
+
currentState: chain.state,
|
|
285
|
+
headCID: chain.headCID,
|
|
286
|
+
lastCreatedAt: chain.lastCreatedAt,
|
|
287
|
+
newOp: jwsToken
|
|
288
|
+
});
|
|
289
|
+
} catch (err) {
|
|
290
|
+
const message = err instanceof Error ? err.message : "verification failed";
|
|
291
|
+
return { cid, status: "rejected", error: message };
|
|
292
|
+
}
|
|
264
293
|
const updated2 = {
|
|
265
294
|
did: chain.did,
|
|
266
295
|
log: [...chain.log, jwsToken],
|
|
@@ -276,18 +305,34 @@ var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
|
|
|
276
305
|
return { cid, status: "new", kind: "identity-op", chainId: did };
|
|
277
306
|
}
|
|
278
307
|
if (!previousCID || !chainLogContainsCID(chain.log, previousCID)) {
|
|
279
|
-
return {
|
|
308
|
+
return {
|
|
309
|
+
cid,
|
|
310
|
+
status: "rejected",
|
|
311
|
+
error: "unknown previous operation in identity chain",
|
|
312
|
+
dependencyMissing: true
|
|
313
|
+
};
|
|
280
314
|
}
|
|
281
315
|
const forkState = await store.getIdentityStateAtCID(did, previousCID);
|
|
282
316
|
if (!forkState) {
|
|
283
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
cid,
|
|
319
|
+
status: "rejected",
|
|
320
|
+
error: `${FORK_POINT_STATE_ERROR_PREFIX}${previousCID}`,
|
|
321
|
+
dependencyMissing: true
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
let extResult;
|
|
325
|
+
try {
|
|
326
|
+
extResult = await verifyIdentityExtensionFromTrustedState({
|
|
327
|
+
currentState: forkState.state,
|
|
328
|
+
headCID: previousCID,
|
|
329
|
+
lastCreatedAt: forkState.lastCreatedAt,
|
|
330
|
+
newOp: jwsToken
|
|
331
|
+
});
|
|
332
|
+
} catch (err) {
|
|
333
|
+
const message = err instanceof Error ? err.message : "verification failed";
|
|
334
|
+
return { cid, status: "rejected", error: message };
|
|
284
335
|
}
|
|
285
|
-
const extResult = await verifyIdentityExtensionFromTrustedState({
|
|
286
|
-
currentState: forkState.state,
|
|
287
|
-
headCID: previousCID,
|
|
288
|
-
lastCreatedAt: forkState.lastCreatedAt,
|
|
289
|
-
newOp: jwsToken
|
|
290
|
-
});
|
|
291
336
|
const updatedLog = [...chain.log, jwsToken];
|
|
292
337
|
const head = selectDeterministicHead(updatedLog);
|
|
293
338
|
let headState = chain.state;
|
|
@@ -345,15 +390,28 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
|
|
|
345
390
|
}
|
|
346
391
|
const resolveKey = createKeyResolver(store);
|
|
347
392
|
const resolveIdentity = createHistoricalIdentityResolver(store);
|
|
393
|
+
const isRevoked = (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
|
|
348
394
|
const opType = payload["type"];
|
|
349
395
|
const isGenesis = opType === "create";
|
|
350
396
|
if (isGenesis) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
397
|
+
let content;
|
|
398
|
+
try {
|
|
399
|
+
content = await verifyContentChain({
|
|
400
|
+
log: [jwsToken],
|
|
401
|
+
resolveKey,
|
|
402
|
+
enforceAuthorization: true,
|
|
403
|
+
resolveIdentity,
|
|
404
|
+
isRevoked
|
|
405
|
+
});
|
|
406
|
+
} catch (err) {
|
|
407
|
+
const message = err instanceof Error ? err.message : "verification failed";
|
|
408
|
+
return {
|
|
409
|
+
cid,
|
|
410
|
+
status: "rejected",
|
|
411
|
+
error: message,
|
|
412
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
413
|
+
};
|
|
414
|
+
}
|
|
357
415
|
const createdAt = payload["createdAt"];
|
|
358
416
|
const chain2 = {
|
|
359
417
|
contentId: content.contentId,
|
|
@@ -375,26 +433,48 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
|
|
|
375
433
|
}
|
|
376
434
|
const prevOp = await store.getOperation(previousCID);
|
|
377
435
|
if (!prevOp)
|
|
378
|
-
return {
|
|
436
|
+
return {
|
|
437
|
+
cid,
|
|
438
|
+
status: "rejected",
|
|
439
|
+
error: `unknown previous operation: ${previousCID}`,
|
|
440
|
+
dependencyMissing: true
|
|
441
|
+
};
|
|
379
442
|
if (prevOp.chainType !== "content") {
|
|
380
443
|
return { cid, status: "rejected", error: "previousOperationCID is not a content operation" };
|
|
381
444
|
}
|
|
382
445
|
const chain = await store.getContentChain(prevOp.chainId);
|
|
383
446
|
if (!chain)
|
|
384
|
-
return {
|
|
447
|
+
return {
|
|
448
|
+
cid,
|
|
449
|
+
status: "rejected",
|
|
450
|
+
error: `content chain not found: ${prevOp.chainId}`,
|
|
451
|
+
dependencyMissing: true
|
|
452
|
+
};
|
|
385
453
|
const creatorIdentity = await store.getIdentityChain(chain.state.creatorDID);
|
|
386
454
|
if (creatorIdentity?.state.isDeleted) {
|
|
387
455
|
return { cid, status: "rejected", error: "content creator identity is deleted" };
|
|
388
456
|
}
|
|
389
457
|
if (chain.state.headCID === previousCID) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
458
|
+
let extResult2;
|
|
459
|
+
try {
|
|
460
|
+
extResult2 = await verifyContentExtensionFromTrustedState({
|
|
461
|
+
currentState: chain.state,
|
|
462
|
+
lastCreatedAt: chain.lastCreatedAt,
|
|
463
|
+
newOp: jwsToken,
|
|
464
|
+
resolveKey,
|
|
465
|
+
enforceAuthorization: true,
|
|
466
|
+
resolveIdentity,
|
|
467
|
+
isRevoked
|
|
468
|
+
});
|
|
469
|
+
} catch (err) {
|
|
470
|
+
const message = err instanceof Error ? err.message : "verification failed";
|
|
471
|
+
return {
|
|
472
|
+
cid,
|
|
473
|
+
status: "rejected",
|
|
474
|
+
error: message,
|
|
475
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
398
478
|
const updated2 = {
|
|
399
479
|
contentId: chain.contentId,
|
|
400
480
|
genesisCID: chain.genesisCID,
|
|
@@ -410,20 +490,42 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
|
|
|
410
490
|
return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
|
|
411
491
|
}
|
|
412
492
|
if (!chainLogContainsCID(chain.log, previousCID)) {
|
|
413
|
-
return {
|
|
493
|
+
return {
|
|
494
|
+
cid,
|
|
495
|
+
status: "rejected",
|
|
496
|
+
error: "unknown previous operation in content chain",
|
|
497
|
+
dependencyMissing: true
|
|
498
|
+
};
|
|
414
499
|
}
|
|
415
500
|
const forkState = await store.getContentStateAtCID(chain.contentId, previousCID);
|
|
416
501
|
if (!forkState) {
|
|
417
|
-
return {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
502
|
+
return {
|
|
503
|
+
cid,
|
|
504
|
+
status: "rejected",
|
|
505
|
+
error: `${FORK_POINT_STATE_ERROR_PREFIX}${previousCID}`,
|
|
506
|
+
dependencyMissing: true
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
let extResult;
|
|
510
|
+
try {
|
|
511
|
+
extResult = await verifyContentExtensionFromTrustedState({
|
|
512
|
+
currentState: forkState.state,
|
|
513
|
+
lastCreatedAt: forkState.lastCreatedAt,
|
|
514
|
+
newOp: jwsToken,
|
|
515
|
+
resolveKey,
|
|
516
|
+
enforceAuthorization: true,
|
|
517
|
+
resolveIdentity,
|
|
518
|
+
isRevoked
|
|
519
|
+
});
|
|
520
|
+
} catch (err) {
|
|
521
|
+
const message = err instanceof Error ? err.message : "verification failed";
|
|
522
|
+
return {
|
|
523
|
+
cid,
|
|
524
|
+
status: "rejected",
|
|
525
|
+
error: message,
|
|
526
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
427
529
|
const updatedLog = [...chain.log, jwsToken];
|
|
428
530
|
const head = selectDeterministicHead(updatedLog);
|
|
429
531
|
let headState = chain.state;
|
|
@@ -447,13 +549,18 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
|
|
|
447
549
|
return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
|
|
448
550
|
};
|
|
449
551
|
var ingestBeacon = async (jwsToken, store, logEnabled) => {
|
|
450
|
-
const resolveKey =
|
|
552
|
+
const resolveKey = createCurrentKeyResolver(store);
|
|
451
553
|
let verified;
|
|
452
554
|
try {
|
|
453
555
|
verified = await verifyBeacon({ jwsToken, resolveKey });
|
|
454
556
|
} catch (err) {
|
|
455
557
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
456
|
-
return {
|
|
558
|
+
return {
|
|
559
|
+
cid: await computeOpCID(jwsToken),
|
|
560
|
+
status: "rejected",
|
|
561
|
+
error: message,
|
|
562
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
563
|
+
};
|
|
457
564
|
}
|
|
458
565
|
const did = verified.did;
|
|
459
566
|
const cid = verified.beaconCID;
|
|
@@ -465,7 +572,7 @@ var ingestBeacon = async (jwsToken, store, logEnabled) => {
|
|
|
465
572
|
if (existing) {
|
|
466
573
|
const existingTime = new Date(existing.state.createdAt).getTime();
|
|
467
574
|
const newTime = new Date(verified.createdAt).getTime();
|
|
468
|
-
if (newTime <=
|
|
575
|
+
if (newTime < existingTime || newTime === existingTime && cid <= existing.beaconCID) {
|
|
469
576
|
return { cid, status: "duplicate", kind: "beacon", chainId: did };
|
|
470
577
|
}
|
|
471
578
|
}
|
|
@@ -483,7 +590,12 @@ var ingestCountersign = async (jwsToken, store, logEnabled) => {
|
|
|
483
590
|
verified = await verifyCountersignature({ jwsToken, resolveKey });
|
|
484
591
|
} catch (err) {
|
|
485
592
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
486
|
-
return {
|
|
593
|
+
return {
|
|
594
|
+
cid: await computeOpCID(jwsToken),
|
|
595
|
+
status: "rejected",
|
|
596
|
+
error: message,
|
|
597
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
598
|
+
};
|
|
487
599
|
}
|
|
488
600
|
const cid = verified.countersignCID;
|
|
489
601
|
const { witnessDID, targetCID } = verified;
|
|
@@ -500,7 +612,12 @@ var ingestCountersign = async (jwsToken, store, logEnabled) => {
|
|
|
500
612
|
}
|
|
501
613
|
const targetOp = await store.getOperation(targetCID);
|
|
502
614
|
if (!targetOp) {
|
|
503
|
-
return {
|
|
615
|
+
return {
|
|
616
|
+
cid,
|
|
617
|
+
status: "rejected",
|
|
618
|
+
error: `unknown target operation: ${targetCID}`,
|
|
619
|
+
dependencyMissing: true
|
|
620
|
+
};
|
|
504
621
|
}
|
|
505
622
|
let targetAuthorDID = null;
|
|
506
623
|
if (targetOp.chainType === "identity") {
|
|
@@ -542,7 +659,12 @@ var ingestArtifact = async (jwsToken, store, logEnabled) => {
|
|
|
542
659
|
verified = await verifyArtifact({ jwsToken, resolveKey });
|
|
543
660
|
} catch (err) {
|
|
544
661
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
545
|
-
return {
|
|
662
|
+
return {
|
|
663
|
+
cid: await computeOpCID(jwsToken),
|
|
664
|
+
status: "rejected",
|
|
665
|
+
error: message,
|
|
666
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
667
|
+
};
|
|
546
668
|
}
|
|
547
669
|
const cid = verified.artifactCID;
|
|
548
670
|
const did = verified.payload.did;
|
|
@@ -574,7 +696,12 @@ var ingestRevocation = async (jwsToken, store, logEnabled) => {
|
|
|
574
696
|
verified = await verifyRevocation({ jwsToken, resolveKey });
|
|
575
697
|
} catch (err) {
|
|
576
698
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
577
|
-
return {
|
|
699
|
+
return {
|
|
700
|
+
cid: await computeOpCID(jwsToken),
|
|
701
|
+
status: "rejected",
|
|
702
|
+
error: message,
|
|
703
|
+
dependencyMissing: isKeyResolutionFailure(message)
|
|
704
|
+
};
|
|
578
705
|
}
|
|
579
706
|
const cid = verified.revocationCID;
|
|
580
707
|
const did = verified.did;
|
|
@@ -613,11 +740,16 @@ var ingestPublicCredential = async (jwsToken, store, logEnabled) => {
|
|
|
613
740
|
verified = await verifyDFOSCredential(jwsToken, { resolveIdentity });
|
|
614
741
|
} catch (err) {
|
|
615
742
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
616
|
-
return {
|
|
743
|
+
return {
|
|
744
|
+
cid: await computeOpCID(jwsToken),
|
|
745
|
+
status: "rejected",
|
|
746
|
+
error: message,
|
|
747
|
+
dependencyMissing: isCredentialDependencyFailure(message)
|
|
748
|
+
};
|
|
617
749
|
}
|
|
618
750
|
const cid = verified.credentialCID;
|
|
619
751
|
if (verified.aud !== "*") {
|
|
620
|
-
return { cid
|
|
752
|
+
return { cid, status: "rejected", error: "not a public credential" };
|
|
621
753
|
}
|
|
622
754
|
const existing = await store.getOperation(cid);
|
|
623
755
|
if (existing) {
|
|
@@ -726,9 +858,10 @@ var selectDeterministicHead = (log) => {
|
|
|
726
858
|
if (previousCID) hasChild.add(previousCID);
|
|
727
859
|
}
|
|
728
860
|
const tips = ops.filter((op) => !hasChild.has(op.cid));
|
|
861
|
+
const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
|
|
729
862
|
tips.sort((a, b) => {
|
|
730
|
-
if (a.createdAt !== b.createdAt) return b.createdAt
|
|
731
|
-
return b.cid
|
|
863
|
+
if (a.createdAt !== b.createdAt) return cmp(b.createdAt, a.createdAt);
|
|
864
|
+
return cmp(b.cid, a.cid);
|
|
732
865
|
});
|
|
733
866
|
return tips[0] ?? { cid: "", createdAt: "" };
|
|
734
867
|
};
|
|
@@ -763,14 +896,18 @@ var ingestOperations = async (tokens, store, options) => {
|
|
|
763
896
|
result = await ingestPublicCredential(op.jwsToken, store, logEnabled);
|
|
764
897
|
break;
|
|
765
898
|
default:
|
|
766
|
-
result = {
|
|
899
|
+
result = {
|
|
900
|
+
cid: await computeOpCID(op.jwsToken),
|
|
901
|
+
status: "rejected",
|
|
902
|
+
error: "unrecognized operation type"
|
|
903
|
+
};
|
|
767
904
|
}
|
|
768
905
|
indexedResults.push({ index: op.originalIndex, result });
|
|
769
906
|
} catch (err) {
|
|
770
907
|
const message = err instanceof Error ? err.message : "unexpected error";
|
|
771
908
|
indexedResults.push({
|
|
772
909
|
index: op.originalIndex,
|
|
773
|
-
result: { cid:
|
|
910
|
+
result: { cid: await computeOpCID(op.jwsToken), status: "rejected", error: message }
|
|
774
911
|
});
|
|
775
912
|
}
|
|
776
913
|
}
|
|
@@ -781,8 +918,28 @@ var ingestOperations = async (tokens, store, options) => {
|
|
|
781
918
|
var bootstrapRelayIdentity = async (store) => {
|
|
782
919
|
const keypair = createNewEd25519Keypair();
|
|
783
920
|
const keyId = generateId("key");
|
|
784
|
-
|
|
785
|
-
|
|
921
|
+
return bootstrapWithKeyMaterial(store, {
|
|
922
|
+
privateKey: keypair.privateKey,
|
|
923
|
+
publicKey: keypair.publicKey,
|
|
924
|
+
keyId
|
|
925
|
+
});
|
|
926
|
+
};
|
|
927
|
+
var bootstrapRelayIdentityFromKey = async (store, params) => {
|
|
928
|
+
const { publicKey } = importEd25519Keypair(params.privateKey);
|
|
929
|
+
return bootstrapWithKeyMaterial(store, {
|
|
930
|
+
privateKey: params.privateKey,
|
|
931
|
+
publicKey,
|
|
932
|
+
keyId: params.keyId,
|
|
933
|
+
...params.name !== void 0 ? { name: params.name } : {},
|
|
934
|
+
...params.createdAt !== void 0 ? { createdAt: params.createdAt } : {}
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
var bootstrapWithKeyMaterial = async (store, params) => {
|
|
938
|
+
const { privateKey, publicKey, keyId } = params;
|
|
939
|
+
const name = params.name ?? "DFOS Relay";
|
|
940
|
+
const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
941
|
+
const multibase = encodeEd25519Multikey(publicKey);
|
|
942
|
+
const signer = async (msg) => signPayloadEd25519(msg, privateKey);
|
|
786
943
|
const key = { id: keyId, type: "Multikey", publicKeyMultibase: multibase };
|
|
787
944
|
const identityOp = {
|
|
788
945
|
version: 1,
|
|
@@ -790,7 +947,7 @@ var bootstrapRelayIdentity = async (store) => {
|
|
|
790
947
|
authKeys: [key],
|
|
791
948
|
assertKeys: [key],
|
|
792
949
|
controllerKeys: [key],
|
|
793
|
-
createdAt
|
|
950
|
+
createdAt
|
|
794
951
|
};
|
|
795
952
|
const { jwsToken: identityJws } = await signIdentityOperation({
|
|
796
953
|
operation: identityOp,
|
|
@@ -808,9 +965,9 @@ var bootstrapRelayIdentity = async (store) => {
|
|
|
808
965
|
did,
|
|
809
966
|
content: {
|
|
810
967
|
$schema: "https://schemas.dfos.com/profile/v1",
|
|
811
|
-
name
|
|
968
|
+
name
|
|
812
969
|
},
|
|
813
|
-
createdAt
|
|
970
|
+
createdAt
|
|
814
971
|
};
|
|
815
972
|
const kid = `${did}#${keyId}`;
|
|
816
973
|
const { jwsToken: profileArtifactJws } = await signArtifact({
|
|
@@ -865,11 +1022,16 @@ var createHttpPeerClient = () => {
|
|
|
865
1022
|
},
|
|
866
1023
|
async submitOperations(peerUrl, operations) {
|
|
867
1024
|
try {
|
|
868
|
-
await fetch(new URL("/operations", peerUrl).toString(), {
|
|
1025
|
+
const res = await fetch(new URL("/operations", peerUrl).toString(), {
|
|
869
1026
|
method: "POST",
|
|
870
1027
|
headers: { "Content-Type": "application/json" },
|
|
871
1028
|
body: JSON.stringify({ operations })
|
|
872
1029
|
});
|
|
1030
|
+
if (!res.ok) {
|
|
1031
|
+
console.warn(
|
|
1032
|
+
`gossip submitOperations to ${peerUrl} returned ${res.status} (${operations.length} ops dropped)`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
873
1035
|
} catch {
|
|
874
1036
|
}
|
|
875
1037
|
}
|
|
@@ -878,7 +1040,7 @@ var createHttpPeerClient = () => {
|
|
|
878
1040
|
|
|
879
1041
|
// src/relay.ts
|
|
880
1042
|
import { createRequire } from "module";
|
|
881
|
-
import { dagCborCanonicalEncode as
|
|
1043
|
+
import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
|
|
882
1044
|
import { Hono } from "hono";
|
|
883
1045
|
import { z } from "zod";
|
|
884
1046
|
|
|
@@ -890,7 +1052,8 @@ import {
|
|
|
890
1052
|
verifyDFOSCredential as verifyDFOSCredential2
|
|
891
1053
|
} from "@metalabel/dfos-protocol/credentials";
|
|
892
1054
|
import { decodeJwsUnsafe as decodeJwsUnsafe2 } from "@metalabel/dfos-protocol/crypto";
|
|
893
|
-
var
|
|
1055
|
+
var DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS = 86400;
|
|
1056
|
+
var authenticateRequest = async (authHeader, relayDID, store, maxAuthTokenTTLSeconds = DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS) => {
|
|
894
1057
|
if (!authHeader) return null;
|
|
895
1058
|
if (!authHeader.startsWith("Bearer ")) return null;
|
|
896
1059
|
const token = authHeader.substring(7);
|
|
@@ -907,11 +1070,42 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
|
|
|
907
1070
|
return null;
|
|
908
1071
|
}
|
|
909
1072
|
try {
|
|
910
|
-
|
|
1073
|
+
const verified = verifyAuthToken({ token, publicKey, audience: relayDID });
|
|
1074
|
+
if (maxAuthTokenTTLSeconds > 0 && verified.exp - verified.iat > maxAuthTokenTTLSeconds) {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
return verified;
|
|
911
1078
|
} catch {
|
|
912
1079
|
return null;
|
|
913
1080
|
}
|
|
914
1081
|
};
|
|
1082
|
+
var hasPublicStandingAuth = async (contentId, action, store) => {
|
|
1083
|
+
const resource = `chain:${contentId}`;
|
|
1084
|
+
const publicCreds = await store.getPublicCredentials(resource);
|
|
1085
|
+
if (publicCreds.length === 0) return false;
|
|
1086
|
+
const chain = await store.getContentChain(contentId);
|
|
1087
|
+
if (!chain) return false;
|
|
1088
|
+
const resolveIdentity = createHistoricalIdentityResolver(store);
|
|
1089
|
+
const isRevoked = async (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
|
|
1090
|
+
for (const credJws of publicCreds) {
|
|
1091
|
+
try {
|
|
1092
|
+
const cred = await verifyDFOSCredential2(credJws, { resolveIdentity });
|
|
1093
|
+
const leafRevoked = await isRevoked(cred.iss, cred.credentialCID);
|
|
1094
|
+
if (leafRevoked) continue;
|
|
1095
|
+
const covers = await matchesResource(cred.att, resource, action);
|
|
1096
|
+
if (!covers) continue;
|
|
1097
|
+
await verifyDelegationChain(cred, {
|
|
1098
|
+
resolveIdentity,
|
|
1099
|
+
rootDID: chain.state.creatorDID,
|
|
1100
|
+
isRevoked
|
|
1101
|
+
});
|
|
1102
|
+
return true;
|
|
1103
|
+
} catch {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return false;
|
|
1108
|
+
};
|
|
915
1109
|
var verifyContentAccess = async (options) => {
|
|
916
1110
|
const { credentialJWS, requestedResource, action, store, creatorDID, requesterDID } = options;
|
|
917
1111
|
if (requesterDID && requesterDID === creatorDID) {
|
|
@@ -919,33 +1113,13 @@ var verifyContentAccess = async (options) => {
|
|
|
919
1113
|
}
|
|
920
1114
|
const resolveIdentity = createHistoricalIdentityResolver(store);
|
|
921
1115
|
const isRevoked = async (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
|
|
922
|
-
const manifestLookup = async (manifestContentId) => {
|
|
923
|
-
const chain = await store.getContentChain(manifestContentId);
|
|
924
|
-
if (!chain) return [];
|
|
925
|
-
const docCID = chain.state.currentDocumentCID;
|
|
926
|
-
if (!docCID) return [];
|
|
927
|
-
const blob = await store.getBlob({ creatorDID: chain.state.creatorDID, documentCID: docCID });
|
|
928
|
-
if (!blob) return [];
|
|
929
|
-
try {
|
|
930
|
-
const doc = JSON.parse(new TextDecoder().decode(blob));
|
|
931
|
-
const entries = doc["entries"];
|
|
932
|
-
if (!entries || typeof entries !== "object") return [];
|
|
933
|
-
return Object.values(entries).filter(
|
|
934
|
-
(v) => typeof v === "string" && !v.startsWith("did:") && !v.startsWith("bafyrei")
|
|
935
|
-
);
|
|
936
|
-
} catch {
|
|
937
|
-
return [];
|
|
938
|
-
}
|
|
939
|
-
};
|
|
940
1116
|
const publicCreds = await store.getPublicCredentials(requestedResource);
|
|
941
1117
|
for (const credJws of publicCreds) {
|
|
942
1118
|
try {
|
|
943
1119
|
const cred = await verifyDFOSCredential2(credJws, { resolveIdentity });
|
|
944
1120
|
const leafRevoked = await isRevoked(cred.iss, cred.credentialCID);
|
|
945
1121
|
if (leafRevoked) continue;
|
|
946
|
-
const covers = await matchesResource(cred.att, requestedResource, action
|
|
947
|
-
manifestLookup
|
|
948
|
-
});
|
|
1122
|
+
const covers = await matchesResource(cred.att, requestedResource, action);
|
|
949
1123
|
if (!covers) continue;
|
|
950
1124
|
await verifyDelegationChain(cred, { resolveIdentity, rootDID: creatorDID, isRevoked });
|
|
951
1125
|
return { granted: true, source: "public-credential", credential: cred };
|
|
@@ -966,9 +1140,7 @@ var verifyContentAccess = async (options) => {
|
|
|
966
1140
|
return { granted: false, source: "none" };
|
|
967
1141
|
}
|
|
968
1142
|
}
|
|
969
|
-
const covers = await matchesResource(cred.att, requestedResource, action
|
|
970
|
-
manifestLookup
|
|
971
|
-
});
|
|
1143
|
+
const covers = await matchesResource(cred.att, requestedResource, action);
|
|
972
1144
|
if (!covers) {
|
|
973
1145
|
return { granted: false, source: "none" };
|
|
974
1146
|
}
|
|
@@ -981,22 +1153,7 @@ var verifyContentAccess = async (options) => {
|
|
|
981
1153
|
};
|
|
982
1154
|
|
|
983
1155
|
// src/sequencer.ts
|
|
984
|
-
|
|
985
|
-
var isDependencyFailure = (error) => {
|
|
986
|
-
const patterns = [
|
|
987
|
-
"unknown previous operation",
|
|
988
|
-
"unknown identity:",
|
|
989
|
-
"content chain not found:",
|
|
990
|
-
"failed to compute state at fork point:"
|
|
991
|
-
];
|
|
992
|
-
return patterns.some((p) => error.includes(p));
|
|
993
|
-
};
|
|
994
|
-
var computeOpCID = async (jwsToken) => {
|
|
995
|
-
const decoded = decodeJwsUnsafe3(jwsToken);
|
|
996
|
-
if (!decoded) return void 0;
|
|
997
|
-
const encoded = await dagCborCanonicalEncode2(decoded.payload);
|
|
998
|
-
return encoded.cid.toString();
|
|
999
|
-
};
|
|
1156
|
+
var isDependencyFailure = (res) => res.dependencyMissing === true;
|
|
1000
1157
|
var sequenceOps = async (store) => {
|
|
1001
1158
|
const newOps = [];
|
|
1002
1159
|
const result = { sequenced: 0, rejected: 0, pending: 0 };
|
|
@@ -1017,7 +1174,7 @@ var sequenceOps = async (store) => {
|
|
|
1017
1174
|
} else if (res.status === "duplicate") {
|
|
1018
1175
|
sequencedCIDs.push(res.cid);
|
|
1019
1176
|
progress = true;
|
|
1020
|
-
} else if (res.status === "rejected" && !isDependencyFailure(res
|
|
1177
|
+
} else if (res.status === "rejected" && !isDependencyFailure(res)) {
|
|
1021
1178
|
await store.markOpRejected(res.cid, res.error ?? "unknown");
|
|
1022
1179
|
result.rejected++;
|
|
1023
1180
|
progress = true;
|
|
@@ -1036,13 +1193,37 @@ var sequenceOps = async (store) => {
|
|
|
1036
1193
|
// src/relay.ts
|
|
1037
1194
|
var require2 = createRequire(import.meta.url);
|
|
1038
1195
|
var { version: RELAY_VERSION } = require2("../package.json");
|
|
1196
|
+
var MAX_OPERATIONS_PER_BATCH = 100;
|
|
1039
1197
|
var IngestBody = z.object({
|
|
1040
|
-
operations: z.array(z.string()).min(1).max(
|
|
1198
|
+
operations: z.array(z.string()).min(1).max(MAX_OPERATIONS_PER_BATCH)
|
|
1041
1199
|
});
|
|
1200
|
+
var MAX_GOSSIP_BATCH = MAX_OPERATIONS_PER_BATCH;
|
|
1201
|
+
var chunkOps = (items, size) => {
|
|
1202
|
+
const chunks = [];
|
|
1203
|
+
for (let start = 0; start < items.length; start += size) {
|
|
1204
|
+
chunks.push(items.slice(start, start + size));
|
|
1205
|
+
}
|
|
1206
|
+
return chunks;
|
|
1207
|
+
};
|
|
1208
|
+
var MAX_BODY_BYTES = 16 << 20;
|
|
1209
|
+
var exceedsBodyCap = (contentLength) => {
|
|
1210
|
+
if (!contentLength) return false;
|
|
1211
|
+
const n = Number(contentLength);
|
|
1212
|
+
return Number.isFinite(n) && n > MAX_BODY_BYTES;
|
|
1213
|
+
};
|
|
1214
|
+
var parseLimit = (raw, defaultLimit, maxLimit) => {
|
|
1215
|
+
if (raw === void 0 || raw === "") return defaultLimit;
|
|
1216
|
+
if (!/^-?\d+$/.test(raw)) return defaultLimit;
|
|
1217
|
+
const n = Number(raw);
|
|
1218
|
+
if (!Number.isSafeInteger(n) || n < 1) return defaultLimit;
|
|
1219
|
+
if (n > maxLimit) return maxLimit;
|
|
1220
|
+
return n;
|
|
1221
|
+
};
|
|
1042
1222
|
var createRelay = async (options) => {
|
|
1043
1223
|
const { store } = options;
|
|
1044
1224
|
const contentEnabled = options.content !== false;
|
|
1045
1225
|
const logEnabled = options.log !== false;
|
|
1226
|
+
const maxAuthTokenTTLSeconds = options.maxAuthTokenTTLSeconds ?? DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS;
|
|
1046
1227
|
const peers = options.peers ?? [];
|
|
1047
1228
|
const peerClient = options.peerClient;
|
|
1048
1229
|
const gossipPeers = peers.filter((p) => p.gossip !== false);
|
|
@@ -1054,8 +1235,10 @@ var createRelay = async (options) => {
|
|
|
1054
1235
|
const gossip = (ops) => {
|
|
1055
1236
|
if (ops.length === 0 || gossipPeers.length === 0 || !peerClient) return;
|
|
1056
1237
|
for (const peer of gossipPeers) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1238
|
+
for (const chunk of chunkOps(ops, MAX_GOSSIP_BATCH)) {
|
|
1239
|
+
peerClient.submitOperations(peer.url, chunk).catch(() => {
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1059
1242
|
}
|
|
1060
1243
|
};
|
|
1061
1244
|
const ingestWithGossip = async (tokens) => {
|
|
@@ -1081,6 +1264,20 @@ var createRelay = async (options) => {
|
|
|
1081
1264
|
return results;
|
|
1082
1265
|
};
|
|
1083
1266
|
const app = new Hono();
|
|
1267
|
+
app.use("*", async (c, next) => {
|
|
1268
|
+
if (c.req.method === "OPTIONS") {
|
|
1269
|
+
return c.body(null, 204, {
|
|
1270
|
+
"Access-Control-Allow-Origin": "*",
|
|
1271
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
|
|
1272
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
await next();
|
|
1276
|
+
c.res.headers.set("Access-Control-Allow-Origin", "*");
|
|
1277
|
+
c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
|
|
1278
|
+
c.res.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1279
|
+
return;
|
|
1280
|
+
});
|
|
1084
1281
|
app.get("/.well-known/dfos-relay", (c) => {
|
|
1085
1282
|
return c.json({
|
|
1086
1283
|
did: relayDID,
|
|
@@ -1096,6 +1293,9 @@ var createRelay = async (options) => {
|
|
|
1096
1293
|
});
|
|
1097
1294
|
});
|
|
1098
1295
|
app.post("/operations", async (c) => {
|
|
1296
|
+
if (exceedsBodyCap(c.req.header("content-length"))) {
|
|
1297
|
+
return c.json({ error: "request body too large" }, 413);
|
|
1298
|
+
}
|
|
1099
1299
|
let body;
|
|
1100
1300
|
try {
|
|
1101
1301
|
body = await c.req.json();
|
|
@@ -1125,9 +1325,9 @@ var createRelay = async (options) => {
|
|
|
1125
1325
|
const chain = await store.getIdentityChain(did);
|
|
1126
1326
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
1127
1327
|
const after = c.req.query("after");
|
|
1128
|
-
const limit =
|
|
1328
|
+
const limit = parseLimit(c.req.query("limit"), 100, 1e3);
|
|
1129
1329
|
const entries = chain.log.map((jws) => {
|
|
1130
|
-
const decoded =
|
|
1330
|
+
const decoded = decodeJwsUnsafe3(jws);
|
|
1131
1331
|
return { cid: decoded?.header.cid || "", jwsToken: jws };
|
|
1132
1332
|
});
|
|
1133
1333
|
let startIdx = 0;
|
|
@@ -1171,9 +1371,9 @@ var createRelay = async (options) => {
|
|
|
1171
1371
|
const chain = await store.getContentChain(contentId);
|
|
1172
1372
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
1173
1373
|
const after = c.req.query("after");
|
|
1174
|
-
const limit =
|
|
1374
|
+
const limit = parseLimit(c.req.query("limit"), 100, 1e3);
|
|
1175
1375
|
const entries = chain.log.map((jws) => {
|
|
1176
|
-
const decoded =
|
|
1376
|
+
const decoded = decodeJwsUnsafe3(jws);
|
|
1177
1377
|
return { cid: decoded?.header.cid || "", jwsToken: jws };
|
|
1178
1378
|
});
|
|
1179
1379
|
let startIdx = 0;
|
|
@@ -1188,20 +1388,28 @@ var createRelay = async (options) => {
|
|
|
1188
1388
|
app.get("/content/:contentId/documents", async (c) => {
|
|
1189
1389
|
if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
|
|
1190
1390
|
const contentId = c.req.param("contentId");
|
|
1191
|
-
const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
|
|
1192
|
-
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
1193
1391
|
const chain = await store.getContentChain(contentId);
|
|
1194
1392
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1393
|
+
const publicAccess = await hasPublicStandingAuth(contentId, "read", store);
|
|
1394
|
+
if (!publicAccess) {
|
|
1395
|
+
const auth = await authenticateRequest(
|
|
1396
|
+
c.req.header("authorization"),
|
|
1397
|
+
relayDID,
|
|
1398
|
+
store,
|
|
1399
|
+
maxAuthTokenTTLSeconds
|
|
1400
|
+
);
|
|
1401
|
+
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
1402
|
+
const accessError = await verifyReadAccess(
|
|
1403
|
+
auth,
|
|
1404
|
+
chain,
|
|
1405
|
+
contentId,
|
|
1406
|
+
c.req.header("x-credential"),
|
|
1407
|
+
store
|
|
1408
|
+
);
|
|
1409
|
+
if (accessError) return accessError;
|
|
1410
|
+
}
|
|
1203
1411
|
const after = c.req.query("after");
|
|
1204
|
-
const limit =
|
|
1412
|
+
const limit = parseLimit(c.req.query("limit"), 100, 1e3);
|
|
1205
1413
|
const result = await store.getDocuments(contentId, {
|
|
1206
1414
|
...after ? { after } : {},
|
|
1207
1415
|
limit
|
|
@@ -1273,7 +1481,7 @@ var createRelay = async (options) => {
|
|
|
1273
1481
|
app.get("/log", async (c) => {
|
|
1274
1482
|
if (!logEnabled) return c.json({ error: "global log not available" }, 501);
|
|
1275
1483
|
const afterParam = c.req.query("after");
|
|
1276
|
-
const limit =
|
|
1484
|
+
const limit = parseLimit(c.req.query("limit"), 100, 1e3);
|
|
1277
1485
|
const result = await store.readLog(afterParam ? { after: afterParam, limit } : { limit });
|
|
1278
1486
|
return c.json(result);
|
|
1279
1487
|
});
|
|
@@ -1281,14 +1489,22 @@ var createRelay = async (options) => {
|
|
|
1281
1489
|
if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
|
|
1282
1490
|
const contentId = c.req.param("contentId");
|
|
1283
1491
|
const operationCID = c.req.param("operationCID");
|
|
1284
|
-
|
|
1492
|
+
if (exceedsBodyCap(c.req.header("content-length"))) {
|
|
1493
|
+
return c.json({ error: "request body too large" }, 413);
|
|
1494
|
+
}
|
|
1495
|
+
const auth = await authenticateRequest(
|
|
1496
|
+
c.req.header("authorization"),
|
|
1497
|
+
relayDID,
|
|
1498
|
+
store,
|
|
1499
|
+
maxAuthTokenTTLSeconds
|
|
1500
|
+
);
|
|
1285
1501
|
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
1286
1502
|
const chain = await store.getContentChain(contentId);
|
|
1287
1503
|
if (!chain) return c.json({ error: "content chain not found" }, 404);
|
|
1288
1504
|
let documentCID = null;
|
|
1289
1505
|
let operationSignerDID = null;
|
|
1290
1506
|
for (const token of chain.log) {
|
|
1291
|
-
const decoded =
|
|
1507
|
+
const decoded = decodeJwsUnsafe3(token);
|
|
1292
1508
|
if (!decoded) continue;
|
|
1293
1509
|
if (decoded.header.cid !== operationCID) continue;
|
|
1294
1510
|
const payload = decoded.payload;
|
|
@@ -1303,9 +1519,12 @@ var createRelay = async (options) => {
|
|
|
1303
1519
|
return c.json({ error: "not authorized \u2014 must be chain creator or operation signer" }, 403);
|
|
1304
1520
|
}
|
|
1305
1521
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
1522
|
+
if (bytes.byteLength > MAX_BODY_BYTES) {
|
|
1523
|
+
return c.json({ error: "request body too large" }, 413);
|
|
1524
|
+
}
|
|
1306
1525
|
try {
|
|
1307
1526
|
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
1308
|
-
const encoded = await
|
|
1527
|
+
const encoded = await dagCborCanonicalEncode2(parsed);
|
|
1309
1528
|
if (encoded.cid.toString() !== documentCID) {
|
|
1310
1529
|
return c.json({ error: "blob bytes do not match documentCID" }, 400);
|
|
1311
1530
|
}
|
|
@@ -1323,7 +1542,8 @@ var createRelay = async (options) => {
|
|
|
1323
1542
|
authHeader: c.req.header("authorization"),
|
|
1324
1543
|
credHeader: c.req.header("x-credential"),
|
|
1325
1544
|
relayDID,
|
|
1326
|
-
store
|
|
1545
|
+
store,
|
|
1546
|
+
maxAuthTokenTTLSeconds
|
|
1327
1547
|
});
|
|
1328
1548
|
});
|
|
1329
1549
|
app.get("/content/:contentId/blob/:ref", async (c) => {
|
|
@@ -1334,20 +1554,24 @@ var createRelay = async (options) => {
|
|
|
1334
1554
|
authHeader: c.req.header("authorization"),
|
|
1335
1555
|
credHeader: c.req.header("x-credential"),
|
|
1336
1556
|
relayDID,
|
|
1337
|
-
store
|
|
1557
|
+
store,
|
|
1558
|
+
maxAuthTokenTTLSeconds
|
|
1338
1559
|
});
|
|
1339
1560
|
});
|
|
1561
|
+
const maxOpsPerSyncCycle = 5e3;
|
|
1340
1562
|
const syncFromPeers = async () => {
|
|
1341
1563
|
if (!peerClient) return;
|
|
1342
1564
|
for (const peer of syncPeers) {
|
|
1343
1565
|
let cursor = await store.getPeerCursor(peer.url);
|
|
1344
|
-
|
|
1566
|
+
let fetched = 0;
|
|
1567
|
+
while (fetched < maxOpsPerSyncCycle) {
|
|
1345
1568
|
const page = await peerClient.getOperationLog(peer.url, {
|
|
1346
1569
|
...cursor ? { after: cursor } : {},
|
|
1347
1570
|
limit: 1e3
|
|
1348
1571
|
});
|
|
1349
1572
|
if (!page || page.entries.length === 0) break;
|
|
1350
1573
|
await ingestWithGossip(page.entries.map((e) => e.jwsToken));
|
|
1574
|
+
fetched += page.entries.length;
|
|
1351
1575
|
cursor = page.cursor ?? page.entries[page.entries.length - 1].cid;
|
|
1352
1576
|
await store.setPeerCursor(peer.url, cursor);
|
|
1353
1577
|
if (!page.cursor) break;
|
|
@@ -1361,20 +1585,23 @@ var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
|
1361
1585
|
headers: { "content-type": "application/json" }
|
|
1362
1586
|
});
|
|
1363
1587
|
var readBlob = async (params) => {
|
|
1364
|
-
const { contentId, ref, authHeader, credHeader, relayDID, store } = params;
|
|
1365
|
-
const auth = await authenticateRequest(authHeader, relayDID, store);
|
|
1366
|
-
if (!auth) return jsonResponse({ error: "authentication required" }, 401);
|
|
1588
|
+
const { contentId, ref, authHeader, credHeader, relayDID, store, maxAuthTokenTTLSeconds } = params;
|
|
1367
1589
|
const chain = await store.getContentChain(contentId);
|
|
1368
1590
|
if (!chain) return jsonResponse({ error: "content chain not found" }, 404);
|
|
1369
|
-
const
|
|
1370
|
-
if (
|
|
1591
|
+
const publicAccess = await hasPublicStandingAuth(contentId, "read", store);
|
|
1592
|
+
if (!publicAccess) {
|
|
1593
|
+
const auth = await authenticateRequest(authHeader, relayDID, store, maxAuthTokenTTLSeconds);
|
|
1594
|
+
if (!auth) return jsonResponse({ error: "authentication required" }, 401);
|
|
1595
|
+
const credError = await verifyReadAccess(auth, chain, contentId, credHeader, store);
|
|
1596
|
+
if (credError) return credError;
|
|
1597
|
+
}
|
|
1371
1598
|
let documentCID = null;
|
|
1372
1599
|
let operationFound = ref === "head";
|
|
1373
1600
|
if (ref === "head") {
|
|
1374
1601
|
documentCID = chain.state.currentDocumentCID;
|
|
1375
1602
|
} else {
|
|
1376
1603
|
for (const token of chain.log) {
|
|
1377
|
-
const decoded =
|
|
1604
|
+
const decoded = decodeJwsUnsafe3(token);
|
|
1378
1605
|
if (!decoded) continue;
|
|
1379
1606
|
if (decoded.header.cid === ref) {
|
|
1380
1607
|
operationFound = true;
|
|
@@ -1410,7 +1637,7 @@ var verifyReadAccess = async (auth, chain, contentId, credHeader, store) => {
|
|
|
1410
1637
|
|
|
1411
1638
|
// src/store.ts
|
|
1412
1639
|
import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
|
|
1413
|
-
import { decodeJwsUnsafe as
|
|
1640
|
+
import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
|
|
1414
1641
|
var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
|
|
1415
1642
|
var MemoryRelayStore = class {
|
|
1416
1643
|
operations = /* @__PURE__ */ new Map();
|
|
@@ -1460,12 +1687,12 @@ var MemoryRelayStore = class {
|
|
|
1460
1687
|
}
|
|
1461
1688
|
async addCountersignature(operationCID, jwsToken) {
|
|
1462
1689
|
const existing = this.countersignatures.get(operationCID) ?? [];
|
|
1463
|
-
const decoded =
|
|
1690
|
+
const decoded = decodeJwsUnsafe4(jwsToken);
|
|
1464
1691
|
if (decoded) {
|
|
1465
1692
|
const kid = decoded.header.kid;
|
|
1466
1693
|
const witnessDID = kid.includes("#") ? kid.split("#")[0] : kid;
|
|
1467
1694
|
for (const cs of existing) {
|
|
1468
|
-
const d =
|
|
1695
|
+
const d = decodeJwsUnsafe4(cs);
|
|
1469
1696
|
if (!d) continue;
|
|
1470
1697
|
const existingKid = d.header.kid;
|
|
1471
1698
|
const existingDID = existingKid.includes("#") ? existingKid.split("#")[0] : existingKid;
|
|
@@ -1504,10 +1731,6 @@ var MemoryRelayStore = class {
|
|
|
1504
1731
|
tokens.push(cred.jwsToken);
|
|
1505
1732
|
break;
|
|
1506
1733
|
}
|
|
1507
|
-
if (isChainRequest && att.resource.startsWith("manifest:")) {
|
|
1508
|
-
tokens.push(cred.jwsToken);
|
|
1509
|
-
break;
|
|
1510
|
-
}
|
|
1511
1734
|
}
|
|
1512
1735
|
}
|
|
1513
1736
|
return tokens;
|
|
@@ -1524,7 +1747,7 @@ var MemoryRelayStore = class {
|
|
|
1524
1747
|
if (!chain) return { documents: [], cursor: null };
|
|
1525
1748
|
const entries = [];
|
|
1526
1749
|
for (const jws of chain.log) {
|
|
1527
|
-
const decoded =
|
|
1750
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
1528
1751
|
if (!decoded) continue;
|
|
1529
1752
|
const payload = decoded.payload;
|
|
1530
1753
|
const cid = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
@@ -1586,7 +1809,7 @@ var MemoryRelayStore = class {
|
|
|
1586
1809
|
if (!chain) return null;
|
|
1587
1810
|
const opsByCID = /* @__PURE__ */ new Map();
|
|
1588
1811
|
for (const jws of chain.log) {
|
|
1589
|
-
const decoded =
|
|
1812
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
1590
1813
|
if (!decoded) continue;
|
|
1591
1814
|
const payload = decoded.payload;
|
|
1592
1815
|
const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
@@ -1603,7 +1826,7 @@ var MemoryRelayStore = class {
|
|
|
1603
1826
|
currentCID = op.previousCID;
|
|
1604
1827
|
}
|
|
1605
1828
|
const identity = await verifyIdentityChain2({ didPrefix: "did:dfos", log: path });
|
|
1606
|
-
const targetDecoded =
|
|
1829
|
+
const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
|
|
1607
1830
|
const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
|
|
1608
1831
|
return { state: identity, lastCreatedAt };
|
|
1609
1832
|
}
|
|
@@ -1612,7 +1835,7 @@ var MemoryRelayStore = class {
|
|
|
1612
1835
|
if (!chain) return null;
|
|
1613
1836
|
const opsByCID = /* @__PURE__ */ new Map();
|
|
1614
1837
|
for (const jws of chain.log) {
|
|
1615
|
-
const decoded =
|
|
1838
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
1616
1839
|
if (!decoded) continue;
|
|
1617
1840
|
const payload = decoded.payload;
|
|
1618
1841
|
const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
@@ -1639,7 +1862,7 @@ var MemoryRelayStore = class {
|
|
|
1639
1862
|
enforceAuthorization: true,
|
|
1640
1863
|
resolveIdentity
|
|
1641
1864
|
});
|
|
1642
|
-
const targetDecoded =
|
|
1865
|
+
const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
|
|
1643
1866
|
const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
|
|
1644
1867
|
return { state: content, lastCreatedAt };
|
|
1645
1868
|
}
|
|
@@ -1673,8 +1896,7 @@ var MemoryRelayStore = class {
|
|
|
1673
1896
|
}
|
|
1674
1897
|
}
|
|
1675
1898
|
async markOpRejected(cid, _reason) {
|
|
1676
|
-
|
|
1677
|
-
if (entry) entry.status = "rejected";
|
|
1899
|
+
this.rawOps.delete(cid);
|
|
1678
1900
|
}
|
|
1679
1901
|
async countUnsequenced() {
|
|
1680
1902
|
let count = 0;
|
|
@@ -1692,6 +1914,8 @@ var MemoryRelayStore = class {
|
|
|
1692
1914
|
export {
|
|
1693
1915
|
MemoryRelayStore,
|
|
1694
1916
|
bootstrapRelayIdentity,
|
|
1917
|
+
bootstrapRelayIdentityFromKey,
|
|
1918
|
+
chunkOps,
|
|
1695
1919
|
computeOpCID,
|
|
1696
1920
|
createCurrentKeyResolver,
|
|
1697
1921
|
createHistoricalIdentityResolver,
|
package/dist/serve.js
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
// src/serve.ts
|
|
2
2
|
import { createServer } from "http";
|
|
3
|
+
var MAX_STREAM_BODY_BYTES = (16 << 20) + (1 << 20);
|
|
3
4
|
var serve = (app, options = {}) => {
|
|
4
5
|
const { port = 4444, hostname } = options;
|
|
5
6
|
const server = createServer(async (req, res) => {
|
|
6
7
|
const url = new URL(req.url ?? "/", `http://${hostname ?? "localhost"}:${port}`);
|
|
8
|
+
const reject413 = () => {
|
|
9
|
+
if (res.headersSent) return;
|
|
10
|
+
res.writeHead(413, {
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
connection: "close"
|
|
13
|
+
});
|
|
14
|
+
res.end(JSON.stringify({ error: "request body too large" }));
|
|
15
|
+
};
|
|
16
|
+
const declaredLength = Number(req.headers["content-length"]);
|
|
17
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_STREAM_BODY_BYTES) {
|
|
18
|
+
reject413();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
7
21
|
const chunks = [];
|
|
8
|
-
|
|
22
|
+
let total = 0;
|
|
23
|
+
let aborted = false;
|
|
24
|
+
for await (const chunk of req) {
|
|
25
|
+
total += chunk.length;
|
|
26
|
+
if (total > MAX_STREAM_BODY_BYTES) {
|
|
27
|
+
aborted = true;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
chunks.push(chunk);
|
|
31
|
+
}
|
|
32
|
+
if (aborted) {
|
|
33
|
+
reject413();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
9
36
|
const body = Buffer.concat(chunks);
|
|
10
37
|
const headers = new Headers();
|
|
11
38
|
for (const [k, v] of Object.entries(req.headers)) {
|
package/openapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
3
|
title: DFOS Web Relay
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
description: |
|
|
6
6
|
HTTP relay for the DFOS protocol. Receives, verifies, stores, and serves
|
|
7
7
|
identity chains, content chains, beacons, countersignatures, and content blobs.
|
|
@@ -185,7 +185,7 @@ paths:
|
|
|
185
185
|
required: true
|
|
186
186
|
schema:
|
|
187
187
|
type: string
|
|
188
|
-
description: Content identifier (
|
|
188
|
+
description: Content identifier (31-char hash)
|
|
189
189
|
responses:
|
|
190
190
|
'200':
|
|
191
191
|
description: Content chain state and log
|
|
@@ -416,6 +416,70 @@ paths:
|
|
|
416
416
|
'501':
|
|
417
417
|
description: Documents capability not enabled
|
|
418
418
|
|
|
419
|
+
/log:
|
|
420
|
+
get:
|
|
421
|
+
operationId: getLog
|
|
422
|
+
summary: Paginated global log of all accepted operations
|
|
423
|
+
description: |
|
|
424
|
+
Returns every operation the relay has accepted — across all identity and
|
|
425
|
+
content chains, plus beacons and countersignatures — in acceptance order.
|
|
426
|
+
Cursor-based pagination. Used by peer relays to background-sync. Available
|
|
427
|
+
only when the relay advertises the `log` capability; otherwise returns 501.
|
|
428
|
+
tags: [Proof Plane]
|
|
429
|
+
parameters:
|
|
430
|
+
- name: after
|
|
431
|
+
in: query
|
|
432
|
+
required: false
|
|
433
|
+
schema:
|
|
434
|
+
type: string
|
|
435
|
+
description: CID cursor — start after this operation CID
|
|
436
|
+
- name: limit
|
|
437
|
+
in: query
|
|
438
|
+
required: false
|
|
439
|
+
schema:
|
|
440
|
+
type: integer
|
|
441
|
+
default: 100
|
|
442
|
+
maximum: 1000
|
|
443
|
+
responses:
|
|
444
|
+
'200':
|
|
445
|
+
description: Global log entries
|
|
446
|
+
content:
|
|
447
|
+
application/json:
|
|
448
|
+
schema:
|
|
449
|
+
type: object
|
|
450
|
+
required: [entries, cursor]
|
|
451
|
+
properties:
|
|
452
|
+
entries:
|
|
453
|
+
type: array
|
|
454
|
+
items:
|
|
455
|
+
type: object
|
|
456
|
+
required: [cid, jwsToken, kind, chainId]
|
|
457
|
+
properties:
|
|
458
|
+
cid:
|
|
459
|
+
type: string
|
|
460
|
+
jwsToken:
|
|
461
|
+
type: string
|
|
462
|
+
kind:
|
|
463
|
+
type: string
|
|
464
|
+
enum:
|
|
465
|
+
[
|
|
466
|
+
identity-op,
|
|
467
|
+
content-op,
|
|
468
|
+
beacon,
|
|
469
|
+
artifact,
|
|
470
|
+
countersign,
|
|
471
|
+
revocation,
|
|
472
|
+
credential,
|
|
473
|
+
]
|
|
474
|
+
chainId:
|
|
475
|
+
type: string
|
|
476
|
+
description: Chain identifier (DID or contentId)
|
|
477
|
+
cursor:
|
|
478
|
+
type: string
|
|
479
|
+
nullable: true
|
|
480
|
+
'501':
|
|
481
|
+
description: Global log capability not enabled
|
|
482
|
+
|
|
419
483
|
/identities/{did}/log:
|
|
420
484
|
get:
|
|
421
485
|
operationId: getIdentityLog
|
|
@@ -483,7 +547,7 @@ paths:
|
|
|
483
547
|
required: true
|
|
484
548
|
schema:
|
|
485
549
|
type: string
|
|
486
|
-
description: Content identifier (
|
|
550
|
+
description: Content identifier (31-char hash)
|
|
487
551
|
- name: after
|
|
488
552
|
in: query
|
|
489
553
|
required: false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalabel/dfos-web-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, beacons, and content blobs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,18 +41,18 @@
|
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"hono": "^4.12.
|
|
45
|
-
"zod": "^4.3
|
|
44
|
+
"hono": "^4.12.23",
|
|
45
|
+
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@metalabel/dfos-protocol": "^0.
|
|
48
|
+
"@metalabel/dfos-protocol": "^0.10.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.4",
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
|
-
"tsx": "^4.
|
|
54
|
-
"vitest": "^4.1.
|
|
55
|
-
"@metalabel/dfos-protocol": "0.
|
|
53
|
+
"tsx": "^4.22.4",
|
|
54
|
+
"vitest": "^4.1.8",
|
|
55
|
+
"@metalabel/dfos-protocol": "0.10.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|