@metalabel/dfos-web-relay 0.8.1 → 0.9.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 +2 -2
- package/dist/index.js +49 -42
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,14 +49,14 @@ serve({ port: 4444 });
|
|
|
49
49
|
| `GET` | `/countersignatures/:cid` | Get countersignatures for an operation |
|
|
50
50
|
| `GET` | `/operations/:cid/countersignatures` | Same as above (alias) |
|
|
51
51
|
| `PUT` | `/content/:contentId/blob/:operationCID` | Upload blob (auth required) |
|
|
52
|
-
| `GET` | `/content/:contentId/blob` | Download blob at head (auth + credential)
|
|
52
|
+
| `GET` | `/content/:contentId/blob` | Download blob at head (standing auth, or auth + credential) |
|
|
53
53
|
| `GET` | `/content/:contentId/blob/:ref` | Download blob at specific operation ref |
|
|
54
54
|
|
|
55
55
|
## Blob Authorization
|
|
56
56
|
|
|
57
57
|
**Upload**: Auth token required. Caller must be the chain creator or the signer of the referenced operation (enables delegated upload).
|
|
58
58
|
|
|
59
|
-
**Download**:
|
|
59
|
+
**Download**: If a public credential (`aud: *`) exists as a standing authorization, the blob is served without authentication. Otherwise, auth token required — chain creator can download directly, other identities must present a DFOS read credential (issued by the creator) in the `X-Credential` header.
|
|
60
60
|
|
|
61
61
|
## Peering
|
|
62
62
|
|
package/dist/index.js
CHANGED
|
@@ -912,6 +912,33 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
|
|
|
912
912
|
return null;
|
|
913
913
|
}
|
|
914
914
|
};
|
|
915
|
+
var hasPublicStandingAuth = async (contentId, action, store) => {
|
|
916
|
+
const resource = `chain:${contentId}`;
|
|
917
|
+
const publicCreds = await store.getPublicCredentials(resource);
|
|
918
|
+
if (publicCreds.length === 0) return false;
|
|
919
|
+
const chain = await store.getContentChain(contentId);
|
|
920
|
+
if (!chain) return false;
|
|
921
|
+
const resolveIdentity = createHistoricalIdentityResolver(store);
|
|
922
|
+
const isRevoked = async (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
|
|
923
|
+
for (const credJws of publicCreds) {
|
|
924
|
+
try {
|
|
925
|
+
const cred = await verifyDFOSCredential2(credJws, { resolveIdentity });
|
|
926
|
+
const leafRevoked = await isRevoked(cred.iss, cred.credentialCID);
|
|
927
|
+
if (leafRevoked) continue;
|
|
928
|
+
const covers = await matchesResource(cred.att, resource, action);
|
|
929
|
+
if (!covers) continue;
|
|
930
|
+
await verifyDelegationChain(cred, {
|
|
931
|
+
resolveIdentity,
|
|
932
|
+
rootDID: chain.state.creatorDID,
|
|
933
|
+
isRevoked
|
|
934
|
+
});
|
|
935
|
+
return true;
|
|
936
|
+
} catch {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return false;
|
|
941
|
+
};
|
|
915
942
|
var verifyContentAccess = async (options) => {
|
|
916
943
|
const { credentialJWS, requestedResource, action, store, creatorDID, requesterDID } = options;
|
|
917
944
|
if (requesterDID && requesterDID === creatorDID) {
|
|
@@ -919,33 +946,13 @@ var verifyContentAccess = async (options) => {
|
|
|
919
946
|
}
|
|
920
947
|
const resolveIdentity = createHistoricalIdentityResolver(store);
|
|
921
948
|
const isRevoked = async (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
|
|
922
|
-
const manifestLookup = async (manifestContentId) => {
|
|
923
|
-
const chain = await store.getContentChain(manifestContentId);
|
|
924
|
-
if (!chain) return [];
|
|
925
|
-
const docCID = chain.state.currentDocumentCID;
|
|
926
|
-
if (!docCID) return [];
|
|
927
|
-
const blob = await store.getBlob({ creatorDID: chain.state.creatorDID, documentCID: docCID });
|
|
928
|
-
if (!blob) return [];
|
|
929
|
-
try {
|
|
930
|
-
const doc = JSON.parse(new TextDecoder().decode(blob));
|
|
931
|
-
const entries = doc["entries"];
|
|
932
|
-
if (!entries || typeof entries !== "object") return [];
|
|
933
|
-
return Object.values(entries).filter(
|
|
934
|
-
(v) => typeof v === "string" && !v.startsWith("did:") && !v.startsWith("bafyrei")
|
|
935
|
-
);
|
|
936
|
-
} catch {
|
|
937
|
-
return [];
|
|
938
|
-
}
|
|
939
|
-
};
|
|
940
949
|
const publicCreds = await store.getPublicCredentials(requestedResource);
|
|
941
950
|
for (const credJws of publicCreds) {
|
|
942
951
|
try {
|
|
943
952
|
const cred = await verifyDFOSCredential2(credJws, { resolveIdentity });
|
|
944
953
|
const leafRevoked = await isRevoked(cred.iss, cred.credentialCID);
|
|
945
954
|
if (leafRevoked) continue;
|
|
946
|
-
const covers = await matchesResource(cred.att, requestedResource, action
|
|
947
|
-
manifestLookup
|
|
948
|
-
});
|
|
955
|
+
const covers = await matchesResource(cred.att, requestedResource, action);
|
|
949
956
|
if (!covers) continue;
|
|
950
957
|
await verifyDelegationChain(cred, { resolveIdentity, rootDID: creatorDID, isRevoked });
|
|
951
958
|
return { granted: true, source: "public-credential", credential: cred };
|
|
@@ -966,9 +973,7 @@ var verifyContentAccess = async (options) => {
|
|
|
966
973
|
return { granted: false, source: "none" };
|
|
967
974
|
}
|
|
968
975
|
}
|
|
969
|
-
const covers = await matchesResource(cred.att, requestedResource, action
|
|
970
|
-
manifestLookup
|
|
971
|
-
});
|
|
976
|
+
const covers = await matchesResource(cred.att, requestedResource, action);
|
|
972
977
|
if (!covers) {
|
|
973
978
|
return { granted: false, source: "none" };
|
|
974
979
|
}
|
|
@@ -1188,18 +1193,21 @@ var createRelay = async (options) => {
|
|
|
1188
1193
|
app.get("/content/:contentId/documents", async (c) => {
|
|
1189
1194
|
if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
|
|
1190
1195
|
const contentId = c.req.param("contentId");
|
|
1191
|
-
const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
|
|
1192
|
-
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
1193
1196
|
const chain = await store.getContentChain(contentId);
|
|
1194
1197
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1198
|
+
const publicAccess = await hasPublicStandingAuth(contentId, "read", store);
|
|
1199
|
+
if (!publicAccess) {
|
|
1200
|
+
const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
|
|
1201
|
+
if (!auth) return c.json({ error: "authentication required" }, 401);
|
|
1202
|
+
const accessError = await verifyReadAccess(
|
|
1203
|
+
auth,
|
|
1204
|
+
chain,
|
|
1205
|
+
contentId,
|
|
1206
|
+
c.req.header("x-credential"),
|
|
1207
|
+
store
|
|
1208
|
+
);
|
|
1209
|
+
if (accessError) return accessError;
|
|
1210
|
+
}
|
|
1203
1211
|
const after = c.req.query("after");
|
|
1204
1212
|
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
1205
1213
|
const result = await store.getDocuments(contentId, {
|
|
@@ -1362,12 +1370,15 @@ var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
|
1362
1370
|
});
|
|
1363
1371
|
var readBlob = async (params) => {
|
|
1364
1372
|
const { contentId, ref, authHeader, credHeader, relayDID, store } = params;
|
|
1365
|
-
const auth = await authenticateRequest(authHeader, relayDID, store);
|
|
1366
|
-
if (!auth) return jsonResponse({ error: "authentication required" }, 401);
|
|
1367
1373
|
const chain = await store.getContentChain(contentId);
|
|
1368
1374
|
if (!chain) return jsonResponse({ error: "content chain not found" }, 404);
|
|
1369
|
-
const
|
|
1370
|
-
if (
|
|
1375
|
+
const publicAccess = await hasPublicStandingAuth(contentId, "read", store);
|
|
1376
|
+
if (!publicAccess) {
|
|
1377
|
+
const auth = await authenticateRequest(authHeader, relayDID, store);
|
|
1378
|
+
if (!auth) return jsonResponse({ error: "authentication required" }, 401);
|
|
1379
|
+
const credError = await verifyReadAccess(auth, chain, contentId, credHeader, store);
|
|
1380
|
+
if (credError) return credError;
|
|
1381
|
+
}
|
|
1371
1382
|
let documentCID = null;
|
|
1372
1383
|
let operationFound = ref === "head";
|
|
1373
1384
|
if (ref === "head") {
|
|
@@ -1504,10 +1515,6 @@ var MemoryRelayStore = class {
|
|
|
1504
1515
|
tokens.push(cred.jwsToken);
|
|
1505
1516
|
break;
|
|
1506
1517
|
}
|
|
1507
|
-
if (isChainRequest && att.resource.startsWith("manifest:")) {
|
|
1508
|
-
tokens.push(cred.jwsToken);
|
|
1509
|
-
break;
|
|
1510
|
-
}
|
|
1511
1518
|
}
|
|
1512
1519
|
}
|
|
1513
1520
|
return tokens;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalabel/dfos-web-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"tsx": "^4.20.3",
|
|
54
54
|
"vitest": "^4.1.2",
|
|
55
|
-
"@metalabel/dfos-protocol": "0.
|
|
55
|
+
"@metalabel/dfos-protocol": "0.9.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|