@chitmark/sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/edge.cjs CHANGED
@@ -1,5 +1,122 @@
1
1
  'use strict';
2
2
 
3
+ // src/attribution.ts
4
+ function attributionEvidence(options, mode) {
5
+ const { request, agentProof, session, purpose, 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 url;
11
+ if (!options.site && request.url) {
12
+ try {
13
+ url = new URL(request.url);
14
+ } catch {
15
+ }
16
+ }
17
+ const site = options.site ?? (url?.protocol === "https:" ? url.origin : void 0);
18
+ const proof = agentProof ?? (site ? parseProof(header) : void 0);
19
+ return { site, session, piiMode, purpose, agentProof: proof };
20
+ }
21
+ function parseProof(header) {
22
+ if (typeof header !== "string" || header.length > 2048) return;
23
+ try {
24
+ const value = JSON.parse(header);
25
+ if (!value || typeof value.nonce !== "string" || value.nonce.length > 36 || typeof value.issuedAt !== "number" || !Number.isSafeInteger(value.issuedAt) || typeof value.signature !== "string" || value.signature.length > 86)
26
+ return;
27
+ const { nonce, issuedAt, signature } = value;
28
+ if ("iss" in value) {
29
+ const key = value.agentKey;
30
+ if (value.iss === "chitmark-agent-key-v1" && key?.kty === "OKP" && key.crv === "Ed25519" && typeof key.x === "string" && key.x.length === 43 && !("d" in key)) {
31
+ return {
32
+ iss: value.iss,
33
+ agentKey: { kty: key.kty, crv: key.crv, x: key.x },
34
+ nonce,
35
+ issuedAt,
36
+ signature
37
+ };
38
+ }
39
+ return;
40
+ }
41
+ if (typeof value.linkId === "string" && value.linkId.length <= 36)
42
+ return { linkId: value.linkId, nonce, issuedAt, signature };
43
+ } catch {
44
+ }
45
+ return void 0;
46
+ }
47
+
48
+ // src/credentials.ts
49
+ var encode = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
50
+ var json = (value) => encode(new TextEncoder().encode(JSON.stringify(value)));
51
+ async function boundedCredential(work, signal) {
52
+ signal.throwIfAborted();
53
+ let abort;
54
+ const aborted = new Promise((_, reject) => {
55
+ abort = () => reject(new Error("Credential deadline exceeded"));
56
+ signal.addEventListener("abort", abort, { once: true });
57
+ });
58
+ try {
59
+ return await Promise.race([work, aborted]);
60
+ } finally {
61
+ signal.removeEventListener("abort", abort);
62
+ }
63
+ }
64
+ function createDPoPCredentialProvider(options) {
65
+ const { privateKey, publicJwk } = options;
66
+ if (publicJwk.d || publicJwk.kty !== "EC" || publicJwk.crv !== "P-256" || !publicJwk.x || !publicJwk.y)
67
+ throw new Error("DPoP requires a public P-256 JWK");
68
+ const jwk = { kty: "EC", crv: "P-256", x: publicJwk.x, y: publicJwk.y };
69
+ let cached;
70
+ let pending;
71
+ const valid = (c) => !!c && typeof c.accessToken === "string" && /^[\w-]+\.[\w-]+\.[\w-]+$/.test(c.accessToken) && [c.expiresAt, c.trustExpiresAt, c.revocationValidUntil].every(
72
+ (v) => Number.isSafeInteger(v) && v > Date.now() / 1e3 + 1
73
+ );
74
+ return {
75
+ async headers({ url, method, signal }) {
76
+ signal.throwIfAborted();
77
+ const target = new URL(url);
78
+ if (target.protocol !== "https:" || target.username || target.password)
79
+ throw new Error("DPoP requires HTTPS");
80
+ if (!valid(cached)) {
81
+ if (!pending) {
82
+ const refreshSignal = AbortSignal.timeout(options.refreshTimeoutMs ?? 500);
83
+ pending = Promise.resolve().then(() => options.load(refreshSignal)).then((c) => {
84
+ if (!valid(c) || refreshSignal.aborted) throw new Error("Stale credential");
85
+ cached = { ...c };
86
+ return cached;
87
+ }).finally(() => {
88
+ pending = void 0;
89
+ });
90
+ }
91
+ await boundedCredential(
92
+ pending,
93
+ AbortSignal.any([signal, AbortSignal.timeout(options.refreshTimeoutMs ?? 500)])
94
+ );
95
+ }
96
+ if (!valid(cached)) throw new Error("Stale credential");
97
+ const credential = cached;
98
+ const token = credential.accessToken;
99
+ const ath = encode(
100
+ new Uint8Array(
101
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))
102
+ )
103
+ );
104
+ 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 })}`;
105
+ const signature = await crypto.subtle.sign(
106
+ { name: "ECDSA", hash: "SHA-256" },
107
+ privateKey,
108
+ new TextEncoder().encode(payload)
109
+ );
110
+ signal.throwIfAborted();
111
+ if (!valid(credential)) throw new Error("Stale credential");
112
+ return {
113
+ Authorization: `DPoP ${token}`,
114
+ DPoP: `${payload}.${encode(new Uint8Array(signature))}`
115
+ };
116
+ }
117
+ };
118
+ }
119
+
3
120
  // src/errors.ts
4
121
  var ChitmarkError = class extends Error {
5
122
  name = "ChitmarkError";
@@ -62,7 +179,7 @@ var DEFAULT_BASE_URLS = {
62
179
  };
63
180
  var DEFAULT_TIMEOUT_MS = 800;
64
181
  var LOCAL_TIMEOUT_MS = 2e3;
65
- var SDK_VERSION = "0.7.0";
182
+ var SDK_VERSION = "0.9.0";
66
183
  function defaultTimeoutMs(baseUrl) {
67
184
  try {
68
185
  const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -125,11 +242,11 @@ function minimizeFormData(formData) {
125
242
  }
126
243
  function bufferToHex(buffer) {
127
244
  const bytes = new Uint8Array(buffer);
128
- let hex = "";
245
+ let hex2 = "";
129
246
  for (const b of bytes) {
130
- hex += b.toString(16).padStart(2, "0");
247
+ hex2 += b.toString(16).padStart(2, "0");
131
248
  }
132
- return hex;
249
+ return hex2;
133
250
  }
134
251
 
135
252
  // src/internal/prepare.ts
@@ -205,6 +322,46 @@ var VerdictTokenError = class extends ChitmarkError {
205
322
  var DEFAULT_JWKS_URL = "https://api.chitmark.com/.well-known/jwks.json";
206
323
  var DEFAULT_LEEWAY_SECONDS = 30;
207
324
  async function verifyVerdictToken(token, options) {
325
+ return verifyReceipt(token, options, "JWT");
326
+ }
327
+ var SENSITIVE_RECEIPT_ACTIONS = [
328
+ "api_key",
329
+ "data_export",
330
+ "session_establish",
331
+ "privilege_elevate",
332
+ "authenticator_replace",
333
+ "recovery_contact_change",
334
+ "session_use",
335
+ "privilege_change",
336
+ "release_deploy",
337
+ "bulk_mutation",
338
+ "commitment_change",
339
+ "agent_effect",
340
+ "agent_escalation",
341
+ "quote_generate",
342
+ "queue_priority"
343
+ ];
344
+ async function verifyActionReceipt(token, options) {
345
+ const claims = await verifyReceipt(
346
+ token,
347
+ {
348
+ ...options,
349
+ aud: options.audience,
350
+ issuer: VERDICT_TOKEN_ISSUER,
351
+ leewaySeconds: 0
352
+ },
353
+ "chitmark-action+jwt"
354
+ );
355
+ const now = Math.floor((options.nowMs ?? Date.now()) / 1e3);
356
+ 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) {
357
+ throw new VerdictTokenError(
358
+ "wrong_action_binding",
359
+ "Receipt cannot authorize this operation"
360
+ );
361
+ }
362
+ return claims;
363
+ }
364
+ async function verifyReceipt(token, options, expectedType) {
208
365
  const nowMs = options.nowMs ?? Date.now();
209
366
  const leewaySeconds = options.leewaySeconds ?? DEFAULT_LEEWAY_SECONDS;
210
367
  const parts = token.split(".");
@@ -226,10 +383,10 @@ async function verifyVerdictToken(token, options) {
226
383
  `verdictToken must use ES256, got ${String(header["alg"])}`
227
384
  );
228
385
  }
229
- if (header["typ"] !== "JWT") {
386
+ if (header["typ"] !== expectedType) {
230
387
  throw new VerdictTokenError(
231
388
  "wrong_type",
232
- `verdictToken must be a JWT, got ${String(header["typ"])}`
389
+ `verdictToken must have type ${expectedType}, got ${String(header["typ"])}`
233
390
  );
234
391
  }
235
392
  const kid = typeof header["kid"] === "string" ? header["kid"] : "";
@@ -275,6 +432,12 @@ async function verifyVerdictToken(token, options) {
275
432
  if (!jwk) {
276
433
  throw new VerdictTokenError("unknown_kid", `JWKS has no key for kid ${kid}`);
277
434
  }
435
+ if (jwk["kty"] !== "EC" || jwk["crv"] !== "P-256" || jwk["alg"] !== void 0 && jwk["alg"] !== "ES256") {
436
+ throw new VerdictTokenError(
437
+ "wrong_algorithm",
438
+ "Verification requires an ES256 P-256 key"
439
+ );
440
+ }
278
441
  let publicKey;
279
442
  try {
280
443
  publicKey = await crypto.subtle.importKey(
@@ -347,7 +510,27 @@ function decodeBase64UrlBytes(value) {
347
510
 
348
511
  // src/client.ts
349
512
  var Chitmark = class {
513
+ attribution = {
514
+ resolve: (input) => this.request("/v1/attributions/resolve", {
515
+ method: "POST",
516
+ body: attributionEvidence(input, this.piiMode),
517
+ idempotencyKey: input.idempotencyKey
518
+ }),
519
+ get: (id2) => this.request(`/v1/attributions/${encodeURIComponent(id2)}`, { method: "GET" }),
520
+ createLink: (input, idempotencyKey) => this.request("/v1/identity-links", { method: "POST", body: input, idempotencyKey }),
521
+ revokeLink: (id2) => this.request(`/v1/identity-links/${encodeURIComponent(id2)}`, { method: "DELETE" })
522
+ };
523
+ /** Paid access lifecycle. Transport uncertainty must be resumed, never treated as payment. */
524
+ async charge(request) {
525
+ return this.request("/v1/charge", {
526
+ method: "POST",
527
+ body: request,
528
+ idempotencyKey: request.idempotencyKey,
529
+ timeoutMs: Math.max(this.timeoutMs, 15e3)
530
+ });
531
+ }
350
532
  apiKey;
533
+ credentialProvider;
351
534
  baseUrl;
352
535
  timeoutMs;
353
536
  piiMode;
@@ -355,7 +538,7 @@ var Chitmark = class {
355
538
  fetchImpl;
356
539
  clientName;
357
540
  constructor(options) {
358
- if (!options.apiKey) {
541
+ if (!!options.apiKey === !!options.credentialProvider) {
359
542
  throw new ChitmarkError("Chitmark apiKey is required");
360
543
  }
361
544
  if (options.onDegraded && options.onDegraded !== "challenge") {
@@ -364,6 +547,7 @@ var Chitmark = class {
364
547
  );
365
548
  }
366
549
  this.apiKey = options.apiKey;
550
+ this.credentialProvider = options.credentialProvider;
367
551
  this.baseUrl = resolveBaseUrl(options.env, options.baseUrl ?? envBaseUrl());
368
552
  this.timeoutMs = options.timeoutMs ?? defaultTimeoutMs(this.baseUrl);
369
553
  this.piiMode = options.piiMode ?? "hashed";
@@ -456,7 +640,8 @@ var Chitmark = class {
456
640
  /**
457
641
  * Complete a challenge by submitting its solved proof. Clears only the bound
458
642
  * session, once. Does not authorize the action: re-verify with
459
- * `context.challengeId` and proceed only on non-degraded allow.
643
+ * `context.challengeId` for standalone challenges or the original action
644
+ * binding plus the challenge and event IDs for operation-bound challenges.
460
645
  */
461
646
  async completeChallenge(request) {
462
647
  return this.request("/v1/challenge", {
@@ -484,15 +669,35 @@ var Chitmark = class {
484
669
  */
485
670
  async request(path, init) {
486
671
  const controller = new AbortController();
487
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
672
+ const timeoutMs = init.timeoutMs ?? this.timeoutMs;
673
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
488
674
  try {
489
675
  const headers = {
490
- Authorization: `Bearer ${this.apiKey}`,
676
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
491
677
  "Content-Type": "application/json",
492
678
  Accept: "application/json",
493
679
  "User-Agent": this.clientName,
494
680
  "X-Chitmark-Client": this.clientName
495
681
  };
682
+ if (this.credentialProvider) {
683
+ try {
684
+ Object.assign(
685
+ headers,
686
+ await boundedCredential(
687
+ this.credentialProvider.headers({
688
+ url: `${this.baseUrl}${path}`,
689
+ method: init.method,
690
+ signal: controller.signal
691
+ }),
692
+ controller.signal
693
+ )
694
+ );
695
+ } catch {
696
+ throw new ChitmarkError("Credential unavailable", { code: "credential" });
697
+ }
698
+ if (!headers.Authorization?.startsWith("DPoP ") || !headers.DPoP)
699
+ throw new ChitmarkError("Invalid DPoP credentials");
700
+ }
496
701
  if (init.idempotencyKey) {
497
702
  headers["Idempotency-Key"] = init.idempotencyKey;
498
703
  }
@@ -500,7 +705,8 @@ var Chitmark = class {
500
705
  method: init.method,
501
706
  headers,
502
707
  body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
503
- signal: controller.signal
708
+ signal: controller.signal,
709
+ redirect: "error"
504
710
  });
505
711
  const text = await res.text();
506
712
  let parsed = void 0;
@@ -524,7 +730,7 @@ var Chitmark = class {
524
730
  } catch (err) {
525
731
  if (err instanceof ChitmarkApiError) throw err;
526
732
  if (err instanceof Error && err.name === "AbortError") {
527
- throw new ChitmarkError(`Request timed out after ${this.timeoutMs}ms`, {
733
+ throw new ChitmarkError(`Request timed out after ${timeoutMs}ms`, {
528
734
  code: "timeout"
529
735
  });
530
736
  }
@@ -594,6 +800,371 @@ function verdictToHttpStatus(verdict) {
594
800
  return CHITMARK_HTTP_STATUS.ok;
595
801
  }
596
802
 
803
+ // src/action-binding.ts
804
+ var opaqueId = /^[A-Za-z0-9_.:-]{1,128}$/;
805
+ async function trialCreditOperationDigest(operation) {
806
+ const { operationId, target, quota, recipient } = operation;
807
+ if (Object.keys(operation).some(
808
+ (key) => !["operationId", "target", "quota", "recipient"].includes(key)
809
+ ) || 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))) {
810
+ throw new TypeError("Invalid trial credit operation");
811
+ }
812
+ const bytes = new TextEncoder().encode(
813
+ JSON.stringify([
814
+ "chitmark-action-v1",
815
+ "trial_activation",
816
+ operationId,
817
+ target,
818
+ quota,
819
+ recipient ?? null
820
+ ])
821
+ );
822
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
823
+ return Array.from(
824
+ new Uint8Array(digest),
825
+ (byte) => byte.toString(16).padStart(2, "0")
826
+ ).join("");
827
+ }
828
+
829
+ // src/evidence.ts
830
+ var EvidenceError = class extends Error {
831
+ constructor(code) {
832
+ super(code);
833
+ this.code = code;
834
+ this.name = "EvidenceError";
835
+ }
836
+ code;
837
+ };
838
+ function evidenceAssert(value, code) {
839
+ if (!value) throw new EvidenceError(code);
840
+ }
841
+ function canonicalEvidence(value) {
842
+ if (value === null || typeof value === "boolean") return JSON.stringify(value);
843
+ if (typeof value === "number") {
844
+ evidenceAssert(
845
+ Number.isSafeInteger(value) && !Object.is(value, -0),
846
+ "noncanonical_number"
847
+ );
848
+ return String(value);
849
+ }
850
+ if (typeof value === "string") {
851
+ evidenceAssert(/^[\x20-\x7e]*$/.test(value), "noncanonical_string");
852
+ return JSON.stringify(value);
853
+ }
854
+ if (Array.isArray(value)) return `[${value.map(canonicalEvidence).join(",")}]`;
855
+ evidenceAssert(
856
+ value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype,
857
+ "noncanonical_object"
858
+ );
859
+ const obj = value;
860
+ return `{${Object.keys(obj).sort().map((key) => `${canonicalEvidence(key)}:${canonicalEvidence(obj[key])}`).join(",")}}`;
861
+ }
862
+ async function evidenceHash(value) {
863
+ return Array.from(
864
+ new Uint8Array(
865
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
866
+ ),
867
+ (b) => b.toString(16).padStart(2, "0")
868
+ ).join("");
869
+ }
870
+ var encode2 = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
871
+ var decode = (part) => Uint8Array.from(
872
+ atob(part.replace(/-/g, "+").replace(/_/g, "/")),
873
+ (c) => c.charCodeAt(0)
874
+ );
875
+ function evidenceKeyAt(history, kid, at) {
876
+ const matches = history.filter((key2) => key2.kid === kid);
877
+ evidenceAssert(matches.length === 1, "unknown_trust_root");
878
+ const key = matches[0];
879
+ evidenceAssert(
880
+ key.jwk.kty === "EC" && key.jwk.crv === "P-256" && (!key.jwk.alg || key.jwk.alg === "ES256") && !key.jwk.d,
881
+ "unsupported_key"
882
+ );
883
+ evidenceAssert(
884
+ Number.isSafeInteger(at) && Number.isSafeInteger(key.notBefore) && Number.isSafeInteger(key.notAfter) && key.notBefore <= at && at < key.notAfter,
885
+ "key_outside_validity"
886
+ );
887
+ evidenceAssert(key.revokedAt === void 0, "revoked_key");
888
+ return key;
889
+ }
890
+ async function verifyEvidenceSignature(token, typ, history, at) {
891
+ evidenceAssert(
892
+ typeof token === "string" && token.length <= 8e6 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token),
893
+ "malformed_signature"
894
+ );
895
+ const [h, p, s] = token.split(".");
896
+ let head, payload;
897
+ try {
898
+ head = JSON.parse(
899
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(h))
900
+ );
901
+ payload = JSON.parse(
902
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decode(p))
903
+ );
904
+ } catch {
905
+ throw new EvidenceError("malformed_signature");
906
+ }
907
+ evidenceAssert(
908
+ head?.alg === "ES256" && head.typ === typ && Object.keys(head).length === 3,
909
+ "unsupported_profile"
910
+ );
911
+ const trusted = evidenceKeyAt(history, head.kid, at);
912
+ const key = await crypto.subtle.importKey(
913
+ "jwk",
914
+ trusted.jwk,
915
+ { name: "ECDSA", namedCurve: "P-256" },
916
+ false,
917
+ ["verify"]
918
+ );
919
+ evidenceAssert(
920
+ await crypto.subtle.verify(
921
+ { name: "ECDSA", hash: "SHA-256" },
922
+ key,
923
+ decode(s),
924
+ new TextEncoder().encode(`${h}.${p}`)
925
+ ),
926
+ "invalid_signature"
927
+ );
928
+ evidenceAssert(
929
+ payload && typeof payload === "object" && !Array.isArray(payload),
930
+ "malformed_payload"
931
+ );
932
+ return payload;
933
+ }
934
+ async function signEvidence(payload, typ, kid, jwk) {
935
+ evidenceAssert(
936
+ jwk.kty === "EC" && jwk.crv === "P-256" && (!jwk.alg || jwk.alg === "ES256"),
937
+ "unsupported_key"
938
+ );
939
+ const input = `${encode2(new TextEncoder().encode(canonicalEvidence({ alg: "ES256", kid, typ })))}.${encode2(new TextEncoder().encode(canonicalEvidence(payload)))}`;
940
+ evidenceAssert(input.length + 87 <= 8e6, "bundle_too_large");
941
+ const key = await crypto.subtle.importKey(
942
+ "jwk",
943
+ jwk,
944
+ { name: "ECDSA", namedCurve: "P-256" },
945
+ false,
946
+ ["sign"]
947
+ );
948
+ return `${input}.${encode2(new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, new TextEncoder().encode(input))))}`;
949
+ }
950
+ var id = /^[A-Za-z0-9_.:/-]{1,160}$/;
951
+ function validateManifest(value, tenantId) {
952
+ evidenceAssert(
953
+ 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",
954
+ "unminimized_manifest"
955
+ );
956
+ canonicalEvidence(value);
957
+ evidenceAssert(
958
+ value.version === 1 && value.issuer === "https://portal.chitmark.com" && value.purpose === "historical_evidence" && value.tenantId === tenantId && id.test(tenantId),
959
+ "wrong_bundle_scope"
960
+ );
961
+ evidenceAssert(
962
+ [
963
+ value.startAt,
964
+ value.endAt,
965
+ value.createdAt,
966
+ value.sourceAsOf,
967
+ value.retention.deleteAfter
968
+ ].every(Number.isSafeInteger) && value.startAt < value.endAt && value.endAt <= value.sourceAsOf && value.sourceAsOf <= value.createdAt && value.createdAt < value.retention.deleteAfter,
969
+ "invalid_window"
970
+ );
971
+ evidenceAssert(
972
+ 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)),
973
+ "invalid_manifest"
974
+ );
975
+ evidenceAssert(
976
+ Array.isArray(value.records) && value.records.length <= 1e4,
977
+ "record_limit"
978
+ );
979
+ const seen = /* @__PURE__ */ new Set();
980
+ for (const row of value.records) {
981
+ evidenceAssert(
982
+ Object.keys(row).sort().join(",") === "eventId,id,kind,occurredAt,policyRevision,receipt,recordedAt,result,source",
983
+ "unminimized_record"
984
+ );
985
+ evidenceAssert(
986
+ 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),
987
+ "invalid_record"
988
+ );
989
+ evidenceAssert(
990
+ 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"),
991
+ "invalid_provenance"
992
+ );
993
+ const identity = `${row.kind}:${row.id}`;
994
+ evidenceAssert(!seen.has(identity), "duplicate_record");
995
+ seen.add(identity);
996
+ }
997
+ }
998
+ async function verifyEvidenceBundle(bundle, options) {
999
+ validateManifest(bundle.manifest, options.tenantId);
1000
+ evidenceAssert(
1001
+ bundle.manifest.retention.recipient === options.recipient,
1002
+ "wrong_recipient"
1003
+ );
1004
+ evidenceAssert(
1005
+ options.now < bundle.manifest.retention.deleteAfter && options.now >= bundle.manifest.createdAt,
1006
+ "retention_expired"
1007
+ );
1008
+ const signed = await verifyEvidenceSignature(
1009
+ bundle.signature,
1010
+ "chitmark-evidence+jwt",
1011
+ options.trustedKeys,
1012
+ bundle.manifest.createdAt
1013
+ );
1014
+ evidenceAssert(
1015
+ canonicalEvidence(signed) === canonicalEvidence(bundle.manifest),
1016
+ "manifest_mismatch"
1017
+ );
1018
+ const head = JSON.parse(
1019
+ new TextDecoder().decode(decode(bundle.signature.split(".")[0]))
1020
+ );
1021
+ evidenceAssert(
1022
+ evidenceKeyAt(options.trustedKeys, head.kid, bundle.manifest.createdAt).compromisedAt === void 0,
1023
+ "compromised_bundle_key"
1024
+ );
1025
+ return {
1026
+ purpose: "historical_evidence",
1027
+ executionAuthorized: false,
1028
+ integrity: "valid",
1029
+ completeness: "unknown",
1030
+ outcomeTruth: "not_established",
1031
+ gaps: bundle.manifest.gaps
1032
+ };
1033
+ }
1034
+ async function verifyHistoricalReceipt(token, options) {
1035
+ evidenceAssert(
1036
+ typeof token === "string" && token.length <= 8192,
1037
+ "malformed_signature"
1038
+ );
1039
+ let claims, head;
1040
+ try {
1041
+ const parts = token.split(".");
1042
+ head = JSON.parse(new TextDecoder().decode(decode(parts[0])));
1043
+ claims = JSON.parse(new TextDecoder().decode(decode(parts[1])));
1044
+ } catch {
1045
+ throw new EvidenceError("malformed_signature");
1046
+ }
1047
+ evidenceAssert(
1048
+ head.typ === "JWT" || head.typ === "chitmark-action+jwt",
1049
+ "unsupported_profile"
1050
+ );
1051
+ evidenceAssert(
1052
+ 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),
1053
+ "invalid_receipt_time"
1054
+ );
1055
+ const at = Number(claims.iat);
1056
+ evidenceAssert(
1057
+ typeof claims.eventId === "string" && /^[A-Za-z0-9_-]{1,160}$/.test(claims.eventId),
1058
+ "invalid_receipt_context"
1059
+ );
1060
+ const trusted = evidenceKeyAt(options.trustedKeys, String(head.kid), at);
1061
+ claims = await verifyEvidenceSignature(
1062
+ token,
1063
+ String(head.typ),
1064
+ options.trustedKeys,
1065
+ at
1066
+ );
1067
+ evidenceAssert(
1068
+ claims.tid === options.tenantId && claims.sub === options.session && claims.iss === "https://api.chitmark.com",
1069
+ "wrong_receipt_scope"
1070
+ );
1071
+ const witnessed = options.witness && Number.isSafeInteger(options.witness.observedAt) && options.witness.observedAt >= at && options.witness.tokenHash === await evidenceHash(token);
1072
+ const beforeCompromise = witnessed && (trusted.compromisedAt === void 0 || options.witness.observedAt < trusted.compromisedAt);
1073
+ return {
1074
+ purpose: "historical_evidence",
1075
+ executionAuthorized: false,
1076
+ eventId: String(claims.eventId),
1077
+ policyRevision: typeof claims.policy === "string" ? claims.policy : null,
1078
+ timeIntegrity: beforeCompromise ? "independently_checkpointed" : trusted.compromisedAt !== void 0 ? "uncertain_after_compromise" : "self_asserted"
1079
+ };
1080
+ }
1081
+
1082
+ // src/evidence-checkpoint.ts
1083
+ var hex = /^[a-f0-9]{64}$/;
1084
+ function scope(checkpoint, tenantId, now) {
1085
+ evidenceAssert(
1086
+ 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),
1087
+ "checkpoint_scope_or_expiry"
1088
+ );
1089
+ }
1090
+ var genesis = (cp) => evidenceHash(
1091
+ canonicalEvidence(["chitmark-private-chain-v1", cp.tenantId, cp.epoch, cp.expiresAt])
1092
+ );
1093
+ var evidenceCommitment = (cp, record, salt) => {
1094
+ evidenceAssert(hex.test(salt), "invalid_commitment_salt");
1095
+ return evidenceHash(
1096
+ canonicalEvidence([
1097
+ "chitmark-record-v1",
1098
+ cp.tenantId,
1099
+ cp.epoch,
1100
+ cp.expiresAt,
1101
+ salt,
1102
+ record
1103
+ ])
1104
+ );
1105
+ };
1106
+ var advance = (root, leaf, index) => evidenceHash(canonicalEvidence(["chitmark-append-v1", index, root, leaf]));
1107
+ async function checkpointFromCommitments(input) {
1108
+ evidenceAssert(
1109
+ input.commitments.length <= 1e4 && input.commitments.every((leaf) => hex.test(leaf)),
1110
+ "invalid_commitments"
1111
+ );
1112
+ let root = await genesis(input);
1113
+ for (let index = 0; index < input.commitments.length; index++)
1114
+ root = await advance(root, input.commitments[index], index);
1115
+ const cp = {
1116
+ version: 1,
1117
+ tenantId: input.tenantId,
1118
+ epoch: input.epoch,
1119
+ expiresAt: input.expiresAt,
1120
+ observedAt: input.observedAt,
1121
+ size: input.commitments.length,
1122
+ root
1123
+ };
1124
+ scope(cp, input.tenantId, input.observedAt);
1125
+ return cp;
1126
+ }
1127
+ async function verifyCheckpointAppend(older, newer, appended, options) {
1128
+ scope(older, options.tenantId, options.now);
1129
+ scope(newer, options.tenantId, options.now);
1130
+ if (older.size === 0)
1131
+ evidenceAssert(older.root === await genesis(older), "invalid_empty_checkpoint");
1132
+ evidenceAssert(
1133
+ older.epoch === newer.epoch && older.expiresAt === newer.expiresAt && older.observedAt <= newer.observedAt && older.size <= newer.size,
1134
+ "inconsistent_checkpoint"
1135
+ );
1136
+ if (older.size === newer.size)
1137
+ evidenceAssert(older.root === newer.root, "divergent_history");
1138
+ if (appended === null)
1139
+ return { status: "unavailable", completeness: "unknown" };
1140
+ evidenceAssert(
1141
+ appended.length === newer.size - older.size && appended.every((leaf) => hex.test(leaf)),
1142
+ "invalid_append_proof"
1143
+ );
1144
+ let root = older.root;
1145
+ for (let index = 0; index < appended.length; index++)
1146
+ root = await advance(root, appended[index], older.size + index);
1147
+ evidenceAssert(root === newer.root, "divergent_history");
1148
+ return { status: "consistent", completeness: "unknown" };
1149
+ }
1150
+ async function verifyCheckpointInclusion(checkpoint, proof, options) {
1151
+ scope(checkpoint, options.tenantId, options.now);
1152
+ if (!proof) return { status: "unavailable", completeness: "unknown" };
1153
+ evidenceAssert(
1154
+ Number.isSafeInteger(proof.index) && proof.index >= 0 && proof.index < checkpoint.size && proof.commitments.length === checkpoint.size,
1155
+ "invalid_inclusion_proof"
1156
+ );
1157
+ const rebuilt = await checkpointFromCommitments({
1158
+ ...checkpoint,
1159
+ commitments: proof.commitments
1160
+ });
1161
+ evidenceAssert(
1162
+ rebuilt.root === checkpoint.root && await evidenceCommitment(checkpoint, proof.record, proof.salt) === proof.commitments[proof.index],
1163
+ "changed_record"
1164
+ );
1165
+ return { status: "included", completeness: "unknown" };
1166
+ }
1167
+
597
1168
  // src/edge.ts
598
1169
  var ChitmarkEdge = class extends Chitmark {
599
1170
  /**
@@ -630,16 +1201,34 @@ exports.ChitmarkApiError = ChitmarkApiError;
630
1201
  exports.ChitmarkCore = Chitmark;
631
1202
  exports.ChitmarkEdge = ChitmarkEdge;
632
1203
  exports.ChitmarkError = ChitmarkError;
1204
+ exports.EvidenceError = EvidenceError;
1205
+ exports.SENSITIVE_RECEIPT_ACTIONS = SENSITIVE_RECEIPT_ACTIONS;
633
1206
  exports.VERDICT_TOKEN_ISSUER = VERDICT_TOKEN_ISSUER;
634
1207
  exports.VerdictTokenError = VerdictTokenError;
1208
+ exports.canonicalEvidence = canonicalEvidence;
1209
+ exports.checkpointFromCommitments = checkpointFromCommitments;
1210
+ exports.createDPoPCredentialProvider = createDPoPCredentialProvider;
635
1211
  exports.degradedChallengeVerdict = degradedChallengeVerdict;
636
1212
  exports.enforceDegradedNeverAllow = enforceDegradedNeverAllow;
1213
+ exports.evidenceAssert = evidenceAssert;
1214
+ exports.evidenceCommitment = evidenceCommitment;
1215
+ exports.evidenceHash = evidenceHash;
1216
+ exports.evidenceKeyAt = evidenceKeyAt;
637
1217
  exports.getTrustedClientIp = getTrustedClientIp;
638
1218
  exports.hashEmailSha256 = hashEmailSha256;
639
1219
  exports.isEligibleAllow = isEligibleAllow;
640
1220
  exports.minimizeFormData = minimizeFormData;
1221
+ exports.signEvidence = signEvidence;
1222
+ exports.trialCreditOperationDigest = trialCreditOperationDigest;
641
1223
  exports.truncateIp = truncateIp;
1224
+ exports.validateManifest = validateManifest;
642
1225
  exports.verdictToHttpStatus = verdictToHttpStatus;
1226
+ exports.verifyActionReceipt = verifyActionReceipt;
1227
+ exports.verifyCheckpointAppend = verifyCheckpointAppend;
1228
+ exports.verifyCheckpointInclusion = verifyCheckpointInclusion;
1229
+ exports.verifyEvidenceBundle = verifyEvidenceBundle;
1230
+ exports.verifyEvidenceSignature = verifyEvidenceSignature;
1231
+ exports.verifyHistoricalReceipt = verifyHistoricalReceipt;
643
1232
  exports.verifyVerdictToken = verifyVerdictToken;
644
1233
  //# sourceMappingURL=edge.cjs.map
645
1234
  //# sourceMappingURL=edge.cjs.map