@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/README.md +20 -3
- package/dist/edge.cjs +575 -12
- package/dist/edge.cjs.map +1 -1
- package/dist/edge.d.cts +2 -2
- package/dist/edge.d.ts +2 -2
- package/dist/edge.js +558 -13
- package/dist/edge.js.map +1 -1
- package/dist/index.cjs +608 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +590 -13
- package/dist/index.js.map +1 -1
- package/dist/{trusted-ip-DnCZnqQx.d.cts → trusted-ip-BvSjdU80.d.cts} +458 -9
- package/dist/{trusted-ip-DnCZnqQx.d.ts → trusted-ip-BvSjdU80.d.ts} +458 -9
- package/package.json +2 -2
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.
|
|
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
|
|
217
|
+
let hex2 = "";
|
|
127
218
|
for (const b of bytes) {
|
|
128
|
-
|
|
219
|
+
hex2 += b.toString(16).padStart(2, "0");
|
|
129
220
|
}
|
|
130
|
-
return
|
|
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"] !==
|
|
358
|
+
if (header["typ"] !== expectedType) {
|
|
228
359
|
throw new VerdictTokenError(
|
|
229
360
|
"wrong_type",
|
|
230
|
-
`verdictToken must
|
|
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 (
|
|
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`
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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
|