@metalabel/dfos-web-relay 0.3.0 → 0.4.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/RELAY.md +10 -6
- package/dist/index.js +29 -20
- package/dist/serve.d.ts +21 -0
- package/dist/serve.js +31 -0
- package/package.json +7 -3
package/RELAY.md
CHANGED
|
@@ -86,6 +86,8 @@ First-seen-wins. If a chain head has already advanced past the `previousOperatio
|
|
|
86
86
|
|
|
87
87
|
Duplicate submissions (same operation CID) are silently accepted (idempotent).
|
|
88
88
|
|
|
89
|
+
Duplicate countersignatures (same witness DID, same target CID) MUST be deduplicated. The relay MUST NOT increase the countersignature count on resubmission. Resubmission SHOULD return `accepted` (idempotent).
|
|
90
|
+
|
|
89
91
|
### Result Ordering
|
|
90
92
|
|
|
91
93
|
Ingestion results are returned in the same order as the input `operations` array, regardless of internal processing order. `results[i]` corresponds to `operations[i]`.
|
|
@@ -105,16 +107,18 @@ The relay does not currently sign operations or participate in the protocol as a
|
|
|
105
107
|
|
|
106
108
|
## Content Plane Access
|
|
107
109
|
|
|
108
|
-
### Blob Upload (`PUT /content/:contentId/blob`)
|
|
110
|
+
### Blob Upload (`PUT /content/:contentId/blob/:operationCID`)
|
|
111
|
+
|
|
112
|
+
The upload path mirrors the download path — the operation CID identifies which operation's document is being uploaded.
|
|
109
113
|
|
|
110
114
|
Requirements:
|
|
111
115
|
|
|
112
116
|
- Valid auth token (Bearer header)
|
|
113
|
-
- The
|
|
114
|
-
- The
|
|
115
|
-
- The uploaded bytes must hash to the
|
|
117
|
+
- The operation CID must reference an operation in this content chain that has a `documentCID`
|
|
118
|
+
- The authenticated DID must be either the chain creator OR the signer of the referenced operation (enabling delegated uploads)
|
|
119
|
+
- The uploaded bytes must hash to the operation's `documentCID` (dag-cbor + sha-256 verification)
|
|
116
120
|
|
|
117
|
-
Blobs are stored by `(creatorDID, documentCID)` —
|
|
121
|
+
Blobs are stored by `(creatorDID, documentCID)` — always keyed to the chain creator regardless of who uploads. If multiple content chains by the same creator reference the same document, the blob is shared (deduplication).
|
|
118
122
|
|
|
119
123
|
### Blob Download (`GET /content/:contentId/blob[/:ref]`)
|
|
120
124
|
|
|
@@ -196,7 +200,7 @@ The returned Hono app exposes:
|
|
|
196
200
|
| `GET` | `/identities/:did` | proof | none |
|
|
197
201
|
| `GET` | `/content/:contentId` | proof | none |
|
|
198
202
|
| `GET` | `/beacons/:did` | proof | none |
|
|
199
|
-
| `PUT` | `/content/:contentId/blob`
|
|
203
|
+
| `PUT` | `/content/:contentId/blob/:opCID` | content | auth token |
|
|
200
204
|
| `GET` | `/content/:contentId/blob[/:ref]` | content | auth token + credential |
|
|
201
205
|
|
|
202
206
|
---
|
package/dist/index.js
CHANGED
|
@@ -512,32 +512,29 @@ var createRelay = (options) => {
|
|
|
512
512
|
payload: beacon.state.payload
|
|
513
513
|
});
|
|
514
514
|
});
|
|
515
|
-
app.put("/content/:contentId/blob", async (c) => {
|
|
515
|
+
app.put("/content/:contentId/blob/:operationCID", async (c) => {
|
|
516
516
|
const contentId = c.req.param("contentId");
|
|
517
|
-
const
|
|
518
|
-
if (!documentCID) {
|
|
519
|
-
return c.json({ error: "missing X-Document-CID header" }, 400);
|
|
520
|
-
}
|
|
517
|
+
const operationCID = c.req.param("operationCID");
|
|
521
518
|
const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
|
|
522
519
|
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
523
520
|
const chain = await store.getContentChain(contentId);
|
|
524
521
|
if (!chain) return c.json({ error: "content chain not found" }, 404);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const chainLog = chain.log;
|
|
529
|
-
let documentReferenced = false;
|
|
530
|
-
for (const token of chainLog) {
|
|
522
|
+
let documentCID = null;
|
|
523
|
+
let operationSignerDID = null;
|
|
524
|
+
for (const token of chain.log) {
|
|
531
525
|
const decoded = decodeJwsUnsafe3(token);
|
|
532
526
|
if (!decoded) continue;
|
|
527
|
+
if (decoded.header.cid !== operationCID) continue;
|
|
533
528
|
const payload = decoded.payload;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
529
|
+
documentCID = typeof payload["documentCID"] === "string" ? payload["documentCID"] : null;
|
|
530
|
+
operationSignerDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
if (!documentCID) {
|
|
534
|
+
return c.json({ error: "operation not found in chain or has no documentCID" }, 404);
|
|
538
535
|
}
|
|
539
|
-
if (
|
|
540
|
-
return c.json({ error: "
|
|
536
|
+
if (auth.iss !== chain.state.creatorDID && auth.iss !== operationSignerDID) {
|
|
537
|
+
return c.json({ error: "not authorized \u2014 must be chain creator or operation signer" }, 403);
|
|
541
538
|
}
|
|
542
539
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
543
540
|
try {
|
|
@@ -549,8 +546,8 @@ var createRelay = (options) => {
|
|
|
549
546
|
} catch {
|
|
550
547
|
return c.json({ error: "blob bytes do not match documentCID" }, 400);
|
|
551
548
|
}
|
|
552
|
-
await store.putBlob({ creatorDID:
|
|
553
|
-
return c.json({ status: "stored", contentId, documentCID });
|
|
549
|
+
await store.putBlob({ creatorDID: chain.state.creatorDID, documentCID }, bytes);
|
|
550
|
+
return c.json({ status: "stored", contentId, documentCID, operationCID });
|
|
554
551
|
});
|
|
555
552
|
app.get("/content/:contentId/blob", async (c) => {
|
|
556
553
|
return await readBlob({
|
|
@@ -651,6 +648,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
651
648
|
};
|
|
652
649
|
|
|
653
650
|
// src/store.ts
|
|
651
|
+
import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
|
|
654
652
|
var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
|
|
655
653
|
var MemoryRelayStore = class {
|
|
656
654
|
operations = /* @__PURE__ */ new Map();
|
|
@@ -694,7 +692,18 @@ var MemoryRelayStore = class {
|
|
|
694
692
|
}
|
|
695
693
|
async addCountersignature(operationCID, jwsToken) {
|
|
696
694
|
const existing = this.countersignatures.get(operationCID) ?? [];
|
|
697
|
-
|
|
695
|
+
const decoded = decodeJwsUnsafe4(jwsToken);
|
|
696
|
+
if (decoded) {
|
|
697
|
+
const kid = decoded.header.kid;
|
|
698
|
+
const witnessDID = kid.includes("#") ? kid.split("#")[0] : kid;
|
|
699
|
+
for (const cs of existing) {
|
|
700
|
+
const d = decodeJwsUnsafe4(cs);
|
|
701
|
+
if (!d) continue;
|
|
702
|
+
const existingKid = d.header.kid;
|
|
703
|
+
const existingDID = existingKid.includes("#") ? existingKid.split("#")[0] : existingKid;
|
|
704
|
+
if (existingDID === witnessDID) return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
698
707
|
existing.push(jwsToken);
|
|
699
708
|
this.countersignatures.set(operationCID, existing);
|
|
700
709
|
}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
interface ServeOptions {
|
|
5
|
+
port?: number;
|
|
6
|
+
hostname?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Start a Node HTTP server for a DFOS web relay.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';
|
|
13
|
+
* import { serve } from '@metalabel/dfos-web-relay/node';
|
|
14
|
+
*
|
|
15
|
+
* const relay = createRelay({ relayDID: 'did:dfos:myrelay', store: new MemoryRelayStore() });
|
|
16
|
+
* serve(relay, { port: 4444 });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare const serve: (app: Hono, options?: ServeOptions) => Server;
|
|
20
|
+
|
|
21
|
+
export { type ServeOptions, serve };
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/serve.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
var serve = (app, options = {}) => {
|
|
4
|
+
const { port = 4444, hostname } = options;
|
|
5
|
+
const server = createServer(async (req, res) => {
|
|
6
|
+
const url = new URL(req.url ?? "/", `http://${hostname ?? "localhost"}:${port}`);
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
9
|
+
const body = Buffer.concat(chunks);
|
|
10
|
+
const headers = new Headers();
|
|
11
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
12
|
+
if (v) headers.set(k, Array.isArray(v) ? v.join(", ") : v);
|
|
13
|
+
}
|
|
14
|
+
const method = req.method ?? "GET";
|
|
15
|
+
const init = { method, headers };
|
|
16
|
+
if (!["GET", "HEAD"].includes(method)) {
|
|
17
|
+
init.body = body;
|
|
18
|
+
}
|
|
19
|
+
const response = await app.fetch(new Request(url.toString(), init));
|
|
20
|
+
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
21
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
22
|
+
res.end(buf);
|
|
23
|
+
});
|
|
24
|
+
server.listen(port, hostname, () => {
|
|
25
|
+
console.log(`DFOS web relay listening on http://${hostname ?? "localhost"}:${port}`);
|
|
26
|
+
});
|
|
27
|
+
return server;
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
serve
|
|
31
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalabel/dfos-web-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, beacons, and content blobs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
".": {
|
|
29
29
|
"import": "./dist/index.js",
|
|
30
30
|
"types": "./dist/index.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./node": {
|
|
33
|
+
"import": "./dist/serve.js",
|
|
34
|
+
"types": "./dist/serve.d.ts"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"files": [
|
|
@@ -42,13 +46,13 @@
|
|
|
42
46
|
"zod": "^4.3.6"
|
|
43
47
|
},
|
|
44
48
|
"peerDependencies": {
|
|
45
|
-
"@metalabel/dfos-protocol": "^0.
|
|
49
|
+
"@metalabel/dfos-protocol": "^0.4.0"
|
|
46
50
|
},
|
|
47
51
|
"devDependencies": {
|
|
48
52
|
"@types/node": "^24.10.4",
|
|
49
53
|
"tsup": "^8.5.1",
|
|
50
54
|
"vitest": "^4.0.18",
|
|
51
|
-
"@metalabel/dfos-protocol": "0.
|
|
55
|
+
"@metalabel/dfos-protocol": "0.4.0"
|
|
52
56
|
},
|
|
53
57
|
"scripts": {
|
|
54
58
|
"build": "tsup",
|