@metalabel/dfos-web-relay 0.10.0 → 0.12.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 CHANGED
@@ -38,19 +38,18 @@ serve({ port: 4444 });
38
38
 
39
39
  ## Routes
40
40
 
41
- | Method | Path | Description |
42
- | ------ | ---------------------------------------- | ---------------------------------------------------------------- |
43
- | `GET` | `/.well-known/dfos-relay` | Relay metadata (DID, protocol version) |
44
- | `POST` | `/operations` | Submit signed operations (identity, content, beacon, countersig) |
45
- | `GET` | `/identities/:did` | Get identity chain state and operation log |
46
- | `GET` | `/content/:contentId` | Get content chain state and operation log |
47
- | `GET` | `/operations/:cid` | Get a single operation by CID |
48
- | `GET` | `/beacons/:did` | Get beacon for an identity |
49
- | `GET` | `/countersignatures/:cid` | Get countersignatures for an operation |
50
- | `GET` | `/operations/:cid/countersignatures` | Same as above (alias) |
51
- | `PUT` | `/content/:contentId/blob/:operationCID` | Upload blob (auth required) |
52
- | `GET` | `/content/:contentId/blob` | Download blob at head (standing auth, or auth + credential) |
53
- | `GET` | `/content/:contentId/blob/:ref` | Download blob at specific operation ref |
41
+ | Method | Path | Description |
42
+ | ------ | --------------------------------------------- | ----------------------------------------------------------- |
43
+ | `GET` | `/.well-known/dfos-relay` | Relay metadata (DID, protocol version) |
44
+ | `POST` | `/proof/v1/operations` | Submit signed operations (identity, content, countersig) |
45
+ | `GET` | `/proof/v1/identities/:did` | Get identity chain state and operation log |
46
+ | `GET` | `/proof/v1/content/:contentId` | Get content chain state and operation log |
47
+ | `GET` | `/proof/v1/operations/:cid` | Get a single operation by CID |
48
+ | `GET` | `/proof/v1/countersignatures/:cid` | Get countersignatures for an operation |
49
+ | `GET` | `/proof/v1/operations/:cid/countersignatures` | Same as above (alias) |
50
+ | `PUT` | `/content/:contentId/blob/:operationCID` | Upload blob (auth required) |
51
+ | `GET` | `/content/:contentId/blob` | Download blob at head (standing auth, or auth + credential) |
52
+ | `GET` | `/content/:contentId/blob/:ref` | Download blob at specific operation ref |
54
53
 
55
54
  ## Blob Authorization
56
55
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { VerifiedIdentity, VerifiedContentChain, VerifiedBeacon } from '@metalabel/dfos-protocol/chain';
1
+ import { VerifiedIdentity, VerifiedContentChain } from '@metalabel/dfos-protocol/chain';
2
2
  import { Attenuation } from '@metalabel/dfos-protocol/credentials';
3
3
  import { Hono } from 'hono';
4
4
 
