@chitmark/sdk 0.7.0 → 0.8.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.cjs CHANGED
@@ -1,5 +1,96 @@
1
1
  'use strict';
2
2
 
3
+ // src/attribution.ts
4
+ function attributionEvidence(options, mode) {
5
+ const { request, agentProof, session, piiMode = mode } = options;
6
+ const headers = request.headers;
7
+ const header = typeof headers.get === "function" ? headers.get("chitmark-agent-proof") : Object.entries(headers).find(
8
+ ([name]) => name.toLowerCase() === "chitmark-agent-proof"
9
+ )?.[1];
10
+ let site = options.site;
11
+ if (!site && request.url?.startsWith("https://")) site = new URL(request.url).origin;
12
+ let proof = agentProof;
13
+ if (!proof && typeof header === "string" && header.length <= 2048 && site) {
14
+ try {
15
+ proof = JSON.parse(header);
16
+ } catch {
17
+ }
18
+ }
19
+ return { site, session, piiMode, agentProof: proof };
20
+ }
21
+
22
+ // src/credentials.ts
23
+ var encode = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
24
+ var json = (value) => encode(new TextEncoder().encode(JSON.stringify(value)));
25
+ async function boundedCredential(work, signal) {
26
+ signal.throwIfAborted();
27
+ let abort;
28
+ const aborted = new Promise((_, reject) => {
29
+ abort = () => reject(new Error("Credential deadline exceeded"));
30
+ signal.addEventListener("abort", abort, { once: true });
31
+ });
32
+ try {
33
+ return await Promise.race([work, aborted]);
34
+ } finally {
35
+ signal.removeEventListener("abort", abort);
36
+ }
37
+ }
38
+ function createDPoPCredentialProvider(options) {
39
+ const { privateKey, publicJwk } = options;
40
+ if (publicJwk.d || publicJwk.kty !== "EC" || publicJwk.crv !== "P-256" || !publicJwk.x || !publicJwk.y)
41
+ throw new Error("DPoP requires a public P-256 JWK");
42
+ const jwk = { kty: "EC", crv: "P-256", x: publicJwk.x, y: publicJwk.y };
43
+ let cached;
44
+ let pending;
45
+ const valid = (c) => !!c && typeof c.accessToken === "string" && /^[\w-]+\.[\w-]+\.[\w-]+$/.test(c.accessToken) && [c.expiresAt, c.trustExpiresAt, c.revocationValidUntil].every(
46
+ (v) => Number.isSafeInteger(v) && v > Date.now() / 1e3 + 1
47
+ );
48
+ return {
49
+ async headers({ url, method, signal }) {
50
+ signal.throwIfAborted();
51
+ const target = new URL(url);
52
+ if (target.protocol !== "https:" || target.username || target.password)
53
+ throw new Error("DPoP requires HTTPS");
54
+ if (!valid(cached)) {
55
+ if (!pending) {
56
+ const refreshSignal = AbortSignal.timeout(options.refreshTimeoutMs ?? 500);
57
+ pending = Promise.resolve().then(() => options.load(refreshSignal)).then((c) => {
58
+ if (!valid(c) || refreshSignal.aborted) throw new Error("Stale credential");
59
+ cached = { ...c };
60
+ return cached;
61
+ }).finally(() => {
62
+ pending = void 0;
63
+ });
64
+ }
65
+ await boundedCredential(
66
+ pending,
67
+ AbortSignal.any([signal, AbortSignal.timeout(options.refreshTimeoutMs ?? 500)])
68
+ );
69
+ }
70
+ if (!valid(cached)) throw new Error("Stale credential");
71
+ const credential = cached;
72
+ const token = credential.accessToken;
73
+ const ath = encode(
74
+ new Uint8Array(
75
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))
76
+ )
77
+ );
78
+ 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 })}`;
79
+ const signature = await crypto.subtle.sign(
80
+ { name: "ECDSA", hash: "SHA-256" },
81
+ privateKey,
82
+ new TextEncoder().encode(payload)
83
+ );
84
+ signal.throwIfAborted();
85
+ if (!valid(credential)) throw new Error("Stale credential");
86
+ return {
87
+ Authorization: `DPoP ${token}`,
88
+ DPoP: `${payload}.${encode(new Uint8Array(signature))}`
89
+ };
90
+ }
91
+ };
92
+ }
93
+
3
94
  // src/errors.ts
4
95
  var ChitmarkError = class extends Error {
5
96
  name = "ChitmarkError";
@@ -62,7 +153,7 @@ var DEFAULT_BASE_URLS = {
62
153
  };
63
154
  var DEFAULT_TIMEOUT_MS = 800;
64
155
  var LOCAL_TIMEOUT_MS = 2e3;
65
- var SDK_VERSION = "0.7.0";
156
+ var SDK_VERSION = "0.8.0";
66
157
  function defaultTimeoutMs(baseUrl) {
67
158
  try {
68
159
  const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -125,11 +216,11 @@ function minimizeFormData(formData) {
125
216
  }
126
217
  function bufferToHex(buffer) {
127
218
  const bytes = new Uint8Array(buffer);
128
- let hex = "";
219
+ let hex2 = "";
129
220
  for (const b of bytes) {
130
- hex += b.toString(16).padStart(2, "0");
221
+ hex2 += b.toString(16).padStart(2, "0");
131
222
  }
132
- return hex;
223
+ return hex2;
133
224
  }
134
225
 
135
226
  // src/internal/prepare.ts
@@ -205,6 +296,46 @@ var VerdictTokenError = class extends ChitmarkError {
205
296
  var DEFAULT_JWKS_URL = "https://api.chitmark.com/.well-known/jwks.json";
206
297
  var DEFAULT_LEEWAY_SECONDS = 30;
207
298
  async function verifyVerdictToken(token, options) {
299
+ return verifyReceipt(token, options, "JWT");
300
+ }
301
+ var SENSITIVE_RECEIPT_ACTIONS = [
302
+ "api_key",
303
+ "data_export",
304
+ "session_establish",
305
+ "privilege_elevate",
306
+ "authenticator_replace",
307
+ "recovery_contact_change",
308
+ "session_use",
309
+ "privilege_change",
310
+ "release_deploy",
311
+ "bulk_mutation",
312
+ "commitment_change",
313
+ "agent_effect",
314
+ "agent_escalation",
315
+ "quote_generate",
316
+ "queue_priority"
317
+ ];
318
+ async function verifyActionReceipt(token, options) {
319
+ const claims = await verifyReceipt(
320
+ token,
321
+ {
322
+ ...options,
323
+ aud: options.audience,
324
+ issuer: VERDICT_TOKEN_ISSUER,
325
+ leewaySeconds: 0
326
+ },
327
+ "chitmark-action+jwt"
328
+ );
329
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
330
+ 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) {
331
+ throw new VerdictTokenError(
332
+ "wrong_action_binding",
333
+ "Receipt cannot authorize this operation"
334
+ );
335
+ }
336
+ return claims;
337
+ }
338
+ async function verifyReceipt(token, options, expectedType) {
208
339
  const nowMs = options.nowMs ?? Date.now();
209
340
  const leewaySeconds = options.leewaySeconds ?? DEFAULT_LEEWAY_SECONDS;
210
341
  const parts = token.split(".");
@@ -226,10 +357,10 @@ async function verifyVerdictToken(token, options) {
226
357
  `verdictToken must use ES256, got ${String(header["alg"])}`
227
358
  );
228
359
  }
229
- if (header["typ"] !== "JWT") {
360
+ if (header["typ"] !== expectedType) {
230
361
  throw new VerdictTokenError(
231
362
  "wrong_type",
232
- `verdictToken must be a JWT, got ${String(header["typ"])}`
363
+ `verdictToken must have type ${expectedType}, got ${String(header["typ"])}`
233
364
  );
234
365
  }
235
366
  const kid = typeof header["kid"] === "string" ? header["kid"] : "";
@@ -275,6 +406,12 @@ async function verifyVerdictToken(token, options) {
275
406
  if (!jwk) {
276
407
  throw new VerdictTokenError("unknown_kid", `JWKS has no key for kid ${kid}`);
277
408
  }
409
+ if (jwk["kty"] !== "EC" || jwk["crv"] !== "P-256" || jwk["alg"] !== void 0 && jwk["alg"] !== "ES256") {
410
+ throw new VerdictTokenError(
411
+ "wrong_algorithm",
412
+ "Verification requires an ES256 P-256 key"
413
+ );
414
+ }
278
415
  let publicKey;
279
416
  try {
280
417
  publicKey = await crypto.subtle.importKey(
@@ -347,7 +484,27 @@ function decodeBase64UrlBytes(value) {
347
484
 
348
485
  // src/client.ts
349
486
  var Chitmark = class {
487
+ attribution = {
488
+ resolve: (input) => this.request("/v1/attributions/resolve", {
489
+ method: "POST",
490
+ body: attributionEvidence(input, this.piiMode),
491
+ idempotencyKey: input.idempotencyKey
492
+ }),
493
+ get: (id2) => this.request(`/v1/attributions/${encodeURIComponent(id2)}`, { method: "GET" }),
494
+ createLink: (input, idempotencyKey) => this.request("/v1/identity-links", { method: "POST", body: input, idempotencyKey }),
495
+ revokeLink: (id2) => this.request(`/v1/identity-links/${encodeURIComponent(id2)}`, { method: "DELETE" })
496
+ };
497
+ /** Paid access lifecycle. Transport uncertainty must be resumed, never treated as payment. */
498
+ async charge(request) {
499
+ return this.request("/v1/charge", {
500
+ method: "POST",
501
+ body: request,
502
+ idempotencyKey: request.idempotencyKey,
503
+ timeoutMs: Math.max(this.timeoutMs, 15e3)
504
+ });
505
+ }
350
506
  apiKey;
507
+ credentialProvider;
351
508
  baseUrl;
352
509
  timeoutMs;
353
510
  piiMode;
@@ -355,7 +512,7 @@ var Chitmark = class {
355
512
  fetchImpl;
356
513
  clientName;
357
514
  constructor(options) {
358
- if (!options.apiKey) {
515
+ if (!!options.apiKey === !!options.credentialProvider) {
359
516
  throw new ChitmarkError("Chitmark apiKey is required");
360
517
  }
361
518
  if (options.onDegraded && options.onDegraded !== "challenge") {
@@ -364,6 +521,7 @@ var Chitmark = class {
364
521
  );
365
522
  }
366
523
  this.apiKey = options.apiKey;
524
+ this.credentialProvider = options.credentialProvider;
367
525
  this.baseUrl = resolveBaseUrl(options.env, options.baseUrl ?? envBaseUrl());
368
526
  this.timeoutMs = options.timeoutMs ?? defaultTimeoutMs(this.baseUrl);
369
527
  this.piiMode = options.piiMode ?? "hashed";
@@ -456,7 +614,8 @@ var Chitmark = class {
456
614
  /**
457
615
  * Complete a challenge by submitting its solved proof. Clears only the bound
458
616
  * session, once. Does not authorize the action: re-verify with
459
- * `context.challengeId` and proceed only on non-degraded allow.
617
+ * `context.challengeId` for standalone challenges or the original action
618
+ * binding plus the challenge and event IDs for operation-bound challenges.
460
619
  */
461
620
  async completeChallenge(request) {
462
621
  return this.request("/v1/challenge", {
@@ -484,15 +643,35 @@ var Chitmark = class {
484
643
  */
485
644
  async request(path, init) {
486
645
  const controller = new AbortController();
487
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
646
+ const timeoutMs = init.timeoutMs ?? this.timeoutMs;
647
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
488
648
  try {
489
649
  const headers = {
490
- Authorization: `Bearer ${this.apiKey}`,
650
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
491
651
  "Content-Type": "application/json",
492
652
  Accept: "application/json",
493
653
  "User-Agent": this.clientName,
494
654
  "X-Chitmark-Client": this.clientName
495
655
  };
656
+ if (this.credentialProvider) {
657
+ try {
658
+ Object.assign(
659
+ headers,
660
+ await boundedCredential(
661
+ this.credentialProvider.headers({
662
+ url: `${this.baseUrl}${path}`,
663
+ method: init.method,
664
+ signal: controller.signal
665
+ }),
666
+ controller.signal
667
+ )
668
+ );
669
+ } catch {
670
+ throw new ChitmarkError("Credential unavailable", { code: "credential" });
671
+ }
672
+ if (!headers.Authorization?.startsWith("DPoP ") || !headers.DPoP)
673
+ throw new ChitmarkError("Invalid DPoP credentials");
674
+ }
496
675
  if (init.idempotencyKey) {
497
676
  headers["Idempotency-Key"] = init.idempotencyKey;
498
677
  }
@@ -500,7 +679,8 @@ var Chitmark = class {
500
679
  method: init.method,
501
680
  headers,
502
681
  body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
503
- signal: controller.signal
682
+ signal: controller.signal,
683
+ redirect: "error"
504
684
  });
505
685
  const text = await res.text();
506
686
  let parsed = void 0;
@@ -524,7 +704,7 @@ var Chitmark = class {
524
704
  } catch (err) {
525
705
  if (err instanceof ChitmarkApiError) throw err;
526
706
  if (err instanceof Error && err.name === "AbortError") {
527
- throw new ChitmarkError(`Request timed out after ${this.timeoutMs}ms`, {
707
+ throw new ChitmarkError(`Request timed out after ${timeoutMs}ms`, {
528
708
  code: "timeout"
529
709
  });
530
710
  }
@@ -550,6 +730,38 @@ function isWellFormedVerdict(value) {
550
730
  return typeof v.decision === "string" && VERDICT_DECISIONS.has(v.decision) && typeof v.eventId === "string" && v.eventId.length > 0 && typeof v.degraded === "boolean";
551
731
  }
552
732
 
733
+ // src/charge.ts
734
+ function summarizeCharge(p, now = Date.now(), sellerCostCents) {
735
+ if (sellerCostCents !== void 0 && (!Number.isSafeInteger(sellerCostCents) || sellerCostCents < 0))
736
+ throw new Error("Invalid seller cost");
737
+ const deliveredUnits = p.delivery.executions.reduce(
738
+ (n, e) => n + (e.state === "delivered" ? e.usedUnits : 0),
739
+ 0
740
+ );
741
+ const unresolvedUnits = p.delivery.executions.reduce(
742
+ (n, e) => n + (e.state === "reserved" || e.state === "unknown" ? e.units : 0),
743
+ 0
744
+ );
745
+ const net = p.payment.providerFee === null ? null : p.payment.capturedAmount - p.payment.refundedAmount - p.payment.providerFee;
746
+ return {
747
+ paidCompletion: p.payment.state === "settled" && p.payment.capturedAmount > 0 && p.payment.refundedAmount === 0 && p.payment.disputedAmount === 0 && deliveredUnits === p.quote.quantity,
748
+ billedCents: p.payment.capturedAmount - p.payment.refundedAmount,
749
+ sellerReportedDeliveredCents: deliveredUnits * p.quote.resource.unitAmount,
750
+ deliveredUnits,
751
+ unresolvedUnits,
752
+ 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,
753
+ refundedCents: p.payment.refundedAmount,
754
+ disputedCents: p.payment.disputedAmount,
755
+ captureFeeCents: p.payment.providerFee,
756
+ sellerNetAfterCaptureFeeCents: net,
757
+ sellerMarginCents: net === null || sellerCostCents === void 0 ? null : net - sellerCostCents,
758
+ // Provider/application reconciliation must supply duplicate evidence; retries alone do not prove absence.
759
+ duplicateCharges: null,
760
+ duplicateEffects: null,
761
+ observedElapsedMs: Math.max(0, p.updatedAt - p.createdAt)
762
+ };
763
+ }
764
+
553
765
  // src/http-status.ts
554
766
  var CHITMARK_HTTP_STATUS = {
555
767
  ok: 200,
@@ -587,6 +799,32 @@ function verdictToHttpStatus(verdict) {
587
799
  return CHITMARK_HTTP_STATUS.ok;
588
800
  }
589
801
 
802
+ // src/action-binding.ts
803
+ var opaqueId = /^[A-Za-z0-9_.:-]{1,128}$/;
804
+ async function trialCreditOperationDigest(operation) {
805
+ const { operationId, target, quota, recipient } = operation;
806
+ if (Object.keys(operation).some(
807
+ (key) => !["operationId", "target", "quota", "recipient"].includes(key)
808
+ ) || 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))) {
809
+ throw new TypeError("Invalid trial credit operation");
810
+ }
811
+ const bytes = new TextEncoder().encode(
812
+ JSON.stringify([
813
+ "chitmark-action-v1",
814
+ "trial_activation",
815
+ operationId,
816
+ target,
817
+ quota,
818
+ recipient ?? null
819
+ ])
820
+ );
821
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
822
+ return Array.from(
823
+ new Uint8Array(digest),
824
+ (byte) => byte.toString(16).padStart(2, "0")
825
+ ).join("");
826
+ }
827
+
590
828
  // src/trusted-ip.ts
591
829
  function getTrustedClientIp(req) {
592
830
  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() || "";
@@ -649,22 +887,380 @@ async function verifyNextRequest(client, req, options) {
649
887
  });
650
888
  }
651
889
 
890
+ // src/evidence.ts
891
+ var EvidenceError = class extends Error {
892
+ constructor(code) {
893
+ super(code);
894
+ this.code = code;
895
+ this.name = "EvidenceError";
896
+ }
897
+ code;
898
+ };
899
+ function evidenceAssert(value, code) {
900
+ if (!value) throw new EvidenceError(code);
901
+ }
902
+ function canonicalEvidence(value) {
903
+ if (value === null || typeof value === "boolean") return JSON.stringify(value);
904
+ if (typeof value === "number") {
905
+ evidenceAssert(
906
+ Number.isSafeInteger(value) && !Object.is(value, -0),
907
+ "noncanonical_number"
908
+ );
909
+ return String(value);
910
+ }
911
+ if (typeof value === "string") {
912
+ evidenceAssert(/^[\x20-\x7e]*$/.test(value), "noncanonical_string");
913
+ return JSON.stringify(value);
914
+ }
915
+ if (Array.isArray(value)) return `[${value.map(canonicalEvidence).join(",")}]`;
916
+ evidenceAssert(
917
+ value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype,
918
+ "noncanonical_object"
919
+ );
920
+ const obj = value;
921
+ return `{${Object.keys(obj).sort().map((key) => `${canonicalEvidence(key)}:${canonicalEvidence(obj[key])}`).join(",")}}`;
922
+ }
923
+ async function evidenceHash(value) {
924
+ return Array.from(
925
+ new Uint8Array(
926
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
927
+ ),
928
+ (b) => b.toString(16).padStart(2, "0")
929
+ ).join("");
930
+ }
931
+ var encode2 = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
932
+ var decode = (part) => Uint8Array.from(
933
+ atob(part.replace(/-/g, "+").replace(/_/g, "/")),
934
+ (c) => c.charCodeAt(0)
935
+ );
936
+ function evidenceKeyAt(history, kid, at) {
937
+ const matches = history.filter((key2) => key2.kid === kid);
938
+ evidenceAssert(matches.length === 1, "unknown_trust_root");
939
+ const key = matches[0];
940
+ evidenceAssert(
941
+ key.jwk.kty === "EC" && key.jwk.crv === "P-256" && (!key.jwk.alg || key.jwk.alg === "ES256") && !key.jwk.d,
942
+ "unsupported_key"
943
+ );
944
+ evidenceAssert(
945
+ Number.isSafeInteger(at) && Number.isSafeInteger(key.notBefore) && Number.isSafeInteger(key.notAfter) && key.notBefore <= at && at < key.notAfter,
946
+ "key_outside_validity"
947
+ );
948
+ evidenceAssert(key.revokedAt === void 0, "revoked_key");
949
+ return key;
950
+ }
951
+ async function verifyEvidenceSignature(token, typ, history, at) {
952
+ evidenceAssert(
953
+ typeof token === "string" && token.length <= 8e6 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token),
954
+ "malformed_signature"
955
+ );
956
+ const [h, p, s] = token.split(".");
957
+ let head, payload;
958
+ try {
959
+ head = JSON.parse(
960
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(h))
961
+ );
962
+ payload = JSON.parse(
963
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(p))
964
+ );
965
+ } catch {
966
+ throw new EvidenceError("malformed_signature");
967
+ }
968
+ evidenceAssert(
969
+ head?.alg === "ES256" && head.typ === typ && Object.keys(head).length === 3,
970
+ "unsupported_profile"
971
+ );
972
+ const trusted = evidenceKeyAt(history, head.kid, at);
973
+ const key = await crypto.subtle.importKey(
974
+ "jwk",
975
+ trusted.jwk,
976
+ { name: "ECDSA", namedCurve: "P-256" },
977
+ false,
978
+ ["verify"]
979
+ );
980
+ evidenceAssert(
981
+ await crypto.subtle.verify(
982
+ { name: "ECDSA", hash: "SHA-256" },
983
+ key,
984
+ decode(s),
985
+ new TextEncoder().encode(`${h}.${p}`)
986
+ ),
987
+ "invalid_signature"
988
+ );
989
+ evidenceAssert(
990
+ payload && typeof payload === "object" && !Array.isArray(payload),
991
+ "malformed_payload"
992
+ );
993
+ return payload;
994
+ }
995
+ async function signEvidence(payload, typ, kid, jwk) {
996
+ evidenceAssert(
997
+ jwk.kty === "EC" && jwk.crv === "P-256" && (!jwk.alg || jwk.alg === "ES256"),
998
+ "unsupported_key"
999
+ );
1000
+ const input = `${encode2(new TextEncoder().encode(canonicalEvidence({ alg: "ES256", kid, typ })))}.${encode2(new TextEncoder().encode(canonicalEvidence(payload)))}`;
1001
+ evidenceAssert(input.length + 87 <= 8e6, "bundle_too_large");
1002
+ const key = await crypto.subtle.importKey(
1003
+ "jwk",
1004
+ jwk,
1005
+ { name: "ECDSA", namedCurve: "P-256" },
1006
+ false,
1007
+ ["sign"]
1008
+ );
1009
+ return `${input}.${encode2(new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, new TextEncoder().encode(input))))}`;
1010
+ }
1011
+ var id = /^[A-Za-z0-9_.:/-]{1,160}$/;
1012
+ function validateManifest(value, tenantId) {
1013
+ evidenceAssert(
1014
+ 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",
1015
+ "unminimized_manifest"
1016
+ );
1017
+ canonicalEvidence(value);
1018
+ evidenceAssert(
1019
+ value.version === 1 && value.issuer === "https://portal.chitmark.com" && value.purpose === "historical_evidence" && value.tenantId === tenantId && id.test(tenantId),
1020
+ "wrong_bundle_scope"
1021
+ );
1022
+ evidenceAssert(
1023
+ [
1024
+ value.startAt,
1025
+ value.endAt,
1026
+ value.createdAt,
1027
+ value.sourceAsOf,
1028
+ value.retention.deleteAfter
1029
+ ].every(Number.isSafeInteger) && value.startAt < value.endAt && value.endAt <= value.sourceAsOf && value.sourceAsOf <= value.createdAt && value.createdAt < value.retention.deleteAfter,
1030
+ "invalid_window"
1031
+ );
1032
+ evidenceAssert(
1033
+ 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)),
1034
+ "invalid_manifest"
1035
+ );
1036
+ evidenceAssert(
1037
+ Array.isArray(value.records) && value.records.length <= 1e4,
1038
+ "record_limit"
1039
+ );
1040
+ const seen = /* @__PURE__ */ new Set();
1041
+ for (const row of value.records) {
1042
+ evidenceAssert(
1043
+ Object.keys(row).sort().join(",") === "eventId,id,kind,occurredAt,policyRevision,receipt,recordedAt,result,source",
1044
+ "unminimized_record"
1045
+ );
1046
+ evidenceAssert(
1047
+ 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),
1048
+ "invalid_record"
1049
+ );
1050
+ evidenceAssert(
1051
+ 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"),
1052
+ "invalid_provenance"
1053
+ );
1054
+ const identity = `${row.kind}:${row.id}`;
1055
+ evidenceAssert(!seen.has(identity), "duplicate_record");
1056
+ seen.add(identity);
1057
+ }
1058
+ }
1059
+ async function verifyEvidenceBundle(bundle, options) {
1060
+ validateManifest(bundle.manifest, options.tenantId);
1061
+ evidenceAssert(
1062
+ bundle.manifest.retention.recipient === options.recipient,
1063
+ "wrong_recipient"
1064
+ );
1065
+ evidenceAssert(
1066
+ options.now < bundle.manifest.retention.deleteAfter && options.now >= bundle.manifest.createdAt,
1067
+ "retention_expired"
1068
+ );
1069
+ const signed = await verifyEvidenceSignature(
1070
+ bundle.signature,
1071
+ "chitmark-evidence+jwt",
1072
+ options.trustedKeys,
1073
+ bundle.manifest.createdAt
1074
+ );
1075
+ evidenceAssert(
1076
+ canonicalEvidence(signed) === canonicalEvidence(bundle.manifest),
1077
+ "manifest_mismatch"
1078
+ );
1079
+ const head = JSON.parse(
1080
+ new TextDecoder().decode(decode(bundle.signature.split(".")[0]))
1081
+ );
1082
+ evidenceAssert(
1083
+ evidenceKeyAt(options.trustedKeys, head.kid, bundle.manifest.createdAt).compromisedAt === void 0,
1084
+ "compromised_bundle_key"
1085
+ );
1086
+ return {
1087
+ purpose: "historical_evidence",
1088
+ executionAuthorized: false,
1089
+ integrity: "valid",
1090
+ completeness: "unknown",
1091
+ outcomeTruth: "not_established",
1092
+ gaps: bundle.manifest.gaps
1093
+ };
1094
+ }
1095
+ async function verifyHistoricalReceipt(token, options) {
1096
+ evidenceAssert(
1097
+ typeof token === "string" && token.length <= 8192,
1098
+ "malformed_signature"
1099
+ );
1100
+ let claims, head;
1101
+ try {
1102
+ const parts = token.split(".");
1103
+ head = JSON.parse(new TextDecoder().decode(decode(parts[0])));
1104
+ claims = JSON.parse(new TextDecoder().decode(decode(parts[1])));
1105
+ } catch {
1106
+ throw new EvidenceError("malformed_signature");
1107
+ }
1108
+ evidenceAssert(
1109
+ head.typ === "JWT" || head.typ === "chitmark-action+jwt",
1110
+ "unsupported_profile"
1111
+ );
1112
+ evidenceAssert(
1113
+ 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),
1114
+ "invalid_receipt_time"
1115
+ );
1116
+ const at = Number(claims.iat);
1117
+ evidenceAssert(
1118
+ typeof claims.eventId === "string" && /^[A-Za-z0-9_-]{1,160}$/.test(claims.eventId),
1119
+ "invalid_receipt_context"
1120
+ );
1121
+ const trusted = evidenceKeyAt(options.trustedKeys, String(head.kid), at);
1122
+ claims = await verifyEvidenceSignature(
1123
+ token,
1124
+ String(head.typ),
1125
+ options.trustedKeys,
1126
+ at
1127
+ );
1128
+ evidenceAssert(
1129
+ claims.tid === options.tenantId && claims.sub === options.session && claims.iss === "https://api.chitmark.com",
1130
+ "wrong_receipt_scope"
1131
+ );
1132
+ const witnessed = options.witness && Number.isSafeInteger(options.witness.observedAt) && options.witness.observedAt >= at && options.witness.tokenHash === await evidenceHash(token);
1133
+ const beforeCompromise = witnessed && (trusted.compromisedAt === void 0 || options.witness.observedAt < trusted.compromisedAt);
1134
+ return {
1135
+ purpose: "historical_evidence",
1136
+ executionAuthorized: false,
1137
+ eventId: String(claims.eventId),
1138
+ policyRevision: typeof claims.policy === "string" ? claims.policy : null,
1139
+ timeIntegrity: beforeCompromise ? "independently_checkpointed" : trusted.compromisedAt !== void 0 ? "uncertain_after_compromise" : "self_asserted"
1140
+ };
1141
+ }
1142
+
1143
+ // src/evidence-checkpoint.ts
1144
+ var hex = /^[a-f0-9]{64}$/;
1145
+ function scope(checkpoint, tenantId, now) {
1146
+ evidenceAssert(
1147
+ 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),
1148
+ "checkpoint_scope_or_expiry"
1149
+ );
1150
+ }
1151
+ var genesis = (cp) => evidenceHash(
1152
+ canonicalEvidence(["chitmark-private-chain-v1", cp.tenantId, cp.epoch, cp.expiresAt])
1153
+ );
1154
+ var evidenceCommitment = (cp, record, salt) => {
1155
+ evidenceAssert(hex.test(salt), "invalid_commitment_salt");
1156
+ return evidenceHash(
1157
+ canonicalEvidence([
1158
+ "chitmark-record-v1",
1159
+ cp.tenantId,
1160
+ cp.epoch,
1161
+ cp.expiresAt,
1162
+ salt,
1163
+ record
1164
+ ])
1165
+ );
1166
+ };
1167
+ var advance = (root, leaf, index) => evidenceHash(canonicalEvidence(["chitmark-append-v1", index, root, leaf]));
1168
+ async function checkpointFromCommitments(input) {
1169
+ evidenceAssert(
1170
+ input.commitments.length <= 1e4 && input.commitments.every((leaf) => hex.test(leaf)),
1171
+ "invalid_commitments"
1172
+ );
1173
+ let root = await genesis(input);
1174
+ for (let index = 0; index < input.commitments.length; index++)
1175
+ root = await advance(root, input.commitments[index], index);
1176
+ const cp = {
1177
+ version: 1,
1178
+ tenantId: input.tenantId,
1179
+ epoch: input.epoch,
1180
+ expiresAt: input.expiresAt,
1181
+ observedAt: input.observedAt,
1182
+ size: input.commitments.length,
1183
+ root
1184
+ };
1185
+ scope(cp, input.tenantId, input.observedAt);
1186
+ return cp;
1187
+ }
1188
+ async function verifyCheckpointAppend(older, newer, appended, options) {
1189
+ scope(older, options.tenantId, options.now);
1190
+ scope(newer, options.tenantId, options.now);
1191
+ if (older.size === 0)
1192
+ evidenceAssert(older.root === await genesis(older), "invalid_empty_checkpoint");
1193
+ evidenceAssert(
1194
+ older.epoch === newer.epoch && older.expiresAt === newer.expiresAt && older.observedAt <= newer.observedAt && older.size <= newer.size,
1195
+ "inconsistent_checkpoint"
1196
+ );
1197
+ if (older.size === newer.size)
1198
+ evidenceAssert(older.root === newer.root, "divergent_history");
1199
+ if (appended === null)
1200
+ return { status: "unavailable", completeness: "unknown" };
1201
+ evidenceAssert(
1202
+ appended.length === newer.size - older.size && appended.every((leaf) => hex.test(leaf)),
1203
+ "invalid_append_proof"
1204
+ );
1205
+ let root = older.root;
1206
+ for (let index = 0; index < appended.length; index++)
1207
+ root = await advance(root, appended[index], older.size + index);
1208
+ evidenceAssert(root === newer.root, "divergent_history");
1209
+ return { status: "consistent", completeness: "unknown" };
1210
+ }
1211
+ async function verifyCheckpointInclusion(checkpoint, proof, options) {
1212
+ scope(checkpoint, options.tenantId, options.now);
1213
+ if (!proof) return { status: "unavailable", completeness: "unknown" };
1214
+ evidenceAssert(
1215
+ Number.isSafeInteger(proof.index) && proof.index >= 0 && proof.index < checkpoint.size && proof.commitments.length === checkpoint.size,
1216
+ "invalid_inclusion_proof"
1217
+ );
1218
+ const rebuilt = await checkpointFromCommitments({
1219
+ ...checkpoint,
1220
+ commitments: proof.commitments
1221
+ });
1222
+ evidenceAssert(
1223
+ rebuilt.root === checkpoint.root && await evidenceCommitment(checkpoint, proof.record, proof.salt) === proof.commitments[proof.index],
1224
+ "changed_record"
1225
+ );
1226
+ return { status: "included", completeness: "unknown" };
1227
+ }
1228
+
652
1229
  exports.CHITMARK_HTTP_STATUS = CHITMARK_HTTP_STATUS;
653
1230
  exports.CHITMARK_HTTP_STATUS_MEANING = CHITMARK_HTTP_STATUS_MEANING;
654
1231
  exports.Chitmark = Chitmark;
655
1232
  exports.ChitmarkApiError = ChitmarkApiError;
656
1233
  exports.ChitmarkError = ChitmarkError;
1234
+ exports.EvidenceError = EvidenceError;
1235
+ exports.SENSITIVE_RECEIPT_ACTIONS = SENSITIVE_RECEIPT_ACTIONS;
657
1236
  exports.VERDICT_TOKEN_ISSUER = VERDICT_TOKEN_ISSUER;
658
1237
  exports.VerdictTokenError = VerdictTokenError;
1238
+ exports.canonicalEvidence = canonicalEvidence;
1239
+ exports.checkpointFromCommitments = checkpointFromCommitments;
659
1240
  exports.chitmarkGate = chitmarkGate;
1241
+ exports.createDPoPCredentialProvider = createDPoPCredentialProvider;
660
1242
  exports.degradedChallengeVerdict = degradedChallengeVerdict;
661
1243
  exports.enforceDegradedNeverAllow = enforceDegradedNeverAllow;
1244
+ exports.evidenceAssert = evidenceAssert;
1245
+ exports.evidenceCommitment = evidenceCommitment;
1246
+ exports.evidenceHash = evidenceHash;
1247
+ exports.evidenceKeyAt = evidenceKeyAt;
662
1248
  exports.getTrustedClientIp = getTrustedClientIp;
663
1249
  exports.hashEmailSha256 = hashEmailSha256;
664
1250
  exports.isEligibleAllow = isEligibleAllow;
665
1251
  exports.minimizeFormData = minimizeFormData;
1252
+ exports.signEvidence = signEvidence;
1253
+ exports.summarizeCharge = summarizeCharge;
1254
+ exports.trialCreditOperationDigest = trialCreditOperationDigest;
666
1255
  exports.truncateIp = truncateIp;
1256
+ exports.validateManifest = validateManifest;
667
1257
  exports.verdictToHttpStatus = verdictToHttpStatus;
1258
+ exports.verifyActionReceipt = verifyActionReceipt;
1259
+ exports.verifyCheckpointAppend = verifyCheckpointAppend;
1260
+ exports.verifyCheckpointInclusion = verifyCheckpointInclusion;
1261
+ exports.verifyEvidenceBundle = verifyEvidenceBundle;
1262
+ exports.verifyEvidenceSignature = verifyEvidenceSignature;
1263
+ exports.verifyHistoricalReceipt = verifyHistoricalReceipt;
668
1264
  exports.verifyNextRequest = verifyNextRequest;
669
1265
  exports.verifyVerdictToken = verifyVerdictToken;
670
1266
  //# sourceMappingURL=index.cjs.map