@metalabel/dfos-protocol 0.0.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/LICENSE +21 -0
- package/PROTOCOL.md +873 -0
- package/README.md +50 -0
- package/dist/chain/index.d.ts +196 -0
- package/dist/chain/index.js +33 -0
- package/dist/chunk-3PB644X2.js +330 -0
- package/dist/chunk-LWC4PWGZ.js +385 -0
- package/dist/chunk-ZXXP5W5N.js +251 -0
- package/dist/crypto/index.d.ts +205 -0
- package/dist/crypto/index.js +46 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +108 -0
- package/dist/registry/index.d.ts +151 -0
- package/dist/registry/index.js +36 -0
- package/examples/content-delete.json +28 -0
- package/examples/content-lifecycle.json +39 -0
- package/examples/identity-delete.json +19 -0
- package/examples/identity-genesis.json +18 -0
- package/examples/identity-rotation.json +19 -0
- package/openapi.yaml +408 -0
- package/package.json +75 -0
- package/schemas/document-envelope.v1.json +37 -0
- package/schemas/post.v1.json +59 -0
- package/schemas/profile.v1.json +52 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createJws,
|
|
3
|
+
dagCborCanonicalEncode,
|
|
4
|
+
decodeJwsUnsafe,
|
|
5
|
+
generateIdNoPrefix,
|
|
6
|
+
verifyJws
|
|
7
|
+
} from "./chunk-ZXXP5W5N.js";
|
|
8
|
+
|
|
9
|
+
// src/chain/schemas.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var MAX_KEY_ID = 64;
|
|
12
|
+
var MAX_PUBLIC_KEY_MULTIBASE = 128;
|
|
13
|
+
var MAX_CID = 256;
|
|
14
|
+
var MAX_NOTE = 256;
|
|
15
|
+
var MAX_KEYS_PER_ROLE = 16;
|
|
16
|
+
var MultikeyPublicKey = z.strictObject({
|
|
17
|
+
id: z.string().max(MAX_KEY_ID),
|
|
18
|
+
type: z.literal("Multikey"),
|
|
19
|
+
publicKeyMultibase: z.string().max(MAX_PUBLIC_KEY_MULTIBASE)
|
|
20
|
+
});
|
|
21
|
+
var Iso8601 = z.iso.datetime({ offset: false, precision: 3 });
|
|
22
|
+
var CIDString = z.string().max(MAX_CID);
|
|
23
|
+
var IdentityCreate = z.strictObject({
|
|
24
|
+
version: z.literal(1),
|
|
25
|
+
type: z.literal("create"),
|
|
26
|
+
authKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
27
|
+
assertKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
28
|
+
controllerKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
29
|
+
createdAt: Iso8601
|
|
30
|
+
});
|
|
31
|
+
var IdentityUpdate = z.strictObject({
|
|
32
|
+
version: z.literal(1),
|
|
33
|
+
type: z.literal("update"),
|
|
34
|
+
previousOperationCID: CIDString,
|
|
35
|
+
authKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
36
|
+
assertKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
37
|
+
controllerKeys: z.array(MultikeyPublicKey).min(1, "update must have at least one controller key").max(MAX_KEYS_PER_ROLE),
|
|
38
|
+
createdAt: Iso8601
|
|
39
|
+
});
|
|
40
|
+
var IdentityDelete = z.strictObject({
|
|
41
|
+
version: z.literal(1),
|
|
42
|
+
type: z.literal("delete"),
|
|
43
|
+
previousOperationCID: CIDString,
|
|
44
|
+
createdAt: Iso8601
|
|
45
|
+
});
|
|
46
|
+
var IdentityOperation = z.discriminatedUnion("type", [
|
|
47
|
+
IdentityCreate,
|
|
48
|
+
IdentityUpdate,
|
|
49
|
+
IdentityDelete
|
|
50
|
+
]);
|
|
51
|
+
var VerifiedIdentity = z.strictObject({
|
|
52
|
+
did: z.string(),
|
|
53
|
+
isDeleted: z.boolean(),
|
|
54
|
+
authKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
55
|
+
assertKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE),
|
|
56
|
+
controllerKeys: z.array(MultikeyPublicKey).max(MAX_KEYS_PER_ROLE)
|
|
57
|
+
});
|
|
58
|
+
var ContentCreate = z.strictObject({
|
|
59
|
+
version: z.literal(1),
|
|
60
|
+
type: z.literal("create"),
|
|
61
|
+
documentCID: CIDString,
|
|
62
|
+
createdAt: Iso8601,
|
|
63
|
+
note: z.string().max(MAX_NOTE).nullable()
|
|
64
|
+
});
|
|
65
|
+
var ContentUpdate = z.strictObject({
|
|
66
|
+
version: z.literal(1),
|
|
67
|
+
type: z.literal("update"),
|
|
68
|
+
previousOperationCID: CIDString,
|
|
69
|
+
documentCID: CIDString.nullable(),
|
|
70
|
+
createdAt: Iso8601,
|
|
71
|
+
note: z.string().max(MAX_NOTE).nullable()
|
|
72
|
+
});
|
|
73
|
+
var ContentDelete = z.strictObject({
|
|
74
|
+
version: z.literal(1),
|
|
75
|
+
type: z.literal("delete"),
|
|
76
|
+
previousOperationCID: CIDString,
|
|
77
|
+
createdAt: Iso8601,
|
|
78
|
+
note: z.string().max(MAX_NOTE).nullable()
|
|
79
|
+
});
|
|
80
|
+
var ContentOperation = z.discriminatedUnion("type", [
|
|
81
|
+
ContentCreate,
|
|
82
|
+
ContentUpdate,
|
|
83
|
+
ContentDelete
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// src/chain/multikey.ts
|
|
87
|
+
import { base58btc } from "multiformats/bases/base58";
|
|
88
|
+
var ED25519_PUB_PREFIX = new Uint8Array([237, 1]);
|
|
89
|
+
var ED25519_PRIV_PREFIX = new Uint8Array([128, 38]);
|
|
90
|
+
var ED25519_PUB_MULTICODEC = 237;
|
|
91
|
+
var ED25519_PRIV_MULTICODEC = 4864;
|
|
92
|
+
var encodeEd25519Multikey = (publicKeyBytes) => {
|
|
93
|
+
if (publicKeyBytes.length !== 32) {
|
|
94
|
+
throw new Error(`expected 32-byte Ed25519 public key, got ${publicKeyBytes.length}`);
|
|
95
|
+
}
|
|
96
|
+
const prefixed = new Uint8Array(ED25519_PUB_PREFIX.length + publicKeyBytes.length);
|
|
97
|
+
prefixed.set(ED25519_PUB_PREFIX);
|
|
98
|
+
prefixed.set(publicKeyBytes, ED25519_PUB_PREFIX.length);
|
|
99
|
+
return base58btc.encode(prefixed);
|
|
100
|
+
};
|
|
101
|
+
var decodeMultikey = (multibase) => {
|
|
102
|
+
const bytes = base58btc.decode(multibase);
|
|
103
|
+
if (bytes.length < 2) {
|
|
104
|
+
throw new Error("multikey too short");
|
|
105
|
+
}
|
|
106
|
+
if (bytes[0] === ED25519_PUB_PREFIX[0] && bytes[1] === ED25519_PUB_PREFIX[1]) {
|
|
107
|
+
const keyBytes = bytes.slice(2);
|
|
108
|
+
if (keyBytes.length !== 32) {
|
|
109
|
+
throw new Error(`expected 32-byte Ed25519 public key, got ${keyBytes.length}`);
|
|
110
|
+
}
|
|
111
|
+
return { keyBytes, codec: ED25519_PUB_MULTICODEC };
|
|
112
|
+
}
|
|
113
|
+
if (bytes[0] === ED25519_PRIV_PREFIX[0] && bytes[1] === ED25519_PRIV_PREFIX[1]) {
|
|
114
|
+
const keyBytes = bytes.slice(2);
|
|
115
|
+
if (keyBytes.length !== 32) {
|
|
116
|
+
throw new Error(`expected 32-byte Ed25519 private key, got ${keyBytes.length}`);
|
|
117
|
+
}
|
|
118
|
+
return { keyBytes, codec: ED25519_PRIV_MULTICODEC };
|
|
119
|
+
}
|
|
120
|
+
throw new Error(
|
|
121
|
+
`unsupported multikey codec: [0x${bytes[0]?.toString(16)}, 0x${bytes[1]?.toString(16)}]`
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/chain/derivation.ts
|
|
126
|
+
var deriveChainIdentifier = (cidBytes, prefix) => {
|
|
127
|
+
const id = generateIdNoPrefix({ seed: cidBytes });
|
|
128
|
+
return `${prefix}:${id}`;
|
|
129
|
+
};
|
|
130
|
+
var deriveEntityId = (cidBytes) => {
|
|
131
|
+
return generateIdNoPrefix({ seed: cidBytes });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/chain/identity-chain.ts
|
|
135
|
+
var signIdentityOperation = async (input) => {
|
|
136
|
+
const kid = input.identityDID ? `${input.identityDID}#${input.keyId}` : input.keyId;
|
|
137
|
+
const encoded = await dagCborCanonicalEncode(input.operation);
|
|
138
|
+
const operationCID = encoded.cid.toString();
|
|
139
|
+
const jwsToken = await createJws({
|
|
140
|
+
header: { alg: "EdDSA", typ: "did:dfos:identity-op", kid, cid: operationCID },
|
|
141
|
+
payload: input.operation,
|
|
142
|
+
sign: input.signer
|
|
143
|
+
});
|
|
144
|
+
return { jwsToken, operationCID };
|
|
145
|
+
};
|
|
146
|
+
var verifyIdentityChain = async (input) => {
|
|
147
|
+
if (input.log.length === 0) throw new Error("log must have at least one operation");
|
|
148
|
+
const state = {
|
|
149
|
+
did: void 0,
|
|
150
|
+
isDeleted: false,
|
|
151
|
+
previousOperationCID: null,
|
|
152
|
+
lastCreatedAt: null,
|
|
153
|
+
authKeys: [],
|
|
154
|
+
assertKeys: [],
|
|
155
|
+
controllerKeys: [],
|
|
156
|
+
seenKeys: /* @__PURE__ */ new Map()
|
|
157
|
+
};
|
|
158
|
+
for (const [idx, jwsToken] of input.log.entries()) {
|
|
159
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
160
|
+
if (!decoded) throw new Error(`log[${idx}]: failed to decode JWS`);
|
|
161
|
+
const result = IdentityOperation.safeParse(decoded.payload);
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
164
|
+
throw new Error(`log[${idx}]: ${messages}`);
|
|
165
|
+
}
|
|
166
|
+
const op = result.data;
|
|
167
|
+
if (state.isDeleted) throw new Error(`log[${idx}]: cannot modify a deleted identity`);
|
|
168
|
+
if (idx === 0 && op.type !== "create") {
|
|
169
|
+
throw new Error(`log[${idx}]: first operation must be create`);
|
|
170
|
+
}
|
|
171
|
+
if (idx > 0 && op.type === "create") {
|
|
172
|
+
throw new Error(`log[${idx}]: create can only be the first operation`);
|
|
173
|
+
}
|
|
174
|
+
if (op.type === "create") {
|
|
175
|
+
if (op.controllerKeys.length === 0) {
|
|
176
|
+
throw new Error(`log[${idx}]: create must have at least one controller key`);
|
|
177
|
+
}
|
|
178
|
+
state.authKeys = op.authKeys;
|
|
179
|
+
state.assertKeys = op.assertKeys;
|
|
180
|
+
state.controllerKeys = op.controllerKeys;
|
|
181
|
+
}
|
|
182
|
+
if (op.type === "update" || op.type === "delete") {
|
|
183
|
+
if (op.previousOperationCID !== state.previousOperationCID) {
|
|
184
|
+
throw new Error(`log[${idx}]: previousCID is incorrect`);
|
|
185
|
+
}
|
|
186
|
+
if (!state.lastCreatedAt) throw new Error(`log[${idx}]: lastCreatedAt is not set`);
|
|
187
|
+
if (op.createdAt <= state.lastCreatedAt) {
|
|
188
|
+
throw new Error(`log[${idx}]: createdAt must be after last op`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (op.type === "create" || op.type === "update") {
|
|
192
|
+
const incomingKeys = [...op.authKeys, ...op.assertKeys, ...op.controllerKeys];
|
|
193
|
+
const currentKeys = [...state.authKeys, ...state.assertKeys, ...state.controllerKeys];
|
|
194
|
+
for (const k of [...currentKeys, ...incomingKeys]) {
|
|
195
|
+
const existing = state.seenKeys.get(k.id);
|
|
196
|
+
if (!existing) {
|
|
197
|
+
state.seenKeys.set(k.id, k);
|
|
198
|
+
} else if (existing.publicKeyMultibase !== k.publicKeyMultibase || existing.type !== k.type) {
|
|
199
|
+
throw new Error(`log[${idx}]: key ${k.id} type or public key inconsistency`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
[op.authKeys, op.assertKeys, op.controllerKeys].forEach((keys) => {
|
|
203
|
+
const set = new Set(keys.map((k) => k.id));
|
|
204
|
+
if (set.size !== keys.length) {
|
|
205
|
+
throw new Error(`log[${idx}]: cannot repeat key ids in same usage`);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const encoded = await dagCborCanonicalEncode(op);
|
|
210
|
+
const operationCID = encoded.cid.toString();
|
|
211
|
+
if (!decoded.header.cid) {
|
|
212
|
+
throw new Error(`log[${idx}]: missing cid in protected header`);
|
|
213
|
+
}
|
|
214
|
+
if (decoded.header.cid !== operationCID) {
|
|
215
|
+
throw new Error(`log[${idx}]: cid mismatch in protected header`);
|
|
216
|
+
}
|
|
217
|
+
const kid = decoded.header.kid;
|
|
218
|
+
let signingKeyId;
|
|
219
|
+
if (kid.includes("#")) {
|
|
220
|
+
const hashIdx = kid.indexOf("#");
|
|
221
|
+
signingKeyId = kid.substring(hashIdx + 1);
|
|
222
|
+
if (idx === 0) {
|
|
223
|
+
throw new Error(`log[${idx}]: genesis op kid must be bare key ID, got DID URL`);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
signingKeyId = kid;
|
|
227
|
+
if (idx > 0) {
|
|
228
|
+
throw new Error(`log[${idx}]: non-genesis op kid must be DID URL, got bare key ID`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const signingKey = state.controllerKeys.find((k) => k.id === signingKeyId);
|
|
232
|
+
if (!signingKey) {
|
|
233
|
+
throw new Error(`log[${idx}]: kid references unknown key: ${signingKeyId}`);
|
|
234
|
+
}
|
|
235
|
+
const { keyBytes } = decodeMultikey(signingKey.publicKeyMultibase);
|
|
236
|
+
try {
|
|
237
|
+
verifyJws({ token: jwsToken, publicKey: keyBytes });
|
|
238
|
+
} catch {
|
|
239
|
+
throw new Error(`log[${idx}]: invalid signature`);
|
|
240
|
+
}
|
|
241
|
+
if (state.did === void 0) {
|
|
242
|
+
state.did = deriveChainIdentifier(encoded.cid.bytes, input.didPrefix);
|
|
243
|
+
}
|
|
244
|
+
if (idx > 0 && kid.includes("#")) {
|
|
245
|
+
const didFromKid = kid.substring(0, kid.indexOf("#"));
|
|
246
|
+
if (didFromKid !== state.did) {
|
|
247
|
+
throw new Error(`log[${idx}]: kid DID does not match identity DID`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
state.previousOperationCID = operationCID;
|
|
251
|
+
state.lastCreatedAt = op.createdAt;
|
|
252
|
+
switch (op.type) {
|
|
253
|
+
case "create":
|
|
254
|
+
break;
|
|
255
|
+
case "update":
|
|
256
|
+
if (op.controllerKeys.length === 0) {
|
|
257
|
+
throw new Error(`log[${idx}]: update must have at least one controller key`);
|
|
258
|
+
}
|
|
259
|
+
state.authKeys = op.authKeys;
|
|
260
|
+
state.assertKeys = op.assertKeys;
|
|
261
|
+
state.controllerKeys = op.controllerKeys;
|
|
262
|
+
break;
|
|
263
|
+
case "delete":
|
|
264
|
+
state.isDeleted = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!state.did) throw new Error("did is not set");
|
|
269
|
+
return {
|
|
270
|
+
did: state.did,
|
|
271
|
+
isDeleted: state.isDeleted,
|
|
272
|
+
authKeys: state.authKeys,
|
|
273
|
+
assertKeys: state.assertKeys,
|
|
274
|
+
controllerKeys: state.controllerKeys
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/chain/content-chain.ts
|
|
279
|
+
var signContentOperation = async (input) => {
|
|
280
|
+
const encoded = await dagCborCanonicalEncode(input.operation);
|
|
281
|
+
const operationCID = encoded.cid.toString();
|
|
282
|
+
const jwsToken = await createJws({
|
|
283
|
+
header: { alg: "EdDSA", typ: "did:dfos:content-op", kid: input.kid, cid: operationCID },
|
|
284
|
+
payload: input.operation,
|
|
285
|
+
sign: input.signer
|
|
286
|
+
});
|
|
287
|
+
return { jwsToken, operationCID };
|
|
288
|
+
};
|
|
289
|
+
var verifyContentChain = async (input) => {
|
|
290
|
+
if (input.log.length === 0) throw new Error("log must have at least one operation");
|
|
291
|
+
const state = {
|
|
292
|
+
entityId: null,
|
|
293
|
+
genesisCID: null,
|
|
294
|
+
headCID: null,
|
|
295
|
+
isDeleted: false,
|
|
296
|
+
currentDocumentCID: null,
|
|
297
|
+
previousCID: null,
|
|
298
|
+
lastCreatedAt: null
|
|
299
|
+
};
|
|
300
|
+
for (const [idx, jwsToken] of input.log.entries()) {
|
|
301
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
302
|
+
if (!decoded) throw new Error(`log[${idx}]: failed to decode JWS`);
|
|
303
|
+
const result = ContentOperation.safeParse(decoded.payload);
|
|
304
|
+
if (!result.success) {
|
|
305
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
306
|
+
throw new Error(`log[${idx}]: ${messages}`);
|
|
307
|
+
}
|
|
308
|
+
const op = result.data;
|
|
309
|
+
if (state.isDeleted) throw new Error(`log[${idx}]: cannot extend a deleted chain`);
|
|
310
|
+
if (idx === 0 && op.type !== "create") {
|
|
311
|
+
throw new Error(`log[${idx}]: first operation must be create`);
|
|
312
|
+
}
|
|
313
|
+
if (idx > 0 && op.type === "create") {
|
|
314
|
+
throw new Error(`log[${idx}]: create can only be the first operation`);
|
|
315
|
+
}
|
|
316
|
+
if (op.type === "update" || op.type === "delete") {
|
|
317
|
+
if (op.previousOperationCID !== state.previousCID) {
|
|
318
|
+
throw new Error(`log[${idx}]: previousOperationCID is incorrect`);
|
|
319
|
+
}
|
|
320
|
+
if (!state.lastCreatedAt) throw new Error(`log[${idx}]: lastCreatedAt is not set`);
|
|
321
|
+
if (op.createdAt <= state.lastCreatedAt) {
|
|
322
|
+
throw new Error(`log[${idx}]: createdAt must be after last op`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const kid = decoded.header.kid;
|
|
326
|
+
const publicKey = await input.resolveKey(kid);
|
|
327
|
+
try {
|
|
328
|
+
verifyJws({ token: jwsToken, publicKey });
|
|
329
|
+
} catch {
|
|
330
|
+
throw new Error(`log[${idx}]: invalid signature`);
|
|
331
|
+
}
|
|
332
|
+
const encoded = await dagCborCanonicalEncode(op);
|
|
333
|
+
const operationCID = encoded.cid.toString();
|
|
334
|
+
if (!decoded.header.cid) {
|
|
335
|
+
throw new Error(`log[${idx}]: missing cid in protected header`);
|
|
336
|
+
}
|
|
337
|
+
if (decoded.header.cid !== operationCID) {
|
|
338
|
+
throw new Error(`log[${idx}]: cid mismatch in protected header`);
|
|
339
|
+
}
|
|
340
|
+
if (idx === 0) {
|
|
341
|
+
state.genesisCID = operationCID;
|
|
342
|
+
state.entityId = deriveEntityId(encoded.cid.bytes);
|
|
343
|
+
}
|
|
344
|
+
state.headCID = operationCID;
|
|
345
|
+
state.previousCID = operationCID;
|
|
346
|
+
state.lastCreatedAt = op.createdAt;
|
|
347
|
+
switch (op.type) {
|
|
348
|
+
case "create":
|
|
349
|
+
state.currentDocumentCID = op.documentCID;
|
|
350
|
+
break;
|
|
351
|
+
case "update":
|
|
352
|
+
state.currentDocumentCID = op.documentCID;
|
|
353
|
+
break;
|
|
354
|
+
case "delete":
|
|
355
|
+
state.isDeleted = true;
|
|
356
|
+
state.currentDocumentCID = null;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
entityId: state.entityId,
|
|
362
|
+
genesisCID: state.genesisCID,
|
|
363
|
+
headCID: state.headCID,
|
|
364
|
+
isDeleted: state.isDeleted,
|
|
365
|
+
currentDocumentCID: state.currentDocumentCID,
|
|
366
|
+
length: input.log.length
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export {
|
|
371
|
+
MultikeyPublicKey,
|
|
372
|
+
IdentityOperation,
|
|
373
|
+
VerifiedIdentity,
|
|
374
|
+
ContentOperation,
|
|
375
|
+
ED25519_PUB_MULTICODEC,
|
|
376
|
+
ED25519_PRIV_MULTICODEC,
|
|
377
|
+
encodeEd25519Multikey,
|
|
378
|
+
decodeMultikey,
|
|
379
|
+
deriveChainIdentifier,
|
|
380
|
+
deriveEntityId,
|
|
381
|
+
signIdentityOperation,
|
|
382
|
+
verifyIdentityChain,
|
|
383
|
+
signContentOperation,
|
|
384
|
+
verifyContentChain
|
|
385
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// src/crypto/base64url.ts
|
|
2
|
+
var base64urlEncode = (data) => {
|
|
3
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
6
|
+
binary += String.fromCharCode(bytes[i]);
|
|
7
|
+
}
|
|
8
|
+
const base64 = btoa(binary);
|
|
9
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
10
|
+
};
|
|
11
|
+
var base64urlDecode = (str) => {
|
|
12
|
+
let padded = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
13
|
+
while (padded.length % 4) padded += "=";
|
|
14
|
+
const binary = atob(padded);
|
|
15
|
+
const bytes = new Uint8Array(binary.length);
|
|
16
|
+
for (let i = 0; i < binary.length; i++) {
|
|
17
|
+
bytes[i] = binary.charCodeAt(i);
|
|
18
|
+
}
|
|
19
|
+
return bytes;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/crypto/ed25519.ts
|
|
23
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
24
|
+
var createNewEd25519Keypair = () => {
|
|
25
|
+
const privateKey = ed25519.utils.randomSecretKey();
|
|
26
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
27
|
+
return { privateKey, publicKey };
|
|
28
|
+
};
|
|
29
|
+
var importEd25519Keypair = (privateKey) => {
|
|
30
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
31
|
+
return { privateKey, publicKey };
|
|
32
|
+
};
|
|
33
|
+
var signPayloadEd25519 = (payload, privateKey) => {
|
|
34
|
+
return ed25519.sign(payload, privateKey);
|
|
35
|
+
};
|
|
36
|
+
var isValidEd25519Signature = (payload, signature, publicKey) => {
|
|
37
|
+
return ed25519.verify(signature, payload, publicKey);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/crypto/id.ts
|
|
41
|
+
import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
|
|
42
|
+
var alphabet = "2346789acdefhknrtvz";
|
|
43
|
+
var idLength = 22;
|
|
44
|
+
var getRandomBytes = (length) => {
|
|
45
|
+
const bytes = new Uint8Array(length);
|
|
46
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
47
|
+
return bytes;
|
|
48
|
+
};
|
|
49
|
+
var generateIdNoPrefix = (options) => {
|
|
50
|
+
const hash = sha256Hash(options?.seed ?? getRandomBytes(32));
|
|
51
|
+
let encoded = "";
|
|
52
|
+
for (let i = 0; i < idLength; i++) {
|
|
53
|
+
const byte = hash[i];
|
|
54
|
+
if (byte === void 0) throw new Error("hash is too short");
|
|
55
|
+
encoded += alphabet.charAt(byte % alphabet.length);
|
|
56
|
+
}
|
|
57
|
+
return encoded;
|
|
58
|
+
};
|
|
59
|
+
var generateId = (prefix, options) => {
|
|
60
|
+
const encoded = generateIdNoPrefix(options);
|
|
61
|
+
return `${prefix}_${encoded}`;
|
|
62
|
+
};
|
|
63
|
+
var isValidId = (prefix, id) => {
|
|
64
|
+
const expectedLength = prefix.length + 1 + idLength;
|
|
65
|
+
return id.startsWith(`${prefix}_`) && id.length === expectedLength;
|
|
66
|
+
};
|
|
67
|
+
var normalizedId = (prefix, id) => {
|
|
68
|
+
const prefixLowered = prefix.toLowerCase();
|
|
69
|
+
const idLowered = id.toLowerCase();
|
|
70
|
+
if (idLowered.startsWith(`${prefixLowered}_`)) {
|
|
71
|
+
return idLowered;
|
|
72
|
+
} else if (idLowered.includes("_")) {
|
|
73
|
+
throw new Error(`unexpected id prefix for ${id}`);
|
|
74
|
+
}
|
|
75
|
+
return `${prefixLowered}_${idLowered}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/crypto/jws.ts
|
|
79
|
+
var createJws = async (options) => {
|
|
80
|
+
const headerB64 = base64urlEncode(JSON.stringify(options.header));
|
|
81
|
+
const payloadB64 = base64urlEncode(JSON.stringify(options.payload));
|
|
82
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
83
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
84
|
+
const signatureBytes = await options.sign(signingInputBytes);
|
|
85
|
+
const signatureB64 = base64urlEncode(signatureBytes);
|
|
86
|
+
return `${signingInput}.${signatureB64}`;
|
|
87
|
+
};
|
|
88
|
+
var verifyJws = (options) => {
|
|
89
|
+
const parts = options.token.split(".");
|
|
90
|
+
if (parts.length !== 3) {
|
|
91
|
+
throw new JwsVerificationError("Invalid token format");
|
|
92
|
+
}
|
|
93
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
94
|
+
let header;
|
|
95
|
+
let payload;
|
|
96
|
+
try {
|
|
97
|
+
header = JSON.parse(new TextDecoder().decode(base64urlDecode(headerB64)));
|
|
98
|
+
payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
|
|
99
|
+
} catch {
|
|
100
|
+
throw new JwsVerificationError("Failed to decode token");
|
|
101
|
+
}
|
|
102
|
+
if (header.alg !== "EdDSA") {
|
|
103
|
+
throw new JwsVerificationError(`Unsupported algorithm: ${header.alg}`);
|
|
104
|
+
}
|
|
105
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
106
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
107
|
+
const signatureBytes = base64urlDecode(signatureB64);
|
|
108
|
+
const isValid = isValidEd25519Signature(signingInputBytes, signatureBytes, options.publicKey);
|
|
109
|
+
if (!isValid) {
|
|
110
|
+
throw new JwsVerificationError("Invalid signature");
|
|
111
|
+
}
|
|
112
|
+
return { header, payload };
|
|
113
|
+
};
|
|
114
|
+
var decodeJwsUnsafe = (token) => {
|
|
115
|
+
const parts = token.split(".");
|
|
116
|
+
if (parts.length !== 3) return null;
|
|
117
|
+
try {
|
|
118
|
+
const [headerB64, payloadB64] = parts;
|
|
119
|
+
const header = JSON.parse(new TextDecoder().decode(base64urlDecode(headerB64)));
|
|
120
|
+
const payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
|
|
121
|
+
return { header, payload };
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var JwsVerificationError = class extends Error {
|
|
127
|
+
constructor(message) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "JwsVerificationError";
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/crypto/jwt.ts
|
|
134
|
+
var createJwt = async (options) => {
|
|
135
|
+
const headerJson = JSON.stringify(options.header);
|
|
136
|
+
const payloadJson = JSON.stringify(options.payload);
|
|
137
|
+
const headerB64 = base64urlEncode(headerJson);
|
|
138
|
+
const payloadB64 = base64urlEncode(payloadJson);
|
|
139
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
140
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
141
|
+
const signatureBytes = await options.sign(signingInputBytes);
|
|
142
|
+
const signatureB64 = base64urlEncode(signatureBytes);
|
|
143
|
+
return `${signingInput}.${signatureB64}`;
|
|
144
|
+
};
|
|
145
|
+
var decodeJwtUnsafe = (token) => {
|
|
146
|
+
const parts = token.split(".");
|
|
147
|
+
if (parts.length !== 3) return null;
|
|
148
|
+
try {
|
|
149
|
+
const [headerB64, payloadB64] = parts;
|
|
150
|
+
const headerJson = new TextDecoder().decode(base64urlDecode(headerB64));
|
|
151
|
+
const payloadJson = new TextDecoder().decode(base64urlDecode(payloadB64));
|
|
152
|
+
const header = JSON.parse(headerJson);
|
|
153
|
+
const payload = JSON.parse(payloadJson);
|
|
154
|
+
return { header, payload };
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var verifyJwt = (options) => {
|
|
160
|
+
const parts = options.token.split(".");
|
|
161
|
+
if (parts.length !== 3) {
|
|
162
|
+
throw new JwtVerificationError("Invalid token format");
|
|
163
|
+
}
|
|
164
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
165
|
+
let header;
|
|
166
|
+
let payload;
|
|
167
|
+
try {
|
|
168
|
+
const headerJson = new TextDecoder().decode(base64urlDecode(headerB64));
|
|
169
|
+
const payloadJson = new TextDecoder().decode(base64urlDecode(payloadB64));
|
|
170
|
+
header = JSON.parse(headerJson);
|
|
171
|
+
payload = JSON.parse(payloadJson);
|
|
172
|
+
} catch {
|
|
173
|
+
throw new JwtVerificationError("Failed to decode token");
|
|
174
|
+
}
|
|
175
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
176
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
177
|
+
const signatureBytes = base64urlDecode(signatureB64);
|
|
178
|
+
if (header.alg !== "EdDSA") {
|
|
179
|
+
throw new JwtVerificationError(`Unsupported algorithm: ${header.alg}`);
|
|
180
|
+
}
|
|
181
|
+
const isValid = isValidEd25519Signature(signingInputBytes, signatureBytes, options.publicKey);
|
|
182
|
+
if (!isValid) throw new JwtVerificationError("Invalid signature");
|
|
183
|
+
const currentTime = options.currentTime ?? Math.floor(Date.now() / 1e3);
|
|
184
|
+
if (payload.exp <= currentTime) {
|
|
185
|
+
throw new JwtVerificationError("Token expired");
|
|
186
|
+
}
|
|
187
|
+
if (options.issuer !== void 0 && payload.iss !== options.issuer) {
|
|
188
|
+
throw new JwtVerificationError(
|
|
189
|
+
`Invalid issuer: expected ${options.issuer}, got ${payload.iss}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (options.audience !== void 0 && payload.aud !== options.audience) {
|
|
193
|
+
throw new JwtVerificationError(
|
|
194
|
+
`Invalid audience: expected ${options.audience}, got ${payload.aud}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return { header, payload };
|
|
198
|
+
};
|
|
199
|
+
var JwtVerificationError = class extends Error {
|
|
200
|
+
constructor(message) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "JwtVerificationError";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/crypto/multiformats.ts
|
|
207
|
+
import * as dagCborCodec from "@ipld/dag-cbor";
|
|
208
|
+
import * as Block from "multiformats/block";
|
|
209
|
+
import { CID } from "multiformats/cid";
|
|
210
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
211
|
+
var dagCborCanonicalEncode = async (value) => {
|
|
212
|
+
return await Block.encode({
|
|
213
|
+
// removes any undefineds or other non-serializable values, kinda whack but
|
|
214
|
+
// it works for now
|
|
215
|
+
value: JSON.parse(JSON.stringify(value)),
|
|
216
|
+
codec: dagCborCodec,
|
|
217
|
+
hasher: sha256
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
var parseDagCborCID = (cid) => {
|
|
221
|
+
return CID.parse(cid);
|
|
222
|
+
};
|
|
223
|
+
var isCanonicallyEqual = async (data1, data2) => {
|
|
224
|
+
const block1 = await dagCborCanonicalEncode(data1);
|
|
225
|
+
const block2 = await dagCborCanonicalEncode(data2);
|
|
226
|
+
return block1.cid.toString() === block2.cid.toString();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
base64urlEncode,
|
|
231
|
+
base64urlDecode,
|
|
232
|
+
createNewEd25519Keypair,
|
|
233
|
+
importEd25519Keypair,
|
|
234
|
+
signPayloadEd25519,
|
|
235
|
+
isValidEd25519Signature,
|
|
236
|
+
generateIdNoPrefix,
|
|
237
|
+
generateId,
|
|
238
|
+
isValidId,
|
|
239
|
+
normalizedId,
|
|
240
|
+
createJws,
|
|
241
|
+
verifyJws,
|
|
242
|
+
decodeJwsUnsafe,
|
|
243
|
+
JwsVerificationError,
|
|
244
|
+
createJwt,
|
|
245
|
+
decodeJwtUnsafe,
|
|
246
|
+
verifyJwt,
|
|
247
|
+
JwtVerificationError,
|
|
248
|
+
dagCborCanonicalEncode,
|
|
249
|
+
parseDagCborCID,
|
|
250
|
+
isCanonicallyEqual
|
|
251
|
+
};
|