@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 +12 -12
- package/dist/index.d.ts +10 -0
- package/dist/index.js +47 -14
- package/openapi.yaml +19 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -38,18 +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` | `/countersignatures/:cid`
|
|
49
|
-
| `GET` | `/operations/:cid/countersignatures`
|
|
50
|
-
| `PUT` | `/content/:contentId/blob/:operationCID`
|
|
51
|
-
| `GET` | `/content/:contentId/blob`
|
|
52
|
-
| `GET` | `/content/:contentId/blob/: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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|