@chitmark/sdk 0.7.0 → 0.9.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/dist/index.js CHANGED
@@ -1,3 +1,120 @@
1
+ // src/attribution.ts
2
+ function attributionEvidence(options, mode) {
3
+ const { request, agentProof, session, purpose, piiMode = mode } = options;
4
+ const headers = request.headers;
5
+ const header = typeof headers.get === "function" ? headers.get("chitmark-agent-proof") : Object.entries(headers).find(
6
+ ([name]) => name.toLowerCase() === "chitmark-agent-proof"
7
+ )?.[1];
8
+ let url;
9
+ if (!options.site && request.url) {
10
+ try {
11
+ url = new URL(request.url);
12
+ } catch {
13
+ }
14
+ }
15
+ const site = options.site ?? (url?.protocol === "https:" ? url.origin : void 0);
16
+ const proof = agentProof ?? (site ? parseProof(header) : void 0);
17
+ return { site, session, piiMode, purpose, agentProof: proof };
18
+ }
19
+ function parseProof(header) {
20
+ if (typeof header !== "string" || header.length > 2048) return;
21
+ try {
22
+ const value = JSON.parse(header);
23
+ if (!value || typeof value.nonce !== "string" || value.nonce.length > 36 || typeof value.issuedAt !== "number" || !Number.isSafeInteger(value.issuedAt) || typeof value.signature !== "string" || value.signature.length > 86)
24
+ return;
25
+ const { nonce, issuedAt, signature } = value;
26
+ if ("iss" in value) {
27
+ const key = value.agentKey;
28
+ if (value.iss === "chitmark-agent-key-v1" && key?.kty === "OKP" && key.crv === "Ed25519" && typeof key.x === "string" && key.x.length === 43 && !("d" in key)) {
29
+ return {
30
+ iss: value.iss,
31
+ agentKey: { kty: key.kty, crv: key.crv, x: key.x },
32
+ nonce,
33
+ issuedAt,
34
+ signature
35
+ };
36
+ }
37
+ return;
38
+ }
39
+ if (typeof value.linkId === "string" && value.linkId.length <= 36)
40
+ return { linkId: value.linkId, nonce, issuedAt, signature };
41
+ } catch {
42
+ }
43
+ return void 0;
44
+ }
45
+
46
+ // src/credentials.ts
47
+ var encode = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
48
+ var json = (value) => encode(new TextEncoder().encode(JSON.stringify(value)));
49
+ async function boundedCredential(work, signal) {
50
+ signal.throwIfAborted();
51
+ let abort;
52
+ const aborted = new Promise((_, reject) => {
53
+ abort = () => reject(new Error("Credential deadline exceeded"));
54
+ signal.addEventListener("abort", abort, { once: true });
55
+ });
56
+ try {
57
+ return await Promise.race([work, aborted]);
58
+ } finally {
59
+ signal.removeEventListener("abort", abort);
60
+ }
61
+ }
62
+ function createDPoPCredentialProvider(options) {
63
+ const { privateKey, publicJwk } = options;
64
+ if (publicJwk.d || publicJwk.kty !== "EC" || publicJwk.crv !== "P-256" || !publicJwk.x || !publicJwk.y)
65
+ throw new Error("DPoP requires a public P-256 JWK");
66
+ const jwk = { kty: "EC", crv: "P-256", x: publicJwk.x, y: publicJwk.y };
67
+ let cached;
68
+ let pending;
69
+ const valid = (c) => !!c && typeof c.accessToken === "string" && /^[\w-]+\.[\w-]+\.[\w-]+$/.test(c.accessToken) && [c.expiresAt, c.trustExpiresAt, c.revocationValidUntil].every(
70
+ (v) => Number.isSafeInteger(v) && v > Date.now() / 1e3 + 1
71
+ );
72
+ return {
73
+ async headers({ url, method, signal }) {
74
+ signal.throwIfAborted();
75
+ const target = new URL(url);
76
+ if (target.protocol !== "https:" || target.username || target.password)
77
+ throw new Error("DPoP requires HTTPS");
78
+ if (!valid(cached)) {
79
+ if (!pending) {
80
+ const refreshSignal = AbortSignal.timeout(options.refreshTimeoutMs ?? 500);
81
+ pending = Promise.resolve().then(() => options.load(refreshSignal)).then((c) => {
82
+ if (!valid(c) || refreshSignal.aborted) throw new Error("Stale credential");
83
+ cached = { ...c };
84
+ return cached;
85
+ }).finally(() => {
86
+ pending = void 0;
87
+ });
88
+ }
89
+ await boundedCredential(
90
+ pending,
91
+ AbortSignal.any([signal, AbortSignal.timeout(options.refreshTimeoutMs ?? 500)])
92
+ );
93
+ }
94
+ if (!valid(cached)) throw new Error("Stale credential");
95
+ const credential = cached;
96
+ const token = credential.accessToken;
97
+ const ath = encode(
98
+ new Uint8Array(
99
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))
100
+ )
101
+ );
102
+ const payload = `${json({ typ: "dpop+jwt", alg: "ES256", jwk })}.${json({ jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1e3), htm: method.toUpperCase(), htu: `${target.origin}${target.pathname}`, ath })}`;
103
+ const signature = await crypto.subtle.sign(
104
+ { name: "ECDSA", hash: "SHA-256" },
105
+ privateKey,
106
+ new TextEncoder().encode(payload)
107
+ );
108
+ signal.throwIfAborted();
109
+ if (!valid(credential)) throw new Error("Stale credential");
110
+ return {
111
+ Authorization: `DPoP ${token}`,
112
+ DPoP: `${payload}.${encode(new Uint8Array(signature))}`
113
+ };
114
+ }
115
+ };
116
+ }
117
+
1
118
  // src/errors.ts
