@metalabel/dfos-protocol 0.0.3 → 0.2.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/CONTENT-MODEL.md +63 -22
- package/DID-METHOD.md +7 -8
- package/PROTOCOL.md +414 -94
- package/README.md +17 -11
- package/dist/chain/index.d.ts +115 -2
- package/dist/chain/index.js +14 -1
- package/dist/chunk-CZSEEZLL.js +258 -0
- package/dist/chunk-E5CFQG2B.js +99 -0
- package/dist/{chunk-VEBMLR37.js → chunk-GEVJ3SEV.js} +232 -6
- package/dist/credentials/index.d.ts +206 -0
- package/dist/credentials/index.js +35 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +58 -32
- package/dist/merkle/index.d.ts +45 -0
- package/dist/merkle/index.js +14 -0
- package/examples/beacon.json +14 -0
- package/examples/content-delegated.json +44 -0
- package/examples/content-delete.json +5 -4
- package/examples/content-lifecycle.json +9 -7
- package/examples/credential-read.json +12 -0
- package/examples/credential-write.json +14 -0
- package/examples/merkle-tree.json +28 -0
- package/package.json +10 -9
- package/schemas/manifest.v1.json +29 -0
- package/schemas/post.v1.json +5 -0
- package/schemas/profile.v1.json +5 -0
- package/REGISTRY-API.md +0 -242
- package/dist/chunk-U6DANYPT.js +0 -311
- package/dist/registry/index.d.ts +0 -143
- package/dist/registry/index.js +0 -34
- package/openapi.yaml +0 -376
- package/schemas/document-envelope.v1.json +0 -37
package/README.md
CHANGED
|
@@ -13,19 +13,22 @@ npm install @metalabel/dfos-protocol
|
|
|
13
13
|
```ts
|
|
14
14
|
// Chain verification
|
|
15
15
|
import { verifyContentChain, verifyIdentityChain } from '@metalabel/dfos-protocol/chain';
|
|
16
|
+
// Credentials (auth tokens + VC-JWT)
|
|
17
|
+
import { createAuthToken, verifyCredential } from '@metalabel/dfos-protocol/credentials';
|
|
16
18
|
// Crypto primitives
|
|
17
19
|
import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-protocol/crypto';
|
|
18
|
-
//
|
|
19
|
-
import {
|
|
20
|
+
// Merkle trees
|
|
21
|
+
import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/merkle';
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
## Subpath Exports
|
|
23
25
|
|
|
24
|
-
| Export
|
|
25
|
-
|
|
|
26
|
-
| `@metalabel/dfos-protocol/chain`
|
|
27
|
-
| `@metalabel/dfos-protocol/
|
|
28
|
-
| `@metalabel/dfos-protocol/
|
|
26
|
+
| Export | Description |
|
|
27
|
+
| -------------------------------------- | ----------------------------------------------------------------------- |
|
|
28
|
+
| `@metalabel/dfos-protocol/chain` | Identity and content chain signing, verification, beacons, countersigns |
|
|
29
|
+
| `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
|
|
30
|
+
| `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
|
|
31
|
+
| `@metalabel/dfos-protocol/merkle` | SHA-256 binary merkle tree, inclusion proofs |
|
|
29
32
|
|
|
30
33
|
## Specifications
|
|
31
34
|
|
|
@@ -33,19 +36,22 @@ import { createRegistryServer } from '@metalabel/dfos-protocol/registry';
|
|
|
33
36
|
| -------------------------------------- | -------------------------------------------------------------- |
|
|
34
37
|
| [PROTOCOL.md](./PROTOCOL.md) | Core protocol — chains, signatures, verification, test vectors |
|
|
35
38
|
| [DID-METHOD.md](./DID-METHOD.md) | W3C DID method specification for `did:dfos` |
|
|
36
|
-
| [CONTENT-MODEL.md](./CONTENT-MODEL.md) | Standard content schemas (post, profile,
|
|
37
|
-
| [REGISTRY-API.md](./REGISTRY-API.md) | HTTP API for chain storage and resolution |
|
|
38
|
-
| [openapi.yaml](./openapi.yaml) | OpenAPI 3.1 machine-readable registry spec |
|
|
39
|
+
| [CONTENT-MODEL.md](./CONTENT-MODEL.md) | Standard content schemas (post, profile, manifest) |
|
|
39
40
|
|
|
40
41
|
## Examples
|
|
41
42
|
|
|
42
|
-
The `examples/` directory contains deterministic reference
|
|
43
|
+
The `examples/` directory contains deterministic reference fixtures that can be independently verified by any Ed25519 + dag-cbor implementation:
|
|
43
44
|
|
|
44
45
|
- `identity-genesis.json` — single create operation
|
|
45
46
|
- `identity-rotation.json` — genesis + key rotation
|
|
46
47
|
- `identity-delete.json` — genesis + delete (terminal)
|
|
47
48
|
- `content-lifecycle.json` — create + update (with both documents)
|
|
48
49
|
- `content-delete.json` — create + delete
|
|
50
|
+
- `content-delegated.json` — creator genesis + delegated update with DFOSContentWrite VC-JWT
|
|
51
|
+
- `credential-write.json` — DFOSContentWrite VC-JWT (broad + content-narrowed)
|
|
52
|
+
- `credential-read.json` — DFOSContentRead VC-JWT
|
|
53
|
+
- `merkle-tree.json` — 5 content IDs → sorted tree → root, with inclusion proof
|
|
54
|
+
- `beacon.json` — signed merkle root announcement with witness countersignature
|
|
49
55
|
|
|
50
56
|
## License
|
|
51
57
|
|
package/dist/chain/index.d.ts
CHANGED
|
@@ -77,24 +77,42 @@ type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
|
|
|
77
77
|
declare const ContentOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
78
78
|
version: z.ZodLiteral<1>;
|
|
79
79
|
type: z.ZodLiteral<"create">;
|
|
80
|
+
did: z.ZodString;
|
|
80
81
|
documentCID: z.ZodString;
|
|
82
|
+
baseDocumentCID: z.ZodNullable<z.ZodString>;
|
|
81
83
|
createdAt: z.ZodISODateTime;
|
|
82
84
|
note: z.ZodNullable<z.ZodString>;
|
|
83
85
|
}, z.core.$strict>, z.ZodObject<{
|
|
84
86
|
version: z.ZodLiteral<1>;
|
|
85
87
|
type: z.ZodLiteral<"update">;
|
|
88
|
+
did: z.ZodString;
|
|
86
89
|
previousOperationCID: z.ZodString;
|
|
87
90
|
documentCID: z.ZodNullable<z.ZodString>;
|
|
91
|
+
baseDocumentCID: z.ZodNullable<z.ZodString>;
|
|
88
92
|
createdAt: z.ZodISODateTime;
|
|
89
93
|
note: z.ZodNullable<z.ZodString>;
|
|
94
|
+
/** VC-JWT authorizing this operation when signer is not the chain creator */
|
|
95
|
+
authorization: z.ZodOptional<z.ZodString>;
|
|
90
96
|
}, z.core.$strict>, z.ZodObject<{
|
|
91
97
|
version: z.ZodLiteral<1>;
|
|
92
98
|
type: z.ZodLiteral<"delete">;
|
|
99
|
+
did: z.ZodString;
|
|
93
100
|
previousOperationCID: z.ZodString;
|
|
94
101
|
createdAt: z.ZodISODateTime;
|
|
95
102
|
note: z.ZodNullable<z.ZodString>;
|
|
103
|
+
/** VC-JWT authorizing this operation when signer is not the chain creator */
|
|
104
|
+
authorization: z.ZodOptional<z.ZodString>;
|
|
96
105
|
}, z.core.$strict>], "type">;
|
|
97
106
|
type ContentOperation = z.infer<typeof ContentOperation>;
|
|
107
|
+
/** Beacon: floating signed merkle root announcement */
|
|
108
|
+
declare const BeaconPayload: z.ZodObject<{
|
|
109
|
+
version: z.ZodLiteral<1>;
|
|
110
|
+
type: z.ZodLiteral<"beacon">;
|
|
111
|
+
did: z.ZodString;
|
|
112
|
+
merkleRoot: z.ZodString;
|
|
113
|
+
createdAt: z.ZodISODateTime;
|
|
114
|
+
}, z.core.$strict>;
|
|
115
|
+
type BeaconPayload = z.infer<typeof BeaconPayload>;
|
|
98
116
|
|
|
99
117
|
/** Ed25519 public key multicodec value */
|
|
100
118
|
declare const ED25519_PUB_MULTICODEC = 237;
|
|
@@ -168,6 +186,8 @@ interface VerifiedContentChain {
|
|
|
168
186
|
currentDocumentCID: string | null;
|
|
169
187
|
/** Number of operations in the chain */
|
|
170
188
|
length: number;
|
|
189
|
+
/** The DID that created the chain (signer of genesis operation) */
|
|
190
|
+
creatorDID: string;
|
|
171
191
|
}
|
|
172
192
|
/**
|
|
173
193
|
* Sign a content chain operation as a JWS and derive the operation CID
|
|
@@ -182,15 +202,108 @@ declare const signContentOperation: (input: {
|
|
|
182
202
|
operationCID: string;
|
|
183
203
|
}>;
|
|
184
204
|
/**
|
|
185
|
-
* Verify a content chain's structural integrity and
|
|
205
|
+
* Verify a content chain's structural integrity, signatures, and authorization
|
|
186
206
|
*
|
|
187
207
|
* The caller provides a key resolver to look up public keys from kid values.
|
|
188
208
|
* This keeps the content chain protocol independent of identity resolution.
|
|
209
|
+
*
|
|
210
|
+
* Authorization rules:
|
|
211
|
+
* - Genesis (create) operation: the signer is the chain creator, always authorized
|
|
212
|
+
* - Subsequent operations signed by the creator DID: authorized (no credential needed)
|
|
213
|
+
* - Subsequent operations signed by a different DID: must include an `authorization`
|
|
214
|
+
* field containing a valid DFOSContentWrite VC-JWT issued by the creator DID
|
|
189
215
|
*/
|
|
190
216
|
declare const verifyContentChain: (input: {
|
|
191
217
|
log: string[];
|
|
192
218
|
/** Resolve a kid (DID URL) to the raw Ed25519 public key bytes */
|
|
193
219
|
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
220
|
+
/**
|
|
221
|
+
* Enforce creator-sovereignty authorization. When true, non-creator signers
|
|
222
|
+
* must include a DFOSContentWrite VC-JWT in the operation's `authorization`
|
|
223
|
+
* field. When false (default), any signer with a valid signature is accepted.
|
|
224
|
+
*
|
|
225
|
+
* Web relays should set this to true. Applications migrating to VC-based
|
|
226
|
+
* authorization can enable this once all chains include authorization fields.
|
|
227
|
+
*/
|
|
228
|
+
enforceAuthorization?: boolean;
|
|
194
229
|
}) => Promise<VerifiedContentChain>;
|
|
195
230
|
|
|
196
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Sign a beacon announcement as a JWS
|
|
233
|
+
*/
|
|
234
|
+
declare const signBeacon: (input: {
|
|
235
|
+
payload: BeaconPayload;
|
|
236
|
+
signer: Signer;
|
|
237
|
+
kid: string;
|
|
238
|
+
}) => Promise<{
|
|
239
|
+
jwsToken: string;
|
|
240
|
+
beaconCID: string;
|
|
241
|
+
}>;
|
|
242
|
+
interface VerifiedBeacon {
|
|
243
|
+
payload: BeaconPayload;
|
|
244
|
+
beaconCID: string;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Verify a beacon JWS — signature, CID, payload schema, clock skew
|
|
248
|
+
*/
|
|
249
|
+
declare const verifyBeacon: (input: {
|
|
250
|
+
jwsToken: string;
|
|
251
|
+
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
252
|
+
/** Current time for clock skew check (defaults to Date.now()) */
|
|
253
|
+
now?: number;
|
|
254
|
+
}) => Promise<VerifiedBeacon>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Sign an existing content operation as a countersignature (witness JWS)
|
|
258
|
+
*
|
|
259
|
+
* The witness signs the same payload as the author, producing a different
|
|
260
|
+
* JWS token with their own kid. The CID is identical because the payload
|
|
261
|
+
* is identical.
|
|
262
|
+
*/
|
|
263
|
+
declare const signCountersignature: (input: {
|
|
264
|
+
/** The original operation payload (must include did of the author) */
|
|
265
|
+
operationPayload: ContentOperation;
|
|
266
|
+
/** Witness signer */
|
|
267
|
+
signer: Signer;
|
|
268
|
+
/** Witness kid — DID URL of the witness (must differ from payload.did) */
|
|
269
|
+
kid: string;
|
|
270
|
+
}) => Promise<{
|
|
271
|
+
jwsToken: string;
|
|
272
|
+
operationCID: string;
|
|
273
|
+
}>;
|
|
274
|
+
interface VerifiedCountersignature {
|
|
275
|
+
operationCID: string;
|
|
276
|
+
/** The DID that authored the operation (payload.did) */
|
|
277
|
+
authorDID: string;
|
|
278
|
+
/** The DID that witnessed the operation (kid DID) */
|
|
279
|
+
witnessDID: string;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Verify a countersignature JWS against an expected operation CID
|
|
283
|
+
*
|
|
284
|
+
* Checks: valid signature, CID matches, kid DID differs from payload did
|
|
285
|
+
*/
|
|
286
|
+
declare const verifyCountersignature: (input: {
|
|
287
|
+
jwsToken: string;
|
|
288
|
+
expectedCID: string;
|
|
289
|
+
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
290
|
+
}) => Promise<VerifiedCountersignature>;
|
|
291
|
+
interface VerifiedBeaconCountersignature {
|
|
292
|
+
beaconCID: string;
|
|
293
|
+
/** The DID that controls the beacon (payload.did) */
|
|
294
|
+
controllerDID: string;
|
|
295
|
+
/** The DID that witnessed the beacon (kid DID) */
|
|
296
|
+
witnessDID: string;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Verify a beacon countersignature JWS against an expected beacon CID
|
|
300
|
+
*
|
|
301
|
+
* Checks: valid signature, CID matches, kid DID differs from payload did
|
|
302
|
+
*/
|
|
303
|
+
declare const verifyBeaconCountersignature: (input: {
|
|
304
|
+
jwsToken: string;
|
|
305
|
+
expectedCID: string;
|
|
306
|
+
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
307
|
+
}) => Promise<VerifiedBeaconCountersignature>;
|
|
308
|
+
|
|
309
|
+
export { BeaconPayload, ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, type Signer, type VerifiedBeacon, type VerifiedBeaconCountersignature, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyBeacon, verifyBeaconCountersignature, verifyContentChain, verifyCountersignature, verifyIdentityChain };
|
package/dist/chain/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BeaconPayload,
|
|
2
3
|
ContentOperation,
|
|
3
4
|
ED25519_PRIV_MULTICODEC,
|
|
4
5
|
ED25519_PUB_MULTICODEC,
|
|
@@ -9,13 +10,20 @@ import {
|
|
|
9
10
|
deriveChainIdentifier,
|
|
10
11
|
deriveContentId,
|
|
11
12
|
encodeEd25519Multikey,
|
|
13
|
+
signBeacon,
|
|
12
14
|
signContentOperation,
|
|
15
|
+
signCountersignature,
|
|
13
16
|
signIdentityOperation,
|
|
17
|
+
verifyBeacon,
|
|
18
|
+
verifyBeaconCountersignature,
|
|
14
19
|
verifyContentChain,
|
|
20
|
+
verifyCountersignature,
|
|
15
21
|
verifyIdentityChain
|
|
16
|
-
} from "../chunk-
|
|
22
|
+
} from "../chunk-GEVJ3SEV.js";
|
|
23
|
+
import "../chunk-CZSEEZLL.js";
|
|
17
24
|
import "../chunk-ZXXP5W5N.js";
|
|
18
25
|
export {
|
|
26
|
+
BeaconPayload,
|
|
19
27
|
ContentOperation,
|
|
20
28
|
ED25519_PRIV_MULTICODEC,
|
|
21
29
|
ED25519_PUB_MULTICODEC,
|
|
@@ -26,8 +34,13 @@ export {
|
|
|
26
34
|
deriveChainIdentifier,
|
|
27
35
|
deriveContentId,
|
|
28
36
|
encodeEd25519Multikey,
|
|
37
|
+
signBeacon,
|
|
29
38
|
signContentOperation,
|
|
39
|
+
signCountersignature,
|
|
30
40
|
signIdentityOperation,
|
|
41
|
+
verifyBeacon,
|
|
42
|
+
verifyBeaconCountersignature,
|
|
31
43
|
verifyContentChain,
|
|
44
|
+
verifyCountersignature,
|
|
32
45
|
verifyIdentityChain
|
|
33
46
|
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
base64urlDecode,
|
|
3
|
+
base64urlEncode,
|
|
4
|
+
createJwt,
|
|
5
|
+
isValidEd25519Signature,
|
|
6
|
+
verifyJwt
|
|
7
|
+
} from "./chunk-ZXXP5W5N.js";
|
|
8
|
+
|
|
9
|
+
// src/credentials/schemas.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var MAX_DID = 256;
|
|
12
|
+
var MAX_AUD = 512;
|
|
13
|
+
var MAX_CONTENT_ID = 256;
|
|
14
|
+
var VC_TYPE_CONTENT_WRITE = "DFOSContentWrite";
|
|
15
|
+
var VC_TYPE_CONTENT_READ = "DFOSContentRead";
|
|
16
|
+
var DFOSCredentialType = z.enum([VC_TYPE_CONTENT_WRITE, VC_TYPE_CONTENT_READ]);
|
|
17
|
+
var AuthTokenClaims = z.strictObject({
|
|
18
|
+
/** Issuer — the DID proving identity */
|
|
19
|
+
iss: z.string().max(MAX_DID),
|
|
20
|
+
/** Subject — same as iss for auth tokens */
|
|
21
|
+
sub: z.string().max(MAX_DID),
|
|
22
|
+
/** Audience — target relay hostname (prevents cross-relay replay) */
|
|
23
|
+
aud: z.string().max(MAX_AUD),
|
|
24
|
+
/** Expiration — unix seconds, short-lived (minutes) */
|
|
25
|
+
exp: z.number().int().positive(),
|
|
26
|
+
/** Issued at — unix seconds */
|
|
27
|
+
iat: z.number().int().positive()
|
|
28
|
+
});
|
|
29
|
+
var ContentWriteSubject = z.strictObject({
|
|
30
|
+
/** Optional content chain narrowing — if absent, grants broad write access */
|
|
31
|
+
contentId: z.string().max(MAX_CONTENT_ID).optional()
|
|
32
|
+
});
|
|
33
|
+
var ContentReadSubject = z.strictObject({
|
|
34
|
+
/** Optional content chain narrowing — if absent, grants broad read access */
|
|
35
|
+
contentId: z.string().max(MAX_CONTENT_ID).optional()
|
|
36
|
+
});
|
|
37
|
+
var VCClaim = z.strictObject({
|
|
38
|
+
"@context": z.tuple([z.literal("https://www.w3.org/ns/credentials/v2")]),
|
|
39
|
+
type: z.tuple([z.literal("VerifiableCredential"), DFOSCredentialType]).transform((t) => t),
|
|
40
|
+
credentialSubject: z.union([ContentWriteSubject, ContentReadSubject])
|
|
41
|
+
});
|
|
42
|
+
var CredentialClaims = z.strictObject({
|
|
43
|
+
/** Issuer — the DID granting the credential */
|
|
44
|
+
iss: z.string().max(MAX_DID),
|
|
45
|
+
/** Subject — the DID receiving the credential */
|
|
46
|
+
sub: z.string().max(MAX_DID),
|
|
47
|
+
/** Expiration — unix seconds */
|
|
48
|
+
exp: z.number().int().positive(),
|
|
49
|
+
/** Issued at — unix seconds */
|
|
50
|
+
iat: z.number().int().positive(),
|
|
51
|
+
/** Verifiable credential claim */
|
|
52
|
+
vc: VCClaim
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// src/credentials/auth-token.ts
|
|
56
|
+
var createAuthToken = async (options) => {
|
|
57
|
+
if (!options.kid.includes("#")) {
|
|
58
|
+
throw new Error("kid must be a DID URL (did:dfos:xxx#key_yyy)");
|
|
59
|
+
}
|
|
60
|
+
const kidDid = options.kid.substring(0, options.kid.indexOf("#"));
|
|
61
|
+
if (kidDid !== options.iss) {
|
|
62
|
+
throw new Error("kid DID does not match iss");
|
|
63
|
+
}
|
|
64
|
+
const now = options.iat ?? Math.floor(Date.now() / 1e3);
|
|
65
|
+
const header = { alg: "EdDSA", typ: "JWT", kid: options.kid };
|
|
66
|
+
const payload = {
|
|
67
|
+
iss: options.iss,
|
|
68
|
+
sub: options.iss,
|
|
69
|
+
aud: options.aud,
|
|
70
|
+
exp: options.exp,
|
|
71
|
+
iat: now
|
|
72
|
+
};
|
|
73
|
+
return createJwt({ header, payload, sign: options.sign });
|
|
74
|
+
};
|
|
75
|
+
var verifyAuthToken = (options) => {
|
|
76
|
+
const { header, payload } = verifyJwt({
|
|
77
|
+
token: options.token,
|
|
78
|
+
publicKey: options.publicKey,
|
|
79
|
+
audience: options.audience,
|
|
80
|
+
...options.currentTime !== void 0 ? { currentTime: options.currentTime } : {}
|
|
81
|
+
});
|
|
82
|
+
const result = AuthTokenClaims.safeParse(payload);
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
85
|
+
throw new AuthTokenVerificationError(`invalid auth token claims: ${messages}`);
|
|
86
|
+
}
|
|
87
|
+
const currentTime = options.currentTime ?? Math.floor(Date.now() / 1e3);
|
|
88
|
+
if (result.data.iat > currentTime) {
|
|
89
|
+
throw new AuthTokenVerificationError("auth token not yet valid (iat is in the future)");
|
|
90
|
+
}
|
|
91
|
+
const kid = header.kid;
|
|
92
|
+
if (!kid || !kid.includes("#")) {
|
|
93
|
+
throw new AuthTokenVerificationError("auth token kid must be a DID URL");
|
|
94
|
+
}
|
|
95
|
+
const kidDid = kid.substring(0, kid.indexOf("#"));
|
|
96
|
+
if (kidDid !== result.data.iss) {
|
|
97
|
+
throw new AuthTokenVerificationError("auth token kid DID does not match iss");
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
iss: result.data.iss,
|
|
101
|
+
aud: result.data.aud,
|
|
102
|
+
exp: result.data.exp,
|
|
103
|
+
kid
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
var AuthTokenVerificationError = class extends Error {
|
|
107
|
+
constructor(message) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "AuthTokenVerificationError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/credentials/credential.ts
|
|
114
|
+
var createCredential = async (options) => {
|
|
115
|
+
if (!options.kid.includes("#")) {
|
|
116
|
+
throw new Error("kid must be a DID URL (did:dfos:xxx#key_yyy)");
|
|
117
|
+
}
|
|
118
|
+
const kidDid = options.kid.substring(0, options.kid.indexOf("#"));
|
|
119
|
+
if (kidDid !== options.iss) {
|
|
120
|
+
throw new Error("kid DID does not match iss");
|
|
121
|
+
}
|
|
122
|
+
const now = options.iat ?? Math.floor(Date.now() / 1e3);
|
|
123
|
+
const header = { alg: "EdDSA", typ: "vc+jwt", kid: options.kid };
|
|
124
|
+
const credentialSubject = {};
|
|
125
|
+
if (options.contentId) {
|
|
126
|
+
credentialSubject.contentId = options.contentId;
|
|
127
|
+
}
|
|
128
|
+
const payload = {
|
|
129
|
+
iss: options.iss,
|
|
130
|
+
sub: options.sub,
|
|
131
|
+
exp: options.exp,
|
|
132
|
+
iat: now,
|
|
133
|
+
vc: {
|
|
134
|
+
"@context": ["https://www.w3.org/ns/credentials/v2"],
|
|
135
|
+
type: ["VerifiableCredential", options.type],
|
|
136
|
+
credentialSubject
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const headerB64 = base64urlEncode(JSON.stringify(header));
|
|
140
|
+
const payloadB64 = base64urlEncode(JSON.stringify(payload));
|
|
141
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
142
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
143
|
+
const signatureBytes = await options.sign(signingInputBytes);
|
|
144
|
+
const signatureB64 = base64urlEncode(signatureBytes);
|
|
145
|
+
return `${signingInput}.${signatureB64}`;
|
|
146
|
+
};
|
|
147
|
+
var verifyCredential = (options) => {
|
|
148
|
+
const parts = options.token.split(".");
|
|
149
|
+
if (parts.length !== 3) {
|
|
150
|
+
throw new CredentialVerificationError("invalid token format");
|
|
151
|
+
}
|
|
152
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
153
|
+
let header;
|
|
154
|
+
let payload;
|
|
155
|
+
try {
|
|
156
|
+
header = JSON.parse(new TextDecoder().decode(base64urlDecode(headerB64)));
|
|
157
|
+
payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
|
|
158
|
+
} catch {
|
|
159
|
+
throw new CredentialVerificationError("failed to decode token");
|
|
160
|
+
}
|
|
161
|
+
if (header.alg !== "EdDSA") {
|
|
162
|
+
throw new CredentialVerificationError(`unsupported algorithm: ${header.alg}`);
|
|
163
|
+
}
|
|
164
|
+
if (header.typ !== "vc+jwt") {
|
|
165
|
+
throw new CredentialVerificationError(`invalid typ: ${header.typ}`);
|
|
166
|
+
}
|
|
167
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
168
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
169
|
+
let signatureBytes;
|
|
170
|
+
try {
|
|
171
|
+
signatureBytes = base64urlDecode(signatureB64);
|
|
172
|
+
} catch {
|
|
173
|
+
throw new CredentialVerificationError("failed to decode signature");
|
|
174
|
+
}
|
|
175
|
+
const isValid = isValidEd25519Signature(signingInputBytes, signatureBytes, options.publicKey);
|
|
176
|
+
if (!isValid) {
|
|
177
|
+
throw new CredentialVerificationError("invalid signature");
|
|
178
|
+
}
|
|
179
|
+
const result = CredentialClaims.safeParse(payload);
|
|
180
|
+
if (!result.success) {
|
|
181
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
182
|
+
throw new CredentialVerificationError(`invalid credential claims: ${messages}`);
|
|
183
|
+
}
|
|
184
|
+
const claims = result.data;
|
|
185
|
+
const kid = header.kid;
|
|
186
|
+
if (!kid || !kid.includes("#")) {
|
|
187
|
+
throw new CredentialVerificationError("credential kid must be a DID URL");
|
|
188
|
+
}
|
|
189
|
+
const kidDid = kid.substring(0, kid.indexOf("#"));
|
|
190
|
+
if (kidDid !== claims.iss) {
|
|
191
|
+
throw new CredentialVerificationError("credential kid DID does not match iss");
|
|
192
|
+
}
|
|
193
|
+
const currentTime = options.currentTime ?? Math.floor(Date.now() / 1e3);
|
|
194
|
+
if (claims.iat > currentTime) {
|
|
195
|
+
throw new CredentialVerificationError("credential not yet valid (iat is in the future)");
|
|
196
|
+
}
|
|
197
|
+
if (claims.exp <= currentTime) {
|
|
198
|
+
throw new CredentialVerificationError("credential expired");
|
|
199
|
+
}
|
|
200
|
+
if (options.subject !== void 0 && claims.sub !== options.subject) {
|
|
201
|
+
throw new CredentialVerificationError(
|
|
202
|
+
`subject mismatch: expected ${options.subject}, got ${claims.sub}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const vcType = claims.vc.type[1];
|
|
206
|
+
if (options.expectedType !== void 0 && vcType !== options.expectedType) {
|
|
207
|
+
throw new CredentialVerificationError(
|
|
208
|
+
`type mismatch: expected ${options.expectedType}, got ${vcType}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const contentId = claims.vc.credentialSubject.contentId;
|
|
212
|
+
return {
|
|
213
|
+
iss: claims.iss,
|
|
214
|
+
sub: claims.sub,
|
|
215
|
+
exp: claims.exp,
|
|
216
|
+
type: vcType,
|
|
217
|
+
kid,
|
|
218
|
+
...contentId !== void 0 ? { contentId } : {}
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
var decodeCredentialUnsafe = (token) => {
|
|
222
|
+
const parts = token.split(".");
|
|
223
|
+
if (parts.length !== 3) return null;
|
|
224
|
+
try {
|
|
225
|
+
const [headerB64, payloadB64] = parts;
|
|
226
|
+
const header = JSON.parse(new TextDecoder().decode(base64urlDecode(headerB64)));
|
|
227
|
+
const payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
|
|
228
|
+
const result = CredentialClaims.safeParse(payload);
|
|
229
|
+
if (!result.success) return null;
|
|
230
|
+
return { header, claims: result.data };
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var CredentialVerificationError = class extends Error {
|
|
236
|
+
constructor(message) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.name = "CredentialVerificationError";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export {
|
|
243
|
+
VC_TYPE_CONTENT_WRITE,
|
|
244
|
+
VC_TYPE_CONTENT_READ,
|
|
245
|
+
DFOSCredentialType,
|
|
246
|
+
AuthTokenClaims,
|
|
247
|
+
ContentWriteSubject,
|
|
248
|
+
ContentReadSubject,
|
|
249
|
+
VCClaim,
|
|
250
|
+
CredentialClaims,
|
|
251
|
+
createAuthToken,
|
|
252
|
+
verifyAuthToken,
|
|
253
|
+
AuthTokenVerificationError,
|
|
254
|
+
createCredential,
|
|
255
|
+
verifyCredential,
|
|
256
|
+
decodeCredentialUnsafe,
|
|
257
|
+
CredentialVerificationError
|
|
258
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/merkle/tree.ts
|
|
2
|
+
var sha256 = async (data) => {
|
|
3
|
+
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
4
|
+
return new Uint8Array(buf);
|
|
5
|
+
};
|
|
6
|
+
var toHex = (bytes) => {
|
|
7
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
};
|
|
9
|
+
var hexToBytes = (hex) => {
|
|
10
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
11
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
12
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
13
|
+
}
|
|
14
|
+
return bytes;
|
|
15
|
+
};
|
|
16
|
+
var concat = (a, b) => {
|
|
17
|
+
const result = new Uint8Array(a.length + b.length);
|
|
18
|
+
result.set(a, 0);
|
|
19
|
+
result.set(b, a.length);
|
|
20
|
+
return result;
|
|
21
|
+
};
|
|
22
|
+
var hashLeaf = async (contentId) => {
|
|
23
|
+
return sha256(new TextEncoder().encode(contentId));
|
|
24
|
+
};
|
|
25
|
+
var hashInterior = async (left, right) => {
|
|
26
|
+
return sha256(concat(left, right));
|
|
27
|
+
};
|
|
28
|
+
var buildMerkleTree = async (contentIds) => {
|
|
29
|
+
const sorted = [...new Set(contentIds)].sort();
|
|
30
|
+
if (sorted.length === 0) return { root: null, leafCount: 0 };
|
|
31
|
+
let level = await Promise.all(sorted.map(hashLeaf));
|
|
32
|
+
while (level.length > 1) {
|
|
33
|
+
const nextLevel = [];
|
|
34
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
35
|
+
if (i + 1 < level.length) {
|
|
36
|
+
nextLevel.push(await hashInterior(level[i], level[i + 1]));
|
|
37
|
+
} else {
|
|
38
|
+
nextLevel.push(level[i]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
level = nextLevel;
|
|
42
|
+
}
|
|
43
|
+
return { root: toHex(level[0]), leafCount: sorted.length };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/merkle/proof.ts
|
|
47
|
+
var generateMerkleProof = async (contentIds, targetId) => {
|
|
48
|
+
const sorted = [...new Set(contentIds)].sort();
|
|
49
|
+
const targetIdx = sorted.indexOf(targetId);
|
|
50
|
+
if (targetIdx < 0) return null;
|
|
51
|
+
const { root } = await buildMerkleTree(sorted);
|
|
52
|
+
if (!root) return null;
|
|
53
|
+
const leaves = await Promise.all(sorted.map(hashLeaf));
|
|
54
|
+
const path = [];
|
|
55
|
+
let level = leaves;
|
|
56
|
+
let idx = targetIdx;
|
|
57
|
+
while (level.length > 1) {
|
|
58
|
+
const nextLevel = [];
|
|
59
|
+
const nextIdx = Math.floor(idx / 2);
|
|
60
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
61
|
+
if (i + 1 < level.length) {
|
|
62
|
+
if (i === idx || i + 1 === idx) {
|
|
63
|
+
const siblingIdx = i === idx ? i + 1 : i;
|
|
64
|
+
path.push({
|
|
65
|
+
hash: toHex(level[siblingIdx]),
|
|
66
|
+
position: siblingIdx < idx ? "left" : "right"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const interior = await sha256(concat(level[i], level[i + 1]));
|
|
70
|
+
nextLevel.push(interior);
|
|
71
|
+
} else {
|
|
72
|
+
nextLevel.push(level[i]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
level = nextLevel;
|
|
76
|
+
idx = nextIdx;
|
|
77
|
+
}
|
|
78
|
+
return { contentId: targetId, root, path };
|
|
79
|
+
};
|
|
80
|
+
var verifyMerkleProof = async (proof) => {
|
|
81
|
+
let current = await hashLeaf(proof.contentId);
|
|
82
|
+
for (const step of proof.path) {
|
|
83
|
+
const sibling = hexToBytes(step.hash);
|
|
84
|
+
if (step.position === "left") {
|
|
85
|
+
current = await sha256(concat(sibling, current));
|
|
86
|
+
} else {
|
|
87
|
+
current = await sha256(concat(current, sibling));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return toHex(current) === proof.root;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
hexToBytes,
|
|
95
|
+
hashLeaf,
|
|
96
|
+
buildMerkleTree,
|
|
97
|
+
generateMerkleProof,
|
|
98
|
+
verifyMerkleProof
|
|
99
|
+
};
|