@chitmark/sdk 0.6.5 → 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/edge.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.6.5";
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
  }
@@ -592,6 +772,371 @@ function verdictToHttpStatus(verdict) {
592
772
  return CHITMARK_HTTP_STATUS.ok;
593
773
  }
594
774
 
775
+ // src/action-binding.ts
776
+ var opaqueId = /^[A-Za-z0-9_.:-]{1,128}$/;
777
+ async function trialCreditOperationDigest(operation) {
778
+ const { operationId, target, quota, recipient } = operation;
779
+ if (Object.keys(operation).some(
780
+ (key) => !["operationId", "target", "quota", "recipient"].includes(key)
781
+ ) || 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))) {
782
+ throw new TypeError("Invalid trial credit operation");
783
+ }
784
+ const bytes = new TextEncoder().encode(
785
+ JSON.stringify([
786
+ "chitmark-action-v1",
787
+ "trial_activation",
788
+ operationId,
789
+ target,
790
+ quota,
791
+ recipient ?? null
792
+ ])
793
+ );
794
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
795
+ return Array.from(
796
+ new Uint8Array(digest),
797
+ (byte) => byte.toString(16).padStart(2, "0")
798
+ ).join("");
799
+ }
800
+
801
+ // src/evidence.ts
802
+ var EvidenceError = class extends Error {
803
+ constructor(code) {
804
+ super(code);
805
+ this.code = code;
806
+ this.name = "EvidenceError";
807
+ }
808
+ code;
809
+ };
810
+ function evidenceAssert(value, code) {
811
+ if (!value) throw new EvidenceError(code);
812
+ }
813
+ function canonicalEvidence(value) {
814
+ if (value === null || typeof value === "boolean") return JSON.stringify(value);
815
+ if (typeof value === "number") {
816
+ evidenceAssert(
817
+ Number.isSafeInteger(value) && !Object.is(value, -0),
818
+ "noncanonical_number"
819
+ );
820
+ return String(value);
821
+ }
822
+ if (typeof value === "string") {
823
+ evidenceAssert(/^[\x20-\x7e]*$/.test(value), "noncanonical_string");
824
+ return JSON.stringify(value);
825
+ }
826
+ if (Array.isArray(value)) return `[${value.map(canonicalEvidence).join(",")}]`;
827
+ evidenceAssert(
828
+ value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype,
829
+ "noncanonical_object"
830
+ );
831
+ const obj = value;
832
+ return `{${Object.keys(obj).sort().map((key) => `${canonicalEvidence(key)}:${canonicalEvidence(obj[key])}`).join(",")}}`;
833
+ }
834
+ async function evidenceHash(value) {
835
+ return Array.from(
836
+ new Uint8Array(
837
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
838
+ ),
839
+ (b) => b.toString(16).padStart(2, "0")
840
+ ).join("");
841
+ }
842
+ var encode2 = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
843
+ var decode = (part) => Uint8Array.from(
844
+ atob(part.replace(/-/g, "+").replace(/_/g, "/")),
845
+ (c) => c.charCodeAt(0)
846
+ );
847
+ function evidenceKeyAt(history, kid, at) {
848
+ const matches = history.filter((key2) => key2.kid === kid);
849
+ evidenceAssert(matches.length === 1, "unknown_trust_root");
850
+ const key = matches[0];
851
+ evidenceAssert(
852
+ key.jwk.kty === "EC" && key.jwk.crv === "P-256" && (!key.jwk.alg || key.jwk.alg === "ES256") && !key.jwk.d,
853
+ "unsupported_key"
854
+ );
855
+ evidenceAssert(
856
+ Number.isSafeInteger(at) && Number.isSafeInteger(key.notBefore) && Number.isSafeInteger(key.notAfter) && key.notBefore <= at && at < key.notAfter,
857
+ "key_outside_validity"
858
+ );
859
+ evidenceAssert(key.revokedAt === void 0, "revoked_key");
860
+ return key;
861
+ }
862
+ async function verifyEvidenceSignature(token, typ, history, at) {
863
+ evidenceAssert(
864
+ typeof token === "string" && token.length <= 8e6 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token),
865
+ "malformed_signature"
866
+ );
867
+ const [h, p, s] = token.split(".");
868
+ let head, payload;
869
+ try {
870
+ head = JSON.parse(
871
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(h))
872
+ );
873
+ payload = JSON.parse(
874
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(p))
875
+ );
876
+ } catch {
877
+ throw new EvidenceError("malformed_signature");
878
+ }
879
+ evidenceAssert(
880
+ head?.alg === "ES256" && head.typ === typ && Object.keys(head).length === 3,
881
+ "unsupported_profile"
882
+ );
883
+ const trusted = evidenceKeyAt(history, head.kid, at);
884
+ const key = await crypto.subtle.importKey(
885
+ "jwk",
886
+ trusted.jwk,
887
+ { name: "ECDSA", namedCurve: "P-256" },
888
+ false,
889
+ ["verify"]
890
+ );
891
+ evidenceAssert(
892
+ await crypto.subtle.verify(
893
+ { name: "ECDSA", hash: "SHA-256" },
894
+ key,
895
+ decode(s),
896
+ new TextEncoder().encode(`${h}.${p}`)
897
+ ),
898
+ "invalid_signature"
899
+ );
900
+ evidenceAssert(
901
+ payload && typeof payload === "object" && !Array.isArray(payload),
902
+ "malformed_payload"
903
+ );
904
+ return payload;
905
+ }
906
+ async function signEvidence(payload, typ, kid, jwk) {
907
+ evidenceAssert(
908
+ jwk.kty === "EC" && jwk.crv === "P-256" && (!jwk.alg || jwk.alg === "ES256"),
909
+ "unsupported_key"
910
+ );
911
+ const input = `${encode2(new TextEncoder().encode(canonicalEvidence({ alg: "ES256", kid, typ })))}.${encode2(new TextEncoder().encode(canonicalEvidence(payload)))}`;
912
+ evidenceAssert(input.length + 87 <= 8e6, "bundle_too_large");
913
+ const key = await crypto.subtle.importKey(
914
+ "jwk",
915
+ jwk,
916
+ { name: "ECDSA", namedCurve: "P-256" },
917
+ false,
918
+ ["sign"]
919
+ );
920
+ return `${input}.${encode2(new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, new TextEncoder().encode(input))))}`;
921
+ }
922
+ var id = /^[A-Za-z0-9_.:/-]{1,160}$/;
923
+ function validateManifest(value, tenantId) {
924
+ evidenceAssert(
925
+ 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",
926
+ "unminimized_manifest"
927
+ );
928
+ canonicalEvidence(value);
929
+ evidenceAssert(
930
+ value.version === 1 && value.issuer === "https://portal.chitmark.com" && value.purpose === "historical_evidence" && value.tenantId === tenantId && id.test(tenantId),
931
+ "wrong_bundle_scope"
932
+ );
933
+ evidenceAssert(
934
+ [
935
+ value.startAt,
936
+ value.endAt,
937
+ value.createdAt,
938
+ value.sourceAsOf,
939
+ value.retention.deleteAfter
940
+ ].every(Number.isSafeInteger) && value.startAt < value.endAt && value.endAt <= value.sourceAsOf && value.sourceAsOf <= value.createdAt && value.createdAt < value.retention.deleteAfter,
941
+ "invalid_window"
942
+ );
943
+ evidenceAssert(
944
+ 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)),
945
+ "invalid_manifest"
946
+ );
947
+ evidenceAssert(
948
+ Array.isArray(value.records) && value.records.length <= 1e4,
949
+ "record_limit"
950
+ );
951
+ const seen = /* @__PURE__ */ new Set();
952
+ for (const row of value.records) {
953
+ evidenceAssert(
954
+ Object.keys(row).sort().join(",") === "eventId,id,kind,occurredAt,policyRevision,receipt,recordedAt,result,source",
955
+ "unminimized_record"
956
+ );
957
+ evidenceAssert(
958
+ 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),
959
+ "invalid_record"
960
+ );
961
+ evidenceAssert(
962
+ 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"),
963
+ "invalid_provenance"
964
+ );
965
+ const identity = `${row.kind}:${row.id}`;
966
+ evidenceAssert(!seen.has(identity), "duplicate_record");
967
+ seen.add(identity);
968
+ }
969
+ }
970
+ async function verifyEvidenceBundle(bundle, options) {
971
+ validateManifest(bundle.manifest, options.tenantId);
972
+ evidenceAssert(
973
+ bundle.manifest.retention.recipient === options.recipient,
974
+ "wrong_recipient"
975
+ );
976
+ evidenceAssert(
977
+ options.now < bundle.manifest.retention.deleteAfter && options.now >= bundle.manifest.createdAt,
978
+ "retention_expired"
979
+ );
980
+ const signed = await verifyEvidenceSignature(
981
+ bundle.signature,
982
+ "chitmark-evidence+jwt",
983
+ options.trustedKeys,
984
+ bundle.manifest.createdAt
985
+ );
986
+ evidenceAssert(
987
+ canonicalEvidence(signed) === canonicalEvidence(bundle.manifest),
988
+ "manifest_mismatch"
989
+ );
990
+ const head = JSON.parse(
991
+ new TextDecoder().decode(decode(bundle.signature.split(".")[0]))
992
+ );
993
+ evidenceAssert(
994
+ evidenceKeyAt(options.trustedKeys, head.kid, bundle.manifest.createdAt).compromisedAt === void 0,
995
+ "compromised_bundle_key"
996
+ );
997
+ return {
998
+ purpose: "historical_evidence",
999
+ executionAuthorized: false,
1000
+ integrity: "valid",
1001
+ completeness: "unknown",
1002
+ outcomeTruth: "not_established",
1003
+ gaps: bundle.manifest.gaps
1004
+ };
1005
+ }
1006
+ async function verifyHistoricalReceipt(token, options) {
1007
+ evidenceAssert(
1008
+ typeof token === "string" && token.length <= 8192,
1009
+ "malformed_signature"
1010
+ );
1011
+ let claims, head;
1012
+ try {
1013
+ const parts = token.split(".");
1014
+ head = JSON.parse(new TextDecoder().decode(decode(parts[0])));
1015
+ claims = JSON.parse(new TextDecoder().decode(decode(parts[1])));
1016
+ } catch {
1017
+ throw new EvidenceError("malformed_signature");
1018
+ }
1019
+ evidenceAssert(
1020
+ head.typ === "JWT" || head.typ === "chitmark-action+jwt",
1021
+ "unsupported_profile"
1022
+ );
1023
+ evidenceAssert(
1024
+ 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),
1025
+ "invalid_receipt_time"
1026
+ );
1027
+ const at = Number(claims.iat);
1028
+ evidenceAssert(
1029
+ typeof claims.eventId === "string" && /^[A-Za-z0-9_-]{1,160}$/.test(claims.eventId),
1030
+ "invalid_receipt_context"
1031
+ );
1032
+ const trusted = evidenceKeyAt(options.trustedKeys, String(head.kid), at);
1033
+ claims = await verifyEvidenceSignature(
1034
+ token,
1035
+ String(head.typ),
1036
+ options.trustedKeys,
1037
+ at
1038
+ );
1039
+ evidenceAssert(
1040
+ claims.tid === options.tenantId && claims.sub === options.session && claims.iss === "https://api.chitmark.com",
1041
+ "wrong_receipt_scope"
1042
+ );
1043
+ const witnessed = options.witness && Number.isSafeInteger(options.witness.observedAt) && options.witness.observedAt >= at && options.witness.tokenHash === await evidenceHash(token);
1044
+ const beforeCompromise = witnessed && (trusted.compromisedAt === void 0 || options.witness.observedAt < trusted.compromisedAt);
1045
+ return {
1046
+ purpose: "historical_evidence",
1047
+ executionAuthorized: false,
1048
+ eventId: String(claims.eventId),
1049
+ policyRevision: typeof claims.policy === "string" ? claims.policy : null,
1050
+ timeIntegrity: beforeCompromise ? "independently_checkpointed" : trusted.compromisedAt !== void 0 ? "uncertain_after_compromise" : "self_asserted"
1051
+ };
1052
+ }
1053
+
1054
+ // src/evidence-checkpoint.ts
1055
+ var hex = /^[a-f0-9]{64}$/;
1056
+ function scope(checkpoint, tenantId, now) {
1057
+ evidenceAssert(
1058
+ 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),
1059
+ "checkpoint_scope_or_expiry"
1060
+ );
1061
+ }
1062
+ var genesis = (cp) => evidenceHash(
1063
+ canonicalEvidence(["chitmark-private-chain-v1", cp.tenantId, cp.epoch, cp.expiresAt])
1064
+ );
1065
+ var evidenceCommitment = (cp, record, salt) => {
1066
+ evidenceAssert(hex.test(salt), "invalid_commitment_salt");
1067
+ return evidenceHash(
1068
+ canonicalEvidence([
1069
+ "chitmark-record-v1",
1070
+ cp.tenantId,
1071
+ cp.epoch,
1072
+ cp.expiresAt,
1073
+ salt,
1074
+ record
1075
+ ])
1076
+ );
1077
+ };
1078
+ var advance = (root, leaf, index) => evidenceHash(canonicalEvidence(["chitmark-append-v1", index, root, leaf]));
1079
+ async function checkpointFromCommitments(input) {
1080
+ evidenceAssert(
1081
+ input.commitments.length <= 1e4 && input.commitments.every((leaf) => hex.test(leaf)),
1082
+ "invalid_commitments"
1083
+ );
1084
+ let root = await genesis(input);
1085
+ for (let index = 0; index < input.commitments.length; index++)
1086
+ root = await advance(root, input.commitments[index], index);
1087
+ const cp = {
1088
+ version: 1,
1089
+ tenantId: input.tenantId,
1090
+ epoch: input.epoch,
1091
+ expiresAt: input.expiresAt,
1092
+ observedAt: input.observedAt,
1093
+ size: input.commitments.length,
1094
+ root
1095
+ };
1096
+ scope(cp, input.tenantId, input.observedAt);
1097
+ return cp;
1098
+ }
1099
+ async function verifyCheckpointAppend(older, newer, appended, options) {
1100
+ scope(older, options.tenantId, options.now);
1101
+ scope(newer, options.tenantId, options.now);
1102
+ if (older.size === 0)
1103
+ evidenceAssert(older.root === await genesis(older), "invalid_empty_checkpoint");
1104
+ evidenceAssert(
1105
+ older.epoch === newer.epoch && older.expiresAt === newer.expiresAt && older.observedAt <= newer.observedAt && older.size <= newer.size,
1106
+ "inconsistent_checkpoint"
1107
+ );
1108
+ if (older.size === newer.size)
1109
+ evidenceAssert(older.root === newer.root, "divergent_history");
1110
+ if (appended === null)
1111
+ return { status: "unavailable", completeness: "unknown" };
1112
+ evidenceAssert(
1113
+ appended.length === newer.size - older.size && appended.every((leaf) => hex.test(leaf)),
1114
+ "invalid_append_proof"
1115
+ );
1116
+ let root = older.root;
1117
+ for (let index = 0; index < appended.length; index++)
1118
+ root = await advance(root, appended[index], older.size + index);
1119
+ evidenceAssert(root === newer.root, "divergent_history");
1120
+ return { status: "consistent", completeness: "unknown" };
1121
+ }
1122
+ async function verifyCheckpointInclusion(checkpoint, proof, options) {
1123
+ scope(checkpoint, options.tenantId, options.now);
1124
+ if (!proof) return { status: "unavailable", completeness: "unknown" };
1125
+ evidenceAssert(
1126
+ Number.isSafeInteger(proof.index) && proof.index >= 0 && proof.index < checkpoint.size && proof.commitments.length === checkpoint.size,
1127
+ "invalid_inclusion_proof"
1128
+ );
1129
+ const rebuilt = await checkpointFromCommitments({
1130
+ ...checkpoint,
1131
+ commitments: proof.commitments
1132
+ });
1133
+ evidenceAssert(
1134
+ rebuilt.root === checkpoint.root && await evidenceCommitment(checkpoint, proof.record, proof.salt) === proof.commitments[proof.index],
1135
+ "changed_record"
1136
+ );
1137
+ return { status: "included", completeness: "unknown" };
1138
+ }
1139
+
595
1140
  // src/edge.ts