2
119
  var ChitmarkError = class extends Error {
3
120
  name = "ChitmarkError";
@@ -60,7 +177,7 @@ var DEFAULT_BASE_URLS = {
60
177
  };
61
178
  var DEFAULT_TIMEOUT_MS = 800;
62
179
  var LOCAL_TIMEOUT_MS = 2e3;
63
- var SDK_VERSION = "0.7.0";
180
+ var SDK_VERSION = "0.9.0";
64
181
  function defaultTimeoutMs(baseUrl) {
65
182
  try {
66
183
  const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -123,11 +240,11 @@ function minimizeFormData(formData) {
123
240
  }
124
241
  function bufferToHex(buffer) {
125
242
  const bytes = new Uint8Array(buffer);
126
- let hex = "";
243
+ let hex2 = "";
127
244
  for (const b of bytes) {
128
- hex += b.toString(16).padStart(2, "0");
245
+ hex2 += b.toString(16).padStart(2, "0");
129
246
  }
130
- return hex;
247
+ return hex2;
131
248
  }
132
249
 
133
250
  // src/internal/prepare.ts
@@ -203,6 +320,46 @@ var VerdictTokenError = class extends ChitmarkError {
203
320
  var DEFAULT_JWKS_URL = "https://api.chitmark.com/.well-known/jwks.json";
204
321
  var DEFAULT_LEEWAY_SECONDS = 30;
205
322
  async function verifyVerdictToken(token, options) {
323
+ return verifyReceipt(token, options, "JWT");
324
+ }
325
+ var SENSITIVE_RECEIPT_ACTIONS = [
326
+ "api_key",
327
+ "data_export",
328
+ "session_establish",
329
+ "privilege_elevate",
330
+ "authenticator_replace",
331
+ "recovery_contact_change",
332
+ "session_use",
333
+ "privilege_change",
334
+ "release_deploy",
335
+ "bulk_mutation",
336
+ "commitment_change",
337
+ "agent_effect",
338
+ "agent_escalation",
339
+ "quote_generate",
340
+ "queue_priority"
341
+ ];
342
+ async function verifyActionReceipt(token, options) {
343
+ const claims = await verifyReceipt(
344
+ token,
345
+ {
346
+ ...options,
347
+ aud: options.audience,
348
+ issuer: VERDICT_TOKEN_ISSUER,
349
+ leewaySeconds: 0
350
+ },
351
+ "chitmark-action+jwt"
352
+ );
353
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
354
+ if (!options.audience || !options.session || !options.tenantId || !options.policy || !/^[a-f0-9]{64}$/.test(options.digest) || !(options.action === "trial_activation" ? options.proof === "none" || options.proof === "pow_severe" : SENSITIVE_RECEIPT_ACTIONS.includes(options.action) && options.proof === "tenant_step_up" && typeof options.subject === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(options.subject) && claims.subject === options.subject) || claims.action !== options.action || options.environment !== "test" && options.environment !== "live" || claims.environment !== options.environment || !options.surface || claims.surface !== options.surface || claims.digest !== options.digest || claims.policy !== options.policy || claims.proof !== options.proof || claims.enforcementMode !== "enforce" || claims.decision !== "allow" || claims.degraded !== false || typeof claims.eventId !== "string" || !claims.eventId || !Number.isSafeInteger(claims.iat) || !Number.isSafeInteger(claims.exp) || claims.iat > now || claims.exp <= now || claims.exp - claims.iat !== 60) {
355
+ throw new VerdictTokenError(
356
+ "wrong_action_binding",
357
+ "Receipt cannot authorize this operation"
358
+ );
359
+ }
360
+ return claims;
361
+ }
362
+ async function verifyReceipt(token, options, expectedType) {
206
363
  const nowMs = options.nowMs ?? Date.now();
207
364
  const leewaySeconds = options.leewaySeconds ?? DEFAULT_LEEWAY_SECONDS;
208
365
  const parts = token.split(".");
@@ -224,10 +381,10 @@ async function verifyVerdictToken(token, options) {
224
381
  `verdictToken must use ES256, got ${String(header["alg"])}`
225
382
  );
226
383
  }
227
- if (header["typ"] !== "JWT") {
384
+ if (header["typ"] !== expectedType) {
228
385
  throw new VerdictTokenError(
229
386
  "wrong_type",
230
- `verdictToken must be a JWT, got ${String(header["typ"])}`
387
+ `verdictToken must have type ${expectedType}, got ${String(header["typ"])}`
231
388
  );
232
389
  }
233
390
  const kid = typeof header["kid"] === "string" ? header["kid"] : "";
@@ -273,6 +430,12 @@ async function verifyVerdictToken(token, options) {
273
430
  if (!jwk) {
274
431
  throw new VerdictTokenError("unknown_kid", `JWKS has no key for kid ${kid}`);
275
432
  }
433
+ if (jwk["kty"] !== "EC" || jwk["crv"] !== "P-256" || jwk["alg"] !== void 0 && jwk["alg"] !== "ES256") {
434
+ throw new VerdictTokenError(
435
+ "wrong_algorithm",
436
+ "Verification requires an ES256 P-256 key"
437
+ );
438
+ }
276
439
  let publicKey;
277
440
  try {
278
441
  publicKey = await crypto.subtle.importKey(
@@ -345,7 +508,27 @@ function decodeBase64UrlBytes(value) {
345
508
 
346
509
  // src/client.ts
347
510
  var Chitmark = class {
511
+ attribution = {
512
+ resolve: (input) => this.request("/v1/attributions/resolve", {
513
+ method: "POST",
514
+ body: attributionEvidence(input, this.piiMode),
515
+ idempotencyKey: input.idempotencyKey
516
+ }),
517
+ get: (id2) => this.request(`/v1/attributions/${encodeURIComponent(id2)}`, { method: "GET" }),
518
+ createLink: (input, idempotencyKey) => this.request("/v1/identity-links", { method: "POST", body: input, idempotencyKey }),
519
+ revokeLink: (id2) => this.request(`/v1/identity-links/${encodeURIComponent(id2)}`, { method: "DELETE" })
520
+ };
521
+ /** Paid access lifecycle. Transport uncertainty must be resumed, never treated as payment. */
522
+ async charge(request) {
523
+ return this.request("/v1/charge", {
524
+ method: "POST",
525
+ body: request,
526
+ idempotencyKey: request.idempotencyKey,
527
+ timeoutMs: Math.max(this.timeoutMs, 15e3)
528
+ });
529
+ }
348
530
  apiKey;
531
+ credentialProvider;
349
532
  baseUrl;
350
533
  timeoutMs;
351
534
  piiMode;
@@ -353,7 +536,7 @@ var Chitmark = class {
353
536
  fetchImpl;
354
537
  clientName;
355
538
  constructor(options) {
356
- if (!options.apiKey) {
539
+ if (!!options.apiKey === !!options.credentialProvider) {
357
540
  throw new ChitmarkError("Chitmark apiKey is required");
358
541
  }
359
542
  if (options.onDegraded && options.onDegraded !== "challenge") {
@@ -362,6 +545,7 @@ var Chitmark = class {
362
545
  );
363
546
  }
364
547
  this.apiKey = options.apiKey;
548
+ this.credentialProvider = options.credentialProvider;
365
549
  this.baseUrl = resolveBaseUrl(options.env, options.baseUrl ?? envBaseUrl());
366
550
  this.timeoutMs = options.timeoutMs ?? defaultTimeoutMs(this.baseUrl);
367
551
  this.piiMode = options.piiMode ?? "hashed";
@@ -454,7 +638,8 @@ var Chitmark = class {
454
638
  /**
455
639
  * Complete a challenge by submitting its solved proof. Clears only the bound
456
640
  * session, once. Does not authorize the action: re-verify with
457
- * `context.challengeId` and proceed only on non-degraded allow.
641
+ * `context.challengeId` for standalone challenges or the original action
642
+ * binding plus the challenge and event IDs for operation-bound challenges.
458
643
  */
459
644
  async completeChallenge(request) {
460
645
  return this.request("/v1/challenge", {
@@ -482,15 +667,35 @@ var Chitmark = class {
482
667
  */
483
668
  async request(path, init) {
484
669
  const controller = new AbortController();
485
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
670
+ const timeoutMs = init.timeoutMs ?? this.timeoutMs;
671
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
486
672
  try {
487
673
  const headers = {
488
- Authorization: `Bearer ${this.apiKey}`,
674
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
489
675
  "Content-Type": "application/json",
490
676
  Accept: "application/json",
491
677
  "User-Agent": this.clientName,
492
678
  "X-Chitmark-Client": this.clientName
493
679
  };
680
+ if (this.credentialProvider) {
681
+ try {
682
+ Object.assign(
683
+ headers,
684
+ await boundedCredential(
685
+ this.credentialProvider.headers({
686
+ url: `${this.baseUrl}${path}`,
687
+ method: init.method,
688
+ signal: controller.signal
689
+ }),
690
+ controller.signal
691
+ )
692
+ );
693
+ } catch {
694
+ throw new ChitmarkError("Credential unavailable", { code: "credential" });
695
+ }
696
+ if (!headers.Authorization?.startsWith("DPoP ") || !headers.DPoP)
697
+ throw new ChitmarkError("Invalid DPoP credentials");
698
+ }
494
699
  if (init.idempotencyKey) {
495
700
  headers["Idempotency-Key"] = init.idempotencyKey;
496
701
  }
@@ -498,7 +703,8 @@ var Chitmark = class {
498
703
  method: init.method,
499
704
  headers,
500
705
  body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
501
- signal: controller.signal
706
+ signal: controller.signal,
707
+ redirect: "error"
502
708
  });
503
709
  const text = await res.text();
504
710
  let parsed = void 0;
@@ -522,7 +728,7 @@ var Chitmark = class {
522
728
  } catch (err) {
523
729
  if (err instanceof ChitmarkApiError) throw err;
524
730
  if (err instanceof Error && err.name === "AbortError") {
525
- throw new ChitmarkError(`Request timed out after ${this.timeoutMs}ms`, {
731
+ throw new ChitmarkError(`Request timed out after ${timeoutMs}ms`, {
526
732
  code: "timeout"
527
733
  });
528
734
  }
@@ -548,6 +754,38 @@ function isWellFormedVerdict(value) {
548
754
  return typeof v.decision === "string" && VERDICT_DECISIONS.has(v.decision) && typeof v.eventId === "string" && v.eventId.length > 0 && typeof v.degraded === "boolean";
549
755
  }
550
756
 
757
+ // src/charge.ts
758
+ function summarizeCharge(p, now = Date.now(), sellerCostCents) {
759
+ if (sellerCostCents !== void 0 && (!Number.isSafeInteger(sellerCostCents) || sellerCostCents < 0))
760
+ throw new Error("Invalid seller cost");
761
+ const deliveredUnits = p.delivery.executions.reduce(
762
+ (n, e) => n + (e.state === "delivered" ? e.usedUnits : 0),
763
+ 0
764
+ );
765
+ const unresolvedUnits = p.delivery.executions.reduce(
766
+ (n, e) => n + (e.state === "reserved" || e.state === "unknown" ? e.units : 0),
767
+ 0
768
+ );
769
+ const net = p.payment.providerFee === null ? null : p.payment.capturedAmount - p.payment.refundedAmount - p.payment.providerFee;
770
+ return {
771
+ paidCompletion: p.payment.state === "settled" && p.payment.capturedAmount > 0 && p.payment.refundedAmount === 0 && p.payment.disputedAmount === 0 && deliveredUnits === p.quote.quantity,
772
+ billedCents: p.payment.capturedAmount - p.payment.refundedAmount,
773
+ sellerReportedDeliveredCents: deliveredUnits * p.quote.resource.unitAmount,
774
+ deliveredUnits,
775
+ unresolvedUnits,
776
+ unresolvedAgeUpperBoundMs: p.payment.state === "pending" || p.payment.refundState === "pending" || p.payment.refundState === "unknown" || p.payment.state === "unknown" || unresolvedUnits > 0 ? Math.max(0, now - p.createdAt) : null,
777
+ refundedCents: p.payment.refundedAmount,
778
+ disputedCents: p.payment.disputedAmount,
779
+ captureFeeCents: p.payment.providerFee,
780
+ sellerNetAfterCaptureFeeCents: net,
781
+ sellerMarginCents: net === null || sellerCostCents === void 0 ? null : net - sellerCostCents,
782
+ // Provider/application reconciliation must supply duplicate evidence; retries alone do not prove absence.
783
+ duplicateCharges: null,
784
+ duplicateEffects: null,
785
+ observedElapsedMs: Math.max(0, p.updatedAt - p.createdAt)
786
+ };
787
+ }
788
+
551
789
  // src/http-status.ts
552
790
  var CHITMARK_HTTP_STATUS = {
553
791
  ok: 200,
@@ -585,6 +823,32 @@ function verdictToHttpStatus(verdict) {
585
823
  return CHITMARK_HTTP_STATUS.ok;
586
824
  }
587
825
 
826
+ // src/action-binding.ts
827
+ var opaqueId = /^[A-Za-z0-9_.:-]{1,128}$/;
828
+ async function trialCreditOperationDigest(operation) {
829
+ const { operationId, target, quota, recipient } = operation;
830
+ if (Object.keys(operation).some(
831
+ (key) => !["operationId", "target", "quota", "recipient"].includes(key)
832
+ ) || typeof operationId !== "string" || !opaqueId.test(operationId) || typeof target !== "string" || !opaqueId.test(target) || !Number.isSafeInteger(quota) || quota < 1 || recipient !== void 0 && (typeof recipient !== "string" || !opaqueId.test(recipient))) {
833
+ throw new TypeError("Invalid trial credit operation");
834
+ }
835
+ const bytes = new TextEncoder().encode(
836
+ JSON.stringify([
837
+ "chitmark-action-v1",
838
+ "trial_activation",
839
+ operationId,
840
+ target,
841
+ quota,
842
+ recipient ?? null
843
+ ])
844
+ );
845
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
846
+ return Array.from(
847
+ new Uint8Array(digest),
848
+ (byte) => byte.toString(16).padStart(2, "0")
849
+ ).join("");
850
+ }
851
+
588
852
  // src/trusted-ip.ts
589
853
  function getTrustedClientIp(req) {
590
854
  const platform = req.headers.get("cf-connecting-ip")?.trim() || req.headers.get("x-real-ip")?.trim() || req.headers.get("x-vercel-forwarded-for")?.split(",")[0]?.trim() || "";
@@ -647,6 +911,345 @@ async function verifyNextRequest(client, req, options) {
647
911
  });
648
912
  }
649
913
 
650
- export { CHITMARK_HTTP_STATUS, CHITMARK_HTTP_STATUS_MEANING, Chitmark, ChitmarkApiError, ChitmarkError, VERDICT_TOKEN_ISSUER, VerdictTokenError, chitmarkGate, degradedChallengeVerdict, enforceDegradedNeverAllow, getTrustedClientIp, hashEmailSha256, isEligibleAllow, minimizeFormData, truncateIp, verdictToHttpStatus, verifyNextRequest, verifyVerdictToken };
914
+ // src/evidence.ts
915
+ var EvidenceError = class extends Error {
916
+ constructor(code) {
917
+ super(code);
918
+ this.code = code;
919
+ this.name = "EvidenceError";
920
+ }
921
+ code;
922
+ };
923
+ function evidenceAssert(value, code) {
924
+ if (!value) throw new EvidenceError(code);
925
+ }
926
+ function canonicalEvidence(value) {
927
+ if (value === null || typeof value === "boolean") return JSON.stringify(value);
928
+ if (typeof value === "number") {
929
+ evidenceAssert(
930
+ Number.isSafeInteger(value) && !Object.is(value, -0),
931
+ "noncanonical_number"
932
+ );
933
+ return String(value);
934
+ }
935
+ if (typeof value === "string") {
936
+ evidenceAssert(/^[\x20-\x7e]*$/.test(value), "noncanonical_string");
937
+ return JSON.stringify(value);
938
+ }
939
+ if (Array.isArray(value)) return `[${value.map(canonicalEvidence).join(",")}]`;
940
+ evidenceAssert(
941
+ value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype,
942
+ "noncanonical_object"
943
+ );
944
+ const obj = value;
945
+ return `{${Object.keys(obj).sort().map((key) => `${canonicalEvidence(key)}:${canonicalEvidence(obj[key])}`).join(",")}}`;
946
+ }
947
+ async function evidenceHash(value) {
948
+ return Array.from(
949
+ new Uint8Array(
950
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
951
+ ),
952
+ (b) => b.toString(16).padStart(2, "0")
953
+ ).join("");
954
+ }
955
+ var encode2 = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
956
+ var decode = (part) => Uint8Array.from(
957
+ atob(part.replace(/-/g, "+").replace(/_/g, "/")),
958
+ (c) => c.charCodeAt(0)
959
+ );
960
+ function evidenceKeyAt(history, kid, at) {
961
+ const matches = history.filter((key2) => key2.kid === kid);
962
+ evidenceAssert(matches.length === 1, "unknown_trust_root");
963
+ const key = matches[0];
964
+ evidenceAssert(
965
+ key.jwk.kty === "EC" && key.jwk.crv === "P-256" && (!key.jwk.alg || key.jwk.alg === "ES256") && !key.jwk.d,
966
+ "unsupported_key"
967
+ );
968
+ evidenceAssert(
969
+ Number.isSafeInteger(at) && Number.isSafeInteger(key.notBefore) && Number.isSafeInteger(key.notAfter) && key.notBefore <= at && at < key.notAfter,
970
+ "key_outside_validity"
971
+ );
972
+ evidenceAssert(key.revokedAt === void 0, "revoked_key");
973
+ return key;
974
+ }
975
+ async function verifyEvidenceSignature(token, typ, history, at) {
976
+ evidenceAssert(
977
+ typeof token === "string" && token.length <= 8e6 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token),
978
+ "malformed_signature"
979
+ );
980
+ const [h, p, s] = token.split(".");
981
+ let head, payload;
982
+ try {
983
+ head = JSON.parse(
984
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(h))
985
+ );
986
+ payload = JSON.parse(
987
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(p))
988
+ );
989
+ } catch {
990
+ throw new EvidenceError("malformed_signature");
991
+ }
992
+ evidenceAssert(
993
+ head?.alg === "ES256" && head.typ === typ && Object.keys(head).length === 3,
994
+ "unsupported_profile"
995
+ );
996
+ const trusted = evidenceKeyAt(history, head.kid, at);
997
+ const key = await crypto.subtle.importKey(
998
+ "jwk",
999
+ trusted.jwk,
1000
+ { name: "ECDSA", namedCurve: "P-256" },
1001
+ false,
1002
+ ["verify"]
1003
+ );
1004
+ evidenceAssert(
1005
+ await crypto.subtle.verify(
1006
+ { name: "ECDSA", hash: "SHA-256" },
1007
+ key,
1008
+ decode(s),
1009
+ new TextEncoder().encode(`${h}.${p}`)
1010
+ ),
1011
+ "invalid_signature"
1012
+ );
1013
+ evidenceAssert(
1014
+ payload && typeof payload === "object" && !Array.isArray(payload),
1015
+ "malformed_payload"
1016
+ );
1017
+ return payload;
1018
+ }
1019
+ async function signEvidence(payload, typ, kid, jwk) {
1020
+ evidenceAssert(
1021
+ jwk.kty === "EC" && jwk.crv === "P-256" && (!jwk.alg || jwk.alg === "ES256"),
1022
+ "unsupported_key"
1023
+ );
1024
+ const input = `${encode2(new TextEncoder().encode(canonicalEvidence({ alg: "ES256", kid, typ })))}.${encode2(new TextEncoder().encode(canonicalEvidence(payload)))}`;
1025
+ evidenceAssert(input.length + 87 <= 8e6, "bundle_too_large");
1026
+ const key = await crypto.subtle.importKey(
1027
+ "jwk",
1028
+ jwk,
1029
+ { name: "ECDSA", namedCurve: "P-256" },
1030
+ false,
1031
+ ["sign"]
1032
+ );
1033
+ return `${input}.${encode2(new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, new TextEncoder().encode(input))))}`;
1034
+ }
1035
+ var id = /^[A-Za-z0-9_.:/-]{1,160}$/;
1036
+ function validateManifest(value, tenantId) {
1037
+ evidenceAssert(
1038
+ Object.keys(value).sort().join(",") === "completeness,createdAt,endAt,gaps,issuer,purpose,records,retention,sourceAsOf,startAt,tenantId,timeBasis,version" && Object.keys(value.retention).sort().join(",") === "deleteAfter,exportedCopies,recipient,redaction",
1039
+ "unminimized_manifest"
1040
+ );
1041
+ canonicalEvidence(value);
1042
+ evidenceAssert(
1043
+ value.version === 1 && value.issuer === "https://portal.chitmark.com" && value.purpose === "historical_evidence" && value.tenantId === tenantId && id.test(tenantId),
1044
+ "wrong_bundle_scope"
1045
+ );
1046
+ evidenceAssert(
1047
+ [
1048
+ value.startAt,
1049
+ value.endAt,
1050
+ value.createdAt,
1051
+ value.sourceAsOf,
1052
+ value.retention.deleteAfter
1053
+ ].every(Number.isSafeInteger) && value.startAt < value.endAt && value.endAt <= value.sourceAsOf && value.sourceAsOf <= value.createdAt && value.createdAt < value.retention.deleteAfter,
1054
+ "invalid_window"
1055
+ );
1056
+ evidenceAssert(
1057
+ value.completeness === "unknown" && value.timeBasis === "issuer_and_ingestion_clocks_not_independent_time" && value.retention.redaction === "minimized" && value.retention.exportedCopies === "recipient_controlled" && id.test(value.retention.recipient) && Array.isArray(value.gaps) && value.gaps.every((gap) => id.test(gap)),
1058
+ "invalid_manifest"
1059
+ );
1060
+ evidenceAssert(
1061
+ Array.isArray(value.records) && value.records.length <= 1e4,
1062
+ "record_limit"
1063
+ );
1064
+ const seen = /* @__PURE__ */ new Set();
1065
+ for (const row of value.records) {
1066
+ evidenceAssert(
1067
+ Object.keys(row).sort().join(",") === "eventId,id,kind,occurredAt,policyRevision,receipt,recordedAt,result,source",
1068
+ "unminimized_record"
1069
+ );
1070
+ evidenceAssert(
1071
+ id.test(row.id) && id.test(row.eventId) && ["decision", "execution", "outcome"].includes(row.kind) && id.test(row.result) && (row.policyRevision === null || id.test(row.policyRevision)) && (row.receipt === null || typeof row.receipt === "string" && row.receipt.length <= 8192),
1072
+ "invalid_record"
1073
+ );
1074
+ evidenceAssert(
1075
+ Number.isSafeInteger(row.occurredAt) && Number.isSafeInteger(row.recordedAt) && value.startAt <= row.occurredAt && row.occurredAt < value.endAt && row.recordedAt <= value.sourceAsOf && row.recordedAt >= row.occurredAt && row.source === (row.kind === "decision" ? "decision_service" : "customer_report"),
1076
+ "invalid_provenance"
1077
+ );
1078
+ const identity = `${row.kind}:${row.id}`;
1079
+ evidenceAssert(!seen.has(identity), "duplicate_record");
1080
+ seen.add(identity);
1081
+ }
1082
+ }
1083
+ async function verifyEvidenceBundle(bundle, options) {
1084
+ validateManifest(bundle.manifest, options.tenantId);
1085
+ evidenceAssert(
1086
+ bundle.manifest.retention.recipient === options.recipient,
1087
+ "wrong_recipient"
1088
+ );
1089
+ evidenceAssert(
1090
+ options.now < bundle.manifest.retention.deleteAfter && options.now >= bundle.manifest.createdAt,
1091
+ "retention_expired"
1092
+ );
1093
+ const signed = await verifyEvidenceSignature(
1094
+ bundle.signature,
1095
+ "chitmark-evidence+jwt",
1096
+ options.trustedKeys,
1097
+ bundle.manifest.createdAt
1098
+ );
1099
+ evidenceAssert(
1100
+ canonicalEvidence(signed) === canonicalEvidence(bundle.manifest),
1101
+ "manifest_mismatch"
1102
+ );
1103
+ const head = JSON.parse(
1104
+ new TextDecoder().decode(decode(bundle.signature.split(".")[0]))
1105
+ );
1106
+ evidenceAssert(
1107
+ evidenceKeyAt(options.trustedKeys, head.kid, bundle.manifest.createdAt).compromisedAt === void 0,
1108
+ "compromised_bundle_key"
1109
+ );
1110
+ return {
1111
+ purpose: "historical_evidence",
1112
+ executionAuthorized: false,
1113
+ integrity: "valid",
1114
+ completeness: "unknown",
1115
+ outcomeTruth: "not_established",
1116
+ gaps: bundle.manifest.gaps
1117
+ };
1118
+ }
1119
+ async function verifyHistoricalReceipt(token, options) {
1120
+ evidenceAssert(
1121
+ typeof token === "string" && token.length <= 8192,
1122
+ "malformed_signature"
1123
+ );
1124
+ let claims, head;
1125
+ try {
1126
+ const parts = token.split(".");
1127
+ head = JSON.parse(new TextDecoder().decode(decode(parts[0])));
1128
+ claims = JSON.parse(new TextDecoder().decode(decode(parts[1])));
1129
+ } catch {
1130
+ throw new EvidenceError("malformed_signature");
1131
+ }
1132
+ evidenceAssert(
1133
+ head.typ === "JWT" || head.typ === "chitmark-action+jwt",
1134
+ "unsupported_profile"
1135
+ );
1136
+ evidenceAssert(
1137
+ Number.isSafeInteger(claims.iat) && Number.isSafeInteger(claims.exp) && Number(claims.iat) < Number(claims.exp) && Number(claims.exp) - Number(claims.iat) <= (head.typ === "JWT" ? 300 : 60),
1138
+ "invalid_receipt_time"
1139
+ );
1140
+ const at = Number(claims.iat);
1141
+ evidenceAssert(
1142
+ typeof claims.eventId === "string" && /^[A-Za-z0-9_-]{1,160}$/.test(claims.eventId),
1143
+ "invalid_receipt_context"
1144
+ );
1145
+ const trusted = evidenceKeyAt(options.trustedKeys, String(head.kid), at);
1146
+ claims = await verifyEvidenceSignature(
1147
+ token,
1148
+ String(head.typ),
1149
+ options.trustedKeys,
1150
+ at
1151
+ );
1152
+ evidenceAssert(
1153
+ claims.tid === options.tenantId && claims.sub === options.session && claims.iss === "https://api.chitmark.com",
1154
+ "wrong_receipt_scope"
1155
+ );
1156
+ const witnessed = options.witness && Number.isSafeInteger(options.witness.observedAt) && options.witness.observedAt >= at && options.witness.tokenHash === await evidenceHash(token);
1157
+ const beforeCompromise = witnessed && (trusted.compromisedAt === void 0 || options.witness.observedAt < trusted.compromisedAt);
1158
+ return {
1159
+ purpose: "historical_evidence",
1160
+ executionAuthorized: false,
1161
+ eventId: String(claims.eventId),
1162
+ policyRevision: typeof claims.policy === "string" ? claims.policy : null,
1163
+ timeIntegrity: beforeCompromise ? "independently_checkpointed" : trusted.compromisedAt !== void 0 ? "uncertain_after_compromise" : "self_asserted"
1164
+ };
1165
+ }
1166
+
1167
+ // src/evidence-checkpoint.ts
1168
+ var hex = /^[a-f0-9]{64}$/;
1169
+ function scope(checkpoint, tenantId, now) {
1170
+ evidenceAssert(
1171
+ checkpoint.version === 1 && checkpoint.tenantId === tenantId && /^[A-Za-z0-9_-]{16,128}$/.test(checkpoint.epoch) && Number.isSafeInteger(checkpoint.expiresAt) && now < checkpoint.expiresAt && Number.isSafeInteger(checkpoint.observedAt) && checkpoint.observedAt <= now && checkpoint.observedAt < checkpoint.expiresAt && Number.isSafeInteger(checkpoint.size) && checkpoint.size >= 0 && checkpoint.size <= 1e4 && hex.test(checkpoint.root),
1172
+ "checkpoint_scope_or_expiry"
1173
+ );
1174
+ }
1175
+ var genesis = (cp) => evidenceHash(
1176
+ canonicalEvidence(["chitmark-private-chain-v1", cp.tenantId, cp.epoch, cp.expiresAt])
1177
+ );
1178
+ var evidenceCommitment = (cp, record, salt) => {
1179
+ evidenceAssert(hex.test(salt), "invalid_commitment_salt");
1180
+ return evidenceHash(
1181
+ canonicalEvidence([
1182
+ "chitmark-record-v1",
1183
+ cp.tenantId,
1184
+ cp.epoch,
1185
+ cp.expiresAt,
1186
+ salt,
1187
+ record
1188
+ ])
1189
+ );
1190
+ };
1191
+ var advance = (root, leaf, index) => evidenceHash(canonicalEvidence(["chitmark-append-v1", index, root, leaf]));
1192
+ async function checkpointFromCommitments(input) {
1193
+ evidenceAssert(
1194
+ input.commitments.length <= 1e4 && input.commitments.every((leaf) => hex.test(leaf)),
1195
+ "invalid_commitments"
1196
+ );
1197
+ let root = await genesis(input);
1198
+ for (let index = 0; index < input.commitments.length; index++)
1199
+ root = await advance(root, input.commitments[index], index);
1200
+ const cp = {
1201
+ version: 1,
1202
+ tenantId: input.tenantId,
1203
+ epoch: input.epoch,
1204
+ expiresAt: input.expiresAt,
1205
+ observedAt: input.observedAt,
1206
+ size: input.commitments.length,
1207
+ root
1208
+ };
1209
+ scope(cp, input.tenantId, input.observedAt);
1210
+ return cp;
1211
+ }
1212
+ async function verifyCheckpointAppend(older, newer, appended, options) {
1213
+ scope(older, options.tenantId, options.now);
1214
+ scope(newer, options.tenantId, options.now);
1215
+ if (older.size === 0)
1216
+ evidenceAssert(older.root === await genesis(older), "invalid_empty_checkpoint");
1217
+ evidenceAssert(
1218
+ older.epoch === newer.epoch && older.expiresAt === newer.expiresAt && older.observedAt <= newer.observedAt && older.size <= newer.size,
1219
+ "inconsistent_checkpoint"
1220
+ );
1221
+ if (older.size === newer.size)
1222
+ evidenceAssert(older.root === newer.root, "divergent_history");
1223
+ if (appended === null)
1224
+ return { status: "unavailable", completeness: "unknown" };
1225
+ evidenceAssert(
1226
+ appended.length === newer.size - older.size && appended.every((leaf) => hex.test(leaf)),
1227
+ "invalid_append_proof"
1228
+ );
1229
+ let root = older.root;
1230
+ for (let index = 0; index < appended.length; index++)
1231
+ root = await advance(root, appended[index], older.size + index);
1232
+ evidenceAssert(root === newer.root, "divergent_history");
1233
+ return { status: "consistent", completeness: "unknown" };
1234
+ }
1235
+ async function verifyCheckpointInclusion(checkpoint, proof, options) {
1236
+ scope(checkpoint, options.tenantId, options.now);
1237
+ if (!proof) return { status: "unavailable", completeness: "unknown" };
1238
+ evidenceAssert(
1239
+ Number.isSafeInteger(proof.index) && proof.index >= 0 && proof.index < checkpoint.size && proof.commitments.length === checkpoint.size,
1240
+ "invalid_inclusion_proof"
1241
+ );
1242
+ const rebuilt = await checkpointFromCommitments({
1243
+ ...checkpoint,
1244
+ commitments: proof.commitments
1245
+ });
1246
+ evidenceAssert(
1247
+ rebuilt.root === checkpoint.root && await evidenceCommitment(checkpoint, proof.record, proof.salt) === proof.commitments[proof.index],
1248
+ "changed_record"
1249
+ );
1250
+ return { status: "included", completeness: "unknown" };
1251
+ }
1252
+
1253
+ export { CHITMARK_HTTP_STATUS, CHITMARK_HTTP_STATUS_MEANING, Chitmark, ChitmarkApiError, ChitmarkError, EvidenceError, SENSITIVE_RECEIPT_ACTIONS, VERDICT_TOKEN_ISSUER, VerdictTokenError, canonicalEvidence, checkpointFromCommitments, chitmarkGate, createDPoPCredentialProvider, degradedChallengeVerdict, enforceDegradedNeverAllow, evidenceAssert, evidenceCommitment, evidenceHash, evidenceKeyAt, getTrustedClientIp, hashEmailSha256, isEligibleAllow, minimizeFormData, signEvidence, summarizeCharge, trialCreditOperationDigest, truncateIp, validateManifest, verdictToHttpStatus, verifyActionReceipt, verifyCheckpointAppend, verifyCheckpointInclusion, verifyEvidenceBundle, verifyEvidenceSignature, verifyHistoricalReceipt, verifyNextRequest, verifyVerdictToken };
651
1254
  //# sourceMappingURL=index.js.map
652
1255
  //# sourceMappingURL=index.js.map