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