@metalabel/dfos-protocol 0.24.0 → 0.26.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 +11 -6
- package/dist/chain/index.d.ts +154 -8
- package/dist/chain/index.js +18 -2
- package/dist/{chunk-4EJCATUC.js → chunk-3USIENKC.js} +207 -4
- package/dist/{chunk-FMHROCFH.js → chunk-PGFHCBGH.js} +1 -1
- package/dist/credentials/index.d.ts +3 -163
- package/dist/credentials/index.js +1 -1
- package/dist/dfos-credential-CAYCqUCP.d.ts +493 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +18 -2
- package/package.json +6 -3
- package/schemas/credit-claim.v1.json +75 -0
- package/schemas/post.v1.json +24 -10
- package/dist/schemas-BXye25k7.d.ts +0 -220
package/README.md
CHANGED
|
@@ -5,9 +5,14 @@ Ed25519 signed chain primitives for cryptographic identity and verifiable conten
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @metalabel/dfos-protocol
|
|
8
|
+
npm install @metalabel/dfos-protocol zod
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`zod` is a peer dependency. The package exports Zod schema objects directly
|
|
12
|
+
(`IdentityOperation`, `ContentOperation`, `MultikeyPublicKey`, …) so you can
|
|
13
|
+
compose them into your own schemas — which only works if your app and this
|
|
14
|
+
package share one Zod install.
|
|
15
|
+
|
|
11
16
|
## Usage
|
|
12
17
|
|
|
13
18
|
```ts
|
|
@@ -21,11 +26,11 @@ import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-pr
|
|
|
21
26
|
|
|
22
27
|
## Subpath Exports
|
|
23
28
|
|
|
24
|
-
| Export | Description
|
|
25
|
-
| -------------------------------------- |
|
|
26
|
-
| `@metalabel/dfos-protocol/chain` | Identity & content chains, services, artifacts, countersigns, revocations |
|
|
27
|
-
| `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and DFOS credentials for authorization
|
|
28
|
-
| `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation
|
|
29
|
+
| Export | Description |
|
|
30
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
31
|
+
| `@metalabel/dfos-protocol/chain` | Identity & content chains, services, artifacts, countersigns, revocations, credit claims |
|
|
32
|
+
| `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and DFOS credentials for authorization |
|
|
33
|
+
| `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
|
|
29
34
|
|
|
30
35
|
## Specifications
|
|
31
36
|
|
package/dist/chain/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as IdentityOperation,
|
|
2
|
-
export { A as ARTIFACT_CID_ANCHOR_RE, C as CONTENT_ID_ANCHOR_RE, M as MAX_ARTIFACT_PAYLOAD_SIZE,
|
|
1
|
+
import { I as IdentityOperation, q as Signer, s as VerifiedIdentity, S as ServiceEntry, d as ContentOperation, R as RevocationChecker, e as CountersignPayload, a as ArtifactPayload } from '../dfos-credential-CAYCqUCP.js';
|
|
2
|
+
export { A as ARTIFACT_CID_ANCHOR_RE, C as CONTENT_ID_ANCHOR_RE, g as CreditClaimPayload, h as Iso8601, M as MAX_ARTIFACT_PAYLOAD_SIZE, j as MAX_CREDIT_CLAIM_SIZE, k as MAX_OPERATION_SIZE, l as MAX_SERVICES_ENTRIES, m as MAX_SERVICES_PAYLOAD_SIZE, n as MultikeyPublicKey, o as RevocationPayload, p as ServicesArray, x as parseProtocolTimestampUnix } from '../dfos-credential-CAYCqUCP.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/** Ed25519 public key multicodec value */
|
|
@@ -168,10 +168,11 @@ declare const verifyContentChain: (input: {
|
|
|
168
168
|
*/
|
|
169
169
|
resolveIdentity?: (did: string) => Promise<VerifiedIdentity | undefined>;
|
|
170
170
|
/**
|
|
171
|
-
* Check whether a credential (leaf or parent) has been revoked.
|
|
172
|
-
*
|
|
171
|
+
* Check whether a credential (leaf or parent) has been revoked. Called with
|
|
172
|
+
* `asOfUnix` = each operation's own `createdAt`, so a fold of committed
|
|
173
|
+
* history is stable under later revocations. See `RevocationChecker`.
|
|
173
174
|
*/
|
|
174
|
-
isRevoked?:
|
|
175
|
+
isRevoked?: RevocationChecker;
|
|
175
176
|
}) => Promise<VerifiedContentChain>;
|
|
176
177
|
/**
|
|
177
178
|
* Verify a single new content operation against already-verified chain state
|
|
@@ -193,8 +194,11 @@ declare const verifyContentExtensionFromTrustedState: (input: {
|
|
|
193
194
|
enforceAuthorization?: boolean;
|
|
194
195
|
/** Resolve a DID to a VerifiedIdentity. Required when enforceAuthorization is true. */
|
|
195
196
|
resolveIdentity?: (did: string) => Promise<VerifiedIdentity | undefined>;
|
|
196
|
-
/**
|
|
197
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Check whether a credential (leaf or parent) has been revoked. Called with
|
|
199
|
+
* `asOfUnix` = the new operation's own `createdAt`. See `RevocationChecker`.
|
|
200
|
+
*/
|
|
201
|
+
isRevoked?: RevocationChecker;
|
|
198
202
|
}) => Promise<{
|
|
199
203
|
state: VerifiedContentChain;
|
|
200
204
|
operationCID: string;
|
|
@@ -291,4 +295,146 @@ declare const verifyRevocation: (input: {
|
|
|
291
295
|
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
292
296
|
}) => Promise<VerifiedRevocation>;
|
|
293
297
|
|
|
294
|
-
|
|
298
|
+
interface VerifiedCreditClaim {
|
|
299
|
+
/** The claimant DID that signed this claim */
|
|
300
|
+
did: string;
|
|
301
|
+
/** The content chain this claim binds to — the 31-char contentId */
|
|
302
|
+
contentId: string;
|
|
303
|
+
/** The claimed role, compared by exact byte equality */
|
|
304
|
+
role: string;
|
|
305
|
+
/** Timestamp of the claim */
|
|
306
|
+
createdAt: string;
|
|
307
|
+
/** Optional pinned document state the claimant credited itself on */
|
|
308
|
+
asOfDocumentCID?: string;
|
|
309
|
+
/** kid from the JWS header */
|
|
310
|
+
signerKeyId: string;
|
|
311
|
+
/** CID of the claim artifact itself */
|
|
312
|
+
claimCID: string;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The four states a `credits[]` entry resolves to (see `specs/CREDITS.md`).
|
|
316
|
+
*
|
|
317
|
+
* `invalid` and `unverifiable` are deliberately distinct and MUST NOT be
|
|
318
|
+
* collapsed: `invalid` means "checked and failed" (a positive signal that
|
|
319
|
+
* something is wrong), `unverifiable` means "could not check" (the claim may be
|
|
320
|
+
* perfectly valid). Rendering `invalid` as `unclaimed` launders a failure into
|
|
321
|
+
* the ordinary case.
|
|
322
|
+
*/
|
|
323
|
+
type CreditEntryState = 'claimed' | 'unclaimed' | 'invalid' | 'unverifiable';
|
|
324
|
+
/** Why a credit-claim verification failed — the consumer-visible verdict split */
|
|
325
|
+
type CreditClaimFailureReason = 'invalid' | 'unverifiable';
|
|
326
|
+
/**
|
|
327
|
+
* Thrown by `verifyCreditClaim`, carrying the spec's verdict as a FIELD.
|
|
328
|
+
*
|
|
329
|
+
* Consumers MUST branch on `reason`, never on the message text. The message is
|
|
330
|
+
* diagnostic prose and may be reworded at any time; `reason` is the contract.
|
|
331
|
+
*/
|
|
332
|
+
declare class CreditClaimVerifyError extends Error {
|
|
333
|
+
readonly reason: CreditClaimFailureReason;
|
|
334
|
+
constructor(reason: CreditClaimFailureReason, message: string);
|
|
335
|
+
}
|
|
336
|
+
/** A `credits[]` entry as it appears in document bytes — every field untrusted */
|
|
337
|
+
interface CreditEntry {
|
|
338
|
+
did?: unknown;
|
|
339
|
+
role?: unknown;
|
|
340
|
+
name?: unknown;
|
|
341
|
+
claim?: unknown;
|
|
342
|
+
}
|
|
343
|
+
interface VerifiedCreditEntry {
|
|
344
|
+
/** The resolved state of this entry */
|
|
345
|
+
state: CreditEntryState;
|
|
346
|
+
/** Human-readable diagnosis — display prose, never a branch target */
|
|
347
|
+
note: string;
|
|
348
|
+
/** The verified claim, present only when `state` is 'claimed' */
|
|
349
|
+
claim?: VerifiedCreditClaim;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Sign a credit claim as a JWS
|
|
353
|
+
*
|
|
354
|
+
* The claim binds to the content chain's stable `contentId`, not to a document —
|
|
355
|
+
* so a signed claim stays valid verbatim across every subsequent edit of the
|
|
356
|
+
* chain. Callers SHOULD sign a given (contentId, did, role) triple exactly once
|
|
357
|
+
* and reuse the token: re-signing mints a fresh `createdAt`, which changes the
|
|
358
|
+
* claim bytes, which changes the embedding document's CID. Pass `createdAt` to
|
|
359
|
+
* re-derive a previously issued claim's exact bytes (the same override register
|
|
360
|
+
* as `createDFOSCredential`'s `iat`).
|
|
361
|
+
*
|
|
362
|
+
* The `kid` is derived from `did` + `keyId` here rather than accepted from the
|
|
363
|
+
* caller, so a claim whose kid DID disagrees with its payload DID — the shape
|
|
364
|
+
* every verifier MUST reject — is unrepresentable at sign time.
|
|
365
|
+
*/
|
|
366
|
+
declare const signCreditClaim: (input: {
|
|
367
|
+
contentId: string;
|
|
368
|
+
did: string;
|
|
369
|
+
role: string;
|
|
370
|
+
/** Optional document state the claimant pins its credit to */
|
|
371
|
+
asOfDocumentCID?: string;
|
|
372
|
+
/**
|
|
373
|
+
* ISO 8601 override; defaults to now. NORMALIZED to whole seconds either way —
|
|
374
|
+
* a sub-second component you pass is discarded, not signed. See
|
|
375
|
+
* `normalizeClaimCreatedAt`.
|
|
376
|
+
*/
|
|
377
|
+
createdAt?: string;
|
|
378
|
+
signer: Signer;
|
|
379
|
+
keyId: string;
|
|
380
|
+
}) => Promise<{
|
|
381
|
+
jwsToken: string;
|
|
382
|
+
claimCID: string;
|
|
383
|
+
}>;
|
|
384
|
+
/**
|
|
385
|
+
* Verify a credit claim JWS — size, signature, CID, payload schema, signer match
|
|
386
|
+
*
|
|
387
|
+
* Prefer `verifyCreditEntry` when you have a `credits[]` entry: this function
|
|
388
|
+
* checks only the `contentId` component of the three-component bind, leaving
|
|
389
|
+
* `did` and `role` to the caller.
|
|
390
|
+
*
|
|
391
|
+
* Pass `expectedContentId` to enforce the anti-replay half of the bind: without
|
|
392
|
+
* it, a valid claim lifted from one chain's document verifies when replayed in
|
|
393
|
+
* another chain's document. A consumer verifying a credits entry ALWAYS knows
|
|
394
|
+
* which chain it is reading, so it should always pass it. Omit the option
|
|
395
|
+
* entirely to skip the check; passing an EMPTY STRING is an error, never a skip
|
|
396
|
+
* (a zero-valued content id is a bug, not a request for unbound semantics).
|
|
397
|
+
*
|
|
398
|
+
* `resolveIdentity` MUST supply the claimant's historical keys — see
|
|
399
|
+
* `resolveKeyFromIdentity` above for why, and what breaks if it does not.
|
|
400
|
+
*
|
|
401
|
+
* Failures throw `CreditClaimVerifyError` carrying `reason: 'invalid' |
|
|
402
|
+
* 'unverifiable'`. Branch on that field, never on the message.
|
|
403
|
+
*
|
|
404
|
+
* **A deleted claimant still verifies.** This function does not consult
|
|
405
|
+
* `identity.isDeleted`, and that omission is the doctrine, not an oversight: a
|
|
406
|
+
* credit claim is a statement of historical fact, and the fact does not stop
|
|
407
|
+
* having happened when the claimant tombstones its identity. Credentials take
|
|
408
|
+
* the opposite rule (a deleted issuer's credentials are dead) because a
|
|
409
|
+
* credential is a live grant of authority, and authority must die with the
|
|
410
|
+
* identity that holds it. Attribution is history; authorization is standing.
|
|
411
|
+
*/
|
|
412
|
+
declare const verifyCreditClaim: (jwsToken: string, options: {
|
|
413
|
+
resolveIdentity: (did: string) => Promise<VerifiedIdentity | undefined>;
|
|
414
|
+
/** The content chain the embedding document belongs to — enforces the binder */
|
|
415
|
+
expectedContentId?: string;
|
|
416
|
+
}) => Promise<VerifiedCreditClaim>;
|
|
417
|
+
/**
|
|
418
|
+
* Resolve one `credits[]` entry to its spec state, performing the FULL bind
|
|
419
|
+
*
|
|
420
|
+
* This is the call most consumers want. `verifyCreditClaim` alone checks only
|
|
421
|
+
* the `contentId` component; the `did` and `role` comparisons are what make a
|
|
422
|
+
* claim an assertion about THIS entry, and skipping them verifies the wrong
|
|
423
|
+
* proposition — that some valid claim exists, not that it is about this credit.
|
|
424
|
+
* Shipping the whole comparison here is deliberate: the safe call should be the
|
|
425
|
+
* easy one.
|
|
426
|
+
*
|
|
427
|
+
* `contentId` is the chain whose document contains the entry. It is REQUIRED,
|
|
428
|
+
* because a consumer reading an entry always knows it, and without it the claim
|
|
429
|
+
* is replayable across chains.
|
|
430
|
+
*
|
|
431
|
+
* Never throws for claim-level problems — every outcome is one of the four
|
|
432
|
+
* states. It throws only when `contentId` is missing, which is a caller bug
|
|
433
|
+
* rather than a verdict about the entry.
|
|
434
|
+
*/
|
|
435
|
+
declare const verifyCreditEntry: (entry: CreditEntry, options: {
|
|
436
|
+
resolveIdentity: (did: string) => Promise<VerifiedIdentity | undefined>;
|
|
437
|
+
contentId: string;
|
|
438
|
+
}) => Promise<VerifiedCreditEntry>;
|
|
439
|
+
|
|
440
|
+
export { type AnchorKind, ArtifactPayload, ContentOperation, CountersignPayload, type CreditClaimFailureReason, CreditClaimVerifyError, type CreditEntry, type CreditEntryState, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, RECOGNIZED_SERVICE_TYPES, ServiceEntry, Signer, type VerifiedArtifact, type VerifiedContentChain, type VerifiedCountersignature, type VerifiedCreditClaim, type VerifiedCreditEntry, VerifiedIdentity, type VerifiedRevocation, anchorsByLabel, assertServicesWithinCap, classifyAnchor, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, isRecognizedServiceType, relayEndpoints, signArtifact, signContentOperation, signCountersignature, signCreditClaim, signIdentityOperation, signRevocation, verifyArtifact, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyCreditClaim, verifyCreditEntry, verifyIdentityChain, verifyIdentityExtensionFromTrustedState, verifyRevocation };
|
package/dist/chain/index.js
CHANGED
|
@@ -4,8 +4,12 @@ import {
|
|
|
4
4
|
CONTENT_ID_ANCHOR_RE,
|
|
5
5
|
ContentOperation,
|
|
6
6
|
CountersignPayload,
|
|
7
|
+
CreditClaimPayload,
|
|
8
|
+
CreditClaimVerifyError,
|
|
7
9
|
IdentityOperation,
|
|
10
|
+
Iso8601,
|
|
8
11
|
MAX_ARTIFACT_PAYLOAD_SIZE,
|
|
12
|
+
MAX_CREDIT_CLAIM_SIZE,
|
|
9
13
|
MAX_OPERATION_SIZE,
|
|
10
14
|
MAX_SERVICES_ENTRIES,
|
|
11
15
|
MAX_SERVICES_PAYLOAD_SIZE,
|
|
@@ -21,26 +25,30 @@ import {
|
|
|
21
25
|
deriveChainIdentifier,
|
|
22
26
|
deriveContentId,
|
|
23
27
|
isRecognizedServiceType,
|
|
28
|
+
parseProtocolTimestampUnix,
|
|
24
29
|
relayEndpoints,
|
|
25
30
|
signArtifact,
|
|
26
31
|
signContentOperation,
|
|
27
32
|
signCountersignature,
|
|
33
|
+
signCreditClaim,
|
|
28
34
|
signIdentityOperation,
|
|
29
35
|
signRevocation,
|
|
30
36
|
verifyArtifact,
|
|
31
37
|
verifyContentChain,
|
|
32
38
|
verifyContentExtensionFromTrustedState,
|
|
33
39
|
verifyCountersignature,
|
|
40
|
+
verifyCreditClaim,
|
|
41
|
+
verifyCreditEntry,
|
|
34
42
|
verifyIdentityChain,
|
|
35
43
|
verifyIdentityExtensionFromTrustedState,
|
|
36
44
|
verifyRevocation
|
|
37
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-3USIENKC.js";
|
|
38
46
|
import {
|
|
39
47
|
ED25519_PRIV_MULTICODEC,
|
|
40
48
|
ED25519_PUB_MULTICODEC,
|
|
41
49
|
decodeMultikey,
|
|
42
50
|
encodeEd25519Multikey
|
|
43
|
-
} from "../chunk-
|
|
51
|
+
} from "../chunk-PGFHCBGH.js";
|
|
44
52
|
import "../chunk-4QQ5HK5M.js";
|
|
45
53
|
export {
|
|
46
54
|
ARTIFACT_CID_ANCHOR_RE,
|
|
@@ -48,10 +56,14 @@ export {
|
|
|
48
56
|
CONTENT_ID_ANCHOR_RE,
|
|
49
57
|
ContentOperation,
|
|
50
58
|
CountersignPayload,
|
|
59
|
+
CreditClaimPayload,
|
|
60
|
+
CreditClaimVerifyError,
|
|
51
61
|
ED25519_PRIV_MULTICODEC,
|
|
52
62
|
ED25519_PUB_MULTICODEC,
|
|
53
63
|
IdentityOperation,
|
|
64
|
+
Iso8601,
|
|
54
65
|
MAX_ARTIFACT_PAYLOAD_SIZE,
|
|
66
|
+
MAX_CREDIT_CLAIM_SIZE,
|
|
55
67
|
MAX_OPERATION_SIZE,
|
|
56
68
|
MAX_SERVICES_ENTRIES,
|
|
57
69
|
MAX_SERVICES_PAYLOAD_SIZE,
|
|
@@ -69,16 +81,20 @@ export {
|
|
|
69
81
|
deriveContentId,
|
|
70
82
|
encodeEd25519Multikey,
|
|
71
83
|
isRecognizedServiceType,
|
|
84
|
+
parseProtocolTimestampUnix,
|
|
72
85
|
relayEndpoints,
|
|
73
86
|
signArtifact,
|
|
74
87
|
signContentOperation,
|
|
75
88
|
signCountersignature,
|
|
89
|
+
signCreditClaim,
|
|
76
90
|
signIdentityOperation,
|
|
77
91
|
signRevocation,
|
|
78
92
|
verifyArtifact,
|
|
79
93
|
verifyContentChain,
|
|
80
94
|
verifyContentExtensionFromTrustedState,
|
|
81
95
|
verifyCountersignature,
|
|
96
|
+
verifyCreditClaim,
|
|
97
|
+
verifyCreditEntry,
|
|
82
98
|
verifyIdentityChain,
|
|
83
99
|
verifyIdentityExtensionFromTrustedState,
|
|
84
100
|
verifyRevocation
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
matchesResource,
|
|
6
6
|
verifyDFOSCredential,
|
|
7
7
|
verifyDelegationChain
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-PGFHCBGH.js";
|
|
9
9
|
import {
|
|
10
10
|
createJws,
|
|
11
11
|
dagCborCanonicalEncode,
|
|
@@ -59,6 +59,11 @@ var ServicesArray = z.array(ServiceEntry).max(MAX_SERVICES_ENTRIES).refine(
|
|
|
59
59
|
"service entry ids must be unique"
|
|
60
60
|
);
|
|
61
61
|
var Iso8601 = z.iso.datetime({ offset: false, precision: 3 });
|
|
62
|
+
var parseProtocolTimestampUnix = (value) => {
|
|
63
|
+
if (!Iso8601.safeParse(value).success) return null;
|
|
64
|
+
const ms = Date.parse(value);
|
|
65
|
+
return Number.isFinite(ms) ? Math.floor(ms / 1e3) : null;
|
|
66
|
+
};
|
|
62
67
|
var CIDString = z.string();
|
|
63
68
|
var IdentityCreate = z.looseObject({
|
|
64
69
|
version: z.literal(1),
|
|
@@ -159,6 +164,16 @@ var RevocationPayload = z.looseObject({
|
|
|
159
164
|
credentialCID: CIDString,
|
|
160
165
|
createdAt: Iso8601
|
|
161
166
|
});
|
|
167
|
+
var MAX_CREDIT_CLAIM_SIZE = 4096;
|
|
168
|
+
var CreditClaimPayload = z.looseObject({
|
|
169
|
+
version: z.literal(1),
|
|
170
|
+
type: z.literal("credit-claim"),
|
|
171
|
+
contentId: z.string().regex(CONTENT_ID_ANCHOR_RE, "contentId must be a 31-char content chain id"),
|
|
172
|
+
did: z.string().regex(/^did:/, "did must be a DID (did: prefix)"),
|
|
173
|
+
role: z.string().min(1),
|
|
174
|
+
createdAt: Iso8601,
|
|
175
|
+
asOfDocumentCID: CIDString.min(1, "asOfDocumentCID must be non-empty when present").optional()
|
|
176
|
+
});
|
|
162
177
|
|
|
163
178
|
// src/chain/derivation.ts
|
|
164
179
|
var deriveChainIdentifier = (cidBytes, prefix) => {
|
|
@@ -467,14 +482,14 @@ var verifyOperationAuthorization = async (input) => {
|
|
|
467
482
|
resolveIdentity: input.resolveIdentity,
|
|
468
483
|
now: opCreatedAtUnix
|
|
469
484
|
});
|
|
470
|
-
if (input.isRevoked && await input.isRevoked(credential.iss, credential.credentialCID)) {
|
|
485
|
+
if (input.isRevoked && await input.isRevoked(credential.iss, credential.credentialCID, opCreatedAtUnix)) {
|
|
471
486
|
throw new Error("credential is revoked");
|
|
472
487
|
}
|
|
473
488
|
await verifyDelegationChain(credential, {
|
|
474
489
|
resolveIdentity: input.resolveIdentity,
|
|
475
490
|
rootDID: input.creatorDID,
|
|
476
491
|
now: opCreatedAtUnix,
|
|
477
|
-
...input.isRevoked ? { isRevoked: input.isRevoked } : {}
|
|
492
|
+
...input.isRevoked ? { isRevoked: input.isRevoked, asOfUnix: opCreatedAtUnix } : {}
|
|
478
493
|
});
|
|
479
494
|
if (credential.aud !== "*" && credential.aud !== input.operationDID) {
|
|
480
495
|
throw new Error(
|
|
@@ -850,6 +865,186 @@ var verifyRevocation = async (input) => {
|
|
|
850
865
|
};
|
|
851
866
|
};
|
|
852
867
|
|
|
868
|
+
// src/chain/credit-claim.ts
|
|
869
|
+
var CreditClaimVerifyError = class extends Error {
|
|
870
|
+
reason;
|
|
871
|
+
constructor(reason, message) {
|
|
872
|
+
super(message);
|
|
873
|
+
this.name = "CreditClaimVerifyError";
|
|
874
|
+
this.reason = reason;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
var invalid = (message) => new CreditClaimVerifyError("invalid", message);
|
|
878
|
+
var unverifiable = (message) => new CreditClaimVerifyError("unverifiable", message);
|
|
879
|
+
var resolveKeyFromIdentity = (identity, kid) => {
|
|
880
|
+
const hashIdx = kid.indexOf("#");
|
|
881
|
+
if (hashIdx < 0) throw invalid("credit claim kid must be a DID URL");
|
|
882
|
+
const keyId = kid.substring(hashIdx + 1);
|
|
883
|
+
const allKeys = [...identity.authKeys, ...identity.assertKeys, ...identity.controllerKeys];
|
|
884
|
+
const key = allKeys.find((k) => k.id === keyId);
|
|
885
|
+
if (!key) {
|
|
886
|
+
throw invalid(`key ${keyId} not found on identity ${identity.did}`);
|
|
887
|
+
}
|
|
888
|
+
const { keyBytes } = decodeMultikey(key.publicKeyMultibase);
|
|
889
|
+
return keyBytes;
|
|
890
|
+
};
|
|
891
|
+
var normalizeClaimCreatedAt = (createdAt) => {
|
|
892
|
+
if (createdAt === void 0) {
|
|
893
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\d{3}Z$/, "000Z");
|
|
894
|
+
}
|
|
895
|
+
const ms = Date.parse(createdAt);
|
|
896
|
+
if (Number.isNaN(ms)) {
|
|
897
|
+
throw new Error(`invalid credit claim payload: unparseable createdAt: ${createdAt}`);
|
|
898
|
+
}
|
|
899
|
+
return new Date(Math.floor(ms / 1e3) * 1e3).toISOString();
|
|
900
|
+
};
|
|
901
|
+
var signCreditClaim = async (input) => {
|
|
902
|
+
const kid = `${input.did}#${input.keyId}`;
|
|
903
|
+
const now = normalizeClaimCreatedAt(input.createdAt);
|
|
904
|
+
if (input.asOfDocumentCID === "") {
|
|
905
|
+
throw new Error(
|
|
906
|
+
"invalid credit claim payload: asOfDocumentCID must be non-empty when present (omit the field instead)"
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const payload = {
|
|
910
|
+
version: 1,
|
|
911
|
+
type: "credit-claim",
|
|
912
|
+
contentId: input.contentId,
|
|
913
|
+
did: input.did,
|
|
914
|
+
role: input.role,
|
|
915
|
+
createdAt: now,
|
|
916
|
+
// omitted when absent — undefined strips under canonical CBOR, so a claim
|
|
917
|
+
// without the flavor encodes identically to one that never had the field
|
|
918
|
+
...input.asOfDocumentCID !== void 0 ? { asOfDocumentCID: input.asOfDocumentCID } : {}
|
|
919
|
+
};
|
|
920
|
+
const parseResult = CreditClaimPayload.safeParse(payload);
|
|
921
|
+
if (!parseResult.success) {
|
|
922
|
+
const messages = parseResult.error.issues.map((e) => e.message).join(", ");
|
|
923
|
+
throw new Error(`invalid credit claim payload: ${messages}`);
|
|
924
|
+
}
|
|
925
|
+
const encoded = await dagCborCanonicalEncode(payload);
|
|
926
|
+
const claimCID = encoded.cid.toString();
|
|
927
|
+
const jwsToken = await createJws({
|
|
928
|
+
header: { alg: "EdDSA", typ: "did:dfos:credit-claim", kid, cid: claimCID },
|
|
929
|
+
payload,
|
|
930
|
+
sign: input.signer
|
|
931
|
+
});
|
|
932
|
+
if (jwsToken.length > MAX_CREDIT_CLAIM_SIZE) {
|
|
933
|
+
throw new Error(`credit claim exceeds max size: ${jwsToken.length} > ${MAX_CREDIT_CLAIM_SIZE}`);
|
|
934
|
+
}
|
|
935
|
+
return { jwsToken, claimCID };
|
|
936
|
+
};
|
|
937
|
+
var verifyCreditClaim = async (jwsToken, options) => {
|
|
938
|
+
if (options.expectedContentId === "") {
|
|
939
|
+
throw invalid(
|
|
940
|
+
"expectedContentId must be a non-empty contentId \u2014 omit the option for the unbound form"
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
if (jwsToken.length > MAX_CREDIT_CLAIM_SIZE) {
|
|
944
|
+
throw invalid(`credit claim exceeds max size: ${jwsToken.length} > ${MAX_CREDIT_CLAIM_SIZE}`);
|
|
945
|
+
}
|
|
946
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
947
|
+
if (!decoded) throw invalid("failed to decode credit claim JWS");
|
|
948
|
+
const rawHeader = decoded.header;
|
|
949
|
+
if (typeof rawHeader["typ"] !== "string" || typeof rawHeader["kid"] !== "string") {
|
|
950
|
+
throw invalid("credit claim header must carry a string typ and kid");
|
|
951
|
+
}
|
|
952
|
+
if (decoded.header.typ !== "did:dfos:credit-claim") {
|
|
953
|
+
throw invalid(`invalid credit claim typ: ${decoded.header.typ}`);
|
|
954
|
+
}
|
|
955
|
+
const result = CreditClaimPayload.safeParse(decoded.payload);
|
|
956
|
+
if (!result.success) {
|
|
957
|
+
const messages = result.error.issues.map((e) => e.message).join(", ");
|
|
958
|
+
throw invalid(`invalid credit claim payload: ${messages}`);
|
|
959
|
+
}
|
|
960
|
+
const payload = result.data;
|
|
961
|
+
const kid = decoded.header.kid;
|
|
962
|
+
const hashIdx = kid.indexOf("#");
|
|
963
|
+
if (hashIdx < 0) throw invalid("credit claim kid must be a DID URL");
|
|
964
|
+
const kidDid = kid.substring(0, hashIdx);
|
|
965
|
+
if (kidDid !== payload.did) {
|
|
966
|
+
throw invalid("credit claim kid DID does not match payload did");
|
|
967
|
+
}
|
|
968
|
+
let identity;
|
|
969
|
+
try {
|
|
970
|
+
identity = await options.resolveIdentity(payload.did);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
973
|
+
throw unverifiable(`could not resolve claimant identity ${payload.did}: ${detail}`);
|
|
974
|
+
}
|
|
975
|
+
if (!identity) {
|
|
976
|
+
throw unverifiable(`claimant identity not found: ${payload.did}`);
|
|
977
|
+
}
|
|
978
|
+
const publicKey = resolveKeyFromIdentity(identity, kid);
|
|
979
|
+
try {
|
|
980
|
+
verifyJws({ token: jwsToken, publicKey });
|
|
981
|
+
} catch {
|
|
982
|
+
throw invalid("invalid credit claim signature");
|
|
983
|
+
}
|
|
984
|
+
const encoded = await dagCborCanonicalEncode(payload);
|
|
985
|
+
const claimCID = encoded.cid.toString();
|
|
986
|
+
if (!decoded.header.cid) throw invalid("missing cid in credit claim header");
|
|
987
|
+
if (decoded.header.cid !== claimCID) throw invalid("credit claim cid mismatch");
|
|
988
|
+
if (options.expectedContentId !== void 0 && payload.contentId !== options.expectedContentId) {
|
|
989
|
+
throw invalid(
|
|
990
|
+
`credit claim contentId ${payload.contentId} does not match expected ${options.expectedContentId}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
did: payload.did,
|
|
995
|
+
contentId: payload.contentId,
|
|
996
|
+
role: payload.role,
|
|
997
|
+
createdAt: payload.createdAt,
|
|
998
|
+
...payload.asOfDocumentCID !== void 0 ? { asOfDocumentCID: payload.asOfDocumentCID } : {},
|
|
999
|
+
signerKeyId: kid,
|
|
1000
|
+
claimCID
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
1003
|
+
var verifyCreditEntry = async (entry, options) => {
|
|
1004
|
+
if (!options.contentId) {
|
|
1005
|
+
throw new Error("verifyCreditEntry requires the hosting chain contentId");
|
|
1006
|
+
}
|
|
1007
|
+
if (entry.claim === void 0) {
|
|
1008
|
+
return {
|
|
1009
|
+
state: "unclaimed",
|
|
1010
|
+
note: "the document signer asserts this credit; the claimant has not signed it"
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (typeof entry.claim !== "string" || entry.claim === "") {
|
|
1014
|
+
return { state: "invalid", note: "claim is present but is not a JWS string" };
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof entry.did !== "string" || entry.did === "") {
|
|
1017
|
+
return { state: "invalid", note: "claim-bearing entry has no credited DID to bind to" };
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof entry.role !== "string" || entry.role === "") {
|
|
1020
|
+
return { state: "invalid", note: "claim-bearing entry has no role to bind to" };
|
|
1021
|
+
}
|
|
1022
|
+
let verified;
|
|
1023
|
+
try {
|
|
1024
|
+
verified = await verifyCreditClaim(entry.claim, {
|
|
1025
|
+
resolveIdentity: options.resolveIdentity,
|
|
1026
|
+
expectedContentId: options.contentId
|
|
1027
|
+
});
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (error instanceof CreditClaimVerifyError) {
|
|
1030
|
+
return { state: error.reason, note: error.message };
|
|
1031
|
+
}
|
|
1032
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1033
|
+
return { state: "unverifiable", note: `verification could not complete: ${detail}` };
|
|
1034
|
+
}
|
|
1035
|
+
if (verified.did !== entry.did) {
|
|
1036
|
+
return { state: "invalid", note: "signed claimant DID does not match the credited DID" };
|
|
1037
|
+
}
|
|
1038
|
+
if (verified.role !== entry.role) {
|
|
1039
|
+
return { state: "invalid", note: "signed role does not match the credited role" };
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
state: "claimed",
|
|
1043
|
+
note: "signature and the exact (contentId, did, role) bind verified",
|
|
1044
|
+
claim: verified
|
|
1045
|
+
};
|
|
1046
|
+
};
|
|
1047
|
+
|
|
853
1048
|
export {
|
|
854
1049
|
MAX_SERVICES_ENTRIES,
|
|
855
1050
|
MAX_SERVICES_PAYLOAD_SIZE,
|
|
@@ -859,6 +1054,8 @@ export {
|
|
|
859
1054
|
ARTIFACT_CID_ANCHOR_RE,
|
|
860
1055
|
ServiceEntry,
|
|
861
1056
|
ServicesArray,
|
|
1057
|
+
Iso8601,
|
|
1058
|
+
parseProtocolTimestampUnix,
|
|
862
1059
|
IdentityOperation,
|
|
863
1060
|
VerifiedIdentity,
|
|
864
1061
|
ContentOperation,
|
|
@@ -866,6 +1063,8 @@ export {
|
|
|
866
1063
|
ArtifactPayload,
|
|
867
1064
|
CountersignPayload,
|
|
868
1065
|
RevocationPayload,
|
|
1066
|
+
MAX_CREDIT_CLAIM_SIZE,
|
|
1067
|
+
CreditClaimPayload,
|
|
869
1068
|
deriveChainIdentifier,
|
|
870
1069
|
deriveContentId,
|
|
871
1070
|
assertServicesWithinCap,
|
|
@@ -885,5 +1084,9 @@ export {
|
|
|
885
1084
|
signArtifact,
|
|
886
1085
|
verifyArtifact,
|
|
887
1086
|
signRevocation,
|
|
888
|
-
verifyRevocation
|
|
1087
|
+
verifyRevocation,
|
|
1088
|
+
CreditClaimVerifyError,
|
|
1089
|
+
signCreditClaim,
|
|
1090
|
+
verifyCreditClaim,
|
|
1091
|
+
verifyCreditEntry
|
|
889
1092
|
};
|
|
@@ -269,7 +269,7 @@ var verifyDelegationChain = async (credential, options) => {
|
|
|
269
269
|
...options.now !== void 0 ? { now: options.now } : {}
|
|
270
270
|
});
|
|
271
271
|
if (options.isRevoked) {
|
|
272
|
-
const revoked = await options.isRevoked(parent.iss, parent.credentialCID);
|
|
272
|
+
const revoked = await options.isRevoked(parent.iss, parent.credentialCID, options.asOfUnix);
|
|
273
273
|
if (revoked) {
|
|
274
274
|
throw new CredentialVerificationError("parent credential in delegation chain is revoked");
|
|
275
275
|
}
|