596
1141
  var ChitmarkEdge = class extends Chitmark {
597
1142
  /**
@@ -621,6 +1166,6 @@ var ChitmarkEdge = class extends Chitmark {
621
1166
  }
622
1167
  };
623
1168
 
624
- export { CHITMARK_HTTP_STATUS, CHITMARK_HTTP_STATUS_MEANING, ChitmarkEdge as Chitmark, ChitmarkApiError, Chitmark as ChitmarkCore, ChitmarkEdge, ChitmarkError, VERDICT_TOKEN_ISSUER, VerdictTokenError, degradedChallengeVerdict, enforceDegradedNeverAllow, getTrustedClientIp, hashEmailSha256, isEligibleAllow, minimizeFormData, truncateIp, verdictToHttpStatus, verifyVerdictToken };
1169
+ export { CHITMARK_HTTP_STATUS, CHITMARK_HTTP_STATUS_MEANING, ChitmarkEdge as Chitmark, ChitmarkApiError, Chitmark as ChitmarkCore, ChitmarkEdge, ChitmarkError, EvidenceError, SENSITIVE_RECEIPT_ACTIONS, VERDICT_TOKEN_ISSUER, VerdictTokenError, canonicalEvidence, checkpointFromCommitments, createDPoPCredentialProvider, degradedChallengeVerdict, enforceDegradedNeverAllow, evidenceAssert, evidenceCommitment, evidenceHash, evidenceKeyAt, getTrustedClientIp, hashEmailSha256, isEligibleAllow, minimizeFormData, signEvidence, trialCreditOperationDigest, truncateIp, validateManifest, verdictToHttpStatus, verifyActionReceipt, verifyCheckpointAppend, verifyCheckpointInclusion, verifyEvidenceBundle, verifyEvidenceSignature, verifyHistoricalReceipt, verifyVerdictToken };
625
1170
  //# sourceMappingURL=edge.js.map
626
1171
  //# sourceMappingURL=edge.js.map