@metalabel/dfos-protocol 0.24.0 → 0.25.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 CHANGED
@@ -21,11 +21,11 @@ import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-pr
21
21
 
22
22
  ## Subpath Exports
23
23
 
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 |
24
+ | Export | Description |
25
+ | -------------------------------------- | ---------------------------------------------------------------------------------------- |
26
+ | `@metalabel/dfos-protocol/chain` | Identity & content chains, services, artifacts, countersigns, revocations, credit claims |
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
29
 
30
30
  ## Specifications
31
31
 
@@ -1,5 +1,5 @@
1
- import { I as IdentityOperation, i as Signer, V as VerifiedIdentity, S as ServiceEntry, b as ContentOperation, c as CountersignPayload, a as ArtifactPayload } from '../schemas-BXye25k7.js';
2
- export { A as ARTIFACT_CID_ANCHOR_RE, C as CONTENT_ID_ANCHOR_RE, M as MAX_ARTIFACT_PAYLOAD_SIZE, d as MAX_OPERATION_SIZE, e as MAX_SERVICES_ENTRIES, f as MAX_SERVICES_PAYLOAD_SIZE, g as MultikeyPublicKey, R as RevocationPayload, h as ServicesArray } from '../schemas-BXye25k7.js';
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. Threaded onto
172
- * the WRITE path so revoked credentials no longer authorize writes.
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?: (issuerDID: string, credentialCID: string) => Promise<boolean>;
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
- /** Check whether a credential (leaf or parent) has been revoked. */
197
- isRevoked?: (issuerDID: string, credentialCID: string) => Promise<boolean>;
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
- export { type AnchorKind, ArtifactPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, RECOGNIZED_SERVICE_TYPES, ServiceEntry, Signer, type VerifiedArtifact, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, type VerifiedRevocation, anchorsByLabel, assertServicesWithinCap, classifyAnchor, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, isRecognizedServiceType, relayEndpoints, signArtifact, signContentOperation, signCountersignature, signIdentityOperation, signRevocation, verifyArtifact, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState, verifyRevocation };
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 };
@@ -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-4EJCATUC.js";
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-FMHROCFH.js";
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-FMHROCFH.js";
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
  }