@simplr-ai/express 1.0.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 +21 -0
- package/README.md +122 -0
- package/dist/fastify.cjs +241 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +11 -0
- package/dist/fastify.d.ts +11 -0
- package/dist/fastify.js +214 -0
- package/dist/fastify.js.map +1 -0
- package/dist/hono.cjs +246 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +25 -0
- package/dist/hono.d.ts +25 -0
- package/dist/hono.js +218 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +244 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +269 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +41 -0
- package/dist/next.d.ts +41 -0
- package/dist/next.js +241 -0
- package/dist/next.js.map +1 -0
- package/dist/types-CmzpR5S4.d.cts +115 -0
- package/dist/types-CmzpR5S4.d.ts +115 -0
- package/package.json +108 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
SimplrError: () => SimplrError,
|
|
24
|
+
blockEnvelope: () => blockEnvelope,
|
|
25
|
+
createClient: () => createClient,
|
|
26
|
+
createGuard: () => createGuard,
|
|
27
|
+
defaultExtract: () => defaultExtract,
|
|
28
|
+
riskAtLeast: () => riskAtLeast,
|
|
29
|
+
riskRank: () => riskRank,
|
|
30
|
+
simplrExpress: () => simplrExpress
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(src_exports);
|
|
33
|
+
|
|
34
|
+
// src/client.ts
|
|
35
|
+
var DEFAULT_BASE_URL = "https://api.simplr.sh";
|
|
36
|
+
var SimplrError = class extends Error {
|
|
37
|
+
status;
|
|
38
|
+
body;
|
|
39
|
+
constructor(message, status, body) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "SimplrError";
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.body = body;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function createClient(config) {
|
|
47
|
+
if (!config?.apiKey) throw new Error("Simplr: `apiKey` is required");
|
|
48
|
+
const baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
49
|
+
const timeoutMs = config.timeoutMs ?? 15e3;
|
|
50
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
51
|
+
if (typeof fetchImpl !== "function") {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Simplr: no global fetch available \u2014 use Node 18+ or pass `fetch` in options"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
async function apiRequest(method, path, body) {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetchImpl(`${baseUrl}${path}`, {
|
|
61
|
+
method,
|
|
62
|
+
headers: { "Content-Type": "application/json", "X-API-Key": config.apiKey },
|
|
63
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
64
|
+
signal: controller.signal
|
|
65
|
+
});
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
70
|
+
} catch {
|
|
71
|
+
parsed = text;
|
|
72
|
+
}
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const message = parsed && (parsed.message || parsed.error) || `Simplr API error ${res.status}`;
|
|
75
|
+
throw new SimplrError(message, res.status, parsed);
|
|
76
|
+
}
|
|
77
|
+
return parsed && typeof parsed === "object" && "content" in parsed ? parsed.content : parsed;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof SimplrError) throw err;
|
|
80
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
81
|
+
throw new SimplrError(`Request to ${path} timed out after ${timeoutMs}ms`, 0, null);
|
|
82
|
+
}
|
|
83
|
+
throw new SimplrError(err instanceof Error ? err.message : "Network error", 0, null);
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
check: (input) => apiRequest("POST", "/v1/check", input),
|
|
90
|
+
ingestLogs: (deviceId, logs) => apiRequest("POST", "/v1/edge/logs", { device_id: deviceId, logs })
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/core.ts
|
|
95
|
+
var RISK_ORDER = {
|
|
96
|
+
low: 0,
|
|
97
|
+
medium: 1,
|
|
98
|
+
high: 2,
|
|
99
|
+
critical: 3
|
|
100
|
+
};
|
|
101
|
+
var DEFAULT_DEVICE_ID = "simplr-edge-middleware";
|
|
102
|
+
var DEFAULT_THRESHOLD = "high";
|
|
103
|
+
function riskRank(level) {
|
|
104
|
+
return RISK_ORDER[level] ?? 0;
|
|
105
|
+
}
|
|
106
|
+
function riskAtLeast(level, min) {
|
|
107
|
+
return riskRank(level) >= riskRank(min);
|
|
108
|
+
}
|
|
109
|
+
function readHeader(headers, name) {
|
|
110
|
+
if (!headers) return void 0;
|
|
111
|
+
const h = headers;
|
|
112
|
+
if (typeof h.get === "function") {
|
|
113
|
+
const v = h.get(name);
|
|
114
|
+
return v == null ? void 0 : String(v);
|
|
115
|
+
}
|
|
116
|
+
const lower = name.toLowerCase();
|
|
117
|
+
if (lower in h && h[lower] != null) return String(h[lower]);
|
|
118
|
+
for (const key of Object.keys(h)) {
|
|
119
|
+
if (key.toLowerCase() === lower && h[key] != null) return String(h[key]);
|
|
120
|
+
}
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
function extractIp(req) {
|
|
124
|
+
const fwd = readHeader(req?.headers, "x-forwarded-for");
|
|
125
|
+
if (fwd) return fwd.split(",")[0].trim();
|
|
126
|
+
const real = readHeader(req?.headers, "x-real-ip");
|
|
127
|
+
if (real) return real;
|
|
128
|
+
return req?.ip || req?.socket?.remoteAddress || req?.connection?.remoteAddress || void 0;
|
|
129
|
+
}
|
|
130
|
+
function defaultExtract(req) {
|
|
131
|
+
const input = {};
|
|
132
|
+
const device = {};
|
|
133
|
+
const ip = extractIp(req);
|
|
134
|
+
if (ip) device.ip = ip;
|
|
135
|
+
const ua = readHeader(req?.headers, "user-agent");
|
|
136
|
+
if (ua) device.user_agent = ua;
|
|
137
|
+
if (Object.keys(device).length > 0) input.device = device;
|
|
138
|
+
const body = req?.body;
|
|
139
|
+
if (body && typeof body === "object") {
|
|
140
|
+
const b = body;
|
|
141
|
+
if (typeof b.email === "string") input.email = b.email;
|
|
142
|
+
if (typeof b.phone === "string") input.phone = b.phone;
|
|
143
|
+
}
|
|
144
|
+
return input;
|
|
145
|
+
}
|
|
146
|
+
function normalizeBlock(block) {
|
|
147
|
+
if (typeof block === "function") return block;
|
|
148
|
+
const threshold = block?.whenRiskAtLeast ?? DEFAULT_THRESHOLD;
|
|
149
|
+
return (result) => riskAtLeast(result.risk_level, threshold);
|
|
150
|
+
}
|
|
151
|
+
function createGuard(options) {
|
|
152
|
+
if (!options?.apiKey) throw new Error("createGuard: `apiKey` is required");
|
|
153
|
+
const client = createClient({
|
|
154
|
+
apiKey: options.apiKey,
|
|
155
|
+
baseUrl: options.baseUrl,
|
|
156
|
+
timeoutMs: options.timeoutMs,
|
|
157
|
+
fetch: options.fetch
|
|
158
|
+
});
|
|
159
|
+
const extract = options.extract ?? defaultExtract;
|
|
160
|
+
const shouldBlock = normalizeBlock(options.block);
|
|
161
|
+
const failOpen = options.failOpen ?? true;
|
|
162
|
+
const ingestLogs = options.ingestLogs ?? false;
|
|
163
|
+
const deviceId = options.deviceId ?? DEFAULT_DEVICE_ID;
|
|
164
|
+
async function run(req) {
|
|
165
|
+
const input = await extract(req);
|
|
166
|
+
let result;
|
|
167
|
+
try {
|
|
168
|
+
result = await client.check(input);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (failOpen) {
|
|
171
|
+
return { result: null, blocked: false, input, error };
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
const blocked = shouldBlock(result);
|
|
176
|
+
const ctx = { req, input, blocked };
|
|
177
|
+
if (options.onResult) {
|
|
178
|
+
try {
|
|
179
|
+
await options.onResult(result, ctx);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (ingestLogs) {
|
|
184
|
+
try {
|
|
185
|
+
await client.ingestLogs(deviceId, [
|
|
186
|
+
{
|
|
187
|
+
category: "security",
|
|
188
|
+
level: blocked ? "warn" : "info",
|
|
189
|
+
message: blocked ? "simplr guard blocked request" : "simplr guard checked request",
|
|
190
|
+
risk_level: result.risk_level,
|
|
191
|
+
risk_score: result.risk_score
|
|
192
|
+
}
|
|
193
|
+
]);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { result, blocked, input };
|
|
198
|
+
}
|
|
199
|
+
return { client, run };
|
|
200
|
+
}
|
|
201
|
+
function blockEnvelope(result) {
|
|
202
|
+
return {
|
|
203
|
+
error: "request_blocked",
|
|
204
|
+
message: "This request was blocked by Simplr fraud protection.",
|
|
205
|
+
risk_level: result.risk_level,
|
|
206
|
+
risk_score: result.risk_score
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/express.ts
|
|
211
|
+
function simplrExpress(options) {
|
|
212
|
+
const guard = createGuard(options);
|
|
213
|
+
const failOpen = options.failOpen ?? true;
|
|
214
|
+
return async function simplrMiddleware(req, res, next) {
|
|
215
|
+
try {
|
|
216
|
+
const outcome = await guard.run(req);
|
|
217
|
+
req.simplr = outcome.result;
|
|
218
|
+
if (outcome.blocked && outcome.result) {
|
|
219
|
+
res.status(403).json(blockEnvelope(outcome.result));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
next();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (failOpen) {
|
|
225
|
+
req.simplr = null;
|
|
226
|
+
next();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next(err);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
234
|
+
0 && (module.exports = {
|
|
235
|
+
SimplrError,
|
|
236
|
+
blockEnvelope,
|
|
237
|
+
createClient,
|
|
238
|
+
createGuard,
|
|
239
|
+
defaultExtract,
|
|
240
|
+
riskAtLeast,
|
|
241
|
+
riskRank,
|
|
242
|
+
simplrExpress
|
|
243
|
+
});
|
|
244
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/core.ts","../src/express.ts"],"sourcesContent":["/**\n * `@simplr-ai/express` — drop-in server middleware that auto-runs Simplr\n * fraud/identity checks (and optionally ingests edge logs) per request.\n *\n * The default entry exports the framework-agnostic engine (`createGuard`) and the\n * Express adapter (`simplrExpress`). Framework-specific adapters live behind\n * subpaths: `@simplr-ai/express/fastify`, `/hono`, and `/next`.\n *\n * In production this pairs with `@simplr-ai/node`; the request/response shapes,\n * auth header, and endpoints here are compatible with that SDK.\n */\nexport {\n createGuard,\n defaultExtract,\n riskRank,\n riskAtLeast,\n blockEnvelope,\n type Guard,\n} from \"./core.js\";\n\nexport { simplrExpress, type ExpressMiddleware } from \"./express.js\";\n\nexport { SimplrError, createClient, type SimplrClient, type ClientConfig } from \"./client.js\";\n\nexport type {\n RiskLevel,\n CheckInput,\n CheckResult,\n EdgeLogEntry,\n GuardOptions,\n GuardContext,\n GuardOutcome,\n BlockThreshold,\n RequestLike,\n} from \"./types.js\";\n","/**\n * Thin internal client.\n *\n * This is a ~40-line port of the minimal call path from `@simplr-ai/node`\n * (`apiRequest` + `check` + `edge.ingestLogs`) so that this package builds and\n * tests without an unbuilt workspace dependency or any network access.\n *\n * In production this package pairs with `@simplr-ai/node`; the endpoints, auth\n * header (`X-API-Key`), `{ success, message, content }` envelope unwrapping, and\n * 15s default timeout here are byte-for-byte compatible with that SDK and the\n * Simplr API contract.\n */\nimport type { CheckInput, CheckResult, EdgeLogEntry } from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.simplr.sh\";\n\n/** Thrown when the Simplr API returns a non-2xx response or the request fails. */\nexport class SimplrError extends Error {\n readonly status: number;\n readonly body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"SimplrError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport interface ClientConfig {\n apiKey: string;\n baseUrl?: string;\n timeoutMs?: number;\n fetch?: typeof fetch;\n}\n\nexport interface SimplrClient {\n check(input: CheckInput): Promise<CheckResult>;\n ingestLogs(deviceId: string, logs: EdgeLogEntry[]): Promise<unknown>;\n}\n\nexport function createClient(config: ClientConfig): SimplrClient {\n if (!config?.apiKey) throw new Error(\"Simplr: `apiKey` is required\");\n const baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const timeoutMs = config.timeoutMs ?? 15000;\n const fetchImpl = config.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\n \"Simplr: no global fetch available — use Node 18+ or pass `fetch` in options\",\n );\n }\n\n async function apiRequest<T>(method: string, path: string, body?: unknown): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const res = await fetchImpl(`${baseUrl}${path}`, {\n method,\n headers: { \"Content-Type\": \"application/json\", \"X-API-Key\": config.apiKey },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n const text = await res.text();\n let parsed: any;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n parsed = text;\n }\n if (!res.ok) {\n const message =\n (parsed && (parsed.message || parsed.error)) || `Simplr API error ${res.status}`;\n throw new SimplrError(message, res.status, parsed);\n }\n return (parsed && typeof parsed === \"object\" && \"content\" in parsed\n ? parsed.content\n : parsed) as T;\n } catch (err) {\n if (err instanceof SimplrError) throw err;\n if (err instanceof Error && err.name === \"AbortError\") {\n throw new SimplrError(`Request to ${path} timed out after ${timeoutMs}ms`, 0, null);\n }\n throw new SimplrError(err instanceof Error ? err.message : \"Network error\", 0, null);\n } finally {\n clearTimeout(timer);\n }\n }\n\n return {\n check: (input) => apiRequest<CheckResult>(\"POST\", \"/v1/check\", input),\n ingestLogs: (deviceId, logs) =>\n apiRequest(\"POST\", \"/v1/edge/logs\", { device_id: deviceId, logs }),\n };\n}\n","/**\n * Framework-agnostic guard engine.\n *\n * `createGuard` returns an object that, given any request-like object, builds a\n * `CheckInput`, calls `/v1/check`, decides whether to block, optionally ships an\n * edge log, and fails open on error. Framework adapters (express/fastify/hono/\n * next) are thin wrappers over this engine.\n */\nimport { createClient, type SimplrClient } from \"./client.js\";\nimport type {\n BlockThreshold,\n CheckInput,\n CheckResult,\n GuardContext,\n GuardOptions,\n GuardOutcome,\n RiskLevel,\n} from \"./types.js\";\n\nconst RISK_ORDER: Record<RiskLevel, number> = {\n low: 0,\n medium: 1,\n high: 2,\n critical: 3,\n};\n\nconst DEFAULT_DEVICE_ID = \"simplr-edge-middleware\";\nconst DEFAULT_THRESHOLD: RiskLevel = \"high\";\n\n/** Numeric rank of a risk level. Unknown levels rank as the lowest (0). */\nexport function riskRank(level: RiskLevel | string | undefined): number {\n return RISK_ORDER[level as RiskLevel] ?? 0;\n}\n\n/** True when `level` is at least `min` in the low<medium<high<critical ordering. */\nexport function riskAtLeast(level: RiskLevel | string | undefined, min: RiskLevel): boolean {\n return riskRank(level) >= riskRank(min);\n}\n\n/** Read a header from either a plain bag (lowercase keys) or a Headers-like getter. */\nfunction readHeader(headers: unknown, name: string): string | undefined {\n if (!headers) return undefined;\n const h = headers as any;\n if (typeof h.get === \"function\") {\n const v = h.get(name);\n return v == null ? undefined : String(v);\n }\n // Plain object: try the lowercase key, then a case-insensitive scan.\n const lower = name.toLowerCase();\n if (lower in h && h[lower] != null) return String(h[lower]);\n for (const key of Object.keys(h)) {\n if (key.toLowerCase() === lower && h[key] != null) return String(h[key]);\n }\n return undefined;\n}\n\n/** Best-effort client IP from common header and socket locations. */\nfunction extractIp(req: any): string | undefined {\n const fwd = readHeader(req?.headers, \"x-forwarded-for\");\n if (fwd) return fwd.split(\",\")[0]!.trim();\n const real = readHeader(req?.headers, \"x-real-ip\");\n if (real) return real;\n return (\n req?.ip ||\n req?.socket?.remoteAddress ||\n req?.connection?.remoteAddress ||\n undefined\n );\n}\n\n/**\n * Default request → CheckInput extractor. Pulls the client IP and user-agent into\n * `device`, and lifts `email`/`phone` from a parsed JSON body when present.\n */\nexport function defaultExtract(req: any): CheckInput {\n const input: CheckInput = {};\n\n const device: Record<string, unknown> = {};\n const ip = extractIp(req);\n if (ip) device.ip = ip;\n const ua = readHeader(req?.headers, \"user-agent\");\n if (ua) device.user_agent = ua;\n if (Object.keys(device).length > 0) input.device = device;\n\n const body = req?.body;\n if (body && typeof body === \"object\") {\n const b = body as Record<string, unknown>;\n if (typeof b.email === \"string\") input.email = b.email;\n if (typeof b.phone === \"string\") input.phone = b.phone;\n }\n\n return input;\n}\n\nfunction normalizeBlock(\n block: GuardOptions[\"block\"],\n): (result: CheckResult) => boolean {\n if (typeof block === \"function\") return block;\n const threshold = (block as BlockThreshold | undefined)?.whenRiskAtLeast ?? DEFAULT_THRESHOLD;\n return (result) => riskAtLeast(result.risk_level, threshold);\n}\n\nexport interface Guard<Req = any> {\n /** The underlying thin Simplr client (check + edge log ingestion). */\n readonly client: SimplrClient;\n /** Run the full engine against a request; never throws when `failOpen`. */\n run(req: Req): Promise<GuardOutcome>;\n}\n\nexport function createGuard<Req = any>(options: GuardOptions<Req>): Guard<Req> {\n if (!options?.apiKey) throw new Error(\"createGuard: `apiKey` is required\");\n\n const client = createClient({\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n timeoutMs: options.timeoutMs,\n fetch: options.fetch,\n });\n\n const extract = options.extract ?? defaultExtract;\n const shouldBlock = normalizeBlock(options.block);\n const failOpen = options.failOpen ?? true;\n const ingestLogs = options.ingestLogs ?? false;\n const deviceId = options.deviceId ?? DEFAULT_DEVICE_ID;\n\n async function run(req: Req): Promise<GuardOutcome> {\n const input = await extract(req);\n let result: CheckResult;\n try {\n result = await client.check(input);\n } catch (error) {\n if (failOpen) {\n return { result: null, blocked: false, input, error };\n }\n throw error;\n }\n\n const blocked = shouldBlock(result);\n const ctx: GuardContext<Req> = { req, input, blocked };\n\n if (options.onResult) {\n try {\n await options.onResult(result, ctx);\n } catch {\n // onResult is observational; never let it break the request.\n }\n }\n\n if (ingestLogs) {\n try {\n await client.ingestLogs(deviceId, [\n {\n category: \"security\",\n level: blocked ? \"warn\" : \"info\",\n message: blocked ? \"simplr guard blocked request\" : \"simplr guard checked request\",\n risk_level: result.risk_level,\n risk_score: result.risk_score,\n },\n ]);\n } catch {\n // Log shipping is best-effort and must never affect the request.\n }\n }\n\n return { result, blocked, input };\n }\n\n return { client, run };\n}\n\n/** Standard JSON envelope returned to the client on a blocked request. */\nexport function blockEnvelope(result: CheckResult) {\n return {\n error: \"request_blocked\",\n message: \"This request was blocked by Simplr fraud protection.\",\n risk_level: result.risk_level,\n risk_score: result.risk_score,\n };\n}\n","/**\n * Express adapter. Attaches `req.simplr` (the CheckResult) and, on block, sends\n * a 403 JSON envelope. On a Simplr error it fails open by default (calls next()).\n */\nimport { blockEnvelope, createGuard } from \"./core.js\";\nimport type { CheckResult, GuardOptions } from \"./types.js\";\n\n// Loosely typed so we never hard-depend on `express` at type or runtime level.\ntype ExpressRequest = any;\ntype ExpressResponse = {\n status(code: number): ExpressResponse;\n json(body: unknown): unknown;\n};\ntype ExpressNext = (err?: unknown) => void;\n\nexport type ExpressMiddleware = (\n req: ExpressRequest,\n res: ExpressResponse,\n next: ExpressNext,\n) => Promise<void> | void;\n\n/**\n * Build Express middleware that runs a Simplr check per request.\n *\n * ```ts\n * import express from \"express\";\n * import { simplrExpress } from \"@simplr-ai/express\";\n * const app = express();\n * app.use(express.json());\n * app.use(simplrExpress({ apiKey: process.env.SIMPLR_API_KEY! }));\n * ```\n */\nexport function simplrExpress(options: GuardOptions): ExpressMiddleware {\n const guard = createGuard(options);\n const failOpen = options.failOpen ?? true;\n\n return async function simplrMiddleware(req, res, next) {\n try {\n const outcome = await guard.run(req);\n // Attach the result (null when failed-open) for downstream handlers.\n (req as any).simplr = outcome.result;\n if (outcome.blocked && outcome.result) {\n res.status(403).json(blockEnvelope(outcome.result));\n return;\n }\n next();\n } catch (err) {\n // guard.run already swallows check errors when failOpen; this catches\n // anything else (e.g. a throwing custom extract). Honor failOpen here too.\n if (failOpen) {\n (req as any).simplr = null;\n next();\n return;\n }\n next(err);\n }\n };\n}\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** Simplr check result for this request (null if the check failed open). */\n simplr?: CheckResult | null;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,IAAM,mBAAmB;AAGlB,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EACA;AAAA,EACT,YAAY,SAAiB,QAAgB,MAAe;AAC1D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAcO,SAAS,aAAa,QAAoC;AAC/D,MAAI,CAAC,QAAQ,OAAQ,OAAM,IAAI,MAAM,8BAA8B;AACnE,QAAM,WAAW,OAAO,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AACvE,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,YAAY,OAAO,SAAS,WAAW;AAC7C,MAAI,OAAO,cAAc,YAAY;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,WAAc,QAAgB,MAAc,MAA4B;AACrF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,QAAI;AACF,YAAM,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,QAC/C;AAAA,QACA,SAAS,EAAE,gBAAgB,oBAAoB,aAAa,OAAO,OAAO;AAAA,QAC1E,MAAM,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI;AAAA,QAClD,QAAQ,WAAW;AAAA,MACrB,CAAC;AACD,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAI;AACJ,UAAI;AACF,iBAAS,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,MACrC,QAAQ;AACN,iBAAS;AAAA,MACX;AACA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,UACH,WAAW,OAAO,WAAW,OAAO,UAAW,oBAAoB,IAAI,MAAM;AAChF,cAAM,IAAI,YAAY,SAAS,IAAI,QAAQ,MAAM;AAAA,MACnD;AACA,aAAQ,UAAU,OAAO,WAAW,YAAY,aAAa,SACzD,OAAO,UACP;AAAA,IACN,SAAS,KAAK;AACZ,UAAI,eAAe,YAAa,OAAM;AACtC,UAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,cAAM,IAAI,YAAY,cAAc,IAAI,oBAAoB,SAAS,MAAM,GAAG,IAAI;AAAA,MACpF;AACA,YAAM,IAAI,YAAY,eAAe,QAAQ,IAAI,UAAU,iBAAiB,GAAG,IAAI;AAAA,IACrF,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,CAAC,UAAU,WAAwB,QAAQ,aAAa,KAAK;AAAA,IACpE,YAAY,CAAC,UAAU,SACrB,WAAW,QAAQ,iBAAiB,EAAE,WAAW,UAAU,KAAK,CAAC;AAAA,EACrE;AACF;;;ACzEA,IAAM,aAAwC;AAAA,EAC5C,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,UAAU;AACZ;AAEA,IAAM,oBAAoB;AAC1B,IAAM,oBAA+B;AAG9B,SAAS,SAAS,OAA+C;AACtE,SAAO,WAAW,KAAkB,KAAK;AAC3C;AAGO,SAAS,YAAY,OAAuC,KAAyB;AAC1F,SAAO,SAAS,KAAK,KAAK,SAAS,GAAG;AACxC;AAGA,SAAS,WAAW,SAAkB,MAAkC;AACtE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,QAAQ,YAAY;AAC/B,UAAM,IAAI,EAAE,IAAI,IAAI;AACpB,WAAO,KAAK,OAAO,SAAY,OAAO,CAAC;AAAA,EACzC;AAEA,QAAM,QAAQ,KAAK,YAAY;AAC/B,MAAI,SAAS,KAAK,EAAE,KAAK,KAAK,KAAM,QAAO,OAAO,EAAE,KAAK,CAAC;AAC1D,aAAW,OAAO,OAAO,KAAK,CAAC,GAAG;AAChC,QAAI,IAAI,YAAY,MAAM,SAAS,EAAE,GAAG,KAAK,KAAM,QAAO,OAAO,EAAE,GAAG,CAAC;AAAA,EACzE;AACA,SAAO;AACT;AAGA,SAAS,UAAU,KAA8B;AAC/C,QAAM,MAAM,WAAW,KAAK,SAAS,iBAAiB;AACtD,MAAI,IAAK,QAAO,IAAI,MAAM,GAAG,EAAE,CAAC,EAAG,KAAK;AACxC,QAAM,OAAO,WAAW,KAAK,SAAS,WAAW;AACjD,MAAI,KAAM,QAAO;AACjB,SACE,KAAK,MACL,KAAK,QAAQ,iBACb,KAAK,YAAY,iBACjB;AAEJ;AAMO,SAAS,eAAe,KAAsB;AACnD,QAAM,QAAoB,CAAC;AAE3B,QAAM,SAAkC,CAAC;AACzC,QAAM,KAAK,UAAU,GAAG;AACxB,MAAI,GAAI,QAAO,KAAK;AACpB,QAAM,KAAK,WAAW,KAAK,SAAS,YAAY;AAChD,MAAI,GAAI,QAAO,aAAa;AAC5B,MAAI,OAAO,KAAK,MAAM,EAAE,SAAS,EAAG,OAAM,SAAS;AAEnD,QAAM,OAAO,KAAK;AAClB,MAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,UAAM,IAAI;AACV,QAAI,OAAO,EAAE,UAAU,SAAU,OAAM,QAAQ,EAAE;AACjD,QAAI,OAAO,EAAE,UAAU,SAAU,OAAM,QAAQ,EAAE;AAAA,EACnD;AAEA,SAAO;AACT;AAEA,SAAS,eACP,OACkC;AAClC,MAAI,OAAO,UAAU,WAAY,QAAO;AACxC,QAAM,YAAa,OAAsC,mBAAmB;AAC5E,SAAO,CAAC,WAAW,YAAY,OAAO,YAAY,SAAS;AAC7D;AASO,SAAS,YAAuB,SAAwC;AAC7E,MAAI,CAAC,SAAS,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAEzE,QAAM,SAAS,aAAa;AAAA,IAC1B,QAAQ,QAAQ;AAAA,IAChB,SAAS,QAAQ;AAAA,IACjB,WAAW,QAAQ;AAAA,IACnB,OAAO,QAAQ;AAAA,EACjB,CAAC;AAED,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,cAAc,eAAe,QAAQ,KAAK;AAChD,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AAErC,iBAAe,IAAI,KAAiC;AAClD,UAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,OAAO,MAAM,KAAK;AAAA,IACnC,SAAS,OAAO;AACd,UAAI,UAAU;AACZ,eAAO,EAAE,QAAQ,MAAM,SAAS,OAAO,OAAO,MAAM;AAAA,MACtD;AACA,YAAM;AAAA,IACR;AAEA,UAAM,UAAU,YAAY,MAAM;AAClC,UAAM,MAAyB,EAAE,KAAK,OAAO,QAAQ;AAErD,QAAI,QAAQ,UAAU;AACpB,UAAI;AACF,cAAM,QAAQ,SAAS,QAAQ,GAAG;AAAA,MACpC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,YAAY;AACd,UAAI;AACF,cAAM,OAAO,WAAW,UAAU;AAAA,UAChC;AAAA,YACE,UAAU;AAAA,YACV,OAAO,UAAU,SAAS;AAAA,YAC1B,SAAS,UAAU,iCAAiC;AAAA,YACpD,YAAY,OAAO;AAAA,YACnB,YAAY,OAAO;AAAA,UACrB;AAAA,QACF,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,WAAO,EAAE,QAAQ,SAAS,MAAM;AAAA,EAClC;AAEA,SAAO,EAAE,QAAQ,IAAI;AACvB;AAGO,SAAS,cAAc,QAAqB;AACjD,SAAO;AAAA,IACL,OAAO;AAAA,IACP,SAAS;AAAA,IACT,YAAY,OAAO;AAAA,IACnB,YAAY,OAAO;AAAA,EACrB;AACF;;;AClJO,SAAS,cAAc,SAA0C;AACtE,QAAM,QAAQ,YAAY,OAAO;AACjC,QAAM,WAAW,QAAQ,YAAY;AAErC,SAAO,eAAe,iBAAiB,KAAK,KAAK,MAAM;AACrD,QAAI;AACF,YAAM,UAAU,MAAM,MAAM,IAAI,GAAG;AAEnC,MAAC,IAAY,SAAS,QAAQ;AAC9B,UAAI,QAAQ,WAAW,QAAQ,QAAQ;AACrC,YAAI,OAAO,GAAG,EAAE,KAAK,cAAc,QAAQ,MAAM,CAAC;AAClD;AAAA,MACF;AACA,WAAK;AAAA,IACP,SAAS,KAAK;AAGZ,UAAI,UAAU;AACZ,QAAC,IAAY,SAAS;AACtB,aAAK;AACL;AAAA,MACF;AACA,WAAK,GAAG;AAAA,IACV;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { a as CheckInput, C as CheckResult, E as EdgeLogEntry, b as GuardOutcome, R as RiskLevel, G as GuardOptions } from './types-CmzpR5S4.cjs';
|
|
2
|
+
export { B as BlockThreshold, c as GuardContext, d as RequestLike } from './types-CmzpR5S4.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin internal client.
|
|
6
|
+
*
|
|
7
|
+
* This is a ~40-line port of the minimal call path from `@simplr-ai/node`
|
|
8
|
+
* (`apiRequest` + `check` + `edge.ingestLogs`) so that this package builds and
|
|
9
|
+
* tests without an unbuilt workspace dependency or any network access.
|
|
10
|
+
*
|
|
11
|
+
* In production this package pairs with `@simplr-ai/node`; the endpoints, auth
|
|
12
|
+
* header (`X-API-Key`), `{ success, message, content }` envelope unwrapping, and
|
|
13
|
+
* 15s default timeout here are byte-for-byte compatible with that SDK and the
|
|
14
|
+
* Simplr API contract.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Thrown when the Simplr API returns a non-2xx response or the request fails. */
|
|
18
|
+
declare class SimplrError extends Error {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly body: unknown;
|
|
21
|
+
constructor(message: string, status: number, body: unknown);
|
|
22
|
+
}
|
|
23
|
+
interface ClientConfig {
|
|
24
|
+
apiKey: string;
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
interface SimplrClient {
|
|
30
|
+
check(input: CheckInput): Promise<CheckResult>;
|
|
31
|
+
ingestLogs(deviceId: string, logs: EdgeLogEntry[]): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
declare function createClient(config: ClientConfig): SimplrClient;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Framework-agnostic guard engine.
|
|
37
|
+
*
|
|
38
|
+
* `createGuard` returns an object that, given any request-like object, builds a
|
|
39
|
+
* `CheckInput`, calls `/v1/check`, decides whether to block, optionally ships an
|
|
40
|
+
* edge log, and fails open on error. Framework adapters (express/fastify/hono/
|
|
41
|
+
* next) are thin wrappers over this engine.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** Numeric rank of a risk level. Unknown levels rank as the lowest (0). */
|
|
45
|
+
declare function riskRank(level: RiskLevel | string | undefined): number;
|
|
46
|
+
/** True when `level` is at least `min` in the low<medium<high<critical ordering. */
|
|
47
|
+
declare function riskAtLeast(level: RiskLevel | string | undefined, min: RiskLevel): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Default request → CheckInput extractor. Pulls the client IP and user-agent into
|
|
50
|
+
* `device`, and lifts `email`/`phone` from a parsed JSON body when present.
|
|
51
|
+
*/
|
|
52
|
+
declare function defaultExtract(req: any): CheckInput;
|
|
53
|
+
interface Guard<Req = any> {
|
|
54
|
+
/** The underlying thin Simplr client (check + edge log ingestion). */
|
|
55
|
+
readonly client: SimplrClient;
|
|
56
|
+
/** Run the full engine against a request; never throws when `failOpen`. */
|
|
57
|
+
run(req: Req): Promise<GuardOutcome>;
|
|
58
|
+
}
|
|
59
|
+
declare function createGuard<Req = any>(options: GuardOptions<Req>): Guard<Req>;
|
|
60
|
+
/** Standard JSON envelope returned to the client on a blocked request. */
|
|
61
|
+
declare function blockEnvelope(result: CheckResult): {
|
|
62
|
+
error: string;
|
|
63
|
+
message: string;
|
|
64
|
+
risk_level: RiskLevel;
|
|
65
|
+
risk_score: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ExpressRequest = any;
|
|
69
|
+
type ExpressResponse = {
|
|
70
|
+
status(code: number): ExpressResponse;
|
|
71
|
+
json(body: unknown): unknown;
|
|
72
|
+
};
|
|
73
|
+
type ExpressNext = (err?: unknown) => void;
|
|
74
|
+
type ExpressMiddleware = (req: ExpressRequest, res: ExpressResponse, next: ExpressNext) => Promise<void> | void;
|
|
75
|
+
/**
|
|
76
|
+
* Build Express middleware that runs a Simplr check per request.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* import express from "express";
|
|
80
|
+
* import { simplrExpress } from "@simplr-ai/express";
|
|
81
|
+
* const app = express();
|
|
82
|
+
* app.use(express.json());
|
|
83
|
+
* app.use(simplrExpress({ apiKey: process.env.SIMPLR_API_KEY! }));
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare function simplrExpress(options: GuardOptions): ExpressMiddleware;
|
|
87
|
+
declare global {
|
|
88
|
+
namespace Express {
|
|
89
|
+
interface Request {
|
|
90
|
+
/** Simplr check result for this request (null if the check failed open). */
|
|
91
|
+
simplr?: CheckResult | null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { CheckInput, CheckResult, type ClientConfig, EdgeLogEntry, type ExpressMiddleware, type Guard, GuardOptions, GuardOutcome, RiskLevel, type SimplrClient, SimplrError, blockEnvelope, createClient, createGuard, defaultExtract, riskAtLeast, riskRank, simplrExpress };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { a as CheckInput, C as CheckResult, E as EdgeLogEntry, b as GuardOutcome, R as RiskLevel, G as GuardOptions } from './types-CmzpR5S4.js';
|
|
2
|
+
export { B as BlockThreshold, c as GuardContext, d as RequestLike } from './types-CmzpR5S4.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin internal client.
|
|
6
|
+
*
|
|
7
|
+
* This is a ~40-line port of the minimal call path from `@simplr-ai/node`
|
|
8
|
+
* (`apiRequest` + `check` + `edge.ingestLogs`) so that this package builds and
|
|
9
|
+
* tests without an unbuilt workspace dependency or any network access.
|
|
10
|
+
*
|
|
11
|
+
* In production this package pairs with `@simplr-ai/node`; the endpoints, auth
|
|
12
|
+
* header (`X-API-Key`), `{ success, message, content }` envelope unwrapping, and
|
|
13
|
+
* 15s default timeout here are byte-for-byte compatible with that SDK and the
|
|
14
|
+
* Simplr API contract.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Thrown when the Simplr API returns a non-2xx response or the request fails. */
|
|
18
|
+
declare class SimplrError extends Error {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly body: unknown;
|
|
21
|
+
constructor(message: string, status: number, body: unknown);
|
|
22
|
+
}
|
|
23
|
+
interface ClientConfig {
|
|
24
|
+
apiKey: string;
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
interface SimplrClient {
|
|
30
|
+
check(input: CheckInput): Promise<CheckResult>;
|
|
31
|
+
ingestLogs(deviceId: string, logs: EdgeLogEntry[]): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
declare function createClient(config: ClientConfig): SimplrClient;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Framework-agnostic guard engine.
|
|
37
|
+
*
|
|
38
|
+
* `createGuard` returns an object that, given any request-like object, builds a
|
|
39
|
+
* `CheckInput`, calls `/v1/check`, decides whether to block, optionally ships an
|
|
40
|
+
* edge log, and fails open on error. Framework adapters (express/fastify/hono/
|
|
41
|
+
* next) are thin wrappers over this engine.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** Numeric rank of a risk level. Unknown levels rank as the lowest (0). */
|
|
45
|
+
declare function riskRank(level: RiskLevel | string | undefined): number;
|
|
46
|
+
/** True when `level` is at least `min` in the low<medium<high<critical ordering. */
|
|
47
|
+
declare function riskAtLeast(level: RiskLevel | string | undefined, min: RiskLevel): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Default request → CheckInput extractor. Pulls the client IP and user-agent into
|
|
50
|
+
* `device`, and lifts `email`/`phone` from a parsed JSON body when present.
|
|
51
|
+
*/
|
|
52
|
+
declare function defaultExtract(req: any): CheckInput;
|
|
53
|
+
interface Guard<Req = any> {
|
|
54
|
+
/** The underlying thin Simplr client (check + edge log ingestion). */
|
|
55
|
+
readonly client: SimplrClient;
|
|
56
|
+
/** Run the full engine against a request; never throws when `failOpen`. */
|
|
57
|
+
run(req: Req): Promise<GuardOutcome>;
|
|
58
|
+
}
|
|
59
|
+
declare function createGuard<Req = any>(options: GuardOptions<Req>): Guard<Req>;
|
|
60
|
+
/** Standard JSON envelope returned to the client on a blocked request. */
|
|
61
|
+
declare function blockEnvelope(result: CheckResult): {
|
|
62
|
+
error: string;
|
|
63
|
+
message: string;
|
|
64
|
+
risk_level: RiskLevel;
|
|
65
|
+
risk_score: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ExpressRequest = any;
|
|
69
|
+
type ExpressResponse = {
|
|
70
|
+
status(code: number): ExpressResponse;
|
|
71
|
+
json(body: unknown): unknown;
|
|
72
|
+
};
|
|
73
|
+
type ExpressNext = (err?: unknown) => void;
|
|
74
|
+
type ExpressMiddleware = (req: ExpressRequest, res: ExpressResponse, next: ExpressNext) => Promise<void> | void;
|
|
75
|
+
/**
|
|
76
|
+
* Build Express middleware that runs a Simplr check per request.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* import express from "express";
|
|
80
|
+
* import { simplrExpress } from "@simplr-ai/express";
|
|
81
|
+
* const app = express();
|
|
82
|
+
* app.use(express.json());
|
|
83
|
+
* app.use(simplrExpress({ apiKey: process.env.SIMPLR_API_KEY! }));
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare function simplrExpress(options: GuardOptions): ExpressMiddleware;
|
|
87
|
+
declare global {
|
|
88
|
+
namespace Express {
|
|
89
|
+
interface Request {
|
|
90
|
+
/** Simplr check result for this request (null if the check failed open). */
|
|
91
|
+
simplr?: CheckResult | null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { CheckInput, CheckResult, type ClientConfig, EdgeLogEntry, type ExpressMiddleware, type Guard, GuardOptions, GuardOutcome, RiskLevel, type SimplrClient, SimplrError, blockEnvelope, createClient, createGuard, defaultExtract, riskAtLeast, riskRank, simplrExpress };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://api.simplr.sh";
|
|
3
|
+
var SimplrError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
constructor(message, status, body) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SimplrError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function createClient(config) {
|
|
14
|
+
if (!config?.apiKey) throw new Error("Simplr: `apiKey` is required");
|
|
15
|
+
const baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
16
|
+
const timeoutMs = config.timeoutMs ?? 15e3;
|
|
17
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
18
|
+
if (typeof fetchImpl !== "function") {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Simplr: no global fetch available \u2014 use Node 18+ or pass `fetch` in options"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
async function apiRequest(method, path, body) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetchImpl(`${baseUrl}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { "Content-Type": "application/json", "X-API-Key": config.apiKey },
|
|
30
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
31
|
+
signal: controller.signal
|
|
32
|
+
});
|
|
33
|
+
const text = await res.text();
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
37
|
+
} catch {
|
|
38
|
+
parsed = text;
|
|
39
|
+
}
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const message = parsed && (parsed.message || parsed.error) || `Simplr API error ${res.status}`;
|
|
42
|
+
throw new SimplrError(message, res.status, parsed);
|
|
43
|
+
}
|
|
44
|
+
return parsed && typeof parsed === "object" && "content" in parsed ? parsed.content : parsed;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof SimplrError) throw err;
|
|
47
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
48
|
+
throw new SimplrError(`Request to ${path} timed out after ${timeoutMs}ms`, 0, null);
|
|
49
|
+
}
|
|
50
|
+
throw new SimplrError(err instanceof Error ? err.message : "Network error", 0, null);
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
check: (input) => apiRequest("POST", "/v1/check", input),
|
|
57
|
+
ingestLogs: (deviceId, logs) => apiRequest("POST", "/v1/edge/logs", { device_id: deviceId, logs })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/core.ts
|
|
62
|
+
var RISK_ORDER = {
|
|
63
|
+
low: 0,
|
|
64
|
+
medium: 1,
|
|
65
|
+
high: 2,
|
|
66
|
+
critical: 3
|
|
67
|
+
};
|
|
68
|
+
var DEFAULT_DEVICE_ID = "simplr-edge-middleware";
|
|
69
|
+
var DEFAULT_THRESHOLD = "high";
|
|
70
|
+
function riskRank(level) {
|
|
71
|
+
return RISK_ORDER[level] ?? 0;
|
|
72
|
+
}
|
|
73
|
+
function riskAtLeast(level, min) {
|
|
74
|
+
return riskRank(level) >= riskRank(min);
|
|
75
|
+
}
|
|
76
|
+
function readHeader(headers, name) {
|
|
77
|
+
if (!headers) return void 0;
|
|
78
|
+
const h = headers;
|
|
79
|
+
if (typeof h.get === "function") {
|
|
80
|
+
const v = h.get(name);
|
|
81
|
+
return v == null ? void 0 : String(v);
|
|
82
|
+
}
|
|
83
|
+
const lower = name.toLowerCase();
|
|
84
|
+
if (lower in h && h[lower] != null) return String(h[lower]);
|
|
85
|
+
for (const key of Object.keys(h)) {
|
|
86
|
+
if (key.toLowerCase() === lower && h[key] != null) return String(h[key]);
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function extractIp(req) {
|
|
91
|
+
const fwd = readHeader(req?.headers, "x-forwarded-for");
|
|
92
|
+
if (fwd) return fwd.split(",")[0].trim();
|
|
93
|
+
const real = readHeader(req?.headers, "x-real-ip");
|
|
94
|
+
if (real) return real;
|
|
95
|
+
return req?.ip || req?.socket?.remoteAddress || req?.connection?.remoteAddress || void 0;
|
|
96
|
+
}
|
|
97
|
+
function defaultExtract(req) {
|
|
98
|
+
const input = {};
|
|
99
|
+
const device = {};
|
|
100
|
+
const ip = extractIp(req);
|
|
101
|
+
if (ip) device.ip = ip;
|
|
102
|
+
const ua = readHeader(req?.headers, "user-agent");
|
|
103
|
+
if (ua) device.user_agent = ua;
|
|
104
|
+
if (Object.keys(device).length > 0) input.device = device;
|
|
105
|
+
const body = req?.body;
|
|
106
|
+
if (body && typeof body === "object") {
|
|
107
|
+
const b = body;
|
|
108
|
+
if (typeof b.email === "string") input.email = b.email;
|
|
109
|
+
if (typeof b.phone === "string") input.phone = b.phone;
|
|
110
|
+
}
|
|
111
|
+
return input;
|
|
112
|
+
}
|
|
113
|
+
function normalizeBlock(block) {
|
|
114
|
+
if (typeof block === "function") return block;
|
|
115
|
+
const threshold = block?.whenRiskAtLeast ?? DEFAULT_THRESHOLD;
|
|
116
|
+
return (result) => riskAtLeast(result.risk_level, threshold);
|
|
117
|
+
}
|
|
118
|
+
function createGuard(options) {
|
|
119
|
+
if (!options?.apiKey) throw new Error("createGuard: `apiKey` is required");
|
|
120
|
+
const client = createClient({
|
|
121
|
+
apiKey: options.apiKey,
|
|
122
|
+
baseUrl: options.baseUrl,
|
|
123
|
+
timeoutMs: options.timeoutMs,
|
|
124
|
+
fetch: options.fetch
|
|
125
|
+
});
|
|
126
|
+
const extract = options.extract ?? defaultExtract;
|
|
127
|
+
const shouldBlock = normalizeBlock(options.block);
|
|
128
|
+
const failOpen = options.failOpen ?? true;
|
|
129
|
+
const ingestLogs = options.ingestLogs ?? false;
|
|
130
|
+
const deviceId = options.deviceId ?? DEFAULT_DEVICE_ID;
|
|
131
|
+
async function run(req) {
|
|
132
|
+
const input = await extract(req);
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
result = await client.check(input);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (failOpen) {
|
|
138
|
+
return { result: null, blocked: false, input, error };
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const blocked = shouldBlock(result);
|
|
143
|
+
const ctx = { req, input, blocked };
|
|
144
|
+
if (options.onResult) {
|
|
145
|
+
try {
|
|
146
|
+
await options.onResult(result, ctx);
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (ingestLogs) {
|
|
151
|
+
try {
|
|
152
|
+
await client.ingestLogs(deviceId, [
|
|
153
|
+
{
|
|
154
|
+
category: "security",
|
|
155
|
+
level: blocked ? "warn" : "info",
|
|
156
|
+
message: blocked ? "simplr guard blocked request" : "simplr guard checked request",
|
|
157
|
+
risk_level: result.risk_level,
|
|
158
|
+
risk_score: result.risk_score
|
|
159
|
+
}
|
|
160
|
+
]);
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { result, blocked, input };
|
|
165
|
+
}
|
|
166
|
+
return { client, run };
|
|
167
|
+
}
|
|
168
|
+
function blockEnvelope(result) {
|
|
169
|
+
return {
|
|
170
|
+
error: "request_blocked",
|
|
171
|
+
message: "This request was blocked by Simplr fraud protection.",
|
|
172
|
+
risk_level: result.risk_level,
|
|
173
|
+
risk_score: result.risk_score
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/express.ts
|
|
178
|
+
function simplrExpress(options) {
|
|
179
|
+
const guard = createGuard(options);
|
|
180
|
+
const failOpen = options.failOpen ?? true;
|
|
181
|
+
return async function simplrMiddleware(req, res, next) {
|
|
182
|
+
try {
|
|
183
|
+
const outcome = await guard.run(req);
|
|
184
|
+
req.simplr = outcome.result;
|
|
185
|
+
if (outcome.blocked && outcome.result) {
|
|
186
|
+
res.status(403).json(blockEnvelope(outcome.result));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
next();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (failOpen) {
|
|
192
|
+
req.simplr = null;
|
|
193
|
+
next();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
next(err);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
SimplrError,
|
|
202
|
+
blockEnvelope,
|
|
203
|
+
createClient,
|
|
204
|
+
createGuard,
|
|
205
|
+
defaultExtract,
|
|
206
|
+
riskAtLeast,
|
|
207
|
+
riskRank,
|
|
208
|
+
simplrExpress
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=index.js.map
|