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