@metalabel/dfos-web-relay 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -1
- package/RELAY.md +25 -7
- package/dist/index.js +68 -22
- package/dist/serve.d.ts +21 -0
- package/dist/serve.js +31 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Portable HTTP relay for the [DFOS protocol](https://protocol.dfos.com). Receives, verifies, stores, and serves identity chains, content chains, beacons, countersignatures, and content blobs.
|
|
4
4
|
|
|
5
|
-
See [RELAY.md](./RELAY.md) for the full
|
|
5
|
+
See [RELAY.md](./RELAY.md) for the full relay specification.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -12,6 +12,8 @@ npm install @metalabel/dfos-web-relay @metalabel/dfos-protocol
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
+
### Embedded (Hono app)
|
|
16
|
+
|
|
15
17
|
```typescript
|
|
16
18
|
import { createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';
|
|
17
19
|
|
|
@@ -24,6 +26,77 @@ const relay = createRelay({
|
|
|
24
26
|
export default relay;
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
### Standalone (Node.js)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { serve } from '@metalabel/dfos-web-relay/node';
|
|
33
|
+
|
|
34
|
+
serve({ port: 4444 });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Routes
|
|
38
|
+
|
|
39
|
+
| Method | Path | Description |
|
|
40
|
+
| ------ | ---------------------------------------- | ---------------------------------------------------------------- |
|
|
41
|
+
| `GET` | `/.well-known/dfos-relay` | Relay metadata (DID, protocol version) |
|
|
42
|
+
| `POST` | `/operations` | Submit signed operations (identity, content, beacon, countersig) |
|
|
43
|
+
| `GET` | `/identities/:did` | Get identity chain state and operation log |
|
|
44
|
+
| `GET` | `/content/:contentId` | Get content chain state and operation log |
|
|
45
|
+
| `GET` | `/operations/:cid` | Get a single operation by CID |
|
|
46
|
+
| `GET` | `/beacons/:did` | Get beacon for an identity |
|
|
47
|
+
| `GET` | `/countersignatures/:cid` | Get countersignatures for an operation |
|
|
48
|
+
| `GET` | `/operations/:cid/countersignatures` | Same as above (alias) |
|
|
49
|
+
| `PUT` | `/content/:contentId/blob/:operationCID` | Upload blob (auth required) |
|
|
50
|
+
| `GET` | `/content/:contentId/blob` | Download blob at head (auth + credential) |
|
|
51
|
+
| `GET` | `/content/:contentId/blob/:ref` | Download blob at specific operation ref |
|
|
52
|
+
|
|
53
|
+
## Blob Authorization
|
|
54
|
+
|
|
55
|
+
**Upload**: Auth token required. Caller must be the chain creator or the signer of the referenced operation (enables delegated upload).
|
|
56
|
+
|
|
57
|
+
**Download**: Auth token required. Chain creator can download directly. Other identities must present a `DFOSContentRead` VC-JWT credential (issued by the creator) in the `X-Credential` header.
|
|
58
|
+
|
|
59
|
+
## Conformance Test Suite
|
|
60
|
+
|
|
61
|
+
The `conformance/` directory contains a Go integration test suite that exercises the full relay HTTP surface. It runs against any live relay via the `RELAY_URL` environment variable.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Run against a local relay
|
|
65
|
+
RELAY_URL=http://localhost:4444 go test -v -count=1 ./conformance/
|
|
66
|
+
|
|
67
|
+
# Run against a remote relay
|
|
68
|
+
RELAY_URL=https://registry.imajin.ai/relay go test -v -count=1 ./conformance/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
71 tests covering:
|
|
72
|
+
|
|
73
|
+
- Well-known discovery
|
|
74
|
+
- Identity lifecycle (create, update, delete, batch, idempotency, controller key rotation)
|
|
75
|
+
- Content lifecycle (create, update, delete, fork rejection, post-delete rejection, notes, long chains)
|
|
76
|
+
- Content update after auth key rotation, multiple independent chains
|
|
77
|
+
- Operations by CID
|
|
78
|
+
- Beacons (create, replacement, not-found, unknown/deleted identity)
|
|
79
|
+
- Countersignatures (dedup, empty result, multi-witness, self-countersign, non-existent operation)
|
|
80
|
+
- Blob upload/download (CID verification, auth, credential-based access, multi-version, idempotent upload)
|
|
81
|
+
- Delegated content operations (write credentials, delegated blob upload, delegated delete)
|
|
82
|
+
- Credentials (expiry, scope mismatch, type enforcement, deleted issuer behavior)
|
|
83
|
+
- Signature verification (tampered signature, wrong signing key)
|
|
84
|
+
- Auth edge cases (wrong audience, expired token, rotated-out key)
|
|
85
|
+
- Batch processing (3-step dependency sort, content-identity sort, large batch, dedup, mixed valid/invalid, multi-chain)
|
|
86
|
+
- Input validation (malformed JSON, empty operations, invalid JWS)
|
|
87
|
+
|
|
88
|
+
The conformance suite depends on [`dfos-protocol-go`](../dfos-protocol-go) for protocol operations.
|
|
89
|
+
|
|
90
|
+
## Custom Store
|
|
91
|
+
|
|
92
|
+
Implement the `RelayStore` interface to use any persistence backend:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import type { RelayStore } from '@metalabel/dfos-web-relay';
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`MemoryRelayStore` is provided as a reference implementation and for testing.
|
|
99
|
+
|
|
27
100
|
## License
|
|
28
101
|
|
|
29
102
|
MIT
|
package/RELAY.md
CHANGED
|
@@ -84,7 +84,23 @@ Each operation is verified against the relay's stored state:
|
|
|
84
84
|
|
|
85
85
|
First-seen-wins. If a chain head has already advanced past the `previousOperationCID` referenced by an incoming operation, the new operation is rejected. The relay does not attempt to resolve forks — the first valid extension wins.
|
|
86
86
|
|
|
87
|
-
Duplicate submissions (same operation CID) are silently accepted (idempotent).
|
|
87
|
+
Duplicate submissions (same operation CID, same JWS token) are silently accepted (idempotent). Submissions with the same CID but a different JWS token are rejected — since Ed25519 is deterministic, a different token for the same payload means a different signing key, which is either a self-countersign attempt or an unauthorized re-sign.
|
|
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
|
+
|
|
91
|
+
### Deletion Semantics
|
|
92
|
+
|
|
93
|
+
Deletion means the identity stops being an active participant. Historical operations remain verifiable — keys persist in state for signature verification — but no new acts flow from a deleted identity.
|
|
94
|
+
|
|
95
|
+
Specifically:
|
|
96
|
+
|
|
97
|
+
- **Identity operations after deletion**: Rejected. A deleted identity chain is sealed.
|
|
98
|
+
- **Content operations after deletion**: Rejected. A deleted content chain is sealed.
|
|
99
|
+
- **Beacons from deleted identities**: Rejected. A deleted identity MUST NOT publish new beacons.
|
|
100
|
+
- **Credentials from deleted issuers**: Rejected. Identity deletion revokes all authority, including outstanding `DFOSContentRead` and `DFOSContentWrite` credentials issued by the deleted identity. Credentials that were valid at time of issuance cease to be honored once the issuer is deleted.
|
|
101
|
+
- **Countersignatures on existing operations**: Still accepted. Deletion of the original author does not prevent other identities from attesting to operations that already exist in the relay.
|
|
102
|
+
|
|
103
|
+
Self-countersignatures — where the witness DID matches the operation author DID — are rejected. A countersignature's semantic is "a distinct witness attests." Signing your own operation is redundant with the original signature.
|
|
88
104
|
|
|
89
105
|
### Result Ordering
|
|
90
106
|
|
|
@@ -105,16 +121,18 @@ The relay does not currently sign operations or participate in the protocol as a
|
|
|
105
121
|
|
|
106
122
|
## Content Plane Access
|
|
107
123
|
|
|
108
|
-
### Blob Upload (`PUT /content/:contentId/blob`)
|
|
124
|
+
### Blob Upload (`PUT /content/:contentId/blob/:operationCID`)
|
|
125
|
+
|
|
126
|
+
The upload path mirrors the download path — the operation CID identifies which operation's document is being uploaded.
|
|
109
127
|
|
|
110
128
|
Requirements:
|
|
111
129
|
|
|
112
130
|
- Valid auth token (Bearer header)
|
|
113
|
-
- The
|
|
114
|
-
- The
|
|
115
|
-
- The uploaded bytes must hash to the
|
|
131
|
+
- The operation CID must reference an operation in this content chain that has a `documentCID`
|
|
132
|
+
- The authenticated DID must be either the chain creator OR the signer of the referenced operation (enabling delegated uploads)
|
|
133
|
+
- The uploaded bytes must hash to the operation's `documentCID` (dag-cbor + sha-256 verification)
|
|
116
134
|
|
|
117
|
-
Blobs are stored by `(creatorDID, documentCID)` —
|
|
135
|
+
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
136
|
|
|
119
137
|
### Blob Download (`GET /content/:contentId/blob[/:ref]`)
|
|
120
138
|
|
|
@@ -196,7 +214,7 @@ The returned Hono app exposes:
|
|
|
196
214
|
| `GET` | `/identities/:did` | proof | none |
|
|
197
215
|
| `GET` | `/content/:contentId` | proof | none |
|
|
198
216
|
| `GET` | `/beacons/:did` | proof | none |
|
|
199
|
-
| `PUT` | `/content/:contentId/blob`
|
|
217
|
+
| `PUT` | `/content/:contentId/blob/:opCID` | content | auth token |
|
|
200
218
|
| `GET` | `/content/:contentId/blob[/:ref]` | content | auth token + credential |
|
|
201
219
|
|
|
202
220
|
---
|
package/dist/index.js
CHANGED
|
@@ -135,7 +135,16 @@ var ingestIdentityOp = async (jwsToken, store) => {
|
|
|
135
135
|
const encoded = await dagCborCanonicalEncode(payload);
|
|
136
136
|
const cid = encoded.cid.toString();
|
|
137
137
|
const existing = await store.getOperation(cid);
|
|
138
|
-
if (existing)
|
|
138
|
+
if (existing) {
|
|
139
|
+
if (existing.jwsToken !== jwsToken) {
|
|
140
|
+
return {
|
|
141
|
+
cid,
|
|
142
|
+
status: "rejected",
|
|
143
|
+
error: "operation already exists with a different signature"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { cid, status: "accepted", kind: "identity-op", chainId: existing.chainId };
|
|
147
|
+
}
|
|
139
148
|
const opType = payload["type"];
|
|
140
149
|
const isGenesis = opType === "create";
|
|
141
150
|
if (isGenesis) {
|
|
@@ -165,7 +174,23 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
165
174
|
const encoded = await dagCborCanonicalEncode(payload);
|
|
166
175
|
const cid = encoded.cid.toString();
|
|
167
176
|
const existing = await store.getOperation(cid);
|
|
168
|
-
if (existing)
|
|
177
|
+
if (existing) {
|
|
178
|
+
if (existing.jwsToken !== jwsToken) {
|
|
179
|
+
return {
|
|
180
|
+
cid,
|
|
181
|
+
status: "rejected",
|
|
182
|
+
error: "operation already exists with a different signature"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { cid, status: "accepted", kind: "content-op", chainId: existing.chainId };
|
|
186
|
+
}
|
|
187
|
+
const signerDID = payload["did"];
|
|
188
|
+
if (typeof signerDID === "string") {
|
|
189
|
+
const signerIdentity = await store.getIdentityChain(signerDID);
|
|
190
|
+
if (signerIdentity?.state.isDeleted) {
|
|
191
|
+
return { cid, status: "rejected", error: "signer identity is deleted" };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
169
194
|
const resolveKey = createKeyResolver(store);
|
|
170
195
|
const opType = payload["type"];
|
|
171
196
|
const isGenesis = opType === "create";
|
|
@@ -198,6 +223,10 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
198
223
|
const chain = await store.getContentChain(prevOp.chainId);
|
|
199
224
|
if (!chain)
|
|
200
225
|
return { cid, status: "rejected", error: `content chain not found: ${prevOp.chainId}` };
|
|
226
|
+
const creatorIdentity = await store.getIdentityChain(chain.state.creatorDID);
|
|
227
|
+
if (creatorIdentity?.state.isDeleted) {
|
|
228
|
+
return { cid, status: "rejected", error: "content creator identity is deleted" };
|
|
229
|
+
}
|
|
201
230
|
if (chain.state.headCID !== previousCID) {
|
|
202
231
|
return { cid, status: "rejected", error: "chain has diverged (first-seen-wins)" };
|
|
203
232
|
}
|
|
@@ -228,6 +257,10 @@ var ingestBeacon = async (jwsToken, store) => {
|
|
|
228
257
|
}
|
|
229
258
|
const did = verified.payload.did;
|
|
230
259
|
const cid = verified.beaconCID;
|
|
260
|
+
const identity = await store.getIdentityChain(did);
|
|
261
|
+
if (identity?.state.isDeleted) {
|
|
262
|
+
return { cid, status: "rejected", error: "identity is deleted" };
|
|
263
|
+
}
|
|
231
264
|
const existing = await store.getBeacon(did);
|
|
232
265
|
if (existing) {
|
|
233
266
|
const existingTime = new Date(existing.state.payload.createdAt).getTime();
|
|
@@ -512,32 +545,29 @@ var createRelay = (options) => {
|
|
|
512
545
|
payload: beacon.state.payload
|
|
513
546
|
});
|
|
514
547
|
});
|
|
515
|
-
app.put("/content/:contentId/blob", async (c) => {
|
|
548
|
+
app.put("/content/:contentId/blob/:operationCID", async (c) => {
|
|
516
549
|
const contentId = c.req.param("contentId");
|
|
517
|
-
const
|
|
518
|
-
if (!documentCID) {
|
|
519
|
-
return c.json({ error: "missing X-Document-CID header" }, 400);
|
|
520
|
-
}
|
|
550
|
+
const operationCID = c.req.param("operationCID");
|
|
521
551
|
const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
|
|
522
552
|
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
523
553
|
const chain = await store.getContentChain(contentId);
|
|
524
554
|
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) {
|
|
555
|
+
let documentCID = null;
|
|
556
|
+
let operationSignerDID = null;
|
|
557
|
+
for (const token of chain.log) {
|
|
531
558
|
const decoded = decodeJwsUnsafe3(token);
|
|
532
559
|
if (!decoded) continue;
|
|
560
|
+
if (decoded.header.cid !== operationCID) continue;
|
|
533
561
|
const payload = decoded.payload;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
562
|
+
documentCID = typeof payload["documentCID"] === "string" ? payload["documentCID"] : null;
|
|
563
|
+
operationSignerDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
if (!documentCID) {
|
|
567
|
+
return c.json({ error: "operation not found in chain or has no documentCID" }, 404);
|
|
538
568
|
}
|
|
539
|
-
if (
|
|
540
|
-
return c.json({ error: "
|
|
569
|
+
if (auth.iss !== chain.state.creatorDID && auth.iss !== operationSignerDID) {
|
|
570
|
+
return c.json({ error: "not authorized \u2014 must be chain creator or operation signer" }, 403);
|
|
541
571
|
}
|
|
542
572
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
543
573
|
try {
|
|
@@ -549,8 +579,8 @@ var createRelay = (options) => {
|
|
|
549
579
|
} catch {
|
|
550
580
|
return c.json({ error: "blob bytes do not match documentCID" }, 400);
|
|
551
581
|
}
|
|
552
|
-
await store.putBlob({ creatorDID:
|
|
553
|
-
return c.json({ status: "stored", contentId, documentCID });
|
|
582
|
+
await store.putBlob({ creatorDID: chain.state.creatorDID, documentCID }, bytes);
|
|
583
|
+
return c.json({ status: "stored", contentId, documentCID, operationCID });
|
|
554
584
|
});
|
|
555
585
|
app.get("/content/:contentId/blob", async (c) => {
|
|
556
586
|
return await readBlob({
|
|
@@ -630,6 +660,10 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
630
660
|
if (vcIssuerDID !== chain.state.creatorDID) {
|
|
631
661
|
throw new Error("credential must be issued by the chain creator");
|
|
632
662
|
}
|
|
663
|
+
const issuerIdentity = await store.getIdentityChain(vcIssuerDID);
|
|
664
|
+
if (issuerIdentity?.state.isDeleted) {
|
|
665
|
+
throw new Error("credential issuer identity is deleted");
|
|
666
|
+
}
|
|
633
667
|
const creatorKey = await resolveKey(vcHeader.kid);
|
|
634
668
|
const credential = verifyCredential({
|
|
635
669
|
token: credHeader,
|
|
@@ -651,6 +685,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
651
685
|
};
|
|
652
686
|
|
|
653
687
|
// src/store.ts
|
|
688
|
+
import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
|
|
654
689
|
var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
|
|
655
690
|
var MemoryRelayStore = class {
|
|
656
691
|
operations = /* @__PURE__ */ new Map();
|
|
@@ -694,7 +729,18 @@ var MemoryRelayStore = class {
|
|
|
694
729
|
}
|
|
695
730
|
async addCountersignature(operationCID, jwsToken) {
|
|
696
731
|
const existing = this.countersignatures.get(operationCID) ?? [];
|
|
697
|
-
|
|
732
|
+
const decoded = decodeJwsUnsafe4(jwsToken);
|
|
733
|
+
if (decoded) {
|
|
734
|
+
const kid = decoded.header.kid;
|
|
735
|
+
const witnessDID = kid.includes("#") ? kid.split("#")[0] : kid;
|
|
736
|
+
for (const cs of existing) {
|
|
737
|
+
const d = decodeJwsUnsafe4(cs);
|
|
738
|
+
if (!d) continue;
|
|
739
|
+
const existingKid = d.header.kid;
|
|
740
|
+
const existingDID = existingKid.includes("#") ? existingKid.split("#")[0] : existingKid;
|
|
741
|
+
if (existingDID === witnessDID) return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
698
744
|
existing.push(jwsToken);
|
|
699
745
|
this.countersignatures.set(operationCID, existing);
|
|
700
746
|
}
|
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.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, beacons, and content blobs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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",
|