@metalabel/dfos-protocol 0.7.1 → 0.8.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 +8 -21
- package/dist/chain/index.d.ts +54 -148
- package/dist/chain/index.js +15 -8
- package/dist/{chunk-QKHP7UVL.js → chunk-LQ56P4SU.js} +137 -110
- package/dist/chunk-MEV6QVLC.js +402 -0
- package/dist/credentials/index.d.ts +133 -117
- package/dist/credentials/index.js +17 -21
- package/dist/index.d.ts +3 -2
- package/dist/index.js +30 -28
- package/dist/schemas-BEl38wrI.d.ts +148 -0
- package/examples/beacon.json +5 -5
- package/examples/content-delegated.json +3 -3
- package/examples/credential-read.json +4 -5
- package/examples/credential-write.json +5 -6
- package/package.json +2 -2
- package/dist/chunk-CZSEEZLL.js +0 -258
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createJws,
|
|
3
|
+
createJwt,
|
|
4
|
+
dagCborCanonicalEncode,
|
|
5
|
+
decodeJwsUnsafe,
|
|
6
|
+
verifyJws,
|
|
7
|
+
verifyJwt
|
|
8
|
+
} from "./chunk-ZXXP5W5N.js";
|
|
9
|
+
|
|
10
|
+
// src/credentials/schemas.ts
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
var MAX_DID = 256;
|
|
13
|
+
var MAX_AUD = 512;
|
|
14
|
+
var MAX_RESOURCE = 512;
|
|
15
|
+
var MAX_ACTION = 64;
|
|
16
|
+
var MAX_ATT = 32;
|
|
17
|
+
var MAX_PRF = 8;
|
|
18
|
+
var Attenuation = z.strictObject({
|
|
19
|
+
resource: z.string().min(1).max(MAX_RESOURCE),
|
|
20
|
+
action: z.string().min(1).max(MAX_ACTION)
|
|
21
|
+
});
|
|
22
|
+
var DFOSCredentialPayload = z.strictObject({
|
|
23
|
+
version: z.literal(1),
|
|
24
|
+
type: z.literal("DFOSCredential"),
|
|
25
|
+
/** Issuer DID */
|
|
26
|
+
iss: z.string().min(1).max(MAX_DID),
|
|
27
|
+
/** Audience DID or "*" for public credentials */
|
|
28
|
+
aud: z.string().min(1).max(MAX_AUD),
|
|
29
|
+
/** Attenuations — resource + action pairs */
|
|
30
|
+
att: z.array(Attenuation).min(1).max(MAX_ATT),
|
|
31
|
+
/** Parent credential JWS tokens (for delegation chains) */
|
|
32
|
+
prf: z.array(z.string()).max(MAX_PRF).default([]),
|
|
33
|
+
/** Expiration — unix seconds */
|
|
34
|
+
exp: z.number().int().positive(),
|
|
35
|
+
/** Issued at — unix seconds */
|
|
36
|
+
iat: z.number().int().positive()
|
|
37
|
+
});
|
|
38
|
+
var AuthTokenClaims = z.strictObject({
|
|
39
|
+
/** Issuer — the DID proving identity */
|
|
40
|
+
iss: z.string().max(MAX_DID),
|
|
41
|
+
/** Subject — same as iss for auth tokens */
|
|
42
|
+
sub: z.string().max(MAX_DID),
|
|
43
|
+
/** Audience — target relay hostname (prevents cross-relay replay) */
|
|
44
|
+
aud: z.string().max(MAX_AUD),
|
|
45
|
+
/** Expiration — unix seconds, short-lived (minutes) */
|
|
46
|
+
exp: z.number().int().positive(),
|
|
47
|
+
/** Issued at — unix seconds */
|
|
48
|
+
iat: z.number().int().positive()
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// src/credentials/auth-token.ts
|
|
52
|
+
var createAuthToken = async (options) => {
|
|
53
|
+
if (!options.kid.includes("#")) {
|
|
54
|
+
throw new Error("kid must be a DID URL (did:dfos:xxx#key_yyy)");
|
|
55
|
+
}
|
|
56
|
+
const kidDid = options.kid.substring(0, options.kid.indexOf("#"));
|
|
57
|
+
if (kidDid !== options.iss) {
|
|
58
|
+
throw new Error("kid DID does not match iss");
|
|
59
|
+
}
|
|
60
|
+
const now = options.iat ?? Math.floor(Date.now() / 1e3);
|
|
61
|
+
const header = { alg: "EdDSA", typ: "JWT", kid: options.kid };
|
|
62
|
+
const payload = {
|
|
63
|
+
iss: options.iss,
|
|
64
|
+
sub: options.iss,
|
|
65
|
+
aud: options.aud,
|
|
66
|
+
exp: options.exp,
|
|
67
|
+
iat: now
|
|
68
|
+
};
|
|
69
|
+
return createJwt({ header, payload, sign: options.sign });
|
|
70
|
+
};
|
|
71
|
+
var verifyAuthToken = (options) => {
|
|
72
|
+
const { header, payload } = verifyJwt({
|
|
73
|
+
token: options.token,
|
|
74
|
+
publicKey: options.publicKey,
|
|
75
|
+
audience: options.audience,
|
|
76
|
+
...options.currentTime !== void 0 ? { currentTime: options.currentTime } : {}
|
|
77
|
+
});
|
|
78
|
+
const result = AuthTokenClaims.safeParse(payload);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
81
|
+
throw new AuthTokenVerificationError(`invalid auth token claims: ${messages}`);
|
|
82
|
+
}
|
|
83
|
+
const currentTime = options.currentTime ?? Math.floor(Date.now() / 1e3);
|
|
84
|
+
if (result.data.iat > currentTime) {
|
|
85
|
+
throw new AuthTokenVerificationError("auth token not yet valid (iat is in the future)");
|
|
86
|
+
}
|
|
87
|
+
const kid = header.kid;
|
|
88
|
+
if (!kid || !kid.includes("#")) {
|
|
89
|
+
throw new AuthTokenVerificationError("auth token kid must be a DID URL");
|
|
90
|
+
}
|
|
91
|
+
const kidDid = kid.substring(0, kid.indexOf("#"));
|
|
92
|
+
if (kidDid !== result.data.iss) {
|
|
93
|
+
throw new AuthTokenVerificationError("auth token kid DID does not match iss");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
iss: result.data.iss,
|
|
97
|
+
aud: result.data.aud,
|
|
98
|
+
exp: result.data.exp,
|
|
99
|
+
kid
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
var AuthTokenVerificationError = class extends Error {
|
|
103
|
+
constructor(message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "AuthTokenVerificationError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/chain/multikey.ts
|
|
110
|
+
import { base58btc } from "multiformats/bases/base58";
|
|
111
|
+
var ED25519_PUB_PREFIX = new Uint8Array([237, 1]);
|
|
112
|
+
var ED25519_PRIV_PREFIX = new Uint8Array([128, 38]);
|
|
113
|
+
var ED25519_PUB_MULTICODEC = 237;
|
|
114
|
+
var ED25519_PRIV_MULTICODEC = 4864;
|
|
115
|
+
var encodeEd25519Multikey = (publicKeyBytes) => {
|
|
116
|
+
if (publicKeyBytes.length !== 32) {
|
|
117
|
+
throw new Error(`expected 32-byte Ed25519 public key, got ${publicKeyBytes.length}`);
|
|
118
|
+
}
|
|
119
|
+
const prefixed = new Uint8Array(ED25519_PUB_PREFIX.length + publicKeyBytes.length);
|
|
120
|
+
prefixed.set(ED25519_PUB_PREFIX);
|
|
121
|
+
prefixed.set(publicKeyBytes, ED25519_PUB_PREFIX.length);
|
|
122
|
+
return base58btc.encode(prefixed);
|
|
123
|
+
};
|
|
124
|
+
var decodeMultikey = (multibase) => {
|
|
125
|
+
const bytes = base58btc.decode(multibase);
|
|
126
|
+
if (bytes.length < 2) {
|
|
127
|
+
throw new Error("multikey too short");
|
|
128
|
+
}
|
|
129
|
+
if (bytes[0] === ED25519_PUB_PREFIX[0] && bytes[1] === ED25519_PUB_PREFIX[1]) {
|
|
130
|
+
const keyBytes = bytes.slice(2);
|
|
131
|
+
if (keyBytes.length !== 32) {
|
|
132
|
+
throw new Error(`expected 32-byte Ed25519 public key, got ${keyBytes.length}`);
|
|
133
|
+
}
|
|
134
|
+
return { keyBytes, codec: ED25519_PUB_MULTICODEC };
|
|
135
|
+
}
|
|
136
|
+
if (bytes[0] === ED25519_PRIV_PREFIX[0] && bytes[1] === ED25519_PRIV_PREFIX[1]) {
|
|
137
|
+
const keyBytes = bytes.slice(2);
|
|
138
|
+
if (keyBytes.length !== 32) {
|
|
139
|
+
throw new Error(`expected 32-byte Ed25519 private key, got ${keyBytes.length}`);
|
|
140
|
+
}
|
|
141
|
+
return { keyBytes, codec: ED25519_PRIV_MULTICODEC };
|
|
142
|
+
}
|
|
143
|
+
throw new Error(
|
|
144
|
+
`unsupported multikey codec: [0x${bytes[0]?.toString(16)}, 0x${bytes[1]?.toString(16)}]`
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/credentials/dfos-credential.ts
|
|
149
|
+
var resolveKeyFromIdentity = (identity, kid) => {
|
|
150
|
+
const hashIdx = kid.indexOf("#");
|
|
151
|
+
if (hashIdx < 0) throw new CredentialVerificationError("kid must be a DID URL");
|
|
152
|
+
const keyId = kid.substring(hashIdx + 1);
|
|
153
|
+
const allKeys = [...identity.authKeys, ...identity.assertKeys, ...identity.controllerKeys];
|
|
154
|
+
const key = allKeys.find((k) => k.id === keyId);
|
|
155
|
+
if (!key) {
|
|
156
|
+
throw new CredentialVerificationError(`key ${keyId} not found on identity ${identity.did}`);
|
|
157
|
+
}
|
|
158
|
+
const { keyBytes } = decodeMultikey(key.publicKeyMultibase);
|
|
159
|
+
return keyBytes;
|
|
160
|
+
};
|
|
161
|
+
var createDFOSCredential = async (options) => {
|
|
162
|
+
const kid = `${options.issuerDID}#${options.keyId}`;
|
|
163
|
+
const now = options.iat ?? Math.floor(Date.now() / 1e3);
|
|
164
|
+
const payload = {
|
|
165
|
+
version: 1,
|
|
166
|
+
type: "DFOSCredential",
|
|
167
|
+
iss: options.issuerDID,
|
|
168
|
+
aud: options.audienceDID,
|
|
169
|
+
att: options.att,
|
|
170
|
+
prf: options.prf ?? [],
|
|
171
|
+
exp: options.exp,
|
|
172
|
+
iat: now
|
|
173
|
+
};
|
|
174
|
+
const parseResult = DFOSCredentialPayload.safeParse(payload);
|
|
175
|
+
if (!parseResult.success) {
|
|
176
|
+
const messages = parseResult.error.issues.map((e) => e.message).join(", ");
|
|
177
|
+
throw new Error(`invalid credential payload: ${messages}`);
|
|
178
|
+
}
|
|
179
|
+
const encoded = await dagCborCanonicalEncode(payload);
|
|
180
|
+
const credentialCID = encoded.cid.toString();
|
|
181
|
+
const jwsToken = await createJws({
|
|
182
|
+
header: { alg: "EdDSA", typ: "did:dfos:credential", kid, cid: credentialCID },
|
|
183
|
+
payload,
|
|
184
|
+
sign: options.signer
|
|
185
|
+
});
|
|
186
|
+
return jwsToken;
|
|
187
|
+
};
|
|
188
|
+
var verifyDFOSCredential = async (jwsToken, options) => {
|
|
189
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
190
|
+
if (!decoded) throw new CredentialVerificationError("failed to decode credential JWS");
|
|
191
|
+
if (decoded.header.typ !== "did:dfos:credential") {
|
|
192
|
+
throw new CredentialVerificationError(`invalid typ: ${decoded.header.typ}`);
|
|
193
|
+
}
|
|
194
|
+
const result = DFOSCredentialPayload.safeParse(decoded.payload);
|
|
195
|
+
if (!result.success) {
|
|
196
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
197
|
+
throw new CredentialVerificationError(`invalid credential payload: ${messages}`);
|
|
198
|
+
}
|
|
199
|
+
const payload = result.data;
|
|
200
|
+
const kid = decoded.header.kid;
|
|
201
|
+
const hashIdx = kid.indexOf("#");
|
|
202
|
+
if (hashIdx < 0) throw new CredentialVerificationError("credential kid must be a DID URL");
|
|
203
|
+
const kidDid = kid.substring(0, hashIdx);
|
|
204
|
+
if (kidDid !== payload.iss) {
|
|
205
|
+
throw new CredentialVerificationError("credential kid DID does not match iss");
|
|
206
|
+
}
|
|
207
|
+
const identity = await options.resolveIdentity(payload.iss);
|
|
208
|
+
if (!identity) {
|
|
209
|
+
throw new CredentialVerificationError(`issuer identity not found: ${payload.iss}`);
|
|
210
|
+
}
|
|
211
|
+
if (identity.isDeleted) {
|
|
212
|
+
throw new CredentialVerificationError(`issuer identity is deleted: ${payload.iss}`);
|
|
213
|
+
}
|
|
214
|
+
const publicKey = resolveKeyFromIdentity(identity, kid);
|
|
215
|
+
try {
|
|
216
|
+
verifyJws({ token: jwsToken, publicKey });
|
|
217
|
+
} catch {
|
|
218
|
+
throw new CredentialVerificationError("invalid credential signature");
|
|
219
|
+
}
|
|
220
|
+
const encoded = await dagCborCanonicalEncode(payload);
|
|
221
|
+
const credentialCID = encoded.cid.toString();
|
|
222
|
+
if (!decoded.header.cid) {
|
|
223
|
+
throw new CredentialVerificationError("missing cid in credential header");
|
|
224
|
+
}
|
|
225
|
+
if (decoded.header.cid !== credentialCID) {
|
|
226
|
+
throw new CredentialVerificationError("credential cid mismatch");
|
|
227
|
+
}
|
|
228
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
229
|
+
if (payload.iat > now) {
|
|
230
|
+
throw new CredentialVerificationError("credential not yet valid (iat is in the future)");
|
|
231
|
+
}
|
|
232
|
+
if (payload.exp <= now) {
|
|
233
|
+
throw new CredentialVerificationError("credential expired");
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
iss: payload.iss,
|
|
237
|
+
aud: payload.aud,
|
|
238
|
+
att: payload.att,
|
|
239
|
+
prf: payload.prf,
|
|
240
|
+
exp: payload.exp,
|
|
241
|
+
iat: payload.iat,
|
|
242
|
+
credentialCID,
|
|
243
|
+
signerKeyId: kid
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
var verifyDelegationChain = async (credential, options) => {
|
|
247
|
+
const chain = [credential];
|
|
248
|
+
let current = credential;
|
|
249
|
+
const maxDepth = 16;
|
|
250
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
251
|
+
if (current.prf.length === 0) {
|
|
252
|
+
if (current.iss !== options.rootDID) {
|
|
253
|
+
throw new CredentialVerificationError(
|
|
254
|
+
`delegation chain root issuer ${current.iss} does not match expected root ${options.rootDID}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return { credential, chain, rootDID: options.rootDID };
|
|
258
|
+
}
|
|
259
|
+
const parents = [];
|
|
260
|
+
for (const parentJws of current.prf) {
|
|
261
|
+
const parent = await verifyDFOSCredential(parentJws, {
|
|
262
|
+
resolveIdentity: options.resolveIdentity,
|
|
263
|
+
...options.now !== void 0 ? { now: options.now } : {}
|
|
264
|
+
});
|
|
265
|
+
if (options.isRevoked) {
|
|
266
|
+
const revoked = await options.isRevoked(parent.iss, parent.credentialCID);
|
|
267
|
+
if (revoked) {
|
|
268
|
+
throw new CredentialVerificationError("parent credential in delegation chain is revoked");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
parents.push(parent);
|
|
272
|
+
}
|
|
273
|
+
const matchingParent = parents.find((p) => p.aud === "*" || p.aud === current.iss);
|
|
274
|
+
if (!matchingParent) {
|
|
275
|
+
throw new CredentialVerificationError(
|
|
276
|
+
`delegation gap: no parent credential has audience matching child issuer ${current.iss}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
for (const parent of parents) {
|
|
280
|
+
if (current.exp > parent.exp) {
|
|
281
|
+
throw new CredentialVerificationError(
|
|
282
|
+
"delegation chain: child credential expiry exceeds parent expiry"
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const parentAttUnion = parents.flatMap((p) => p.att);
|
|
287
|
+
if (!isAttenuated(parentAttUnion, current.att)) {
|
|
288
|
+
throw new CredentialVerificationError(
|
|
289
|
+
"delegation chain: child credential scope exceeds parent scope"
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
chain.push(...parents);
|
|
293
|
+
current = parents[0];
|
|
294
|
+
}
|
|
295
|
+
throw new CredentialVerificationError("delegation chain too deep (max 16 hops)");
|
|
296
|
+
};
|
|
297
|
+
var parseResource = (resource) => {
|
|
298
|
+
const colonIdx = resource.indexOf(":");
|
|
299
|
+
if (colonIdx < 0) return null;
|
|
300
|
+
return { type: resource.substring(0, colonIdx), id: resource.substring(colonIdx + 1) };
|
|
301
|
+
};
|
|
302
|
+
var parseActions = (action) => {
|
|
303
|
+
return new Set(action.split(",").map((a) => a.trim()));
|
|
304
|
+
};
|
|
305
|
+
var isAttenuated = (parentAtt, childAtt) => {
|
|
306
|
+
return childAtt.every((childEntry) => {
|
|
307
|
+
const childRes = parseResource(childEntry.resource);
|
|
308
|
+
if (!childRes) return false;
|
|
309
|
+
const childActions = parseActions(childEntry.action);
|
|
310
|
+
return parentAtt.some((parentEntry) => {
|
|
311
|
+
const parentRes = parseResource(parentEntry.resource);
|
|
312
|
+
if (!parentRes) return false;
|
|
313
|
+
const parentActions = parseActions(parentEntry.action);
|
|
314
|
+
for (const a of childActions) {
|
|
315
|
+
if (!parentActions.has(a)) return false;
|
|
316
|
+
}
|
|
317
|
+
if (parentRes.type === "chain" && parentRes.id === "*") {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (childRes.type === "chain" && childRes.id === "*") {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (childRes.type === "chain" && parentRes.type === "chain") {
|
|
324
|
+
return childRes.id === parentRes.id;
|
|
325
|
+
}
|
|
326
|
+
if (childRes.type === "chain" && parentRes.type === "manifest") {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (childRes.type === "manifest" && parentRes.type === "manifest") {
|
|
330
|
+
return childRes.id === parentRes.id;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
var matchesResource = async (att, resource, action, options) => {
|
|
337
|
+
const requestedRes = parseResource(resource);
|
|
338
|
+
if (!requestedRes) return false;
|
|
339
|
+
const requestedActions = parseActions(action);
|
|
340
|
+
for (const entry of att) {
|
|
341
|
+
const entryRes = parseResource(entry.resource);
|
|
342
|
+
if (!entryRes) continue;
|
|
343
|
+
const entryActions = parseActions(entry.action);
|
|
344
|
+
let actionsCovered = true;
|
|
345
|
+
for (const a of requestedActions) {
|
|
346
|
+
if (!entryActions.has(a)) {
|
|
347
|
+
actionsCovered = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (!actionsCovered) continue;
|
|
352
|
+
if (entryRes.type === "chain" && entryRes.id === "*" && requestedRes.type === "chain") {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
if (entryRes.type === requestedRes.type && entryRes.id === requestedRes.id) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
if (entryRes.type === "manifest" && requestedRes.type === "chain") {
|
|
359
|
+
if (options?.manifestLookup) {
|
|
360
|
+
const indexed = await options.manifestLookup(entryRes.id);
|
|
361
|
+
if (indexed.includes(requestedRes.id)) return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
};
|
|
367
|
+
var decodeDFOSCredentialUnsafe = (jwsToken) => {
|
|
368
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
369
|
+
if (!decoded) return null;
|
|
370
|
+
const result = DFOSCredentialPayload.safeParse(decoded.payload);
|
|
371
|
+
if (!result.success) return null;
|
|
372
|
+
return {
|
|
373
|
+
header: decoded.header,
|
|
374
|
+
payload: result.data
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
var CredentialVerificationError = class extends Error {
|
|
378
|
+
constructor(message) {
|
|
379
|
+
super(message);
|
|
380
|
+
this.name = "CredentialVerificationError";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export {
|
|
385
|
+
ED25519_PUB_MULTICODEC,
|
|
386
|
+
ED25519_PRIV_MULTICODEC,
|
|
387
|
+
encodeEd25519Multikey,
|
|
388
|
+
decodeMultikey,
|
|
389
|
+
Attenuation,
|
|
390
|
+
DFOSCredentialPayload,
|
|
391
|
+
AuthTokenClaims,
|
|
392
|
+
createAuthToken,
|
|
393
|
+
verifyAuthToken,
|
|
394
|
+
AuthTokenVerificationError,
|
|
395
|
+
createDFOSCredential,
|
|
396
|
+
verifyDFOSCredential,
|
|
397
|
+
verifyDelegationChain,
|
|
398
|
+
isAttenuated,
|
|
399
|
+
matchesResource,
|
|
400
|
+
decodeDFOSCredentialUnsafe,
|
|
401
|
+
CredentialVerificationError
|
|
402
|
+
};
|