@metalabel/dfos-web-relay 0.7.1 → 0.8.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @metalabel/dfos-web-relay
2
2
 
3
- Portable HTTP relay for the [DFOS protocol](https://protocol.dfos.com). Receives, verifies, stores, and serves identity chains, content chains, beacons, countersignatures, and content blobs.
3
+ Relays verify everything they receive and serve everything they've verified. No trust between relays, no hierarchy, no central authority. Topology is emergent. Portable HTTP relay for the [DFOS protocol](https://protocol.dfos.com).
4
4
 
5
5
  See [WEB-RELAY.md](../../specs/WEB-RELAY.md) for the full relay specification.
6
6
 
@@ -56,39 +56,7 @@ serve({ port: 4444 });
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**: Auth token required. Chain creator can download directly. Other identities must present a `DFOSContentRead` VC-JWT credential (issued by the creator) in the `X-Credential` header.
60
-
61
- ## Conformance Test Suite
62
-
63
- The `conformance/` directory contains a Go integration test suite that exercises the full relay HTTP surface. It runs against any live relay via the `RELAY_URL` environment variable.
64
-
65
- ```bash
66
- # Run against a local relay
67
- RELAY_URL=http://localhost:4444 go test -v -count=1 ./conformance/
68
-
69
- # Run against a remote relay
70
- RELAY_URL=https://registry.imajin.ai/relay go test -v -count=1 ./conformance/
71
- ```
72
-
73
- 77 tests covering:
74
-
75
- - Well-known discovery
76
- - Identity lifecycle (create, update, delete, batch, idempotency, controller key rotation)
77
- - Content lifecycle (create, update, delete, fork acceptance, DAG logs, deterministic head selection, post-delete rejection, notes, long chains)
78
- - Content update after auth key rotation, multiple independent chains
79
- - Operations by CID
80
- - Beacons (create, replacement, not-found, unknown/deleted identity)
81
- - Countersignatures (dedup, empty result, multi-witness, self-countersign, non-existent operation)
82
- - Blob upload/download (CID verification, auth, credential-based access, multi-version, idempotent upload)
83
- - Delegated content operations (write credentials, delegated blob upload, delegated delete)
84
- - Credentials (expiry, scope mismatch, type enforcement, deleted issuer behavior)
85
- - Signature verification (tampered signature, wrong signing key)
86
- - Auth edge cases (wrong audience, expired token, rotated-out key)
87
- - Batch processing (3-step dependency sort, content-identity sort, large batch, dedup, mixed valid/invalid, multi-chain)
88
- - Input validation (malformed JSON, empty operations, invalid JWS)
89
- - Future timestamp guard (reject identity/content ops >24h ahead)
90
-
91
- The conformance suite depends on [`dfos-protocol-go`](../dfos-protocol-go) for protocol operations.
59
+ **Download**: 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.
92
60
 
93
61
  ## Peering
94
62
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { VerifiedIdentity, VerifiedContentChain, VerifiedBeacon } from '@metalabel/dfos-protocol/chain';
2
+ import { Attenuation } from '@metalabel/dfos-protocol/credentials';
2
3
  import { Hono } from 'hono';
3
4
 
4
5
  interface RelayIdentity {
@@ -93,7 +94,7 @@ interface StoredOperation {
93
94
  cid: string;
94
95
  jwsToken: string;
95
96
  /** Which chain type this operation belongs to */
96
- chainType: 'identity' | 'content' | 'artifact' | 'beacon' | 'countersign';
97
+ chainType: 'identity' | 'content' | 'artifact' | 'beacon' | 'countersign' | 'revocation' | 'credential';
97
98
  /** The chain identifier — DID for identity/beacon/artifact, contentId for content, targetCID for countersign */
98
99
  chainId: string;
99
100
  }
@@ -110,7 +111,27 @@ interface LogEntry {
110
111
  chainId: string;
111
112
  }
112
113
  /** All operation kinds in the protocol */
113
- type OperationKind = 'identity-op' | 'content-op' | 'beacon' | 'artifact' | 'countersign';
114
+ type OperationKind = 'identity-op' | 'content-op' | 'beacon' | 'artifact' | 'countersign' | 'revocation' | 'credential';
115
+ interface StoredRevocation {
116
+ cid: string;
117
+ issuerDID: string;
118
+ credentialCID: string;
119
+ jwsToken: string;
120
+ }
121
+ interface StoredDocument {
122
+ operationCID: string;
123
+ documentCID: string | null;
124
+ document: unknown | null;
125
+ signerDID: string;
126
+ createdAt: string;
127
+ }
128
+ interface StoredPublicCredential {
129
+ cid: string;
130
+ issuerDID: string;
131
+ att: Attenuation[];
132
+ exp: number;
133
+ jwsToken: string;
134
+ }
114
135
  /**
115
136
  * Storage backend for a DFOS web relay
116
137
  *
@@ -164,6 +185,26 @@ interface RelayStore {
164
185
  state: VerifiedContentChain;
165
186
  lastCreatedAt: string;
166
187
  } | null>;
188
+ /** Get all revoked credential CIDs for an issuer */
189
+ getRevocations(issuerDID: string): Promise<string[]>;
190
+ /** Add a revocation to the revocation set */
191
+ addRevocation(revocation: StoredRevocation): Promise<void>;
192
+ /** Check if a specific credential CID has been revoked by a specific issuer */
193
+ isCredentialRevoked(issuerDID: string, credentialCID: string): Promise<boolean>;
194
+ /** Get public credentials covering a specific resource */
195
+ getPublicCredentials(resource: string): Promise<string[]>;
196
+ /** Add a public credential as standing authorization */
197
+ addPublicCredential(credential: StoredPublicCredential): Promise<void>;
198
+ /** Remove a public credential (e.g., after revocation) */
199
+ removePublicCredential(credentialCID: string): Promise<void>;
200
+ /** Get paginated documents for a content chain */
201
+ getDocuments(contentId: string, params: {
202
+ after?: string;
203
+ limit: number;
204
+ }): Promise<{
205
+ documents: StoredDocument[];
206
+ cursor: string | null;
207
+ }>;
167
208
  /** Get last-synced log cursor for a peer relay */
168
209
  getPeerCursor(peerUrl: string): Promise<string | undefined>;
169
210
  /** Update last-synced log cursor for a peer relay */
@@ -249,6 +290,10 @@ declare class MemoryRelayStore implements RelayStore {
249
290
  private countersignatures;
250
291
  private operationLog;
251
292
  private peerCursors;
293
+ /** Keyed by `issuerDID::credentialCID` for issuer-scoped revocation */
294
+ private revocations;
295
+ /** Keyed by credential CID */
296
+ private publicCredentials;
252
297
  getOperation(cid: string): Promise<StoredOperation | undefined>;
253
298
  putOperation(op: StoredOperation): Promise<void>;
254
299
  getIdentityChain(did: string): Promise<StoredIdentityChain | undefined>;
@@ -261,6 +306,19 @@ declare class MemoryRelayStore implements RelayStore {
261
306
  putBlob(key: BlobKey, data: Uint8Array): Promise<void>;
262
307
  getCountersignatures(operationCID: string): Promise<string[]>;
263
308
  addCountersignature(operationCID: string, jwsToken: string): Promise<void>;
309
+ getRevocations(issuerDID: string): Promise<string[]>;
310
+ addRevocation(revocation: StoredRevocation): Promise<void>;
311
+ isCredentialRevoked(issuerDID: string, credentialCID: string): Promise<boolean>;
312
+ getPublicCredentials(resource: string): Promise<string[]>;
313
+ addPublicCredential(credential: StoredPublicCredential): Promise<void>;
314
+ removePublicCredential(credentialCID: string): Promise<void>;
315
+ getDocuments(contentId: string, params: {
316
+ after?: string;
317
+ limit: number;
318
+ }): Promise<{
319
+ documents: StoredDocument[];
320
+ cursor: string | null;
321
+ }>;
264
322
  appendToLog(entry: LogEntry): Promise<void>;
265
323
  readLog(params: {
266
324
  after?: string;
@@ -303,6 +361,36 @@ declare class MemoryRelayStore implements RelayStore {
303
361
  * deterministic CIDs for identical payloads.
304
362
  */
305
363
  declare const createKeyResolver: (store: RelayStore) => (kid: string) => Promise<Uint8Array>;
364
+ /**
365
+ * Create an identity resolver that includes all historical keys.
366
+ *
367
+ * Credentials are long-lived artifacts — their validity persists across key
368
+ * rotations. This resolver walks the full identity chain log to collect all
369
+ * keys that have ever appeared in create and update operations, ensuring
370
+ * credentials signed by rotated-out keys still verify. Revocation (not key
371
+ * rotation) is the invalidation mechanism.
372
+ *
373
+ * Used for credential verification at both ingestion and access-check time.
374
+ */
375
+ declare const createHistoricalIdentityResolver: (store: RelayStore) => (did: string) => Promise<{
376
+ authKeys: {
377
+ id: string;
378
+ type: "Multikey";
379
+ publicKeyMultibase: string;
380
+ }[];
381
+ assertKeys: {
382
+ id: string;
383
+ type: "Multikey";
384
+ publicKeyMultibase: string;
385
+ }[];
386
+ controllerKeys: {
387
+ id: string;
388
+ type: "Multikey";
389
+ publicKeyMultibase: string;
390
+ }[];
391
+ did: string;
392
+ isDeleted: boolean;
393
+ } | undefined>;
306
394
  /**
307
395
  * Create a key resolver that only resolves current-state keys.
308
396
  *
@@ -339,4 +427,4 @@ declare const sequenceOps: (store: RelayStore) => Promise<{
339
427
  result: SequenceResult;
340
428
  }>;
341
429
 
342
- 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, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations, isDependencyFailure, sequenceOps };
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 };
package/dist/index.js CHANGED
@@ -19,8 +19,12 @@ import {
19
19
  verifyContentExtensionFromTrustedState,
20
20
  verifyCountersignature,
21
21
  verifyIdentityChain,
22
- verifyIdentityExtensionFromTrustedState
22
+ verifyIdentityExtensionFromTrustedState,
23
+ verifyRevocation
23
24
  } from "@metalabel/dfos-protocol/chain";
25
+ import {
26
+ verifyDFOSCredential
27
+ } from "@metalabel/dfos-protocol/credentials";
24
28
  import { dagCborCanonicalEncode, decodeJwsUnsafe } from "@metalabel/dfos-protocol/crypto";
25
29
  var MAX_FUTURE_TIMESTAMP_MS = 24 * 60 * 60 * 1e3;
26
30
  var isFutureTimestamp = (createdAt) => {
@@ -91,6 +95,31 @@ var classify = (jwsToken) => {
91
95
  previousCID: null
92
96
  };
93
97
  }
98
+ if (typ === "did:dfos:revocation") {
99
+ const revocationDID = typeof payload["did"] === "string" ? payload["did"] : null;
100
+ return {
101
+ ...base,
102
+ kind: "revocation",
103
+ referencedDID: revocationDID,
104
+ signerDID: null,
105
+ priority: 1,
106
+ // same as beacons — needs identity keys to verify
107
+ previousCID: null
108
+ };
109
+ }
110
+ if (typ === "did:dfos:credential") {
111
+ const aud = typeof payload["aud"] === "string" ? payload["aud"] : null;
112
+ if (aud !== "*") return unknown;
113
+ return {
114
+ ...base,
115
+ kind: "credential",
116
+ referencedDID: kidDID,
117
+ signerDID: null,
118
+ priority: 1,
119
+ // needs identity keys to verify
120
+ previousCID: null
121
+ };
122
+ }
94
123
  return unknown;
95
124
  };
96
125
  var createKeyResolver = (store) => async (kid) => {
@@ -126,6 +155,42 @@ var createKeyResolver = (store) => async (kid) => {
126
155
  }
127
156
  throw new Error(`unknown key ${keyId} on identity ${did}`);
128
157
  };
158
+ var createHistoricalIdentityResolver = (store) => async (did) => {
159
+ const chain = await store.getIdentityChain(did);
160
+ if (!chain) return void 0;
161
+ const { state, log } = chain;
162
+ const keyMaps = {
163
+ authKeys: new Map(state.authKeys.map((k) => [k.id, k])),
164
+ assertKeys: new Map(state.assertKeys.map((k) => [k.id, k])),
165
+ controllerKeys: new Map(state.controllerKeys.map((k) => [k.id, k]))
166
+ };
167
+ for (const token of log) {
168
+ const decoded = decodeJwsUnsafe(token);
169
+ if (!decoded) continue;
170
+ const payload = decoded.payload;
171
+ const opType = payload["type"];
172
+ if (opType !== "create" && opType !== "update") continue;
173
+ for (const arrayName of ["authKeys", "assertKeys", "controllerKeys"]) {
174
+ const keys = payload[arrayName];
175
+ if (!Array.isArray(keys)) continue;
176
+ const map = keyMaps[arrayName];
177
+ for (const k of keys) {
178
+ if (k && typeof k === "object" && "id" in k && "publicKeyMultibase" in k && !map.has(k.id)) {
179
+ map.set(
180
+ k.id,
181
+ k
182
+ );
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return {
188
+ ...state,
189
+ authKeys: [...keyMaps.authKeys.values()],
190
+ assertKeys: [...keyMaps.assertKeys.values()],
191
+ controllerKeys: [...keyMaps.controllerKeys.values()]
192
+ };
193
+ };
129
194
  var createCurrentKeyResolver = (store) => async (kid) => {
130
195
  const hashIdx = kid.indexOf("#");
131
196
  if (hashIdx < 0) throw new Error(`kid must be a DID URL: ${kid}`);
@@ -279,13 +344,15 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
279
344
  }
280
345
  }
281
346
  const resolveKey = createKeyResolver(store);
347
+ const resolveIdentity = createHistoricalIdentityResolver(store);
282
348
  const opType = payload["type"];
283
349
  const isGenesis = opType === "create";
284
350
  if (isGenesis) {
285
351
  const content = await verifyContentChain({
286
352
  log: [jwsToken],
287
353
  resolveKey,
288
- enforceAuthorization: true
354
+ enforceAuthorization: true,
355
+ resolveIdentity
289
356
  });
290
357
  const createdAt = payload["createdAt"];
291
358
  const chain2 = {
@@ -325,7 +392,8 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
325
392
  lastCreatedAt: chain.lastCreatedAt,
326
393
  newOp: jwsToken,
327
394
  resolveKey,
328
- enforceAuthorization: true
395
+ enforceAuthorization: true,
396
+ resolveIdentity
329
397
  });
330
398
  const updated2 = {
331
399
  contentId: chain.contentId,
@@ -353,7 +421,8 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
353
421
  lastCreatedAt: forkState.lastCreatedAt,
354
422
  newOp: jwsToken,
355
423
  resolveKey,
356
- enforceAuthorization: true
424
+ enforceAuthorization: true,
425
+ resolveIdentity
357
426
  });
358
427
  const updatedLog = [...chain.log, jwsToken];
359
428
  const head = selectDeterministicHead(updatedLog);
@@ -386,7 +455,7 @@ var ingestBeacon = async (jwsToken, store, logEnabled) => {
386
455
  const message = err instanceof Error ? err.message : "verification failed";
387
456
  return { cid: "", status: "rejected", error: message };
388
457
  }
389
- const did = verified.payload.did;
458
+ const did = verified.did;
390
459
  const cid = verified.beaconCID;
391
460
  const identity = await store.getIdentityChain(did);
392
461
  if (identity?.state.isDeleted) {
@@ -394,8 +463,8 @@ var ingestBeacon = async (jwsToken, store, logEnabled) => {
394
463
  }
395
464
  const existing = await store.getBeacon(did);
396
465
  if (existing) {
397
- const existingTime = new Date(existing.state.payload.createdAt).getTime();
398
- const newTime = new Date(verified.payload.createdAt).getTime();
466
+ const existingTime = new Date(existing.state.createdAt).getTime();
467
+ const newTime = new Date(verified.createdAt).getTime();
399
468
  if (newTime <= existingTime) {
400
469
  return { cid, status: "duplicate", kind: "beacon", chainId: did };
401
470
  }
@@ -498,6 +567,86 @@ var ingestArtifact = async (jwsToken, store, logEnabled) => {
498
567
  }
499
568
  return { cid, status: "new", kind: "artifact", chainId: did };
500
569
  };
570
+ var ingestRevocation = async (jwsToken, store, logEnabled) => {
571
+ const resolveKey = createKeyResolver(store);
572
+ let verified;
573
+ try {
574
+ verified = await verifyRevocation({ jwsToken, resolveKey });
575
+ } catch (err) {
576
+ const message = err instanceof Error ? err.message : "verification failed";
577
+ return { cid: "", status: "rejected", error: message };
578
+ }
579
+ const cid = verified.revocationCID;
580
+ const did = verified.did;
581
+ const existing = await store.getOperation(cid);
582
+ if (existing) {
583
+ if (existing.jwsToken !== jwsToken) {
584
+ return {
585
+ cid,
586
+ status: "rejected",
587
+ error: "revocation already exists with a different signature"
588
+ };
589
+ }
590
+ return { cid, status: "duplicate", kind: "revocation", chainId: did };
591
+ }
592
+ const identity = await store.getIdentityChain(did);
593
+ if (identity?.state.isDeleted) {
594
+ return { cid, status: "rejected", error: "identity is deleted" };
595
+ }
596
+ await store.addRevocation({
597
+ cid,
598
+ issuerDID: did,
599
+ credentialCID: verified.credentialCID,
600
+ jwsToken
601
+ });
602
+ await store.removePublicCredential(verified.credentialCID);
603
+ await store.putOperation({ cid, jwsToken, chainType: "revocation", chainId: did });
604
+ if (logEnabled) {
605
+ await store.appendToLog({ cid, jwsToken, kind: "revocation", chainId: did });
606
+ }
607
+ return { cid, status: "new", kind: "revocation", chainId: did };
608
+ };
609
+ var ingestPublicCredential = async (jwsToken, store, logEnabled) => {
610
+ const resolveIdentity = createHistoricalIdentityResolver(store);
611
+ let verified;
612
+ try {
613
+ verified = await verifyDFOSCredential(jwsToken, { resolveIdentity });
614
+ } catch (err) {
615
+ const message = err instanceof Error ? err.message : "verification failed";
616
+ return { cid: "", status: "rejected", error: message };
617
+ }
618
+ const cid = verified.credentialCID;
619
+ if (verified.aud !== "*") {
620
+ return { cid: "", status: "rejected", error: "not a public credential" };
621
+ }
622
+ const existing = await store.getOperation(cid);
623
+ if (existing) {
624
+ if (existing.jwsToken !== jwsToken) {
625
+ return {
626
+ cid,
627
+ status: "rejected",
628
+ error: "credential already exists with a different signature"
629
+ };
630
+ }
631
+ return { cid, status: "duplicate", kind: "credential", chainId: verified.iss };
632
+ }
633
+ const revoked = await store.isCredentialRevoked(verified.iss, cid);
634
+ if (revoked) {
635
+ return { cid, status: "rejected", error: "credential is revoked" };
636
+ }
637
+ await store.addPublicCredential({
638
+ cid,
639
+ issuerDID: verified.iss,
640
+ att: verified.att,
641
+ exp: verified.exp,
642
+ jwsToken
643
+ });
644
+ await store.putOperation({ cid, jwsToken, chainType: "credential", chainId: verified.iss });
645
+ if (logEnabled) {
646
+ await store.appendToLog({ cid, jwsToken, kind: "credential", chainId: verified.iss });
647
+ }
648
+ return { cid, status: "new", kind: "credential", chainId: verified.iss };
649
+ };
501
650
  var dependencySort = (ops) => {
502
651
  const buckets = /* @__PURE__ */ new Map();
503
652
  for (const op of ops) {
@@ -607,6 +756,12 @@ var ingestOperations = async (tokens, store, options) => {
607
756
  case "artifact":
608
757
  result = await ingestArtifact(op.jwsToken, store, logEnabled);
609
758
  break;
759
+ case "revocation":
760
+ result = await ingestRevocation(op.jwsToken, store, logEnabled);
761
+ break;
762
+ case "credential":
763
+ result = await ingestPublicCredential(op.jwsToken, store, logEnabled);
764
+ break;
610
765
  default:
611
766
  result = { cid: "", status: "rejected", error: "unrecognized operation type" };
612
767
  }
@@ -722,13 +877,18 @@ var createHttpPeerClient = () => {
722
877
  };
723
878
 
724
879
  // src/relay.ts
725
- import { VC_TYPE_CONTENT_READ, verifyCredential } from "@metalabel/dfos-protocol/credentials";
880
+ import { createRequire } from "module";
726
881
  import { dagCborCanonicalEncode as dagCborCanonicalEncode3, decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
727
882
  import { Hono } from "hono";
728
883
  import { z } from "zod";
729
884
 
730
885
  // src/auth.ts
731
- import { verifyAuthToken } from "@metalabel/dfos-protocol/credentials";
886
+ import {
887
+ matchesResource,
888
+ verifyAuthToken,
889
+ verifyDelegationChain,
890
+ verifyDFOSCredential as verifyDFOSCredential2
891
+ } from "@metalabel/dfos-protocol/credentials";
732
892
  import { decodeJwsUnsafe as decodeJwsUnsafe2 } from "@metalabel/dfos-protocol/crypto";
733
893
  var authenticateRequest = async (authHeader, relayDID, store) => {
734
894
  if (!authHeader) return null;
@@ -752,6 +912,73 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
752
912
  return null;
753
913
  }
754
914
  };
915
+ var verifyContentAccess = async (options) => {
916
+ const { credentialJWS, requestedResource, action, store, creatorDID, requesterDID } = options;
917
+ if (requesterDID && requesterDID === creatorDID) {
918
+ return { granted: true, source: "creator" };
919
+ }
920
+ const resolveIdentity = createHistoricalIdentityResolver(store);
921
+ 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
+ const publicCreds = await store.getPublicCredentials(requestedResource);
941
+ for (const credJws of publicCreds) {
942
+ try {
943
+ const cred = await verifyDFOSCredential2(credJws, { resolveIdentity });
944
+ const leafRevoked = await isRevoked(cred.iss, cred.credentialCID);
945
+ if (leafRevoked) continue;
946
+ const covers = await matchesResource(cred.att, requestedResource, action, {
947
+ manifestLookup
948
+ });
949
+ if (!covers) continue;
950
+ await verifyDelegationChain(cred, { resolveIdentity, rootDID: creatorDID, isRevoked });
951
+ return { granted: true, source: "public-credential", credential: cred };
952
+ } catch {
953
+ continue;
954
+ }
955
+ }
956
+ if (credentialJWS) {
957
+ try {
958
+ const cred = await verifyDFOSCredential2(credentialJWS, { resolveIdentity });
959
+ const leafRevoked = await isRevoked(cred.iss, cred.credentialCID);
960
+ if (leafRevoked) {
961
+ return { granted: false, source: "none" };
962
+ }
963
+ await verifyDelegationChain(cred, { resolveIdentity, rootDID: creatorDID, isRevoked });
964
+ if (cred.aud !== "*") {
965
+ if (!requesterDID || cred.aud !== requesterDID) {
966
+ return { granted: false, source: "none" };
967
+ }
968
+ }
969
+ const covers = await matchesResource(cred.att, requestedResource, action, {
970
+ manifestLookup
971
+ });
972
+ if (!covers) {
973
+ return { granted: false, source: "none" };
974
+ }
975
+ return { granted: true, source: "request-credential", credential: cred };
976
+ } catch {
977
+ return { granted: false, source: "none" };
978
+ }
979
+ }
980
+ return { granted: false, source: "none" };
981
+ };
755
982
 
756
983
  // src/sequencer.ts
757
984
  import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
@@ -807,6 +1034,8 @@ var sequenceOps = async (store) => {
807
1034
  };
808
1035
 
809
1036
  // src/relay.ts
1037
+ var require2 = createRequire(import.meta.url);
1038
+ var { version: RELAY_VERSION } = require2("../package.json");
810
1039
  var IngestBody = z.object({
811
1040
  operations: z.array(z.string()).min(1).max(100)
812
1041
  });
@@ -856,10 +1085,13 @@ var createRelay = async (options) => {
856
1085
  return c.json({
857
1086
  did: relayDID,
858
1087
  protocol: "dfos-web-relay",
859
- version: "0.6.0",
860
- proof: true,
861
- content: contentEnabled,
862
- log: logEnabled,
1088
+ version: RELAY_VERSION,
1089
+ capabilities: {
1090
+ proof: true,
1091
+ content: contentEnabled,
1092
+ documents: contentEnabled,
1093
+ log: logEnabled
1094
+ },
863
1095
  profile: profileArtifactJws
864
1096
  });
865
1097
  });
@@ -953,6 +1185,33 @@ var createRelay = async (options) => {
953
1185
  const cursor = page.length === limit ? page[page.length - 1].cid : null;
954
1186
  return c.json({ entries: page, cursor });
955
1187
  });
1188
+ app.get("/content/:contentId/documents", async (c) => {
1189
+ if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
1190
+ 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
+ const chain = await store.getContentChain(contentId);
1194
+ if (!chain) return c.json({ error: "not found" }, 404);
1195
+ const accessError = await verifyReadAccess(
1196
+ auth,
1197
+ chain,
1198
+ contentId,
1199
+ c.req.header("x-credential"),
1200
+ store
1201
+ );
1202
+ if (accessError) return accessError;
1203
+ const after = c.req.query("after");
1204
+ const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
1205
+ const result = await store.getDocuments(contentId, {
1206
+ ...after ? { after } : {},
1207
+ limit
1208
+ });
1209
+ return c.json({
1210
+ contentId,
1211
+ documents: result.documents,
1212
+ nextCursor: result.cursor
1213
+ });
1214
+ });
956
1215
  app.get("/content/:contentId", async (c) => {
957
1216
  const contentId = c.req.param("contentId");
958
1217
  let chain = await store.getContentChain(contentId);
@@ -1007,7 +1266,8 @@ var createRelay = async (options) => {
1007
1266
  did: beacon.did,
1008
1267
  jwsToken: beacon.jwsToken,
1009
1268
  beaconCID: beacon.beaconCID,
1010
- payload: beacon.state.payload
1269
+ manifestContentId: beacon.state.manifestContentId,
1270
+ createdAt: beacon.state.createdAt
1011
1271
  });
1012
1272
  });
1013
1273
  app.get("/log", async (c) => {
@@ -1106,7 +1366,7 @@ var readBlob = async (params) => {
1106
1366
  if (!auth) return jsonResponse({ error: "authentication required" }, 401);
1107
1367
  const chain = await store.getContentChain(contentId);
1108
1368
  if (!chain) return jsonResponse({ error: "content chain not found" }, 404);
1109
- const credError = await verifyReadCredential(auth, chain, contentId, credHeader, store);
1369
+ const credError = await verifyReadAccess(auth, chain, contentId, credHeader, store);
1110
1370
  if (credError) return credError;
1111
1371
  let documentCID = null;
1112
1372
  let operationFound = ref === "head";
@@ -1135,45 +1395,17 @@ var readBlob = async (params) => {
1135
1395
  }
1136
1396
  });
1137
1397
  };
1138
- var verifyReadCredential = async (auth, chain, contentId, credHeader, store) => {
1139
- if (auth.iss === chain.state.creatorDID) return null;
1140
- if (!credHeader) {
1141
- return jsonResponse({ error: "DFOSContentRead credential required" }, 403);
1142
- }
1143
- const resolveKey = createCurrentKeyResolver(store);
1144
- try {
1145
- const vcDecoded = decodeJwsUnsafe4(credHeader);
1146
- if (!vcDecoded) throw new Error("invalid credential format");
1147
- const vcHeader = vcDecoded.header;
1148
- if (!vcHeader.kid) throw new Error("credential missing kid");
1149
- const kidHashIdx = vcHeader.kid.indexOf("#");
1150
- if (kidHashIdx < 0) throw new Error("credential kid must be a DID URL");
1151
- const vcIssuerDID = vcHeader.kid.substring(0, kidHashIdx);
1152
- if (vcIssuerDID !== chain.state.creatorDID) {
1153
- throw new Error("credential must be issued by the chain creator");
1154
- }
1155
- const issuerIdentity = await store.getIdentityChain(vcIssuerDID);
1156
- if (issuerIdentity?.state.isDeleted) {
1157
- throw new Error("credential issuer identity is deleted");
1158
- }
1159
- const creatorKey = await resolveKey(vcHeader.kid);
1160
- const credential = verifyCredential({
1161
- token: credHeader,
1162
- publicKey: creatorKey,
1163
- subject: auth.iss,
1164
- expectedType: VC_TYPE_CONTENT_READ
1165
- });
1166
- if (credential.iss !== chain.state.creatorDID) {
1167
- throw new Error("credential issuer is not the chain creator");
1168
- }
1169
- if (credential.contentId && credential.contentId !== contentId) {
1170
- return jsonResponse({ error: "credential contentId does not match" }, 403);
1171
- }
1172
- } catch (err) {
1173
- const message = err instanceof Error ? err.message : "credential verification failed";
1174
- return jsonResponse({ error: message }, 403);
1175
- }
1176
- return null;
1398
+ var verifyReadAccess = async (auth, chain, contentId, credHeader, store) => {
1399
+ const result = await verifyContentAccess({
1400
+ ...credHeader ? { credentialJWS: credHeader } : {},
1401
+ requestedResource: `chain:${contentId}`,
1402
+ action: "read",
1403
+ store,
1404
+ creatorDID: chain.state.creatorDID,
1405
+ requesterDID: auth.iss
1406
+ });
1407
+ if (result.granted) return null;
1408
+ return jsonResponse({ error: "read credential required" }, 403);
1177
1409
  };
1178
1410
 
1179
1411
  // src/store.ts
@@ -1189,6 +1421,10 @@ var MemoryRelayStore = class {
1189
1421
  countersignatures = /* @__PURE__ */ new Map();
1190
1422
  operationLog = [];
1191
1423
  peerCursors = /* @__PURE__ */ new Map();
1424
+ /** Keyed by `issuerDID::credentialCID` for issuer-scoped revocation */
1425
+ revocations = /* @__PURE__ */ new Map();
1426
+ /** Keyed by credential CID */
1427
+ publicCredentials = /* @__PURE__ */ new Map();
1192
1428
  async getOperation(cid) {
1193
1429
  return this.operations.get(cid);
1194
1430
  }
@@ -1239,6 +1475,98 @@ var MemoryRelayStore = class {
1239
1475
  existing.push(jwsToken);
1240
1476
  this.countersignatures.set(operationCID, existing);
1241
1477
  }
1478
+ // --- revocations ---
1479
+ async getRevocations(issuerDID) {
1480
+ const cids = [];
1481
+ for (const rev of this.revocations.values()) {
1482
+ if (rev.issuerDID === issuerDID) cids.push(rev.credentialCID);
1483
+ }
1484
+ return cids;
1485
+ }
1486
+ async addRevocation(revocation) {
1487
+ const key = `${revocation.issuerDID}::${revocation.credentialCID}`;
1488
+ this.revocations.set(key, revocation);
1489
+ }
1490
+ async isCredentialRevoked(issuerDID, credentialCID) {
1491
+ return this.revocations.has(`${issuerDID}::${credentialCID}`);
1492
+ }
1493
+ // --- public credentials ---
1494
+ async getPublicCredentials(resource) {
1495
+ const tokens = [];
1496
+ const isChainRequest = resource.startsWith("chain:");
1497
+ for (const cred of this.publicCredentials.values()) {
1498
+ for (const att of cred.att) {
1499
+ if (att.resource === resource) {
1500
+ tokens.push(cred.jwsToken);
1501
+ break;
1502
+ }
1503
+ if (isChainRequest && att.resource === "chain:*") {
1504
+ tokens.push(cred.jwsToken);
1505
+ break;
1506
+ }
1507
+ if (isChainRequest && att.resource.startsWith("manifest:")) {
1508
+ tokens.push(cred.jwsToken);
1509
+ break;
1510
+ }
1511
+ }
1512
+ }
1513
+ return tokens;
1514
+ }
1515
+ async addPublicCredential(credential) {
1516
+ this.publicCredentials.set(credential.cid, credential);
1517
+ }
1518
+ async removePublicCredential(credentialCID) {
1519
+ this.publicCredentials.delete(credentialCID);
1520
+ }
1521
+ // --- documents ---
1522
+ async getDocuments(contentId, params) {
1523
+ const chain = this.contentChains.get(contentId);
1524
+ if (!chain) return { documents: [], cursor: null };
1525
+ const entries = [];
1526
+ for (const jws of chain.log) {
1527
+ const decoded = decodeJwsUnsafe5(jws);
1528
+ if (!decoded) continue;
1529
+ const payload = decoded.payload;
1530
+ const cid = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
1531
+ const documentCID = typeof payload["documentCID"] === "string" ? payload["documentCID"] : null;
1532
+ const signerDID = typeof payload["did"] === "string" ? payload["did"] : "";
1533
+ const createdAt = typeof payload["createdAt"] === "string" ? payload["createdAt"] : "";
1534
+ entries.push({ cid, documentCID, signerDID, createdAt });
1535
+ }
1536
+ let startIdx = 0;
1537
+ if (params.after) {
1538
+ const idx = entries.findIndex((e) => e.cid === params.after);
1539
+ startIdx = idx >= 0 ? idx + 1 : entries.length;
1540
+ }
1541
+ const page = entries.slice(startIdx, startIdx + params.limit);
1542
+ const documents = [];
1543
+ for (const entry of page) {
1544
+ let document = null;
1545
+ if (entry.documentCID) {
1546
+ const blob = await this.getBlob({
1547
+ creatorDID: chain.state.creatorDID,
1548
+ documentCID: entry.documentCID
1549
+ });
1550
+ if (blob) {
1551
+ try {
1552
+ document = JSON.parse(new TextDecoder().decode(blob));
1553
+ } catch {
1554
+ document = null;
1555
+ }
1556
+ }
1557
+ }
1558
+ documents.push({
1559
+ operationCID: entry.cid,
1560
+ documentCID: entry.documentCID,
1561
+ document,
1562
+ signerDID: entry.signerDID,
1563
+ createdAt: entry.createdAt
1564
+ });
1565
+ }
1566
+ const cursor = page.length === params.limit ? page[page.length - 1].cid : null;
1567
+ return { documents, cursor };
1568
+ }
1569
+ // --- operation log ---
1242
1570
  async appendToLog(entry) {
1243
1571
  this.operationLog.push(entry);
1244
1572
  }
@@ -1301,7 +1629,16 @@ var MemoryRelayStore = class {
1301
1629
  currentCID = op.previousCID;
1302
1630
  }
1303
1631
  const resolveKey = createKeyResolver(this);
1304
- const content = await verifyContentChain2({ log: path, resolveKey, enforceAuthorization: true });
1632
+ const resolveIdentity = async (did) => {
1633
+ const chain2 = await this.getIdentityChain(did);
1634
+ return chain2?.state;
1635
+ };
1636
+ const content = await verifyContentChain2({
1637
+ log: path,
1638
+ resolveKey,
1639
+ enforceAuthorization: true,
1640
+ resolveIdentity
1641
+ });
1305
1642
  const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
1306
1643
  const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1307
1644
  return { state: content, lastCreatedAt };
@@ -1357,6 +1694,7 @@ export {
1357
1694
  bootstrapRelayIdentity,
1358
1695
  computeOpCID,
1359
1696
  createCurrentKeyResolver,
1697
+ createHistoricalIdentityResolver,
1360
1698
  createHttpPeerClient,
1361
1699
  createKeyResolver,
1362
1700
  createRelay,
package/openapi.yaml CHANGED
@@ -8,7 +8,7 @@ info:
8
8
 
9
9
  Two data planes:
10
10
  - **Proof plane** (public): signed chain operations, beacons, countersignatures
11
- - **Content plane** (authenticated): raw content blobs gated by DID auth tokens and VC-JWT credentials
11
+ - **Content plane** (authenticated): raw content blobs gated by DID auth tokens and DFOS credentials
12
12
 
13
13
  servers:
14
14
  - url: http://localhost:3000
@@ -28,7 +28,7 @@ paths:
28
28
  application/json:
29
29
  schema:
30
30
  type: object
31
- required: [did, protocol, version, proof, content, log, profile]
31
+ required: [did, protocol, version, capabilities, profile]
32
32
  properties:
33
33
  did:
34
34
  type: string
@@ -39,16 +39,23 @@ paths:
39
39
  enum: [dfos-web-relay]
40
40
  version:
41
41
  type: string
42
- example: '0.1.0'
43
- proof:
44
- type: boolean
45
- description: Always true — a relay without proof plane is not a relay
46
- content:
47
- type: boolean
48
- description: Whether the relay supports content plane (blob upload/download)
49
- log:
50
- type: boolean
51
- description: Whether the global operation log is available (GET /log)
42
+ example: '0.8.0'
43
+ capabilities:
44
+ type: object
45
+ required: [proof, content, documents, log]
46
+ properties:
47
+ proof:
48
+ type: boolean
49
+ description: Always true — a relay without proof plane is not a relay
50
+ content:
51
+ type: boolean
52
+ description: Whether the relay supports content plane (blob upload/download)
53
+ documents:
54
+ type: boolean
55
+ description: Whether the relay serves the documents endpoint
56
+ log:
57
+ type: boolean
58
+ description: Whether the global operation log is available (GET /log)
52
59
  profile:
53
60
  type: string
54
61
  description: The relay's profile artifact as a compact JWS token
@@ -189,13 +196,14 @@ paths:
189
196
  '404':
190
197
  $ref: '#/components/responses/NotFound'
191
198
 
192
- /content/{contentId}/blob:
199
+ /content/{contentId}/blob/{operationCID}:
193
200
  put:
194
201
  operationId: uploadBlob
195
202
  summary: Upload a content blob
196
203
  description: |
197
- Upload raw bytes for a document referenced by a content chain.
198
- Requires auth token. Caller must be the chain creator.
204
+ Upload raw bytes for a document referenced by a content chain operation.
205
+ Requires auth token. Caller must be the chain creator or the signer of the
206
+ referenced operation.
199
207
  tags: [Content Plane]
200
208
  security:
201
209
  - BearerAuth: []
@@ -205,12 +213,12 @@ paths:
205
213
  required: true
206
214
  schema:
207
215
  type: string
208
- - name: X-Document-CID
209
- in: header
216
+ - name: operationCID
217
+ in: path
210
218
  required: true
211
219
  schema:
212
220
  type: string
213
- description: The documentCID this blob corresponds to
221
+ description: CID of the content operation whose documentCID this blob satisfies
214
222
  requestBody:
215
223
  required: true
216
224
  content:
@@ -243,12 +251,13 @@ paths:
243
251
  '404':
244
252
  $ref: '#/components/responses/NotFound'
245
253
 
254
+ /content/{contentId}/blob:
246
255
  get:
247
256
  operationId: downloadBlob
248
257
  summary: Download a content blob at head
249
258
  description: |
250
259
  Download the raw bytes of the current document at chain head.
251
- Requires auth token. Non-creators must present a DFOSContentRead credential.
260
+ Requires auth token. Non-creators must present a DFOS read credential.
252
261
  tags: [Content Plane]
253
262
  security:
254
263
  - BearerAuth: []
@@ -263,7 +272,7 @@ paths:
263
272
  required: false
264
273
  schema:
265
274
  type: string
266
- description: DFOSContentRead VC-JWT (required for non-creators)
275
+ description: DFOS read credential JWS (required for non-creators)
267
276
  responses:
268
277
  '200':
269
278
  description: Blob data
@@ -311,7 +320,7 @@ paths:
311
320
  required: false
312
321
  schema:
313
322
  type: string
314
- description: DFOSContentRead VC-JWT (required for non-creators)
323
+ description: DFOS read credential JWS (required for non-creators)
315
324
  responses:
316
325
  '200':
317
326
  description: Blob data
@@ -331,6 +340,188 @@ paths:
331
340
  '404':
332
341
  $ref: '#/components/responses/NotFound'
333
342
 
343
+ /content/{contentId}/documents:
344
+ get:
345
+ operationId: getDocuments
346
+ summary: List documents in a content chain
347
+ description: |
348
+ Returns all documents committed to a content chain as an ordered list,
349
+ from genesis to head. Cursor-based pagination.
350
+ tags: [Content Plane]
351
+ security:
352
+ - BearerAuth: []
353
+ parameters:
354
+ - name: contentId
355
+ in: path
356
+ required: true
357
+ schema:
358
+ type: string
359
+ - name: after
360
+ in: query
361
+ required: false
362
+ schema:
363
+ type: string
364
+ description: CID cursor — start after this operation CID
365
+ - name: limit
366
+ in: query
367
+ required: false
368
+ schema:
369
+ type: integer
370
+ default: 100
371
+ maximum: 1000
372
+ - name: X-Credential
373
+ in: header
374
+ required: false
375
+ schema:
376
+ type: string
377
+ description: DFOS read credential JWS (required for non-creators)
378
+ responses:
379
+ '200':
380
+ description: Document list
381
+ content:
382
+ application/json:
383
+ schema:
384
+ type: object
385
+ required: [contentId, documents, nextCursor]
386
+ properties:
387
+ contentId:
388
+ type: string
389
+ documents:
390
+ type: array
391
+ items:
392
+ type: object
393
+ required: [operationCID, documentCID, document, signerDID, createdAt]
394
+ properties:
395
+ operationCID:
396
+ type: string
397
+ documentCID:
398
+ type: string
399
+ nullable: true
400
+ document:
401
+ nullable: true
402
+ signerDID:
403
+ type: string
404
+ createdAt:
405
+ type: string
406
+ format: date-time
407
+ nextCursor:
408
+ type: string
409
+ nullable: true
410
+ '401':
411
+ $ref: '#/components/responses/Unauthorized'
412
+ '403':
413
+ $ref: '#/components/responses/Forbidden'
414
+ '404':
415
+ $ref: '#/components/responses/NotFound'
416
+ '501':
417
+ description: Documents capability not enabled
418
+
419
+ /identities/{did}/log:
420
+ get:
421
+ operationId: getIdentityLog
422
+ summary: Paginated log of identity chain operations
423
+ description: |
424
+ Returns operations belonging to this identity chain in chain order.
425
+ Cursor-based pagination.
426
+ tags: [Proof Plane]
427
+ parameters:
428
+ - name: did
429
+ in: path
430
+ required: true
431
+ schema:
432
+ type: string
433
+ description: DID of the identity
434
+ - name: after
435
+ in: query
436
+ required: false
437
+ schema:
438
+ type: string
439
+ description: CID cursor — start after this operation CID
440
+ - name: limit
441
+ in: query
442
+ required: false
443
+ schema:
444
+ type: integer
445
+ default: 100
446
+ maximum: 1000
447
+ responses:
448
+ '200':
449
+ description: Identity chain log entries
450
+ content:
451
+ application/json:
452
+ schema:
453
+ type: object
454
+ required: [entries, cursor]
455
+ properties:
456
+ entries:
457
+ type: array
458
+ items:
459
+ type: object
460
+ required: [cid, jwsToken]
461
+ properties:
462
+ cid:
463
+ type: string
464
+ jwsToken:
465
+ type: string
466
+ cursor:
467
+ type: string
468
+ nullable: true
469
+ '404':
470
+ $ref: '#/components/responses/NotFound'
471
+
472
+ /content/{contentId}/log:
473
+ get:
474
+ operationId: getContentLog
475
+ summary: Paginated log of content chain operations
476
+ description: |
477
+ Returns operations belonging to this content chain in chain order.
478
+ Cursor-based pagination.
479
+ tags: [Proof Plane]
480
+ parameters:
481
+ - name: contentId
482
+ in: path
483
+ required: true
484
+ schema:
485
+ type: string
486
+ description: Content identifier (22-char hash)
487
+ - name: after
488
+ in: query
489
+ required: false
490
+ schema:
491
+ type: string
492
+ description: CID cursor — start after this operation CID
493
+ - name: limit
494
+ in: query
495
+ required: false
496
+ schema:
497
+ type: integer
498
+ default: 100
499
+ maximum: 1000
500
+ responses:
501
+ '200':
502
+ description: Content chain log entries
503
+ content:
504
+ application/json:
505
+ schema:
506
+ type: object
507
+ required: [entries, cursor]
508
+ properties:
509
+ entries:
510
+ type: array
511
+ items:
512
+ type: object
513
+ required: [cid, jwsToken]
514
+ properties:
515
+ cid:
516
+ type: string
517
+ jwsToken:
518
+ type: string
519
+ cursor:
520
+ type: string
521
+ nullable: true
522
+ '404':
523
+ $ref: '#/components/responses/NotFound'
524
+
334
525
  /countersignatures/{cid}:
335
526
  get:
336
527
  operationId: getCountersignaturesByCID
@@ -406,7 +597,7 @@ components:
406
597
  description: Error message if rejected
407
598
  kind:
408
599
  type: string
409
- enum: [identity-op, content-op, beacon, countersig, beacon-countersig]
600
+ enum: [identity-op, content-op, beacon, artifact, countersign, revocation, credential]
410
601
  chainId:
411
602
  type: string
412
603
  description: Chain identifier (DID or contentId)
@@ -421,21 +612,19 @@ components:
421
612
  type: string
422
613
  chainType:
423
614
  type: string
424
- enum: [identity, content]
615
+ enum: [identity, content, artifact, beacon, countersign, revocation, credential]
425
616
  chainId:
426
617
  type: string
427
618
 
428
619
  IdentityChainResponse:
429
620
  type: object
430
- required: [did, log, state]
621
+ required: [did, headCID, state]
431
622
  properties:
432
623
  did:
433
624
  type: string
434
- log:
435
- type: array
436
- items:
437
- type: string
438
- description: Ordered JWS tokens from genesis to head
625
+ headCID:
626
+ type: string
627
+ description: CID of the current head operation
439
628
  state:
440
629
  type: object
441
630
  required: [did, isDeleted, authKeys, assertKeys, controllerKeys]
@@ -492,7 +681,7 @@ components:
492
681
 
493
682
  BeaconResponse:
494
683
  type: object
495
- required: [did, jwsToken, beaconCID, payload]
684
+ required: [did, jwsToken, beaconCID, manifestContentId, createdAt]
496
685
  properties:
497
686
  did:
498
687
  type: string
@@ -500,24 +689,12 @@ components:
500
689
  type: string
501
690
  beaconCID:
502
691
  type: string
503
- payload:
504
- type: object
505
- required: [version, type, did, merkleRoot, createdAt]
506
- properties:
507
- version:
508
- type: integer
509
- enum: [1]
510
- type:
511
- type: string
512
- enum: [beacon]
513
- did:
514
- type: string
515
- merkleRoot:
516
- type: string
517
- pattern: '^[0-9a-f]{64}$'
518
- createdAt:
519
- type: string
520
- format: date-time
692
+ manifestContentId:
693
+ type: string
694
+ description: Content ID of the manifest chain
695
+ createdAt:
696
+ type: string
697
+ format: date-time
521
698
 
522
699
  MultikeyPublicKey:
523
700
  type: object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-web-relay",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
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,7 +41,7 @@
41
41
  "README.md"
42
42
  ],
43
43
  "dependencies": {
44
- "hono": "^4.12.8",
44
+ "hono": "^4.12.9",
45
45
  "zod": "^4.3.6"
46
46
  },
47
47
  "peerDependencies": {
@@ -51,14 +51,13 @@
51
51
  "@types/node": "^24.10.4",
52
52
  "tsup": "^8.5.1",
53
53
  "tsx": "^4.20.3",
54
- "vitest": "^4.1.0",
55
- "@metalabel/dfos-protocol": "0.7.1"
54
+ "vitest": "^4.1.2",
55
+ "@metalabel/dfos-protocol": "0.8.1"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",
59
59
  "clean": "rm -rf dist",
60
60
  "typecheck": "tsc --noEmit",
61
- "test": "vitest run",
62
- "test:conformance": "./tests/run-conformance.sh"
61
+ "test": "vitest run"
63
62
  }
64
63
  }