@vectoral-labs/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 +215 -0
- package/README.md +46 -0
- package/dist/index.cjs +895 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +572 -0
- package/dist/index.d.ts +572 -0
- package/dist/index.js +855 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var VectoralError = class extends Error {
|
|
3
|
+
/** HTTP status code, or 0 for network/timeout errors. */
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
/** Raw response body, when there was one. */
|
|
7
|
+
responseBody;
|
|
8
|
+
constructor(message, opts) {
|
|
9
|
+
super(message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
10
|
+
this.name = "VectoralError";
|
|
11
|
+
this.code = opts.code;
|
|
12
|
+
this.status = opts.status;
|
|
13
|
+
this.responseBody = opts.responseBody;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* True for failures where the request provably did not reach a decision:
|
|
17
|
+
* network errors, timeouts, and 5xx. A retry is safe only if the call also
|
|
18
|
+
* carried an `event_id` — see `docs/concepts/reliability.md`.
|
|
19
|
+
*/
|
|
20
|
+
get transient() {
|
|
21
|
+
return this.code === "network_error" || this.code === "timeout" || this.code === "http_error" && this.status >= 500;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function notifyError(cb, err, context) {
|
|
25
|
+
try {
|
|
26
|
+
cb?.(err, context);
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
var VectoralConfigError = class extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "VectoralConfigError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/http.ts
|
|
38
|
+
function assertAck(body) {
|
|
39
|
+
if (body?.ok !== true) {
|
|
40
|
+
throw new VectoralError(
|
|
41
|
+
"vectoral: write was not acknowledged (expected `ok: true`)",
|
|
42
|
+
{ code: "invalid_response", status: 200, responseBody: JSON.stringify(body) }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
var backoffMs = (attempt) => Math.min(200 * 2 ** attempt, 5e3);
|
|
49
|
+
var Transport = class {
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
this.opts = opts;
|
|
52
|
+
}
|
|
53
|
+
opts;
|
|
54
|
+
async post(path, body, po) {
|
|
55
|
+
const url = `${this.opts.baseUrl}${path}`;
|
|
56
|
+
const maxAttempts = po.idempotent ? this.opts.retries : 0;
|
|
57
|
+
let attempt = 0;
|
|
58
|
+
for (; ; ) {
|
|
59
|
+
try {
|
|
60
|
+
return await this.doPost(
|
|
61
|
+
url,
|
|
62
|
+
body,
|
|
63
|
+
Math.max(po.timeoutMs ?? 0, this.opts.timeoutMs)
|
|
64
|
+
);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const retryable = err instanceof VectoralError && err.transient;
|
|
67
|
+
if (retryable && attempt < maxAttempts) {
|
|
68
|
+
attempt += 1;
|
|
69
|
+
await delay(backoffMs(attempt));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async doPost(url, body, timeoutMs) {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
return await this.send(url, body, controller, timeoutMs);
|
|
81
|
+
} finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async send(url, body, controller, timeoutMs) {
|
|
86
|
+
const failed = (err) => controller.signal.aborted ? new VectoralError(`request timed out after ${timeoutMs}ms`, {
|
|
87
|
+
code: "timeout",
|
|
88
|
+
status: 0,
|
|
89
|
+
cause: err
|
|
90
|
+
}) : new VectoralError(
|
|
91
|
+
`network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
{ code: "network_error", status: 0, cause: err }
|
|
93
|
+
);
|
|
94
|
+
let res;
|
|
95
|
+
try {
|
|
96
|
+
res = await this.opts.fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
...this.opts.authHeader,
|
|
101
|
+
...this.opts.headers
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
signal: controller.signal
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw failed(err);
|
|
108
|
+
}
|
|
109
|
+
let text;
|
|
110
|
+
try {
|
|
111
|
+
text = await res.text();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw failed(err);
|
|
114
|
+
}
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
let message = res.statusText;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(text);
|
|
119
|
+
if (typeof parsed.error === "string") message = parsed.error;
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
throw new VectoralError(`vectoral ${res.status}: ${message}`, {
|
|
123
|
+
code: "http_error",
|
|
124
|
+
status: res.status,
|
|
125
|
+
responseBody: text
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(text);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
throw new VectoralError("vectoral: invalid JSON in response body", {
|
|
132
|
+
code: "invalid_response",
|
|
133
|
+
status: res.status,
|
|
134
|
+
responseBody: text,
|
|
135
|
+
cause: err
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/salt.ts
|
|
142
|
+
var MIN_SALT_LENGTH = 32;
|
|
143
|
+
var env = (name) => {
|
|
144
|
+
const p = globalThis.process;
|
|
145
|
+
return p?.env?.[name];
|
|
146
|
+
};
|
|
147
|
+
var PLACEHOLDER_SALTS = /* @__PURE__ */ new Set([
|
|
148
|
+
"changeme",
|
|
149
|
+
"your-salt-here",
|
|
150
|
+
"test",
|
|
151
|
+
"secret",
|
|
152
|
+
"vectoral"
|
|
153
|
+
]);
|
|
154
|
+
function resolveFingerprintConfig(opts) {
|
|
155
|
+
const warnings = [];
|
|
156
|
+
if (!opts?.enabled) {
|
|
157
|
+
if (opts?.shareGlobal) {
|
|
158
|
+
throw new VectoralConfigError(
|
|
159
|
+
"fingerprint.shareGlobal requires fingerprint.enabled: true"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return { config: null, warnings };
|
|
163
|
+
}
|
|
164
|
+
const salt = opts.salt ?? env("VECTORAL_FINGERPRINT_SALT");
|
|
165
|
+
const saltId = opts.saltId ?? env("VECTORAL_FINGERPRINT_SALT_ID");
|
|
166
|
+
if (!salt || !saltId) {
|
|
167
|
+
warnings.push(
|
|
168
|
+
"fingerprint.enabled is set but salt/saltId are missing (pass them, or set VECTORAL_FINGERPRINT_SALT and VECTORAL_FINGERPRINT_SALT_ID) \u2014 prompt fingerprinting is disabled"
|
|
169
|
+
);
|
|
170
|
+
return { config: null, warnings };
|
|
171
|
+
}
|
|
172
|
+
if (PLACEHOLDER_SALTS.has(salt.toLowerCase())) {
|
|
173
|
+
warnings.push(
|
|
174
|
+
"fingerprint.salt is a well-known placeholder value \u2014 prompt fingerprinting is disabled. Generate one with generateSalt()."
|
|
175
|
+
);
|
|
176
|
+
return { config: null, warnings };
|
|
177
|
+
}
|
|
178
|
+
if (salt.length < MIN_SALT_LENGTH) {
|
|
179
|
+
warnings.push(
|
|
180
|
+
`fingerprint.salt is only ${salt.length} characters; at least ${MIN_SALT_LENGTH} are required for a meaningful search space \u2014 prompt fingerprinting is disabled`
|
|
181
|
+
);
|
|
182
|
+
return { config: null, warnings };
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
config: { salt, saltId, shareGlobal: opts.shareGlobal ?? false },
|
|
186
|
+
warnings
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function generateSalt() {
|
|
190
|
+
const bytes = new Uint8Array(32);
|
|
191
|
+
crypto.getRandomValues(bytes);
|
|
192
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
193
|
+
}
|
|
194
|
+
function suggestSaltId(now = /* @__PURE__ */ new Date()) {
|
|
195
|
+
const yyyy = now.getUTCFullYear();
|
|
196
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
197
|
+
return `s_${yyyy}_${mm}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/resources/registrations.ts
|
|
201
|
+
var RegistrationTier = {
|
|
202
|
+
Allow: 0,
|
|
203
|
+
Challenge: 1,
|
|
204
|
+
StepUp: 2
|
|
205
|
+
};
|
|
206
|
+
var DEADLINE_MIN_MS = 250;
|
|
207
|
+
var DEADLINE_MAX_MS = 2e3;
|
|
208
|
+
var DEADLINE_OVERHEAD_MS = 300;
|
|
209
|
+
var Registrations = class {
|
|
210
|
+
constructor(transport, opts) {
|
|
211
|
+
this.transport = transport;
|
|
212
|
+
this.opts = opts;
|
|
213
|
+
}
|
|
214
|
+
transport;
|
|
215
|
+
opts;
|
|
216
|
+
/**
|
|
217
|
+
* Score a registration at form submit.
|
|
218
|
+
*
|
|
219
|
+
* With `failOpen` (the default) this never throws: any network failure,
|
|
220
|
+
* timeout, or HTTP error yields `tier: 0, degraded: true`. A screening check
|
|
221
|
+
* that can take your signup page down is worse than no screening check.
|
|
222
|
+
*/
|
|
223
|
+
async score(req) {
|
|
224
|
+
try {
|
|
225
|
+
const body = await this.transport.post(
|
|
226
|
+
"/v1/registrations/score",
|
|
227
|
+
req,
|
|
228
|
+
{
|
|
229
|
+
idempotent: req.event_id !== void 0,
|
|
230
|
+
...req.deadline_ms !== void 0 ? { timeoutMs: httpTimeoutFor(req.deadline_ms) } : {}
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
if (typeof body?.tier !== "number" || typeof body?.score !== "number") {
|
|
234
|
+
throw new VectoralError(
|
|
235
|
+
"vectoral: response is not a registration verdict (missing numeric `tier`/`score`)",
|
|
236
|
+
{ code: "invalid_response", status: 200, responseBody: JSON.stringify(body) }
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
registration_id: body.registration_id,
|
|
241
|
+
tier: body.tier,
|
|
242
|
+
score: body.score,
|
|
243
|
+
reasons: body.reasons ?? [],
|
|
244
|
+
...body.duplicate !== void 0 ? { duplicate: body.duplicate } : {},
|
|
245
|
+
...body.shadow_mode !== void 0 ? { shadow_mode: body.shadow_mode } : {},
|
|
246
|
+
degraded: false
|
|
247
|
+
};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (!this.opts.failOpen || !(err instanceof VectoralError)) throw err;
|
|
250
|
+
notifyError(this.opts.onError, err, "registrations.score");
|
|
251
|
+
return {
|
|
252
|
+
registration_id: null,
|
|
253
|
+
tier: RegistrationTier.Allow,
|
|
254
|
+
score: 0,
|
|
255
|
+
reasons: [],
|
|
256
|
+
degraded: true,
|
|
257
|
+
error: err
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
function httpTimeoutFor(deadlineMs) {
|
|
263
|
+
const clamped = Math.min(Math.max(deadlineMs, DEADLINE_MIN_MS), DEADLINE_MAX_MS);
|
|
264
|
+
return clamped + DEADLINE_OVERHEAD_MS;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/fingerprint/encoding.ts
|
|
268
|
+
var encoder = new TextEncoder();
|
|
269
|
+
var utf8 = (s) => encoder.encode(s);
|
|
270
|
+
var hex64 = (v) => v.toString(16).padStart(16, "0");
|
|
271
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
272
|
+
function bytesToBase64(b) {
|
|
273
|
+
let out = "";
|
|
274
|
+
let i = 0;
|
|
275
|
+
for (; i + 3 <= b.length; i += 3) {
|
|
276
|
+
const n = b[i] << 16 | b[i + 1] << 8 | b[i + 2];
|
|
277
|
+
out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + B64[n >> 6 & 63] + B64[n & 63];
|
|
278
|
+
}
|
|
279
|
+
const rem = b.length - i;
|
|
280
|
+
if (rem === 1) {
|
|
281
|
+
const n = b[i] << 16;
|
|
282
|
+
out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + "==";
|
|
283
|
+
} else if (rem === 2) {
|
|
284
|
+
const n = b[i] << 16 | b[i + 1] << 8;
|
|
285
|
+
out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + B64[n >> 6 & 63] + "=";
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/fingerprint/xxhash64.ts
|
|
291
|
+
var MASK64 = (1n << 64n) - 1n;
|
|
292
|
+
var PRIME64_1 = 0x9e3779b185ebca87n;
|
|
293
|
+
var PRIME64_2 = 0xc2b2ae3d27d4eb4fn;
|
|
294
|
+
var PRIME64_3 = 0x165667b19e3779f9n;
|
|
295
|
+
var PRIME64_4 = 0x85ebca77c2b2ae63n;
|
|
296
|
+
var PRIME64_5 = 0x27d4eb2f165667c5n;
|
|
297
|
+
var rotl = (v, r) => (v << r | v >> 64n - r) & MASK64;
|
|
298
|
+
var round = (acc, input) => rotl(acc + input * PRIME64_2 & MASK64, 31n) * PRIME64_1 & MASK64;
|
|
299
|
+
var mergeRound = (acc, val) => (acc ^ round(0n, val)) * PRIME64_1 + PRIME64_4 & MASK64;
|
|
300
|
+
var le64 = (b, i) => BigInt(b[i]) | BigInt(b[i + 1]) << 8n | BigInt(b[i + 2]) << 16n | BigInt(b[i + 3]) << 24n | BigInt(b[i + 4]) << 32n | BigInt(b[i + 5]) << 40n | BigInt(b[i + 6]) << 48n | BigInt(b[i + 7]) << 56n;
|
|
301
|
+
var le32 = (b, i) => BigInt(b[i] | b[i + 1] << 8 | b[i + 2] << 16) | BigInt(b[i + 3]) << 24n;
|
|
302
|
+
function xxh64(data, seed) {
|
|
303
|
+
const n = data.length;
|
|
304
|
+
let i = 0;
|
|
305
|
+
let h;
|
|
306
|
+
if (n >= 32) {
|
|
307
|
+
let v1 = seed + PRIME64_1 + PRIME64_2 & MASK64;
|
|
308
|
+
let v2 = seed + PRIME64_2 & MASK64;
|
|
309
|
+
let v3 = seed & MASK64;
|
|
310
|
+
let v4 = seed - PRIME64_1 & MASK64;
|
|
311
|
+
for (; i + 32 <= n; i += 32) {
|
|
312
|
+
v1 = round(v1, le64(data, i));
|
|
313
|
+
v2 = round(v2, le64(data, i + 8));
|
|
314
|
+
v3 = round(v3, le64(data, i + 16));
|
|
315
|
+
v4 = round(v4, le64(data, i + 24));
|
|
316
|
+
}
|
|
317
|
+
h = rotl(v1, 1n) + rotl(v2, 7n) + rotl(v3, 12n) + rotl(v4, 18n) & MASK64;
|
|
318
|
+
h = mergeRound(h, v1);
|
|
319
|
+
h = mergeRound(h, v2);
|
|
320
|
+
h = mergeRound(h, v3);
|
|
321
|
+
h = mergeRound(h, v4);
|
|
322
|
+
} else {
|
|
323
|
+
h = seed + PRIME64_5 & MASK64;
|
|
324
|
+
}
|
|
325
|
+
h = h + BigInt(n) & MASK64;
|
|
326
|
+
for (; i + 8 <= n; i += 8) {
|
|
327
|
+
h = rotl(h ^ round(0n, le64(data, i)), 27n) * PRIME64_1 + PRIME64_4 & MASK64;
|
|
328
|
+
}
|
|
329
|
+
if (i + 4 <= n) {
|
|
330
|
+
h = rotl(h ^ le32(data, i) * PRIME64_1 & MASK64, 23n) * PRIME64_2 + PRIME64_3 & MASK64;
|
|
331
|
+
i += 4;
|
|
332
|
+
}
|
|
333
|
+
for (; i < n; i++) {
|
|
334
|
+
h = rotl(h ^ BigInt(data[i]) * PRIME64_5 & MASK64, 11n) * PRIME64_1 & MASK64;
|
|
335
|
+
}
|
|
336
|
+
h ^= h >> 33n;
|
|
337
|
+
h = h * PRIME64_2 & MASK64;
|
|
338
|
+
h ^= h >> 29n;
|
|
339
|
+
h = h * PRIME64_3 & MASK64;
|
|
340
|
+
h ^= h >> 32n;
|
|
341
|
+
return h;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/fingerprint/normalize.ts
|
|
345
|
+
var MAX_NORMALIZED_BYTES = 32 * 1024;
|
|
346
|
+
var MIN_TOKENS = 8;
|
|
347
|
+
var WHITESPACE_RE = /[\t\n\v\f\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/gu;
|
|
348
|
+
var bucket = (run) => run.length === 1 ? "0" : run.length <= 3 ? "00" : "000";
|
|
349
|
+
function normalizeTokens(text, collapseDigits) {
|
|
350
|
+
let s = text.normalize("NFKC").toLowerCase();
|
|
351
|
+
if (collapseDigits) s = s.replace(/[0-9]+/g, bucket);
|
|
352
|
+
const tokens = s.split(WHITESPACE_RE).filter((t) => t.length > 0);
|
|
353
|
+
return capTokens(tokens, MAX_NORMALIZED_BYTES);
|
|
354
|
+
}
|
|
355
|
+
var encoder2 = new TextEncoder();
|
|
356
|
+
function capTokens(tokens, maxBytes) {
|
|
357
|
+
let total = 0;
|
|
358
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
359
|
+
const n = encoder2.encode(tokens[i]).length + (i > 0 ? 1 : 0);
|
|
360
|
+
if (total + n > maxBytes) return tokens.slice(0, i);
|
|
361
|
+
total += n;
|
|
362
|
+
}
|
|
363
|
+
return tokens;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/fingerprint/minhash_constants.json
|
|
367
|
+
var minhash_constants_default = {
|
|
368
|
+
a: [
|
|
369
|
+
"2021813335835409049",
|
|
370
|
+
"35211633415242073",
|
|
371
|
+
"1032286529175131953",
|
|
372
|
+
"2069131852558007211",
|
|
373
|
+
"1978694875717179127",
|
|
374
|
+
"92632024908598310",
|
|
375
|
+
"2227348836476178120",
|
|
376
|
+
"1241523794948756209",
|
|
377
|
+
"1281414715180458463",
|
|
378
|
+
"68168729981692814",
|
|
379
|
+
"1590120462813979606",
|
|
380
|
+
"1854116879905405569",
|
|
381
|
+
"1727992762099619348",
|
|
382
|
+
"2119624372221496183",
|
|
383
|
+
"1134157368671934749",
|
|
384
|
+
"1889971077583265806",
|
|
385
|
+
"1331960984677369123",
|
|
386
|
+
"424766757415873078",
|
|
387
|
+
"248769665290609414",
|
|
388
|
+
"379392552670680551",
|
|
389
|
+
"1071663163969004878",
|
|
390
|
+
"1088771757875541551",
|
|
391
|
+
"525798032415552240",
|
|
392
|
+
"1801279986719471057",
|
|
393
|
+
"2160440382124210661",
|
|
394
|
+
"542330089998054146",
|
|
395
|
+
"34780299756627497",
|
|
396
|
+
"864036708820350785",
|
|
397
|
+
"751227794015040822",
|
|
398
|
+
"1831265118418158537",
|
|
399
|
+
"204218925239092992",
|
|
400
|
+
"1011056663065980301",
|
|
401
|
+
"128115764264397803",
|
|
402
|
+
"1505159286259654375",
|
|
403
|
+
"1191262882168263883",
|
|
404
|
+
"740932194332428643",
|
|
405
|
+
"1593133303768919397",
|
|
406
|
+
"1618445643131025398",
|
|
407
|
+
"509860659052195117",
|
|
408
|
+
"1297874447217707170",
|
|
409
|
+
"1085228754310083322",
|
|
410
|
+
"119203851975622499",
|
|
411
|
+
"746088216632479645",
|
|
412
|
+
"259900005950983532",
|
|
413
|
+
"66551348091461166",
|
|
414
|
+
"1059503232844960577",
|
|
415
|
+
"255114382916886367",
|
|
416
|
+
"235744748558065614",
|
|
417
|
+
"2028861987517208555",
|
|
418
|
+
"360011178275786621",
|
|
419
|
+
"1854044834799956775",
|
|
420
|
+
"1374770026548661332",
|
|
421
|
+
"343207213113865673",
|
|
422
|
+
"679958157873282646",
|
|
423
|
+
"37240336268295500",
|
|
424
|
+
"1548562591492889568",
|
|
425
|
+
"1361800152292289303",
|
|
426
|
+
"1932288454007280877",
|
|
427
|
+
"1076765584066084170",
|
|
428
|
+
"177822684145201725",
|
|
429
|
+
"456689636495482067",
|
|
430
|
+
"486814351553121932",
|
|
431
|
+
"794790680498008333",
|
|
432
|
+
"1316082831976020478"
|
|
433
|
+
],
|
|
434
|
+
b: [
|
|
435
|
+
"1610806161966735655",
|
|
436
|
+
"2047677487906336758",
|
|
437
|
+
"1275918690921527573",
|
|
438
|
+
"1189635637049945452",
|
|
439
|
+
"1281403506175249200",
|
|
440
|
+
"2081287680319054019",
|
|
441
|
+
"192492701049451181",
|
|
442
|
+
"1425310758080347673",
|
|
443
|
+
"2039895406543135671",
|
|
444
|
+
"1501410306195161698",
|
|
445
|
+
"1375342227526167841",
|
|
446
|
+
"308564847782375531",
|
|
447
|
+
"1049081053798665627",
|
|
448
|
+
"1584374554533213180",
|
|
449
|
+
"29377296984809279",
|
|
450
|
+
"455684791224639069",
|
|
451
|
+
"1701487829648668302",
|
|
452
|
+
"2052418598727637090",
|
|
453
|
+
"1782181777482596496",
|
|
454
|
+
"1405174944115920234",
|
|
455
|
+
"142697458214177465",
|
|
456
|
+
"2230201305625643893",
|
|
457
|
+
"1274214892560249329",
|
|
458
|
+
"108256409252165718",
|
|
459
|
+
"202993594240856655",
|
|
460
|
+
"1113572638037699701",
|
|
461
|
+
"293926112313331388",
|
|
462
|
+
"1266928255254575415",
|
|
463
|
+
"1378430457629976093",
|
|
464
|
+
"2279739612382497097",
|
|
465
|
+
"253506377110122931",
|
|
466
|
+
"2121229711337812063",
|
|
467
|
+
"1051445940000326502",
|
|
468
|
+
"1585510647031081366",
|
|
469
|
+
"513071450608671516",
|
|
470
|
+
"1692342139284161390",
|
|
471
|
+
"2099966457055163223",
|
|
472
|
+
"80748089313457135",
|
|
473
|
+
"915320896732073477",
|
|
474
|
+
"206016695116487206",
|
|
475
|
+
"313255738298670383",
|
|
476
|
+
"350087690804126655",
|
|
477
|
+
"566597602277147588",
|
|
478
|
+
"2286505508950253560",
|
|
479
|
+
"59749117790775024",
|
|
480
|
+
"1254224403872925255",
|
|
481
|
+
"62443264473934934",
|
|
482
|
+
"879402817051146288",
|
|
483
|
+
"579168338468101788",
|
|
484
|
+
"109843255863370304",
|
|
485
|
+
"2262200584042665321",
|
|
486
|
+
"2222280625931509317",
|
|
487
|
+
"1738258800373110427",
|
|
488
|
+
"208491563184516265",
|
|
489
|
+
"1409983652033055411",
|
|
490
|
+
"1950247065602176885",
|
|
491
|
+
"975504388282036391",
|
|
492
|
+
"804721776541171833",
|
|
493
|
+
"1241808411053857370",
|
|
494
|
+
"2092916582302830364",
|
|
495
|
+
"1783854280345651430",
|
|
496
|
+
"1299024548425035192",
|
|
497
|
+
"125336755862468958",
|
|
498
|
+
"646127779493447141"
|
|
499
|
+
],
|
|
500
|
+
comment: "Canonical MinHash permutation constants. Decimal strings. p = 2^61 - 1. DO NOT REGENERATE (breaking algo_version change).",
|
|
501
|
+
p: "2305843009213693951"
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// src/fingerprint/similarity.ts
|
|
505
|
+
var saltSeed = (salt) => xxh64(utf8(salt), 0n);
|
|
506
|
+
var GLOBAL_SEED = 0n;
|
|
507
|
+
var P61 = (1n << 61n) - 1n;
|
|
508
|
+
var A = minhash_constants_default.a.map(BigInt);
|
|
509
|
+
var B = minhash_constants_default.b.map(BigInt);
|
|
510
|
+
if (A.length !== 64 || B.length !== 64) {
|
|
511
|
+
throw new Error("fingerprint: minhash_constants.json must have 64 a and 64 b values");
|
|
512
|
+
}
|
|
513
|
+
function shingleSet(tokens, seed) {
|
|
514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
515
|
+
const hashes = [];
|
|
516
|
+
for (let i = 0; i + 3 <= tokens.length; i++) {
|
|
517
|
+
const sh = `${tokens[i]} ${tokens[i + 1]} ${tokens[i + 2]}`;
|
|
518
|
+
if (seen.has(sh)) continue;
|
|
519
|
+
seen.add(sh);
|
|
520
|
+
hashes.push(xxh64(utf8(sh), seed));
|
|
521
|
+
}
|
|
522
|
+
return hashes;
|
|
523
|
+
}
|
|
524
|
+
function simHash64(hashes) {
|
|
525
|
+
const sums = new Int32Array(64);
|
|
526
|
+
for (const h of hashes) {
|
|
527
|
+
const lo = Number(h & 0xffffffffn);
|
|
528
|
+
const hi = Number(h >> 32n);
|
|
529
|
+
for (let i = 0; i < 32; i++) {
|
|
530
|
+
sums[i] += lo >>> i & 1 ? 1 : -1;
|
|
531
|
+
sums[i + 32] += hi >>> i & 1 ? 1 : -1;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
let out = 0n;
|
|
535
|
+
for (let i = 0; i < 64; i++) {
|
|
536
|
+
if (sums[i] > 0) out |= 1n << BigInt(i);
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
var permute = (a, b, hp) => (a * hp + b) % P61;
|
|
541
|
+
function minHash64x16(hashes) {
|
|
542
|
+
const sig = new Uint16Array(64);
|
|
543
|
+
const reduced = hashes.map((h) => h % P61);
|
|
544
|
+
for (let i = 0; i < 64; i++) {
|
|
545
|
+
let min = P61;
|
|
546
|
+
const a = A[i];
|
|
547
|
+
const b = B[i];
|
|
548
|
+
for (const hp of reduced) {
|
|
549
|
+
const v = permute(a, b, hp);
|
|
550
|
+
if (v < min) min = v;
|
|
551
|
+
}
|
|
552
|
+
sig[i] = Number(min & 0xffffn);
|
|
553
|
+
}
|
|
554
|
+
return sig;
|
|
555
|
+
}
|
|
556
|
+
function signatureBytes(sig) {
|
|
557
|
+
const out = new Uint8Array(128);
|
|
558
|
+
for (let i = 0; i < 64; i++) {
|
|
559
|
+
out[2 * i] = sig[i] & 255;
|
|
560
|
+
out[2 * i + 1] = sig[i] >> 8;
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
564
|
+
function bandKeys(sig, seed) {
|
|
565
|
+
const keys = [];
|
|
566
|
+
const buf = new Uint8Array(8);
|
|
567
|
+
for (let band = 0; band < 16; band++) {
|
|
568
|
+
for (let r = 0; r < 4; r++) {
|
|
569
|
+
const v = sig[band * 4 + r];
|
|
570
|
+
buf[2 * r] = v & 255;
|
|
571
|
+
buf[2 * r + 1] = v >> 8;
|
|
572
|
+
}
|
|
573
|
+
keys.push(BigInt.asIntN(64, xxh64(buf, seed)));
|
|
574
|
+
}
|
|
575
|
+
return keys;
|
|
576
|
+
}
|
|
577
|
+
function exactHash(text, seed) {
|
|
578
|
+
return xxh64(utf8(normalizeTokens(text, false).join(" ")), seed);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/fingerprint/index.ts
|
|
582
|
+
function computeFingerprint(text, salt, opts) {
|
|
583
|
+
const seed = saltSeed(salt);
|
|
584
|
+
const tokens = normalizeTokens(text, true);
|
|
585
|
+
if (tokens.length < MIN_TOKENS) return { tooShort: true };
|
|
586
|
+
const hashes = shingleSet(tokens, seed);
|
|
587
|
+
const fp = {
|
|
588
|
+
tooShort: false,
|
|
589
|
+
simhash: hex64(simHash64(hashes)),
|
|
590
|
+
exactHash: hex64(exactHash(text, seed))
|
|
591
|
+
};
|
|
592
|
+
if (opts.minHash) {
|
|
593
|
+
const sig = minHash64x16(hashes);
|
|
594
|
+
fp.minhashB64 = bytesToBase64(signatureBytes(sig));
|
|
595
|
+
fp.bandKeys = bandKeys(sig, seed).map((k) => k.toString(10));
|
|
596
|
+
}
|
|
597
|
+
if (opts.global) {
|
|
598
|
+
fp.simhashGlobal = hex64(simHash64(shingleSet(tokens, GLOBAL_SEED)));
|
|
599
|
+
}
|
|
600
|
+
return fp;
|
|
601
|
+
}
|
|
602
|
+
function conversationKey(firstUserMessage, salt) {
|
|
603
|
+
const tokens = normalizeTokens(firstUserMessage, true);
|
|
604
|
+
return "c_" + hex64(xxh64(utf8(tokens.join(" ")), saltSeed(salt)));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/resources/inference.ts
|
|
608
|
+
var FAIL_OPEN_SCORE = {
|
|
609
|
+
score: 0,
|
|
610
|
+
tier: "low",
|
|
611
|
+
reasons: [],
|
|
612
|
+
deep_mode_active: false,
|
|
613
|
+
baseline_ready: false,
|
|
614
|
+
shadow_mode: false,
|
|
615
|
+
degraded: true
|
|
616
|
+
};
|
|
617
|
+
var Inference = class {
|
|
618
|
+
constructor(transport, opts) {
|
|
619
|
+
this.transport = transport;
|
|
620
|
+
this.opts = opts;
|
|
621
|
+
}
|
|
622
|
+
transport;
|
|
623
|
+
opts;
|
|
624
|
+
warnedMissingFingerprintInput = false;
|
|
625
|
+
/** Pre-call risk score. Call before invoking the LLM. */
|
|
626
|
+
async score(req) {
|
|
627
|
+
try {
|
|
628
|
+
const body = await this.transport.post(
|
|
629
|
+
"/v1/score",
|
|
630
|
+
this.prepare(req),
|
|
631
|
+
{ idempotent: false }
|
|
632
|
+
);
|
|
633
|
+
const raw = body;
|
|
634
|
+
if (typeof raw?.score !== "number" || typeof raw?.tier !== "string" || raw.tier === "" || typeof raw?.deep_mode_active !== "boolean" || typeof raw?.baseline_ready !== "boolean" || typeof raw?.shadow_mode !== "boolean" || !(Array.isArray(raw?.reasons) || raw?.reasons === null)) {
|
|
635
|
+
throw new VectoralError(
|
|
636
|
+
"vectoral: response is not a score verdict (missing or malformed required fields)",
|
|
637
|
+
{ code: "invalid_response", status: 200, responseBody: JSON.stringify(body) }
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return raw.reasons === null ? { ...body, reasons: [] } : body;
|
|
641
|
+
} catch (err) {
|
|
642
|
+
if (!this.opts.failOpen || !(err instanceof VectoralError)) throw err;
|
|
643
|
+
notifyError(this.opts.onError, err, "inference.score");
|
|
644
|
+
return { ...FAIL_OPEN_SCORE, error: err };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Post-call token/cost telemetry. Call after the LLM responds.
|
|
649
|
+
*
|
|
650
|
+
* This is the feedback that makes every later score meaningful — it is where
|
|
651
|
+
* token velocity and cost signals come from. It never fails open: telemetry
|
|
652
|
+
* you silently drop is telemetry you never notice missing.
|
|
653
|
+
*/
|
|
654
|
+
postCall(event) {
|
|
655
|
+
return this.transport.post("/v1/events/post-call", event, {
|
|
656
|
+
idempotent: event.event_id !== void 0
|
|
657
|
+
}).then(assertAck);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Consume `prompt_text_to_fingerprint` (always stripped from the wire,
|
|
661
|
+
* enabled or not) and attach the computed block when fingerprinting is
|
|
662
|
+
* active. The caller's object is never mutated.
|
|
663
|
+
*/
|
|
664
|
+
prepare(req) {
|
|
665
|
+
const { prompt_text_to_fingerprint: text, ...rest } = req.request;
|
|
666
|
+
const out = { ...req, request: rest };
|
|
667
|
+
const cfg = this.opts.fingerprint;
|
|
668
|
+
if (!cfg) {
|
|
669
|
+
if (text !== void 0 && text !== "" && !this.warnedMissingFingerprintInput) {
|
|
670
|
+
this.warnedMissingFingerprintInput = true;
|
|
671
|
+
this.opts.onWarning?.(
|
|
672
|
+
"request.prompt_text_to_fingerprint was supplied but prompt fingerprinting is not configured \u2014 the text was stripped and no fingerprint was sent. Set `fingerprint: { enabled: true, salt, saltId }`"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
if (rest.prompt_fingerprint !== void 0) return out;
|
|
678
|
+
if (text === void 0 || text === "") {
|
|
679
|
+
if (!this.warnedMissingFingerprintInput) {
|
|
680
|
+
this.warnedMissingFingerprintInput = true;
|
|
681
|
+
this.opts.onWarning?.(
|
|
682
|
+
"prompt fingerprinting is configured but this score() call has no request.prompt_text_to_fingerprint \u2014 no fingerprint sent"
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
687
|
+
const fp = computeFingerprint(text, cfg.salt, {
|
|
688
|
+
minHash: true,
|
|
689
|
+
global: cfg.shareGlobal
|
|
690
|
+
});
|
|
691
|
+
if (fp.tooShort) return out;
|
|
692
|
+
const block = {
|
|
693
|
+
v: 1,
|
|
694
|
+
salt_id: cfg.saltId,
|
|
695
|
+
simhash: fp.simhash,
|
|
696
|
+
exact_hash: fp.exactHash,
|
|
697
|
+
conversation_key: req.session_id ?? conversationKey(text, cfg.salt),
|
|
698
|
+
minhash: fp.minhashB64,
|
|
699
|
+
band_keys: fp.bandKeys
|
|
700
|
+
};
|
|
701
|
+
if (cfg.shareGlobal) block.simhash_global = fp.simhashGlobal;
|
|
702
|
+
out.request = { ...rest, prompt_fingerprint: block };
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/resources/identity.ts
|
|
708
|
+
var Identity = class {
|
|
709
|
+
constructor(transport) {
|
|
710
|
+
this.transport = transport;
|
|
711
|
+
}
|
|
712
|
+
transport;
|
|
713
|
+
/**
|
|
714
|
+
* Record an authentication event.
|
|
715
|
+
*
|
|
716
|
+
* Use the same IP shape here as on `inference.score()`: the accounts-per-IP
|
|
717
|
+
* query unions both sources, so mixing raw addresses on one and tokens on the
|
|
718
|
+
* other splits your own sybil graph.
|
|
719
|
+
*/
|
|
720
|
+
record(event) {
|
|
721
|
+
return this.transport.post("/v1/identity", event, {
|
|
722
|
+
idempotent: event.event_id !== void 0
|
|
723
|
+
}).then(assertAck);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Convenience for the common signup case: record the event and link the
|
|
727
|
+
* registration in one call.
|
|
728
|
+
*/
|
|
729
|
+
linkRegistration(accountId, registrationId, extra = {}) {
|
|
730
|
+
return this.record({
|
|
731
|
+
...extra,
|
|
732
|
+
account_id: accountId,
|
|
733
|
+
event_type: "signup",
|
|
734
|
+
registration_id: registrationId
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/resources/labels.ts
|
|
740
|
+
var Labels = class {
|
|
741
|
+
constructor(transport) {
|
|
742
|
+
this.transport = transport;
|
|
743
|
+
}
|
|
744
|
+
transport;
|
|
745
|
+
/**
|
|
746
|
+
* Report a verdict you reached yourself — a chargeback, a ban, a support
|
|
747
|
+
* resolution. Label legitimate accounts too: a corpus of only-fraud labels
|
|
748
|
+
* teaches a model nothing about the boundary.
|
|
749
|
+
*/
|
|
750
|
+
submit(req) {
|
|
751
|
+
return this.transport.post("/v1/labels", req, { idempotent: false }).then(assertAck);
|
|
752
|
+
}
|
|
753
|
+
fraud(accountId, notes) {
|
|
754
|
+
return this.submit({
|
|
755
|
+
account_id: accountId,
|
|
756
|
+
label: "fraud",
|
|
757
|
+
...notes !== void 0 ? { notes } : {}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
legitimate(accountId, notes) {
|
|
761
|
+
return this.submit({
|
|
762
|
+
account_id: accountId,
|
|
763
|
+
label: "legitimate",
|
|
764
|
+
...notes !== void 0 ? { notes } : {}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// src/client.ts
|
|
770
|
+
var DEFAULT_BASE_URL = "https://api.vectoral.cloud";
|
|
771
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
772
|
+
var DEFAULT_RETRIES = 2;
|
|
773
|
+
var env2 = (name) => {
|
|
774
|
+
const p = globalThis.process;
|
|
775
|
+
return p?.env?.[name];
|
|
776
|
+
};
|
|
777
|
+
var Vectoral = class {
|
|
778
|
+
registrations;
|
|
779
|
+
inference;
|
|
780
|
+
identity;
|
|
781
|
+
labels;
|
|
782
|
+
/** True when prompt fingerprinting resolved to a usable salt. */
|
|
783
|
+
fingerprintingActive;
|
|
784
|
+
constructor(opts = {}) {
|
|
785
|
+
const customerId = opts.customerId;
|
|
786
|
+
const apiKey = opts.apiKey ?? (customerId ? void 0 : env2("VECTORAL_API_KEY"));
|
|
787
|
+
if (apiKey && customerId) {
|
|
788
|
+
throw new VectoralConfigError(
|
|
789
|
+
"provide either `apiKey` or `customerId`, not both"
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
let authHeader;
|
|
793
|
+
if (apiKey) {
|
|
794
|
+
authHeader = { Authorization: `Bearer ${apiKey}` };
|
|
795
|
+
} else if (customerId) {
|
|
796
|
+
authHeader = { "X-Customer-ID": customerId };
|
|
797
|
+
} else {
|
|
798
|
+
throw new VectoralConfigError(
|
|
799
|
+
"`apiKey` is required (pass it, or set VECTORAL_API_KEY)"
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
803
|
+
if (!fetchImpl) {
|
|
804
|
+
throw new VectoralConfigError(
|
|
805
|
+
"no global fetch available \u2014 pass `fetch` (Node >=18 has it built in)"
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
const onWarning = opts.onWarning ?? ((m) => console.warn(`[vectoral] ${m}`));
|
|
809
|
+
const resolved = resolveFingerprintConfig(opts.fingerprint);
|
|
810
|
+
for (const w of resolved.warnings) onWarning(w);
|
|
811
|
+
this.fingerprintingActive = resolved.config !== null;
|
|
812
|
+
const transport = new Transport({
|
|
813
|
+
baseUrl: (opts.baseUrl ?? env2("VECTORAL_BASE_URL") ?? DEFAULT_BASE_URL).replace(
|
|
814
|
+
/\/+$/,
|
|
815
|
+
""
|
|
816
|
+
),
|
|
817
|
+
authHeader,
|
|
818
|
+
headers: opts.headers ?? {},
|
|
819
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
820
|
+
fetch: fetchImpl,
|
|
821
|
+
retries: opts.retries ?? DEFAULT_RETRIES
|
|
822
|
+
});
|
|
823
|
+
const failOpen = opts.failOpen ?? true;
|
|
824
|
+
this.registrations = new Registrations(transport, {
|
|
825
|
+
failOpen,
|
|
826
|
+
onError: opts.onError
|
|
827
|
+
});
|
|
828
|
+
this.inference = new Inference(transport, {
|
|
829
|
+
failOpen,
|
|
830
|
+
fingerprint: resolved.config,
|
|
831
|
+
onError: opts.onError,
|
|
832
|
+
onWarning
|
|
833
|
+
});
|
|
834
|
+
this.identity = new Identity(transport);
|
|
835
|
+
this.labels = new Labels(transport);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
var createClient = (opts = {}) => new Vectoral(opts);
|
|
839
|
+
export {
|
|
840
|
+
Identity,
|
|
841
|
+
Inference,
|
|
842
|
+
Labels,
|
|
843
|
+
MIN_SALT_LENGTH,
|
|
844
|
+
RegistrationTier,
|
|
845
|
+
Registrations,
|
|
846
|
+
Vectoral,
|
|
847
|
+
VectoralConfigError,
|
|
848
|
+
VectoralError,
|
|
849
|
+
computeFingerprint,
|
|
850
|
+
conversationKey,
|
|
851
|
+
createClient,
|
|
852
|
+
generateSalt,
|
|
853
|
+
suggestSaltId
|
|
854
|
+
};
|
|
855
|
+
//# sourceMappingURL=index.js.map
|