@@ -17,6 +17,13 @@ interface RelayOptions {
17
17
  content?: boolean;
18
18
  /** Whether the global operation log is enabled (default: true) */
19
19
  log?: boolean;
20
+ /**
21
+ * Whether this relay accepts writes (default: true). When false, it is a LITE
22
+ * pull-only proof node: POST /proof/v1/operations is rejected (501), so neither
23
+ * client writes nor peer gossip-in are accepted. The node still ingests by
24
+ * PULLING from peers (syncFromPeers polls their /log).
25
+ */
26
+ write?: boolean;
20
27
  /** Peer relay configurations */
21
28
  peers?: PeerConfig[];
22
29
  /** Injected peer client — if omitted, a default HTTP implementation is used */
@@ -90,18 +97,12 @@ interface StoredContentChain {
90
97
  lastCreatedAt: string;
91
98
  state: VerifiedContentChain;
92
99
  }
93
- interface StoredBeacon {
94
- did: string;
95
- jwsToken: string;
96
- beaconCID: string;
97
- state: VerifiedBeacon;
98
- }
99
100
  interface StoredOperation {
100
101
  cid: string;
101
102
  jwsToken: string;
102
103
  /** Which chain type this operation belongs to */
103
- chainType: 'identity' | 'content' | 'artifact' | 'beacon' | 'countersign' | 'revocation' | 'credential';
104
- /** The chain identifier — DID for identity/beacon/artifact, contentId for content, targetCID for countersign */
104
+ chainType: 'identity' | 'content' | 'artifact' | 'countersign' | 'revocation' | 'credential';
105
+ /** The chain identifier — DID for identity/artifact, contentId for content, targetCID for countersign */
105
106
  chainId: string;
106
107
  }
107
108
  /** Key for blob storage — deduplicates across chains sharing the same document */
@@ -117,7 +118,7 @@ interface LogEntry {
117
118
  chainId: string;
118
119
  }
119
120
  /** All operation kinds in the protocol */
120
- type OperationKind = 'identity-op' | 'content-op' | 'beacon' | 'artifact' | 'countersign' | 'revocation' | 'credential';
121
+ type OperationKind = 'identity-op' | 'content-op' | 'artifact' | 'countersign' | 'revocation' | 'credential';
121
122
  interface StoredRevocation {
122
123
  cid: string;
123
124
  issuerDID: string;
@@ -156,8 +157,6 @@ interface RelayStore {
156
157
  putIdentityChain(chain: StoredIdentityChain): Promise<void>;
157
158
  getContentChain(contentId: string): Promise<StoredContentChain | undefined>;
158
159
  putContentChain(chain: StoredContentChain): Promise<void>;
159
- getBeacon(did: string): Promise<StoredBeacon | undefined>;
160
- putBeacon(beacon: StoredBeacon): Promise<void>;
161
160
  getBlob(key: BlobKey): Promise<Uint8Array | undefined>;
162
161
  putBlob(key: BlobKey, data: Uint8Array): Promise<void>;
163
162
  getCountersignatures(operationCID: string): Promise<string[]>;
@@ -320,7 +319,6 @@ declare class MemoryRelayStore implements RelayStore {
320
319
  private operations;
321
320
  private identityChains;
322
321
  private contentChains;
323
- private beacons;
324
322
  private blobs;
325
323
  private countersignatures;
326
324
  private operationLog;
@@ -335,8 +333,6 @@ declare class MemoryRelayStore implements RelayStore {
335
333
  putIdentityChain(chain: StoredIdentityChain): Promise<void>;
336
334
  getContentChain(contentId: string): Promise<StoredContentChain | undefined>;
337
335
  putContentChain(chain: StoredContentChain): Promise<void>;
338
- getBeacon(did: string): Promise<StoredBeacon | undefined>;
339
- putBeacon(beacon: StoredBeacon): Promise<void>;
340
336
  getBlob(key: BlobKey): Promise<Uint8Array | undefined>;
341
337
  putBlob(key: BlobKey, data: Uint8Array): Promise<void>;
342
338
  getCountersignatures(operationCID: string): Promise<string[]>;
@@ -416,22 +412,30 @@ declare const createKeyResolver: (store: RelayStore) => (kid: string) => Promise
416
412
  */
417
413
  declare const createHistoricalIdentityResolver: (store: RelayStore) => (did: string) => Promise<{
418
414
  authKeys: {
415
+ [x: string]: unknown;
419
416
  id: string;
420
417
  type: "Multikey";
421
418
  publicKeyMultibase: string;
422
419
  }[];
423
420
  assertKeys: {
421
+ [x: string]: unknown;
424
422
  id: string;
425
423
  type: "Multikey";
426
424
  publicKeyMultibase: string;
427
425
  }[];
428
426
  controllerKeys: {
427
+ [x: string]: unknown;
429
428
  id: string;
430
429
  type: "Multikey";
431
430
  publicKeyMultibase: string;
432
431
  }[];
433
432
  did: string;
434
433
  isDeleted: boolean;
434
+ services: {
435
+ [x: string]: unknown;
436
+ id: string;
437
+ type: string;
438
+ }[];
435
439
  } | undefined>;
436
440
  /**
437
441
  * Create a key resolver that only resolves current-state keys.
@@ -445,7 +449,7 @@ declare const createCurrentKeyResolver: (store: RelayStore) => (kid: string) =>
445
449
  * Ingest a batch of JWS operations
446
450
  *
447
451
  * Classifies, dependency-sorts, and processes each token. Identity operations
448
- * are processed first so content chains and beacons can resolve their keys.
452
+ * are processed first so content chains can resolve their keys.
449
453
  * Within each kind, genesis operations are processed before extensions.
450
454
  */
451
455
  declare const ingestOperations: (tokens: string[], store: RelayStore, options?: {
@@ -469,4 +473,4 @@ declare const sequenceOps: (store: RelayStore) => Promise<{
469
473
  result: SequenceResult;
470
474
  }>;
471
475
 
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 };
476
+ 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 StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, bootstrapRelayIdentityFromKey, chunkOps, computeOpCID, createCurrentKeyResolver, createHistoricalIdentityResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations, isDependencyFailure, sequenceOps };
package/dist/index.js CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  import {
16
16
  decodeMultikey,
17
17
  verifyArtifact,
18
- verifyBeacon,
19
18
  verifyContentChain,
20
19
  verifyContentExtensionFromTrustedState,
21
20
  verifyCountersignature,
@@ -71,17 +70,6 @@ var classify = (jwsToken) => {
71
70
  const opDID = typeof payload["did"] === "string" ? payload["did"] : null;
72
71
  return { ...base, kind: "content-op", referencedDID: null, signerDID: opDID, priority: 2 };
73
72
  }
74
- if (typ === "did:dfos:beacon") {
75
- const beaconDID = typeof payload["did"] === "string" ? payload["did"] : null;
76
- return {
77
- ...base,
78
- kind: "beacon",
79
- referencedDID: beaconDID,
80
- signerDID: null,
81
- priority: 1,
82
- previousCID: null
83
- };
84
- }
85
73
  if (typ === "did:dfos:countersign") {
86
74
  const witnessDID = typeof payload["did"] === "string" ? payload["did"] : null;
87
75
  return {
@@ -102,7 +90,7 @@ var classify = (jwsToken) => {
102
90
  referencedDID: artifactDID,
103
91
  signerDID: null,
104
92
  priority: 1,
105
- // same as beacons — needs identity keys resolved first
93
+ // needs identity keys resolved first
106
94
  previousCID: null
107
95
  };
108
96
  }
@@ -114,7 +102,7 @@ var classify = (jwsToken) => {
114
102
  referencedDID: revocationDID,
115
103
  signerDID: null,
116
104
  priority: 1,
117
- // same as beacons — needs identity keys to verify
105
+ // needs identity keys to verify
118
106
  previousCID: null
119
107
  };
120
108
  }
@@ -548,41 +536,6 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
548
536
  }
549
537
  return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
550
538
  };
551
- var ingestBeacon = async (jwsToken, store, logEnabled) => {
552
- const resolveKey = createCurrentKeyResolver(store);
553
- let verified;
554
- try {
555
- verified = await verifyBeacon({ jwsToken, resolveKey });
556
- } catch (err) {
557
- const message = err instanceof Error ? err.message : "verification failed";
558
- return {
559
- cid: await computeOpCID(jwsToken),
560
- status: "rejected",
561
- error: message,
562
- dependencyMissing: isKeyResolutionFailure(message)
563
- };
564
- }
565
- const did = verified.did;
566
- const cid = verified.beaconCID;
567
- const identity = await store.getIdentityChain(did);
568
- if (identity?.state.isDeleted) {
569
- return { cid, status: "rejected", error: "identity is deleted" };
570
- }
571
- const existing = await store.getBeacon(did);
572
- if (existing) {
573
- const existingTime = new Date(existing.state.createdAt).getTime();
574
- const newTime = new Date(verified.createdAt).getTime();
575
- if (newTime < existingTime || newTime === existingTime && cid <= existing.beaconCID) {
576
- return { cid, status: "duplicate", kind: "beacon", chainId: did };
577
- }
578
- }
579
- await store.putBeacon({ did, jwsToken, beaconCID: cid, state: verified });
580
- await store.putOperation({ cid, jwsToken, chainType: "beacon", chainId: did });
581
- if (logEnabled) {
582
- await store.appendToLog({ cid, jwsToken, kind: "beacon", chainId: did });
583
- }
584
- return { cid, status: "new", kind: "beacon", chainId: did };
585
- };
586
539
  var ingestCountersign = async (jwsToken, store, logEnabled) => {
587
540
  const resolveKey = createKeyResolver(store);
588
541
  let verified;
@@ -880,9 +833,6 @@ var ingestOperations = async (tokens, store, options) => {
880
833
  case "content-op":
881
834
  result = await ingestContentOp(op.jwsToken, store, logEnabled);
882
835
  break;
883
- case "beacon":
884
- result = await ingestBeacon(op.jwsToken, store, logEnabled);
885
- break;
886
836
  case "countersign":
887
837
  result = await ingestCountersign(op.jwsToken, store, logEnabled);
888
838
  break;
@@ -984,6 +934,9 @@ var bootstrapWithKeyMaterial = async (store, params) => {
984
934
  return { did, profileArtifactJws };
985
935
  };
986
936
 
937
+ // src/types.ts
938
+ var PROOF_BASE_PATH = "/proof/v1";
939
+
987
940
  // src/peer-client.ts
988
941
  var createHttpPeerClient = () => {
989
942
  const fetchJSON = async (url) => {
@@ -997,7 +950,7 @@ var createHttpPeerClient = () => {
997
950
  };
998
951
  return {
999
952
  async getIdentityLog(peerUrl, did, params) {
1000
- const url = new URL(`/identities/${encodeURIComponent(did)}/log`, peerUrl);
953
+ const url = new URL(`${PROOF_BASE_PATH}/identities/${encodeURIComponent(did)}/log`, peerUrl);
1001
954
  if (params?.after) url.searchParams.set("after", params.after);
1002
955
  if (params?.limit) url.searchParams.set("limit", String(params.limit));
1003
956
  const data = await fetchJSON(url.toString());
@@ -1005,7 +958,10 @@ var createHttpPeerClient = () => {
1005
958
  return { entries: data.entries, cursor: data.cursor ?? null };
1006
959
  },
1007
960
  async getContentLog(peerUrl, contentId, params) {
1008
- const url = new URL(`/content/${encodeURIComponent(contentId)}/log`, peerUrl);
961
+ const url = new URL(
962
+ `${PROOF_BASE_PATH}/content/${encodeURIComponent(contentId)}/log`,
963
+ peerUrl
964
+ );
1009
965
  if (params?.after) url.searchParams.set("after", params.after);
1010
966
  if (params?.limit) url.searchParams.set("limit", String(params.limit));
1011
967
  const data = await fetchJSON(url.toString());
@@ -1013,7 +969,7 @@ var createHttpPeerClient = () => {
1013
969
  return { entries: data.entries, cursor: data.cursor ?? null };
1014
970
  },
1015
971
  async getOperationLog(peerUrl, params) {
1016
- const url = new URL("/log", peerUrl);
972
+ const url = new URL(`${PROOF_BASE_PATH}/log`, peerUrl);
1017
973
  if (params?.after) url.searchParams.set("after", params.after);
1018
974
  if (params?.limit) url.searchParams.set("limit", String(params.limit));
1019
975
  const data = await fetchJSON(url.toString());
@@ -1022,7 +978,7 @@ var createHttpPeerClient = () => {
1022
978
  },
1023
979
  async submitOperations(peerUrl, operations) {
1024
980
  try {
1025
- const res = await fetch(new URL("/operations", peerUrl).toString(), {
981
+ const res = await fetch(new URL(`${PROOF_BASE_PATH}/operations`, peerUrl).toString(), {
1026
982
  method: "POST",
1027
983
  headers: { "Content-Type": "application/json" },
1028
984
  body: JSON.stringify({ operations })
@@ -1106,6 +1062,27 @@ var hasPublicStandingAuth = async (contentId, action, store) => {
1106
1062
  }
1107
1063
  return false;
1108
1064
  };
1065
+ var derivePublicGrants = async (contentId, creatorDID, store) => {
1066
+ const resource = `chain:${contentId}`;
1067
+ const publicCreds = await store.getPublicCredentials(resource);
1068
+ if (publicCreds.length === 0) return [];
1069
+ const resolveIdentity = createHistoricalIdentityResolver(store);
1070
+ const isRevoked = async (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
1071
+ const grants = [];
1072
+ for (const credJws of publicCreds) {
1073
+ try {
1074
+ const cred = await verifyDFOSCredential2(credJws, { resolveIdentity });
1075
+ if (await isRevoked(cred.iss, cred.credentialCID)) continue;
1076
+ if (!await matchesResource(cred.att, resource, "read")) continue;
1077
+ await verifyDelegationChain(cred, { resolveIdentity, rootDID: creatorDID, isRevoked });
1078
+ grants.push(credJws);
1079
+ } catch {
1080
+ continue;
1081
+ }
1082
+ }
1083
+ grants.sort();
1084
+ return grants;
1085
+ };
1109
1086
  var verifyContentAccess = async (options) => {
1110
1087
  const { credentialJWS, requestedResource, action, store, creatorDID, requesterDID } = options;
1111
1088
  if (requesterDID && requesterDID === creatorDID) {
@@ -1223,6 +1200,7 @@ var createRelay = async (options) => {
1223
1200
  const { store } = options;
1224
1201
  const contentEnabled = options.content !== false;
1225
1202
  const logEnabled = options.log !== false;
1203
+ const writeEnabled = options.write !== false;
1226
1204
  const maxAuthTokenTTLSeconds = options.maxAuthTokenTTLSeconds ?? DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS;
1227
1205
  const peers = options.peers ?? [];
1228
1206
  const peerClient = options.peerClient;
@@ -1285,6 +1263,7 @@ var createRelay = async (options) => {
1285
1263
  version: RELAY_VERSION,
1286
1264
  capabilities: {
1287
1265
  proof: true,
1266
+ write: writeEnabled,
1288
1267
  content: contentEnabled,
1289
1268
  documents: contentEnabled,
1290
1269
  log: logEnabled
@@ -1292,7 +1271,10 @@ var createRelay = async (options) => {
1292
1271
  profile: profileArtifactJws
1293
1272
  });
1294
1273
  });
1295
- app.post("/operations", async (c) => {
1274
+ app.post(`${PROOF_BASE_PATH}/operations`, async (c) => {
1275
+ if (!writeEnabled) {
1276
+ return c.json({ error: "this relay is pull-only; writes are disabled" }, 501);
1277
+ }
1296
1278
  if (exceedsBodyCap(c.req.header("content-length"))) {
1297
1279
  return c.json({ error: "request body too large" }, 413);
1298
1280
  }
@@ -1309,7 +1291,7 @@ var createRelay = async (options) => {
1309
1291
  const results = await ingestWithGossip(parsed.data.operations);
1310
1292
  return c.json({ results });
1311
1293
  });
1312
- app.get("/operations/:cid", async (c) => {
1294
+ app.get(`${PROOF_BASE_PATH}/operations/:cid`, async (c) => {
1313
1295
  const cid = c.req.param("cid");
1314
1296
  const op = await store.getOperation(cid);
1315
1297
  if (!op) return c.json({ error: "not found" }, 404);
@@ -1320,7 +1302,7 @@ var createRelay = async (options) => {
1320
1302
  chainId: op.chainId
1321
1303
  });
1322
1304
  });
1323
- app.get("/identities/:did/log", async (c) => {
1305
+ app.get(`${PROOF_BASE_PATH}/identities/:did/log`, async (c) => {
1324
1306
  const did = c.req.param("did");
1325
1307
  const chain = await store.getIdentityChain(did);
1326
1308
  if (!chain) return c.json({ error: "not found" }, 404);
@@ -1339,7 +1321,7 @@ var createRelay = async (options) => {
1339
1321
  const cursor = page.length === limit ? page[page.length - 1].cid : null;
1340
1322
  return c.json({ entries: page, cursor });
1341
1323
  });
1342
- app.get("/identities/:did{.+}", async (c) => {
1324
+ app.get(`${PROOF_BASE_PATH}/identities/:did{.+}`, async (c) => {
1343
1325
  const did = c.req.param("did");
1344
1326
  let chain = await store.getIdentityChain(did);
1345
1327
  if (!chain && readThroughPeers.length > 0 && peerClient) {
@@ -1366,7 +1348,7 @@ var createRelay = async (options) => {
1366
1348
  state: chain.state
1367
1349
  });
1368
1350
  });
1369
- app.get("/content/:contentId/log", async (c) => {
1351
+ app.get(`${PROOF_BASE_PATH}/content/:contentId/log`, async (c) => {
1370
1352
  const contentId = c.req.param("contentId");
1371
1353
  const chain = await store.getContentChain(contentId);
1372
1354
  if (!chain) return c.json({ error: "not found" }, 404);
@@ -1420,7 +1402,7 @@ var createRelay = async (options) => {
1420
1402
  nextCursor: result.cursor
1421
1403
  });
1422
1404
  });
1423
- app.get("/content/:contentId", async (c) => {
1405
+ app.get(`${PROOF_BASE_PATH}/content/:contentId`, async (c) => {
1424
1406
  const contentId = c.req.param("contentId");
1425
1407
  let chain = await store.getContentChain(contentId);
1426
1408
  if (!chain && readThroughPeers.length > 0 && peerClient) {
@@ -1445,10 +1427,11 @@ var createRelay = async (options) => {
1445
1427
  contentId: chain.contentId,
1446
1428
  genesisCID: chain.genesisCID,
1447
1429
  headCID: chain.state.headCID,
1448
- state: chain.state
1430
+ state: chain.state,
1431
+ publicGrants: await derivePublicGrants(contentId, chain.state.creatorDID, store)
1449
1432
  });
1450
1433
  });
1451
- app.get("/countersignatures/:cid", async (c) => {
1434
+ app.get(`${PROOF_BASE_PATH}/countersignatures/:cid`, async (c) => {
1452
1435
  const cid = c.req.param("cid");
1453
1436
  const op = await store.getOperation(cid);
1454
1437
  if (!op) {
@@ -1459,26 +1442,14 @@ var createRelay = async (options) => {
1459
1442
  const countersigs = await store.getCountersignatures(cid);
1460
1443
  return c.json({ operationCID: cid, countersignatures: countersigs });
1461
1444
  });
1462
- app.get("/operations/:cid/countersignatures", async (c) => {
1445
+ app.get(`${PROOF_BASE_PATH}/operations/:cid/countersignatures`, async (c) => {
1463
1446
  const cid = c.req.param("cid");
1464
1447
  const op = await store.getOperation(cid);
1465
1448
  if (!op) return c.json({ error: "not found" }, 404);
1466
1449
  const countersigs = await store.getCountersignatures(cid);
1467
1450
  return c.json({ operationCID: cid, countersignatures: countersigs });
1468
1451
  });
1469
- app.get("/beacons/:did{.+}", async (c) => {
1470
- const did = c.req.param("did");
1471
- const beacon = await store.getBeacon(did);
1472
- if (!beacon) return c.json({ error: "not found" }, 404);
1473
- return c.json({
1474
- did: beacon.did,
1475
- jwsToken: beacon.jwsToken,
1476
- beaconCID: beacon.beaconCID,
1477
- manifestContentId: beacon.state.manifestContentId,
1478
- createdAt: beacon.state.createdAt
1479
- });
1480
- });
1481
- app.get("/log", async (c) => {
1452
+ app.get(`${PROOF_BASE_PATH}/log`, async (c) => {
1482
1453
  if (!logEnabled) return c.json({ error: "global log not available" }, 501);
1483
1454
  const afterParam = c.req.query("after");
1484
1455
  const limit = parseLimit(c.req.query("limit"), 100, 1e3);
@@ -1643,7 +1614,6 @@ var MemoryRelayStore = class {
1643
1614
  operations = /* @__PURE__ */ new Map();
1644
1615
  identityChains = /* @__PURE__ */ new Map();
1645
1616
  contentChains = /* @__PURE__ */ new Map();
1646
- beacons = /* @__PURE__ */ new Map();
1647
1617
  blobs = /* @__PURE__ */ new Map();
1648
1618
  countersignatures = /* @__PURE__ */ new Map();
1649
1619
  operationLog = [];
@@ -1670,12 +1640,6 @@ var MemoryRelayStore = class {
1670
1640
  async putContentChain(chain) {
1671
1641
  this.contentChains.set(chain.contentId, chain);
1672
1642
  }
1673
- async getBeacon(did) {
1674
- return this.beacons.get(did);
1675
- }
1676
- async putBeacon(beacon) {
1677
- this.beacons.set(beacon.did, beacon);
1678
- }
1679
1643
  async getBlob(key) {
1680
1644
  return this.blobs.get(blobKeyString(key));
1681
1645
  }
package/openapi.yaml CHANGED
@@ -4,10 +4,10 @@ info:
4
4
  version: 0.9.0
5
5
  description: |
6
6
  HTTP relay for the DFOS protocol. Receives, verifies, stores, and serves
7
- identity chains, content chains, beacons, countersignatures, and content blobs.
7
+ identity chains, content chains, countersignatures, and content blobs.
8
8
 
9
9
  Two data planes:
10
- - **Proof plane** (public): signed chain operations, beacons, countersignatures
10
+ - **Proof plane** (public): signed chain operations, countersignatures
11
11
  - **Content plane** (authenticated): raw content blobs gated by DID auth tokens and DFOS credentials
12
12
 
13
13
  servers:
@@ -33,7 +33,7 @@ paths:
33
33
  did:
34
34
  type: string
35
35
  description: The relay's DID
36
- example: 'did:dfos:e3vvtck42d4eacdnzvtrn6'
36
+ example: 'did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr'
37
37
  protocol:
38
38
  type: string
39
39
  enum: [dfos-web-relay]
@@ -42,11 +42,14 @@ paths:
42
42
  example: '0.8.0'
43
43
  capabilities:
44
44
  type: object
45
- required: [proof, content, documents, log]
45
+ required: [proof, write, content, documents, log]
46
46
  properties:
47
47
  proof:
48
48
  type: boolean
49
49
  description: Always true — a relay without proof plane is not a relay
50
+ write:
51
+ type: boolean
52
+ description: Whether the relay accepts writes via POST /proof/v1/operations (false = lite pull-only node)
50
53
  content:
51
54
  type: boolean
52
55
  description: Whether the relay supports content plane (blob upload/download)
@@ -55,18 +58,18 @@ paths:
55
58
  description: Whether the relay serves the documents endpoint
56
59
  log:
57
60
  type: boolean
58
- description: Whether the global operation log is available (GET /log)
61
+ description: Whether the global operation log is available (GET /proof/v1/log)
59
62
  profile:
60
63
  type: string
61
64
  description: The relay's profile artifact as a compact JWS token
62
65
 
63
- /operations:
66
+ /proof/v1/operations:
64
67
  post:
65
68
  operationId: ingestOperations
66
69
  summary: Submit operations for ingestion
67
70
  description: |
68
71
  Accept a batch of JWS tokens — identity operations, content operations,
69
- beacons, and countersignatures. The relay classifies, dependency-sorts,
72
+ and countersignatures. The relay classifies, dependency-sorts,
70
73
  verifies, and stores each token.
71
74
  tags: [Proof Plane]
72
75
  requestBody:
@@ -99,8 +102,10 @@ paths:
99
102
  $ref: '#/components/schemas/IngestionResult'
100
103
  '400':
101
104
  $ref: '#/components/responses/BadRequest'
105
+ '501':
106
+ description: Writes disabled — relay is a lite pull-only node (capabilities.write is false)
102
107
 
103
- /operations/{cid}:
108
+ /proof/v1/operations/{cid}:
104
109
  get:
105
110
  operationId: getOperation
106
111
  summary: Get an operation by CID
@@ -122,7 +127,7 @@ paths:
122
127
  '404':
123
128
  $ref: '#/components/responses/NotFound'
124
129
 
125
- /operations/{cid}/countersignatures:
130
+ /proof/v1/operations/{cid}/countersignatures:
126
131
  get:
127
132
  operationId: getCountersignatures
128
133
  summary: Get countersignatures for an operation
@@ -152,7 +157,7 @@ paths:
152
157
  '404':
153
158
  $ref: '#/components/responses/NotFound'
154
159
 
155
- /identities/{did}:
160
+ /proof/v1/identities/{did}:
156
161
  get:
157
162
  operationId: getIdentityChain
158
163
  summary: Get an identity chain by DID
@@ -163,7 +168,7 @@ paths:
163
168
  required: true
164
169
  schema:
165
170
  type: string
166
- description: 'DID of the identity (e.g., did:dfos:e3vvtck42d4eacdnzvtrn6)'
171
+ description: 'DID of the identity (e.g., did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr)'
167
172
  responses:
168
173
  '200':
169
174
  description: Identity chain state and log
@@ -174,7 +179,7 @@ paths:
174
179
  '404':
175
180
  $ref: '#/components/responses/NotFound'
176
181
 
177
- /content/{contentId}:
182
+ /proof/v1/content/{contentId}:
178
183
  get:
179
184
  operationId: getContentChain
180
185
  summary: Get a content chain by content ID
@@ -414,15 +419,15 @@ paths:
414
419
  '404':
415
420
  $ref: '#/components/responses/NotFound'
416
421
  '501':
417
- description: Documents capability not enabled
422
+ description: Content plane not enabled (the documents endpoint is part of the content plane)
418
423
 
419
- /log:
424
+ /proof/v1/log:
420
425
  get:
421
426
  operationId: getLog
422
427
  summary: Paginated global log of all accepted operations
423
428
  description: |
424
429
  Returns every operation the relay has accepted — across all identity and
425
- content chains, plus beacons and countersignatures — in acceptance order.
430
+ content chains, plus countersignatures — in acceptance order.
426
431
  Cursor-based pagination. Used by peer relays to background-sync. Available
427
432
  only when the relay advertises the `log` capability; otherwise returns 501.
428
433
  tags: [Proof Plane]
@@ -462,15 +467,7 @@ paths:
462
467
  kind:
463
468
  type: string
464
469
  enum:
465
- [
466
- identity-op,
467
- content-op,
468
- beacon,
469
- artifact,
470
- countersign,
471
- revocation,
472
- credential,
473
- ]
470
+ [identity-op, content-op, artifact, countersign, revocation, credential]
474
471
  chainId:
475
472
  type: string
476
473
  description: Chain identifier (DID or contentId)
@@ -480,7 +477,7 @@ paths:
480
477
  '501':
481
478
  description: Global log capability not enabled
482
479
 
483
- /identities/{did}/log:
480
+ /proof/v1/identities/{did}/log:
484
481
  get:
485
482
  operationId: getIdentityLog
486
483
  summary: Paginated log of identity chain operations
@@ -533,7 +530,7 @@ paths:
533
530
  '404':
534
531
  $ref: '#/components/responses/NotFound'
535
532
 
536
- /content/{contentId}/log:
533
+ /proof/v1/content/{contentId}/log:
537
534
  get:
538
535
  operationId: getContentLog
539
536
  summary: Paginated log of content chain operations
@@ -586,10 +583,10 @@ paths:
586
583
  '404':
587
584
  $ref: '#/components/responses/NotFound'
588
585
 
589
- /countersignatures/{cid}:
586
+ /proof/v1/countersignatures/{cid}:
590
587
  get:
591
588
  operationId: getCountersignaturesByCID
592
- summary: Get countersignatures for any CID (operation or beacon)
589
+ summary: Get countersignatures for an operation CID
593
590
  tags: [Proof Plane]
594
591
  parameters:
595
592
  - name: cid
@@ -597,7 +594,7 @@ paths:
597
594
  required: true
598
595
  schema:
599
596
  type: string
600
- description: CIDv1 of the operation or beacon
597
+ description: CIDv1 of the operation
601
598
  responses:
602
599
  '200':
603
600
  description: Countersignatures
@@ -617,27 +614,6 @@ paths:
617
614
  '404':
618
615
  $ref: '#/components/responses/NotFound'
619
616
 
620
- /beacons/{did}:
621
- get:
622
- operationId: getBeacon
623
- summary: Get the latest beacon for a DID
624
- tags: [Proof Plane]
625
- parameters:
626
- - name: did
627
- in: path
628
- required: true
629
- schema:
630
- type: string
631
- responses:
632
- '200':
633
- description: Latest beacon
634
- content:
635
- application/json:
636
- schema:
637
- $ref: '#/components/schemas/BeaconResponse'
638
- '404':
639
- $ref: '#/components/responses/NotFound'
640
-
641
617
  components:
642
618
  securitySchemes:
643
619
  BearerAuth:
@@ -661,7 +637,7 @@ components:
661
637
  description: Error message if rejected
662
638
  kind:
663
639
  type: string
664
- enum: [identity-op, content-op, beacon, artifact, countersign, revocation, credential]
640
+ enum: [identity-op, content-op, artifact, countersign, revocation, credential]
665
641
  chainId:
666
642
  type: string
667
643
  description: Chain identifier (DID or contentId)
@@ -676,7 +652,7 @@ components:
676
652
  type: string
677
653
  chainType:
678
654
  type: string
679
- enum: [identity, content, artifact, beacon, countersign, revocation, credential]
655
+ enum: [identity, content, artifact, countersign, revocation, credential]
680
656
  chainId:
681
657
  type: string
682
658
 
@@ -691,7 +667,7 @@ components:
691
667
  description: CID of the current head operation
692
668
  state:
693
669
  type: object
694
- required: [did, isDeleted, authKeys, assertKeys, controllerKeys]
670
+ required: [did, isDeleted, authKeys, assertKeys, controllerKeys, services]
695
671
  properties:
696
672
  did:
697
673
  type: string
@@ -709,6 +685,22 @@ components:
709
685
  type: array
710
686
  items:
711
687
  $ref: '#/components/schemas/MultikeyPublicKey'
688
+ services:
689
+ type: array
690
+ description: >-
691
+ Resolved discovery vocabulary (controller-signed). Each entry has
692
+ a common envelope {id, type}; recognized types DfosRelay and
693
+ ContentAnchor carry type-specific fields. The namespace is open —
694
+ unrecognized types are preserved verbatim.
695
+ items:
696
+ type: object
697
+ required: [id, type]
698
+ properties:
699
+ id:
700
+ type: string
701
+ type:
702
+ type: string
703
+ additionalProperties: true
712
704
 
713
705
  ContentChainResponse:
714
706
  type: object
@@ -743,23 +735,6 @@ components:
743
735
  creatorDID:
744
736
  type: string
745
737
 
746
- BeaconResponse:
747
- type: object
748
- required: [did, jwsToken, beaconCID, manifestContentId, createdAt]
749
- properties:
750
- did:
751
- type: string
752
- jwsToken:
753
- type: string
754
- beaconCID:
755
- type: string
756
- manifestContentId:
757
- type: string
758
- description: Content ID of the manifest chain
759
- createdAt:
760
- type: string
761
- format: date-time
762
-
763
738
  MultikeyPublicKey:
764
739
  type: object
765
740
  required: [id, type, publicKeyMultibase]
@@ -809,6 +784,6 @@ tags:
809
784
  - name: Meta
810
785
  description: Relay metadata and discovery
811
786
  - name: Proof Plane
812
- description: Public routes for signed chain operations, beacons, and countersignatures
787
+ description: Public routes for signed chain operations and countersignatures
813
788
  - name: Content Plane
814
789
  description: Authenticated routes for content blob storage and retrieval
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-web-relay",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
- "description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, beacons, and content blobs",
5
+ "description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, and content blobs",
6
6
  "license": "MIT",
7
7
  "author": "Metalabel <hello@metalabel.com> (https://metalabel.com)",
8
8
  "repository": {
@@ -45,14 +45,14 @@
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "peerDependencies": {
48
- "@metalabel/dfos-protocol": "^0.10.0"
48
+ "@metalabel/dfos-protocol": "^0.12.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^24.10.4",
52
52
  "tsup": "^8.5.1",
53
53
  "tsx": "^4.22.4",
54
54
  "vitest": "^4.1.8",
55
- "@metalabel/dfos-protocol": "0.10.0"
55
+ "@metalabel/dfos-protocol": "0.12.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",