@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 +12 -13
- package/dist/index.d.ts +21 -17
- package/dist/index.js +49 -85
- package/openapi.yaml +46 -71
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -38,19 +38,18 @@ serve({ port: 4444 });
|
|
|
38
38
|
|
|
39
39
|
## Routes
|
|
40
40
|
|
|
41
|
-
| Method | Path
|
|
42
|
-
| ------ |
|
|
43
|
-
| `GET` | `/.well-known/dfos-relay`
|
|
44
|
-
| `POST` | `/operations`
|
|
45
|
-
| `GET` | `/identities/:did`
|
|
46
|
-
| `GET` | `/content/:contentId`
|
|
47
|
-
| `GET` | `/operations/:cid`
|
|
48
|
-
| `GET` | `/
|
|
49
|
-
| `GET` | `/
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `GET` | `/content/:contentId/blob` | Download blob at
|
|
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
|
|
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' | '
|
|
104
|
-
/** The chain identifier — DID for identity/
|
|
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' | '
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
7
|
+
identity chains, content chains, countersignatures, and content blobs.
|
|
8
8
|
|
|
9
9
|
Two data planes:
|
|
10
|
-
- **Proof plane** (public): signed chain operations,
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains,
|
|
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.
|
|
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.
|
|
55
|
+
"@metalabel/dfos-protocol": "0.12.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|