@chitmark/sdk 0.1.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/LICENSE +15 -0
- package/README.md +96 -0
- package/dist/edge.cjs +578 -0
- package/dist/edge.cjs.map +1 -0
- package/dist/edge.d.cts +39 -0
- package/dist/edge.d.ts +39 -0
- package/dist/edge.js +562 -0
- package/dist/edge.js.map +1 -0
- package/dist/http-status-CwxMw-nO.d.cts +384 -0
- package/dist/http-status-CwxMw-nO.d.ts +384 -0
- package/dist/index.cjs +601 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +585 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ChitmarkError = class extends Error {
|
|
3
|
+
name = "ChitmarkError";
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
body;
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = options?.status;
|
|
10
|
+
this.code = options?.code;
|
|
11
|
+
this.body = options?.body;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var ChitmarkApiError = class extends ChitmarkError {
|
|
15
|
+
name = "ChitmarkApiError";
|
|
16
|
+
constructor(status, body, message) {
|
|
17
|
+
const msg = message ?? (typeof body === "string" ? body : body.message || body.error || `Chitmark API error (${status})`);
|
|
18
|
+
super(msg, {
|
|
19
|
+
status,
|
|
20
|
+
code: typeof body === "object" ? body.code ?? body.error : void 0,
|
|
21
|
+
body
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function degradedChallengeVerdict(reason, session) {
|
|
26
|
+
return {
|
|
27
|
+
decision: "challenge",
|
|
28
|
+
actorType: "unknown",
|
|
29
|
+
confidence: 0,
|
|
30
|
+
reasons: [reason],
|
|
31
|
+
principal: {
|
|
32
|
+
resolved: false,
|
|
33
|
+
identityBasis: ["degraded"]
|
|
34
|
+
},
|
|
35
|
+
verdictToken: "",
|
|
36
|
+
eventId: `evt_degraded_${session ?? "unknown"}`,
|
|
37
|
+
degraded: true,
|
|
38
|
+
enforcementMode: "enforce"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function enforceDegradedNeverAllow(verdict) {
|
|
42
|
+
if (verdict.degraded && verdict.decision === "allow") {
|
|
43
|
+
return {
|
|
44
|
+
...verdict,
|
|
45
|
+
decision: "challenge",
|
|
46
|
+
reasons: [
|
|
47
|
+
...verdict.reasons,
|
|
48
|
+
"Client enforced: degraded decision must not be allow"
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return verdict;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/internal/constants.ts
|
|
56
|
+
var DEFAULT_BASE_URLS = {
|
|
57
|
+
production: "https://api.chitmark.com",
|
|
58
|
+
staging: "https://api.staging.chitmark.com",
|
|
59
|
+
development: "http://127.0.0.1:8787"
|
|
60
|
+
};
|
|
61
|
+
var DEFAULT_TIMEOUT_MS = 800;
|
|
62
|
+
var SDK_VERSION = "0.1.0";
|
|
63
|
+
function resolveBaseUrl(env = "production", baseUrl) {
|
|
64
|
+
return (baseUrl ?? DEFAULT_BASE_URLS[env]).replace(/\/$/, "");
|
|
65
|
+
}
|
|
66
|
+
function envBaseUrl() {
|
|
67
|
+
if (typeof process === "undefined") return void 0;
|
|
68
|
+
const value = process.env?.CHITMARK_BASE_URL;
|
|
69
|
+
return value ? value.trim() : void 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/pii.ts
|
|
73
|
+
async function hashEmailSha256(email) {
|
|
74
|
+
const normalized = email.trim().toLowerCase();
|
|
75
|
+
const data = new TextEncoder().encode(normalized);
|
|
76
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
77
|
+
return bufferToHex(digest);
|
|
78
|
+
}
|
|
79
|
+
function truncateIp(ip) {
|
|
80
|
+
const trimmed = ip.trim();
|
|
81
|
+
const first = trimmed.split(",")[0]?.trim() ?? trimmed;
|
|
82
|
+
if (first.includes(".") && !first.includes(":")) {
|
|
83
|
+
const parts = first.split(".");
|
|
84
|
+
if (parts.length === 4) {
|
|
85
|
+
return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (first.includes(":")) {
|
|
89
|
+
const hextets = first.split(":").filter(Boolean);
|
|
90
|
+
const head = hextets.slice(0, 3).join(":");
|
|
91
|
+
return `${head}::/48`;
|
|
92
|
+
}
|
|
93
|
+
return first;
|
|
94
|
+
}
|
|
95
|
+
function minimizeFormData(formData) {
|
|
96
|
+
if (!formData) return void 0;
|
|
97
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
98
|
+
"companyDomain",
|
|
99
|
+
"company_domain",
|
|
100
|
+
"domain",
|
|
101
|
+
"plan",
|
|
102
|
+
"referrer",
|
|
103
|
+
"utm_source",
|
|
104
|
+
"utm_medium",
|
|
105
|
+
"utm_campaign"
|
|
106
|
+
]);
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [key, value] of Object.entries(formData)) {
|
|
109
|
+
if (allowed.has(key) && value != null) {
|
|
110
|
+
out[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
114
|
+
}
|
|
115
|
+
function bufferToHex(buffer) {
|
|
116
|
+
const bytes = new Uint8Array(buffer);
|
|
117
|
+
let hex = "";
|
|
118
|
+
for (const b of bytes) {
|
|
119
|
+
hex += b.toString(16).padStart(2, "0");
|
|
120
|
+
}
|
|
121
|
+
return hex;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/internal/prepare.ts
|
|
125
|
+
async function prepareSubject(subject, piiMode) {
|
|
126
|
+
if (piiMode === "raw") {
|
|
127
|
+
return subject;
|
|
128
|
+
}
|
|
129
|
+
if (piiMode === "none") {
|
|
130
|
+
return {
|
|
131
|
+
userAgent: subject.userAgent,
|
|
132
|
+
headers: pickWebBotAuthHeaders(subject.headers)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const prepared = {
|
|
136
|
+
userAgent: subject.userAgent,
|
|
137
|
+
headers: subject.headers
|
|
138
|
+
};
|
|
139
|
+
if (subject.emailSha256) {
|
|
140
|
+
prepared.emailSha256 = subject.emailSha256;
|
|
141
|
+
} else if (subject.email) {
|
|
142
|
+
prepared.emailSha256 = await hashEmailSha256(subject.email);
|
|
143
|
+
}
|
|
144
|
+
if (subject.ipTrunc) {
|
|
145
|
+
prepared.ipTrunc = subject.ipTrunc;
|
|
146
|
+
} else if (subject.ip) {
|
|
147
|
+
prepared.ipTrunc = truncateIp(subject.ip);
|
|
148
|
+
}
|
|
149
|
+
const hashedForm = subject.formDataHashed ?? minimizeFormData(subject.formData);
|
|
150
|
+
if (hashedForm) {
|
|
151
|
+
prepared.formDataHashed = hashedForm;
|
|
152
|
+
}
|
|
153
|
+
return prepared;
|
|
154
|
+
}
|
|
155
|
+
function pickWebBotAuthHeaders(headers) {
|
|
156
|
+
if (!headers) return void 0;
|
|
157
|
+
const keys = ["signature-agent", "signature-input", "signature", "signature-agent"];
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
160
|
+
const lower = k.toLowerCase();
|
|
161
|
+
if (keys.includes(lower) && typeof v === "string") {
|
|
162
|
+
out[lower] = v;
|
|
163
|
+
} else if (keys.includes(lower) && Array.isArray(v) && v[0]) {
|
|
164
|
+
out[lower] = v[0];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
168
|
+
}
|
|
169
|
+
function flattenHeaders(headers) {
|
|
170
|
+
if (!headers) return void 0;
|
|
171
|
+
if (headers instanceof Headers) {
|
|
172
|
+
const out2 = {};
|
|
173
|
+
headers.forEach((value, key) => {
|
|
174
|
+
out2[key.toLowerCase()] = value;
|
|
175
|
+
});
|
|
176
|
+
return out2;
|
|
177
|
+
}
|
|
178
|
+
const out = {};
|
|
179
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
180
|
+
if (typeof v === "string") out[k.toLowerCase()] = v;
|
|
181
|
+
else if (Array.isArray(v) && v[0]) out[k.toLowerCase()] = v[0];
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/verify-token.ts
|
|
187
|
+
var VerdictTokenError = class extends ChitmarkError {
|
|
188
|
+
name = "VerdictTokenError";
|
|
189
|
+
constructor(code, message) {
|
|
190
|
+
super(message, { code });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var DEFAULT_JWKS_URL = "https://api.chitmark.com/.well-known/jwks.json";
|
|
194
|
+
var DEFAULT_LEEWAY_SECONDS = 30;
|
|
195
|
+
async function verifyVerdictToken(token, options = {}) {
|
|
196
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
197
|
+
const leewaySeconds = options.leewaySeconds ?? DEFAULT_LEEWAY_SECONDS;
|
|
198
|
+
const parts = token.split(".");
|
|
199
|
+
if (parts.length !== 3) {
|
|
200
|
+
throw new VerdictTokenError("malformed_token", "verdictToken must be a JWT");
|
|
201
|
+
}
|
|
202
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
203
|
+
let header;
|
|
204
|
+
let payload;
|
|
205
|
+
try {
|
|
206
|
+
header = JSON.parse(decodeBase64Url(headerB64));
|
|
207
|
+
payload = JSON.parse(decodeBase64Url(payloadB64));
|
|
208
|
+
} catch {
|
|
209
|
+
throw new VerdictTokenError("malformed_token", "verdictToken is not valid JWT JSON");
|
|
210
|
+
}
|
|
211
|
+
if (header["alg"] !== "ES256") {
|
|
212
|
+
throw new VerdictTokenError(
|
|
213
|
+
"wrong_algorithm",
|
|
214
|
+
`verdictToken must use ES256, got ${String(header["alg"])}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const kid = typeof header["kid"] === "string" ? header["kid"] : "";
|
|
218
|
+
if (!kid) {
|
|
219
|
+
throw new VerdictTokenError("unknown_kid", "verdictToken is missing kid");
|
|
220
|
+
}
|
|
221
|
+
const exp = typeof payload["exp"] === "number" ? payload["exp"] : NaN;
|
|
222
|
+
const iat = typeof payload["iat"] === "number" ? payload["iat"] : NaN;
|
|
223
|
+
const nowSeconds = Math.floor(nowMs / 1e3);
|
|
224
|
+
if (Number.isFinite(exp) && exp < nowSeconds - leewaySeconds) {
|
|
225
|
+
throw new VerdictTokenError("expired", "verdictToken has expired");
|
|
226
|
+
}
|
|
227
|
+
if (Number.isFinite(iat) && iat > nowSeconds + leewaySeconds) {
|
|
228
|
+
throw new VerdictTokenError("not_yet_valid", "verdictToken iat is in the future");
|
|
229
|
+
}
|
|
230
|
+
const expectedSession = options.session;
|
|
231
|
+
if (expectedSession !== void 0 && payload["sub"] !== expectedSession) {
|
|
232
|
+
throw new VerdictTokenError(
|
|
233
|
+
"wrong_session",
|
|
234
|
+
"verdictToken was minted for a different session"
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const expectedAud = options.aud;
|
|
238
|
+
if (expectedAud !== void 0 && payload["aud"] !== expectedAud) {
|
|
239
|
+
throw new VerdictTokenError(
|
|
240
|
+
"wrong_aud",
|
|
241
|
+
"verdictToken was minted for a different origin"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const jwk = await findJwk(kid, options);
|
|
245
|
+
if (!jwk) {
|
|
246
|
+
throw new VerdictTokenError("unknown_kid", `JWKS has no key for kid ${kid}`);
|
|
247
|
+
}
|
|
248
|
+
let publicKey;
|
|
249
|
+
try {
|
|
250
|
+
publicKey = await crypto.subtle.importKey(
|
|
251
|
+
"jwk",
|
|
252
|
+
jwk,
|
|
253
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
254
|
+
false,
|
|
255
|
+
["verify"]
|
|
256
|
+
);
|
|
257
|
+
} catch {
|
|
258
|
+
throw new VerdictTokenError(
|
|
259
|
+
"invalid_signature",
|
|
260
|
+
"JWKS key is not a valid P-256 public key"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const signatureBytes = decodeBase64UrlBytes(signatureB64);
|
|
264
|
+
const signatureBuffer = new ArrayBuffer(signatureBytes.byteLength);
|
|
265
|
+
new Uint8Array(signatureBuffer).set(signatureBytes);
|
|
266
|
+
const valid = await crypto.subtle.verify(
|
|
267
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
268
|
+
publicKey,
|
|
269
|
+
signatureBuffer,
|
|
270
|
+
new TextEncoder().encode(`${headerB64}.${payloadB64}`)
|
|
271
|
+
);
|
|
272
|
+
if (!valid) {
|
|
273
|
+
throw new VerdictTokenError(
|
|
274
|
+
"invalid_signature",
|
|
275
|
+
"verdictToken signature failed verification"
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return payload;
|
|
279
|
+
}
|
|
280
|
+
async function findJwk(kid, options) {
|
|
281
|
+
let jwks = options.jwks;
|
|
282
|
+
if (!jwks) {
|
|
283
|
+
const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;
|
|
284
|
+
try {
|
|
285
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
286
|
+
const res = await fetchImpl(jwksUrl);
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
throw new Error(`JWKS fetch failed with ${res.status}`);
|
|
289
|
+
}
|
|
290
|
+
jwks = await res.json();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
throw new VerdictTokenError(
|
|
293
|
+
"jwks_unavailable",
|
|
294
|
+
`Failed to load JWKS from ${jwksUrl}: ${err instanceof Error ? err.message : "unknown error"}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!Array.isArray(jwks.keys)) {
|
|
299
|
+
throw new VerdictTokenError("jwks_unavailable", "JWKS response has no keys array");
|
|
300
|
+
}
|
|
301
|
+
return jwks.keys.find((key) => key["kid"] === kid);
|
|
302
|
+
}
|
|
303
|
+
function decodeBase64Url(value) {
|
|
304
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
305
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
306
|
+
const binary = atob(padded);
|
|
307
|
+
return new TextDecoder().decode(Uint8Array.from(binary, (ch) => ch.charCodeAt(0)));
|
|
308
|
+
}
|
|
309
|
+
function decodeBase64UrlBytes(value) {
|
|
310
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
311
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
312
|
+
const binary = atob(padded);
|
|
313
|
+
const out = new Uint8Array(binary.length);
|
|
314
|
+
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/client.ts
|
|
319
|
+
var Chitmark = class {
|
|
320
|
+
apiKey;
|
|
321
|
+
baseUrl;
|
|
322
|
+
timeoutMs;
|
|
323
|
+
piiMode;
|
|
324
|
+
onDegraded;
|
|
325
|
+
fetchImpl;
|
|
326
|
+
clientName;
|
|
327
|
+
constructor(options) {
|
|
328
|
+
if (!options.apiKey) {
|
|
329
|
+
throw new ChitmarkError("Chitmark apiKey is required");
|
|
330
|
+
}
|
|
331
|
+
if (options.onDegraded && options.onDegraded !== "challenge") {
|
|
332
|
+
throw new ChitmarkError(
|
|
333
|
+
'onDegraded must be "challenge" \u2014 fail to challenge, never to allow'
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
this.apiKey = options.apiKey;
|
|
337
|
+
this.baseUrl = resolveBaseUrl(options.env, options.baseUrl ?? envBaseUrl());
|
|
338
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
339
|
+
this.piiMode = options.piiMode ?? "hashed";
|
|
340
|
+
this.onDegraded = "challenge";
|
|
341
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
342
|
+
this.clientName = options.clientName ?? `chitmark-node/${SDK_VERSION}`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Score + route an inbound action (signup / trial / …).
|
|
346
|
+
* Hot path at the edge; client budget defaults to 800ms.
|
|
347
|
+
* On timeout/transport error — or a malformed verdict shape from the API —
|
|
348
|
+
* returns a degraded challenge verdict (never allow, never crashes).
|
|
349
|
+
*
|
|
350
|
+
* Persist `v.eventId` on the account row: feedback joins only on that id.
|
|
351
|
+
*/
|
|
352
|
+
async verify(request) {
|
|
353
|
+
const subject = await prepareSubject(
|
|
354
|
+
{
|
|
355
|
+
...request.subject,
|
|
356
|
+
headers: flattenHeaders(request.subject.headers)
|
|
357
|
+
},
|
|
358
|
+
this.piiMode
|
|
359
|
+
);
|
|
360
|
+
const body = {
|
|
361
|
+
...request,
|
|
362
|
+
subject
|
|
363
|
+
};
|
|
364
|
+
try {
|
|
365
|
+
const verdict = await this.request("/v1/verify", {
|
|
366
|
+
method: "POST",
|
|
367
|
+
body,
|
|
368
|
+
idempotencyKey: request.idempotencyKey
|
|
369
|
+
});
|
|
370
|
+
if (!isWellFormedVerdict(verdict)) {
|
|
371
|
+
return degradedChallengeVerdict(
|
|
372
|
+
"Degraded: verify returned a malformed verdict shape",
|
|
373
|
+
request.session
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return enforceDegradedNeverAllow(verdict);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (err instanceof ChitmarkApiError) {
|
|
379
|
+
throw err;
|
|
380
|
+
}
|
|
381
|
+
return degradedChallengeVerdict(
|
|
382
|
+
err instanceof Error ? `Degraded: ${err.message}` : "Degraded: verify failed",
|
|
383
|
+
request.session
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Report a downstream outcome joined on the stored eventId from verify.
|
|
389
|
+
* This is the data moat — attribution is a schema contract, not a fuzzy match.
|
|
390
|
+
*/
|
|
391
|
+
async feedback(request) {
|
|
392
|
+
return this.request("/v1/feedback", {
|
|
393
|
+
method: "POST",
|
|
394
|
+
body: request
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Issue an asymmetric-cost challenge after decision === challenge | degraded.
|
|
399
|
+
*/
|
|
400
|
+
async challenge(request) {
|
|
401
|
+
return this.request("/v1/challenge", {
|
|
402
|
+
method: "POST",
|
|
403
|
+
body: request
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Complete a challenge by submitting its solved proof. Clears only the bound
|
|
408
|
+
* session, once. Re-verify with `context.challengeId` to have the next
|
|
409
|
+
* verify honor the cleared session.
|
|
410
|
+
*/
|
|
411
|
+
async completeChallenge(request) {
|
|
412
|
+
return this.request("/v1/challenge", {
|
|
413
|
+
method: "POST",
|
|
414
|
+
body: request
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Verify a verdictToken's signature against the API's JWKS and its session /
|
|
419
|
+
* origin binding (MVP-016). Rejects expired tokens, unknown kids, and
|
|
420
|
+
* invalid signatures with `VerdictTokenError`. Pass `session` to enforce
|
|
421
|
+
* session binding (recommended) and `aud` to enforce origin binding.
|
|
422
|
+
*/
|
|
423
|
+
async verifyVerdictToken(token, options = {}) {
|
|
424
|
+
return verifyVerdictToken(token, {
|
|
425
|
+
...options,
|
|
426
|
+
jwksUrl: `${this.baseUrl}/.well-known/jwks.json`,
|
|
427
|
+
fetch: this.fetchImpl
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Low-level HTTP helper. Prefer verify / feedback / challenge.
|
|
432
|
+
*/
|
|
433
|
+
async request(path, init) {
|
|
434
|
+
const controller = new AbortController();
|
|
435
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
436
|
+
try {
|
|
437
|
+
const headers = {
|
|
438
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
439
|
+
"Content-Type": "application/json",
|
|
440
|
+
Accept: "application/json",
|
|
441
|
+
"User-Agent": this.clientName,
|
|
442
|
+
"X-Chitmark-Client": this.clientName
|
|
443
|
+
};
|
|
444
|
+
if (init.idempotencyKey) {
|
|
445
|
+
headers["Idempotency-Key"] = init.idempotencyKey;
|
|
446
|
+
}
|
|
447
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
448
|
+
method: init.method,
|
|
449
|
+
headers,
|
|
450
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
451
|
+
signal: controller.signal
|
|
452
|
+
});
|
|
453
|
+
const text = await res.text();
|
|
454
|
+
let parsed = void 0;
|
|
455
|
+
if (text) {
|
|
456
|
+
try {
|
|
457
|
+
parsed = JSON.parse(text);
|
|
458
|
+
} catch {
|
|
459
|
+
parsed = text;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!res.ok) {
|
|
463
|
+
const errBody = typeof parsed === "object" && parsed !== null ? parsed : text;
|
|
464
|
+
const normalized = typeof errBody === "string" ? errBody : {
|
|
465
|
+
error: errBody.error ?? `http_${res.status}`,
|
|
466
|
+
message: errBody.message,
|
|
467
|
+
code: errBody.code
|
|
468
|
+
};
|
|
469
|
+
throw new ChitmarkApiError(res.status, normalized);
|
|
470
|
+
}
|
|
471
|
+
return parsed;
|
|
472
|
+
} catch (err) {
|
|
473
|
+
if (err instanceof ChitmarkApiError) throw err;
|
|
474
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
475
|
+
throw new ChitmarkError(`Request timed out after ${this.timeoutMs}ms`, {
|
|
476
|
+
code: "timeout"
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
throw err instanceof Error ? new ChitmarkError(err.message, { code: "transport" }) : new ChitmarkError("Unknown transport error", { code: "transport" });
|
|
480
|
+
} finally {
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/** Public accessors for inspection / tests. */
|
|
485
|
+
get config() {
|
|
486
|
+
return {
|
|
487
|
+
baseUrl: this.baseUrl,
|
|
488
|
+
timeoutMs: this.timeoutMs,
|
|
489
|
+
piiMode: this.piiMode,
|
|
490
|
+
onDegraded: this.onDegraded
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
var VERDICT_DECISIONS = /* @__PURE__ */ new Set(["allow", "challenge", "deny"]);
|
|
495
|
+
function isWellFormedVerdict(value) {
|
|
496
|
+
if (typeof value !== "object" || value === null) return false;
|
|
497
|
+
const v = value;
|
|
498
|
+
return typeof v.decision === "string" && VERDICT_DECISIONS.has(v.decision) && typeof v.eventId === "string" && v.eventId.length > 0 && typeof v.degraded === "boolean";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/http-status.ts
|
|
502
|
+
var CHITMARK_HTTP_STATUS = {
|
|
503
|
+
ok: 200,
|
|
504
|
+
created: 201,
|
|
505
|
+
badRequest: 400,
|
|
506
|
+
unauthorized: 401,
|
|
507
|
+
forbidden: 403,
|
|
508
|
+
notFound: 404,
|
|
509
|
+
conflict: 409,
|
|
510
|
+
preconditionRequired: 428,
|
|
511
|
+
tooManyRequests: 429
|
|
512
|
+
};
|
|
513
|
+
var CHITMARK_HTTP_STATUS_MEANING = {
|
|
514
|
+
ok: "Request succeeded. For verify gates, continue only after an explicit non-degraded allow.",
|
|
515
|
+
created: "Resource was created after an explicit non-degraded allow.",
|
|
516
|
+
badRequest: "Required request fields are missing or malformed.",
|
|
517
|
+
unauthorized: "API key is missing or invalid.",
|
|
518
|
+
forbidden: "Verify returned deny; block the action.",
|
|
519
|
+
notFound: "Referenced Chitmark resource was not found.",
|
|
520
|
+
conflict: "Idempotency key was reused with a materially different payload.",
|
|
521
|
+
preconditionRequired: "Verify returned challenge or degraded; require friction before continuing.",
|
|
522
|
+
tooManyRequests: "Request was rate limited."
|
|
523
|
+
};
|
|
524
|
+
function verdictToHttpStatus(verdict) {
|
|
525
|
+
if (verdict.degraded || verdict.decision === "challenge") {
|
|
526
|
+
return CHITMARK_HTTP_STATUS.preconditionRequired;
|
|
527
|
+
}
|
|
528
|
+
if (verdict.decision === "deny") return CHITMARK_HTTP_STATUS.forbidden;
|
|
529
|
+
return CHITMARK_HTTP_STATUS.ok;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/middleware/express.ts
|
|
533
|
+
function chitmarkGate(client, action, options = {}) {
|
|
534
|
+
return async (req, res, next) => {
|
|
535
|
+
try {
|
|
536
|
+
const surface = options.surface?.(req) ?? `${req.hostname ?? ""}${req.path ?? ""}`;
|
|
537
|
+
const session = options.session?.(req);
|
|
538
|
+
const v = await client.verify({
|
|
539
|
+
action,
|
|
540
|
+
surface,
|
|
541
|
+
session,
|
|
542
|
+
subject: {
|
|
543
|
+
email: req.body?.email,
|
|
544
|
+
ip: req.ip,
|
|
545
|
+
userAgent: (typeof req.headers["user-agent"] === "string" ? req.headers["user-agent"] : void 0) ?? "",
|
|
546
|
+
headers: req.headers,
|
|
547
|
+
formData: req.body
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
res.locals.chitmark = v;
|
|
551
|
+
if (v.degraded || v.decision === "challenge") {
|
|
552
|
+
res.status(CHITMARK_HTTP_STATUS.preconditionRequired).json({ challenge: true, eventId: v.eventId });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (v.decision === "deny") {
|
|
556
|
+
res.status(CHITMARK_HTTP_STATUS.forbidden).json({ error: "blocked", eventId: v.eventId });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
next();
|
|
560
|
+
} catch (err) {
|
|
561
|
+
next(err);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/middleware/next.ts
|
|
567
|
+
async function verifyNextRequest(client, req, options) {
|
|
568
|
+
const url = new URL(req.url);
|
|
569
|
+
return client.verify({
|
|
570
|
+
action: options.action,
|
|
571
|
+
surface: options.surface ?? `${url.host}${url.pathname}`,
|
|
572
|
+
session: options.session,
|
|
573
|
+
subject: {
|
|
574
|
+
email: options.body?.email,
|
|
575
|
+
ip: req.headers.get("x-forwarded-for") ?? "",
|
|
576
|
+
userAgent: req.headers.get("user-agent") ?? "",
|
|
577
|
+
headers: Object.fromEntries(req.headers),
|
|
578
|
+
formData: options.body
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export { CHITMARK_HTTP_STATUS, CHITMARK_HTTP_STATUS_MEANING, Chitmark, ChitmarkApiError, ChitmarkError, VerdictTokenError, chitmarkGate, degradedChallengeVerdict, enforceDegradedNeverAllow, hashEmailSha256, minimizeFormData, truncateIp, verdictToHttpStatus, verifyNextRequest, verifyVerdictToken };
|
|
584
|
+
//# sourceMappingURL=index.js.map
|
|
585
|
+
//# sourceMappingURL=index.js.map
|