@metalabel/dfos-web-relay 0.11.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,18 +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, 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` | `/countersignatures/:cid` | Get countersignatures for an operation |
49
- | `GET` | `/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 |
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 |
53
53
 
54
54
  ## Blob Authorization
55
55
 
package/dist/index.d.ts CHANGED
@@ -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 */
@@ -405,16 +412,19 @@ declare const createKeyResolver: (store: RelayStore) => (kid: string) => Promise
405
412
  */
406
413
  declare const createHistoricalIdentityResolver: (store: RelayStore) => (did: string) => Promise<{
407
414
  authKeys: {
415
+ [x: string]: unknown;
408
416
  id: string;
409
417
  type: "Multikey";
410
418
  publicKeyMultibase: string;
411
419
  }[];
412
420
  assertKeys: {
421
+ [x: string]: unknown;
413
422
  id: string;
414
423
  type: "Multikey";
415
424
  publicKeyMultibase: string;
416
425
  }[];
417
426
  controllerKeys: {
427
+ [x: string]: unknown;
418
428
  id: string;
419
429
  type: "Multikey";
420
430
  publicKeyMultibase: string;
package/dist/index.js CHANGED
@@ -934,6 +934,9 @@ var bootstrapWithKeyMaterial = async (store, params) => {
934
934
  return { did, profileArtifactJws };
935
935
  };
936
936
 
937
+ // src/types.ts
938
+ var PROOF_BASE_PATH = "/proof/v1";
939
+
937
940
  // src/peer-client.ts
938
941
  var createHttpPeerClient = () => {
939
942
  const fetchJSON = async (url) => {
@@ -947,7 +950,7 @@ var createHttpPeerClient = () => {
947
950
  };
948
951
  return {
949
952
  async getIdentityLog(peerUrl, did, params) {
950
- const url = new URL(`/identities/${encodeURIComponent(did)}/log`, peerUrl);
953
+ const url = new URL(`${PROOF_BASE_PATH}/identities/${encodeURIComponent(did)}/log`, peerUrl);
951
954
  if (params?.after) url.searchParams.set("after", params.after);
952
955
  if (params?.limit) url.searchParams.set("limit", String(params.limit));
953
956
  const data = await fetchJSON(url.toString());
@@ -955,7 +958,10 @@ var createHttpPeerClient = () => {
955
958
  return { entries: data.entries, cursor: data.cursor ?? null };
956
959
  },
957
960
  async getContentLog(peerUrl, contentId, params) {
958
- 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
+ );
959
965
  if (params?.after) url.searchParams.set("after", params.after);
960
966
  if (params?.limit) url.searchParams.set("limit", String(params.limit));
961
967
  const data = await fetchJSON(url.toString());
@@ -963,7 +969,7 @@ var createHttpPeerClient = () => {
963
969
  return { entries: data.entries, cursor: data.cursor ?? null };
964
970
  },
965
971
  async getOperationLog(peerUrl, params) {
966
- const url = new URL("/log", peerUrl);
972
+ const url = new URL(`${PROOF_BASE_PATH}/log`, peerUrl);
967
973
  if (params?.after) url.searchParams.set("after", params.after);
968
974
  if (params?.limit) url.searchParams.set("limit", String(params.limit));
969
975
  const data = await fetchJSON(url.toString());
@@ -972,7 +978,7 @@ var createHttpPeerClient = () => {
972
978
  },
973
979
  async submitOperations(peerUrl, operations) {
974
980
  try {
975
- const res = await fetch(new URL("/operations", peerUrl).toString(), {
981
+ const res = await fetch(new URL(`${PROOF_BASE_PATH}/operations`, peerUrl).toString(), {
976
982
  method: "POST",
977
983
  headers: { "Content-Type": "application/json" },
978
984
  body: JSON.stringify({ operations })
@@ -1056,6 +1062,27 @@ var hasPublicStandingAuth = async (contentId, action, store) => {
1056
1062
  }
1057
1063
  return false;
1058
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
+ };
1059
1086
  var verifyContentAccess = async (options) => {
1060
1087
  const { credentialJWS, requestedResource, action, store, creatorDID, requesterDID } = options;
1061
1088
  if (requesterDID && requesterDID === creatorDID) {
@@ -1173,6 +1200,7 @@ var createRelay = async (options) => {
1173
1200
  const { store } = options;
1174
1201
  const contentEnabled = options.content !== false;
1175
1202
  const logEnabled = options.log !== false;
1203
+ const writeEnabled = options.write !== false;
1176
1204
  const maxAuthTokenTTLSeconds = options.maxAuthTokenTTLSeconds ?? DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS;
1177
1205
  const peers = options.peers ?? [];
1178
1206
  const peerClient = options.peerClient;
@@ -1235,6 +1263,7 @@ var createRelay = async (options) => {
1235
1263
  version: RELAY_VERSION,
1236
1264
  capabilities: {
1237
1265
  proof: true,
1266
+ write: writeEnabled,
1238
1267
  content: contentEnabled,
1239
1268
  documents: contentEnabled,
1240
1269
  log: logEnabled
@@ -1242,7 +1271,10 @@ var createRelay = async (options) => {
1242
1271
  profile: profileArtifactJws
1243
1272
  });
1244
1273
  });
1245
- 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
+ }
1246
1278
  if (exceedsBodyCap(c.req.header("content-length"))) {
1247
1279
  return c.json({ error: "request body too large" }, 413);
1248
1280
  }
@@ -1259,7 +1291,7 @@ var createRelay = async (options) => {
1259
1291
  const results = await ingestWithGossip(parsed.data.operations);
1260
1292
  return c.json({ results });
1261
1293
  });
1262
- app.get("/operations/:cid", async (c) => {
1294
+ app.get(`${PROOF_BASE_PATH}/operations/:cid`, async (c) => {
1263
1295
  const cid = c.req.param("cid");
1264
1296
  const op = await store.getOperation(cid);
1265
1297
  if (!op) return c.json({ error: "not found" }, 404);
@@ -1270,7 +1302,7 @@ var createRelay = async (options) => {
1270
1302
  chainId: op.chainId
1271
1303
  });
1272
1304
  });
1273
- app.get("/identities/:did/log", async (c) => {
1305
+ app.get(`${PROOF_BASE_PATH}/identities/:did/log`, async (c) => {
1274
1306
  const did = c.req.param("did");
1275
1307
  const chain = await store.getIdentityChain(did);
1276
1308
  if (!chain) return c.json({ error: "not found" }, 404);
@@ -1289,7 +1321,7 @@ var createRelay = async (options) => {
1289
1321
  const cursor = page.length === limit ? page[page.length - 1].cid : null;
1290
1322
  return c.json({ entries: page, cursor });
1291
1323
  });
1292
- app.get("/identities/:did{.+}", async (c) => {
1324
+ app.get(`${PROOF_BASE_PATH}/identities/:did{.+}`, async (c) => {
1293
1325
  const did = c.req.param("did");
1294
1326
  let chain = await store.getIdentityChain(did);
1295
1327
  if (!chain && readThroughPeers.length > 0 && peerClient) {
@@ -1316,7 +1348,7 @@ var createRelay = async (options) => {
1316
1348
  state: chain.state
1317
1349
  });
1318
1350
  });
1319
- app.get("/content/:contentId/log", async (c) => {
1351
+ app.get(`${PROOF_BASE_PATH}/content/:contentId/log`, async (c) => {
1320
1352
  const contentId = c.req.param("contentId");
1321
1353
  const chain = await store.getContentChain(contentId);
1322
1354
  if (!chain) return c.json({ error: "not found" }, 404);
@@ -1370,7 +1402,7 @@ var createRelay = async (options) => {
1370
1402
  nextCursor: result.cursor
1371
1403
  });
1372
1404
  });
1373
- app.get("/content/:contentId", async (c) => {
1405
+ app.get(`${PROOF_BASE_PATH}/content/:contentId`, async (c) => {
1374
1406
  const contentId = c.req.param("contentId");
1375
1407
  let chain = await store.getContentChain(contentId);
1376
1408
  if (!chain && readThroughPeers.length > 0 && peerClient) {
@@ -1395,10 +1427,11 @@ var createRelay = async (options) => {
1395
1427
  contentId: chain.contentId,
1396
1428
  genesisCID: chain.genesisCID,
1397
1429
  headCID: chain.state.headCID,
1398
- state: chain.state
1430
+ state: chain.state,
1431
+ publicGrants: await derivePublicGrants(contentId, chain.state.creatorDID, store)
1399
1432
  });
1400
1433
  });
1401
- app.get("/countersignatures/:cid", async (c) => {
1434
+ app.get(`${PROOF_BASE_PATH}/countersignatures/:cid`, async (c) => {
1402
1435
  const cid = c.req.param("cid");
1403
1436
  const op = await store.getOperation(cid);
1404
1437
  if (!op) {
@@ -1409,14 +1442,14 @@ var createRelay = async (options) => {
1409
1442
  const countersigs = await store.getCountersignatures(cid);
1410
1443
  return c.json({ operationCID: cid, countersignatures: countersigs });
1411
1444
  });
1412
- app.get("/operations/:cid/countersignatures", async (c) => {
1445
+ app.get(`${PROOF_BASE_PATH}/operations/:cid/countersignatures`, async (c) => {
1413
1446
  const cid = c.req.param("cid");
1414
1447
  const op = await store.getOperation(cid);
1415
1448
  if (!op) return c.json({ error: "not found" }, 404);
1416
1449
  const countersigs = await store.getCountersignatures(cid);
1417
1450
  return c.json({ operationCID: cid, countersignatures: countersigs });
1418
1451
  });
1419
- app.get("/log", async (c) => {
1452
+ app.get(`${PROOF_BASE_PATH}/log`, async (c) => {
1420
1453
  if (!logEnabled) return c.json({ error: "global log not available" }, 501);
1421
1454
  const afterParam = c.req.query("after");
1422
1455
  const limit = parseLimit(c.req.query("limit"), 100, 1e3);
package/openapi.yaml CHANGED
@@ -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,12 +58,12 @@ 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
@@ -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,9 +419,9 @@ 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
@@ -472,7 +477,7 @@ paths:
472
477
  '501':
473
478
  description: Global log capability not enabled
474
479
 
475
- /identities/{did}/log:
480
+ /proof/v1/identities/{did}/log:
476
481
  get:
477
482
  operationId: getIdentityLog
478
483
  summary: Paginated log of identity chain operations
@@ -525,7 +530,7 @@ paths:
525
530
  '404':
526
531
  $ref: '#/components/responses/NotFound'
527
532
 
528
- /content/{contentId}/log:
533
+ /proof/v1/content/{contentId}/log:
529
534
  get:
530
535
  operationId: getContentLog
531
536
  summary: Paginated log of content chain operations
@@ -578,7 +583,7 @@ paths:
578
583
  '404':
579
584
  $ref: '#/components/responses/NotFound'
580
585
 
581
- /countersignatures/{cid}:
586
+ /proof/v1/countersignatures/{cid}:
582
587
  get:
583
588
  operationId: getCountersignaturesByCID
584
589
  summary: Get countersignatures for an operation CID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-web-relay",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, and content blobs",
6
6
  "license": "MIT",
@@ -45,14 +45,14 @@
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "peerDependencies": {
48
- "@metalabel/dfos-protocol": "^0.11.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.11.0"
55
+ "@metalabel/dfos-protocol": "0.12.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",