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