@simplr-ai/express 1.0.0 → 1.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/dist/next.cjs CHANGED
@@ -25,6 +25,150 @@ __export(next_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(next_exports);
27
27
 
28
+ // src/network-log.ts
29
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
30
+ "authorization",
31
+ "cookie",
32
+ "set-cookie",
33
+ "x-api-key",
34
+ "x-auth-token",
35
+ "x-csrf-token",
36
+ "x-xsrf-token"
37
+ ]);
38
+ var SENSITIVE_KEY_PARTS = [
39
+ "password",
40
+ "passwd",
41
+ "secret",
42
+ "token",
43
+ "api_key",
44
+ "apikey",
45
+ "authorization",
46
+ "auth",
47
+ "credential",
48
+ "private_key",
49
+ "card",
50
+ "cardnumber",
51
+ "pan",
52
+ "cvv",
53
+ "cvc",
54
+ "ssn",
55
+ "pin",
56
+ "otp"
57
+ ];
58
+ var MAX_REDACT_DEPTH = 8;
59
+ var MAX_BODY_CHARS = 1e4;
60
+ function isSensitiveKey(key, extraKeys) {
61
+ const lower = key.toLowerCase();
62
+ if (extraKeys.some((k) => lower === k.toLowerCase())) return true;
63
+ return SENSITIVE_KEY_PARTS.some((part) => lower.includes(part));
64
+ }
65
+ function redactDeep(value, extraKeys, depth = 0) {
66
+ if (depth >= MAX_REDACT_DEPTH) return "[truncated]";
67
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, extraKeys, depth + 1));
68
+ if (value && typeof value === "object") {
69
+ const out = {};
70
+ for (const [key, val] of Object.entries(value)) {
71
+ out[key] = isSensitiveKey(key, extraKeys) ? "[REDACTED]" : redactDeep(val, extraKeys, depth + 1);
72
+ }
73
+ return out;
74
+ }
75
+ return value;
76
+ }
77
+ function redactHeaders(headers) {
78
+ if (!headers) return void 0;
79
+ const out = {};
80
+ const set = (key, value) => {
81
+ out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
82
+ };
83
+ if (typeof headers.forEach === "function" && !Array.isArray(headers)) {
84
+ headers.forEach((value, key) => set(key, value));
85
+ } else {
86
+ for (const [key, value] of Object.entries(headers)) {
87
+ set(key, String(value));
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+ function previewBody(raw, redactFields = []) {
93
+ if (raw === void 0 || raw === null) return void 0;
94
+ let value = raw;
95
+ if (typeof raw === "string") {
96
+ try {
97
+ value = JSON.parse(raw);
98
+ } catch {
99
+ return raw.length > MAX_BODY_CHARS ? raw.slice(0, MAX_BODY_CHARS) + "\u2026[truncated]" : raw;
100
+ }
101
+ }
102
+ const redacted = redactDeep(value, redactFields);
103
+ let text;
104
+ try {
105
+ text = JSON.stringify(redacted);
106
+ } catch {
107
+ return "[unserializable]";
108
+ }
109
+ if (text.length > MAX_BODY_CHARS) {
110
+ return text.slice(0, MAX_BODY_CHARS) + "\u2026[truncated]";
111
+ }
112
+ return redacted;
113
+ }
114
+ function newLogId() {
115
+ const uuid = globalThis?.crypto?.randomUUID;
116
+ if (typeof uuid === "function") return uuid.call(globalThis.crypto);
117
+ return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
118
+ }
119
+
120
+ // src/network-shipper.ts
121
+ var DEFAULT_BATCH = 25;
122
+ var DEFAULT_FLUSH_MS = 5e3;
123
+ var NetworkLogShipper = class {
124
+ cfg;
125
+ queue = [];
126
+ timer = null;
127
+ constructor(cfg) {
128
+ this.cfg = cfg;
129
+ }
130
+ start() {
131
+ if (this.timer) return;
132
+ const interval = this.cfg.flushIntervalMs ?? DEFAULT_FLUSH_MS;
133
+ this.timer = setInterval(() => {
134
+ void this.flush();
135
+ }, interval);
136
+ this.timer?.unref?.();
137
+ }
138
+ add(entry) {
139
+ this.queue.push({
140
+ ...entry,
141
+ sdk: this.cfg.sdk,
142
+ applicationId: entry.applicationId ?? this.cfg.applicationId,
143
+ environment: entry.environment ?? this.cfg.environment
144
+ });
145
+ if (this.queue.length >= (this.cfg.batchSize ?? DEFAULT_BATCH)) {
146
+ void this.flush();
147
+ }
148
+ }
149
+ async flush() {
150
+ if (this.queue.length === 0) return;
151
+ const logs = this.queue;
152
+ this.queue = [];
153
+ try {
154
+ await this.cfg.fetchImpl(`${this.cfg.baseUrl}/v1/network-logs`, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ "X-API-Key": this.cfg.apiKey
159
+ },
160
+ body: JSON.stringify({ logs })
161
+ });
162
+ } catch {
163
+ }
164
+ }
165
+ stop() {
166
+ if (this.timer) clearInterval(this.timer);
167
+ this.timer = null;
168
+ void this.flush();
169
+ }
170
+ };
171
+
28
172
  // src/client.ts
29
173
  var DEFAULT_BASE_URL = "https://api.simplr.sh";
30
174
  var SimplrError = class extends Error {
@@ -47,13 +191,51 @@ function createClient(config) {
47
191
  "Simplr: no global fetch available \u2014 use Node 18+ or pass `fetch` in options"
48
192
  );
49
193
  }
194
+ const logBodies = config.logBodies ?? config.shipNetworkLogs;
195
+ const redactFields = config.redactFields;
196
+ let shipper;
197
+ if (config.shipNetworkLogs) {
198
+ shipper = new NetworkLogShipper({
199
+ baseUrl,
200
+ apiKey: config.apiKey,
201
+ fetchImpl,
202
+ sdk: "express",
203
+ applicationId: config.applicationId,
204
+ environment: config.environment
205
+ });
206
+ shipper.start();
207
+ }
208
+ const userLog = config.onNetworkLog;
209
+ const onNetworkLog = shipper || userLog ? (entry) => {
210
+ shipper?.add(entry);
211
+ userLog?.(entry);
212
+ } : void 0;
50
213
  async function apiRequest(method, path, body) {
51
214
  const controller = new AbortController();
52
215
  const timer = setTimeout(() => controller.abort(), timeoutMs);
216
+ const url = `${baseUrl}${path}`;
217
+ const requestHeaders = { "Content-Type": "application/json", "X-API-Key": config.apiKey };
218
+ const startedAt = Date.now();
219
+ const log = onNetworkLog ? {
220
+ id: newLogId(),
221
+ source: "backend",
222
+ timestamp: new Date(startedAt).toISOString(),
223
+ method,
224
+ url,
225
+ requestHeaders: redactHeaders(requestHeaders),
226
+ requestBody: logBodies ? previewBody(body, redactFields) : void 0
227
+ } : null;
228
+ const emit = (extra) => {
229
+ if (!log || !onNetworkLog) return;
230
+ try {
231
+ onNetworkLog({ ...log, durationMs: Date.now() - startedAt, ...extra });
232
+ } catch {
233
+ }
234
+ };
53
235
  try {
54
- const res = await fetchImpl(`${baseUrl}${path}`, {
236
+ const res = await fetchImpl(url, {
55
237
  method,
56
- headers: { "Content-Type": "application/json", "X-API-Key": config.apiKey },
238
+ headers: requestHeaders,
57
239
  body: body !== void 0 ? JSON.stringify(body) : void 0,
58
240
  signal: controller.signal
59
241
  });
@@ -64,6 +246,13 @@ function createClient(config) {
64
246
  } catch {
65
247
  parsed = text;
66
248
  }
249
+ emit({
250
+ status: res.status,
251
+ statusText: res.statusText,
252
+ ok: res.ok,
253
+ responseHeaders: redactHeaders(res.headers),
254
+ responseBody: logBodies ? previewBody(parsed ?? text, redactFields) : void 0
255
+ });
67
256
  if (!res.ok) {
68
257
  const message = parsed && (parsed.message || parsed.error) || `Simplr API error ${res.status}`;
69
258
  throw new SimplrError(message, res.status, parsed);
@@ -72,8 +261,10 @@ function createClient(config) {
72
261
  } catch (err) {
73
262
  if (err instanceof SimplrError) throw err;
74
263
  if (err instanceof Error && err.name === "AbortError") {
264
+ emit({ ok: false, error: `timed out after ${timeoutMs}ms` });
75
265
  throw new SimplrError(`Request to ${path} timed out after ${timeoutMs}ms`, 0, null);
76
266
  }
267
+ emit({ ok: false, error: err instanceof Error ? err.message : "Network error" });
77
268
  throw new SimplrError(err instanceof Error ? err.message : "Network error", 0, null);
78
269
  } finally {
79
270
  clearTimeout(timer);
@@ -81,7 +272,9 @@ function createClient(config) {
81
272
  }
82
273
  return {
83
274
  check: (input) => apiRequest("POST", "/v1/check", input),
84
- ingestLogs: (deviceId, logs) => apiRequest("POST", "/v1/edge/logs", { device_id: deviceId, logs })
275
+ ingestLogs: (deviceId, logs) => apiRequest("POST", "/v1/edge/logs", { device_id: deviceId, logs }),
276
+ flushNetworkLogs: () => shipper?.flush() ?? Promise.resolve(),
277
+ close: () => shipper?.stop()
85
278
  };
86
279
  }
87
280
 
@@ -148,7 +341,13 @@ function createGuard(options) {
148
341
  apiKey: options.apiKey,
149
342
  baseUrl: options.baseUrl,
150
343
  timeoutMs: options.timeoutMs,
151
- fetch: options.fetch
344
+ fetch: options.fetch,
345
+ onNetworkLog: options.onNetworkLog,
346
+ logBodies: options.logBodies,
347
+ redactFields: options.redactFields,
348
+ shipNetworkLogs: options.shipNetworkLogs,
349
+ applicationId: options.applicationId,
350
+ environment: options.environment
152
351
  });
153
352
  const extract = options.extract ?? defaultExtract;
154
353
  const shouldBlock = normalizeBlock(options.block);
package/dist/next.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts","../src/client.ts","../src/core.ts"],"sourcesContent":["/**\n * Next.js adapters.\n *\n * - `simplrNextMiddleware(options)` → an Edge/Node `middleware`-style function\n * that takes a `NextRequest` and returns `NextResponse.next()` or a 403.\n * - `withSimplr(handler, options)` → wraps an app-router route handler so the\n * check runs before your handler; on block it returns a 403 `Response`.\n *\n * `next` is an optional peer dependency and is lazy-imported only when needed, so\n * importing this module never requires `next` to be installed.\n */\nimport { blockEnvelope, createGuard } from \"./core.js\";\nimport type { CheckResult, GuardOptions } from \"./types.js\";\n\n// Loose types so we never hard-depend on `next`'s type packages.\ntype NextRequestLike = {\n headers: { get(name: string): string | null };\n /** NextRequest exposes `.ip` on some runtimes. */\n ip?: string;\n json?: () => Promise<unknown>;\n [key: string]: unknown;\n};\ntype NextResponseLike = Response;\ntype RouteHandler = (req: NextRequestLike, ...rest: any[]) => Response | Promise<Response>;\n\nasync function loadNextResponse(): Promise<{\n next: () => NextResponseLike;\n json: (body: unknown, init?: ResponseInit) => NextResponseLike;\n} | null> {\n try {\n // Lazy, optional. Avoids a hard dependency on `next`. The module specifier is\n // built dynamically so the type checker does not require `next` to be present.\n const spec = \"next/server\";\n const mod: any = await import(/* @vite-ignore */ spec);\n const NextResponse = mod.NextResponse;\n if (!NextResponse) return null;\n return {\n next: () => NextResponse.next(),\n json: (body, init) => NextResponse.json(body, init),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Edge/Node middleware factory for `middleware.ts`.\n *\n * ```ts\n * // middleware.ts\n * import { simplrNextMiddleware } from \"@simplr-ai/express/next\";\n * export const middleware = simplrNextMiddleware({ apiKey: process.env.SIMPLR_API_KEY! });\n * export const config = { matcher: [\"/api/:path*\"] };\n * ```\n */\nexport function simplrNextMiddleware(options: GuardOptions<NextRequestLike>) {\n const guard = createGuard<NextRequestLike>(options);\n const failOpen = options.failOpen ?? true;\n\n return async function middleware(req: NextRequestLike): Promise<NextResponseLike> {\n const next = await loadNextResponse();\n const pass = () => (next ? next.next() : new Response(null, { status: 200 }));\n const deny = (body: unknown) =>\n next ? next.json(body, { status: 403 }) : jsonResponse(body, 403);\n\n try {\n const outcome = await guard.run(req);\n if (outcome.blocked && outcome.result) {\n return deny(blockEnvelope(outcome.result));\n }\n return pass();\n } catch (err) {\n if (failOpen) return pass();\n throw err;\n }\n };\n}\n\n/**\n * Wrap an app-router route handler. The `CheckResult` is attached to the request\n * as `req.simplr` for your handler; blocked requests return a 403 before it runs.\n *\n * ```ts\n * // app/api/signup/route.ts\n * import { withSimplr } from \"@simplr-ai/express/next\";\n * export const POST = withSimplr(async (req) => {\n * // req.simplr is the CheckResult\n * return Response.json({ ok: true });\n * }, { apiKey: process.env.SIMPLR_API_KEY! });\n * ```\n */\nexport function withSimplr(\n handler: RouteHandler,\n options: GuardOptions<NextRequestLike>,\n): RouteHandler {\n const guard = createGuard<NextRequestLike>(options);\n const failOpen = options.failOpen ?? true;\n\n return async function wrapped(req, ...rest) {\n try {\n const outcome = await guard.run(req);\n (req as any).simplr = outcome.result;\n if (outcome.blocked && outcome.result) {\n return jsonResponse(blockEnvelope(outcome.result), 403);\n }\n } catch (err) {\n if (!failOpen) throw err;\n (req as any).simplr = null;\n }\n return handler(req, ...rest);\n };\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nexport type { CheckResult };\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/next.ts","../src/network-log.ts","../src/network-shipper.ts","../src/client.ts","../src/core.ts"],"sourcesContent":["/**\n * Next.js adapters.\n *\n * - `simplrNextMiddleware(options)` → an Edge/Node `middleware`-style function\n * that takes a `NextRequest` and returns `NextResponse.next()` or a 403.\n * - `withSimplr(handler, options)` → wraps an app-router route handler so the\n * check runs before your handler; on block it returns a 403 `Response`.\n *\n * `next` is an optional peer dependency and is lazy-imported only when needed, so\n * importing this module never requires `next` to be installed.\n */\nimport { blockEnvelope, createGuard } from \"./core.js\";\nimport type { CheckResult, GuardOptions } from \"./types.js\";\n\n// Loose types so we never hard-depend on `next`'s type packages.\ntype NextRequestLike = {\n headers: { get(name: string): string | null };\n /** NextRequest exposes `.ip` on some runtimes. */\n ip?: string;\n json?: () => Promise<unknown>;\n [key: string]: unknown;\n};\ntype NextResponseLike = Response;\ntype RouteHandler = (req: NextRequestLike, ...rest: any[]) => Response | Promise<Response>;\n\nasync function loadNextResponse(): Promise<{\n next: () => NextResponseLike;\n json: (body: unknown, init?: ResponseInit) => NextResponseLike;\n} | null> {\n try {\n // Lazy, optional. Avoids a hard dependency on `next`. The module specifier is\n // built dynamically so the type checker does not require `next` to be present.\n const spec = \"next/server\";\n const mod: any = await import(/* @vite-ignore */ spec);\n const NextResponse = mod.NextResponse;\n if (!NextResponse) return null;\n return {\n next: () => NextResponse.next(),\n json: (body, init) => NextResponse.json(body, init),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Edge/Node middleware factory for `middleware.ts`.\n *\n * ```ts\n * // middleware.ts\n * import { simplrNextMiddleware } from \"@simplr-ai/express/next\";\n * export const middleware = simplrNextMiddleware({ apiKey: process.env.SIMPLR_API_KEY! });\n * export const config = { matcher: [\"/api/:path*\"] };\n * ```\n */\nexport function simplrNextMiddleware(options: GuardOptions<NextRequestLike>) {\n const guard = createGuard<NextRequestLike>(options);\n const failOpen = options.failOpen ?? true;\n\n return async function middleware(req: NextRequestLike): Promise<NextResponseLike> {\n const next = await loadNextResponse();\n const pass = () => (next ? next.next() : new Response(null, { status: 200 }));\n const deny = (body: unknown) =>\n next ? next.json(body, { status: 403 }) : jsonResponse(body, 403);\n\n try {\n const outcome = await guard.run(req);\n if (outcome.blocked && outcome.result) {\n return deny(blockEnvelope(outcome.result));\n }\n return pass();\n } catch (err) {\n if (failOpen) return pass();\n throw err;\n }\n };\n}\n\n/**\n * Wrap an app-router route handler. The `CheckResult` is attached to the request\n * as `req.simplr` for your handler; blocked requests return a 403 before it runs.\n *\n * ```ts\n * // app/api/signup/route.ts\n * import { withSimplr } from \"@simplr-ai/express/next\";\n * export const POST = withSimplr(async (req) => {\n * // req.simplr is the CheckResult\n * return Response.json({ ok: true });\n * }, { apiKey: process.env.SIMPLR_API_KEY! });\n * ```\n */\nexport function withSimplr(\n handler: RouteHandler,\n options: GuardOptions<NextRequestLike>,\n): RouteHandler {\n const guard = createGuard<NextRequestLike>(options);\n const failOpen = options.failOpen ?? true;\n\n return async function wrapped(req, ...rest) {\n try {\n const outcome = await guard.run(req);\n (req as any).simplr = outcome.result;\n if (outcome.blocked && outcome.result) {\n return jsonResponse(blockEnvelope(outcome.result), 403);\n }\n } catch (err) {\n if (!failOpen) throw err;\n (req as any).simplr = null;\n }\n return handler(req, ...rest);\n };\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\nexport type { CheckResult };\n","export type NetworkSource = \"frontend\" | \"backend\";\n\nexport interface NetworkLogEntry {\n id: string;\n requestId?: string;\n source: NetworkSource;\n timestamp: string;\n sdk?: string;\n applicationId?: string;\n environment?: string;\n method: string;\n url: string;\n requestHeaders?: Record<string, string>;\n requestBody?: unknown;\n status?: number;\n statusText?: string;\n responseHeaders?: Record<string, string>;\n responseBody?: unknown;\n durationMs?: number;\n ok?: boolean;\n error?: string;\n}\n\nexport type NetworkLogger = (entry: NetworkLogEntry) => void;\n\nconst SENSITIVE_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n]);\n\nconst SENSITIVE_KEY_PARTS = [\n \"password\",\n \"passwd\",\n \"secret\",\n \"token\",\n \"api_key\",\n \"apikey\",\n \"authorization\",\n \"auth\",\n \"credential\",\n \"private_key\",\n \"card\",\n \"cardnumber\",\n \"pan\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"pin\",\n \"otp\",\n];\n\nconst MAX_REDACT_DEPTH = 8;\n\nexport const MAX_BODY_CHARS = 10_000;\n\nfunction isSensitiveKey(key: string, extraKeys: string[]): boolean {\n const lower = key.toLowerCase();\n if (extraKeys.some((k) => lower === k.toLowerCase())) return true;\n return SENSITIVE_KEY_PARTS.some((part) => lower.includes(part));\n}\n\nfunction redactDeep(value: unknown, extraKeys: string[], depth = 0): unknown {\n if (depth >= MAX_REDACT_DEPTH) return \"[truncated]\";\n if (Array.isArray(value)) return value.map((v) => redactDeep(v, extraKeys, depth + 1));\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n out[key] = isSensitiveKey(key, extraKeys)\n ? \"[REDACTED]\"\n : redactDeep(val, extraKeys, depth + 1);\n }\n return out;\n }\n return value;\n}\n\nexport function redactHeaders(\n headers: Record<string, string> | Headers | undefined,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const out: Record<string, string> = {};\n const set = (key: string, value: string) => {\n out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? \"[REDACTED]\" : value;\n };\n if (typeof (headers as Headers).forEach === \"function\" && !Array.isArray(headers)) {\n (headers as Headers).forEach((value, key) => set(key, value));\n } else {\n for (const [key, value] of Object.entries(headers as Record<string, string>)) {\n set(key, String(value));\n }\n }\n return out;\n}\n\nexport function previewBody(raw: unknown, redactFields: string[] = []): unknown {\n if (raw === undefined || raw === null) return undefined;\n let value: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n value = JSON.parse(raw);\n } catch {\n return raw.length > MAX_BODY_CHARS ? raw.slice(0, MAX_BODY_CHARS) + \"…[truncated]\" : raw;\n }\n }\n const redacted = redactDeep(value, redactFields);\n let text: string;\n try {\n text = JSON.stringify(redacted);\n } catch {\n return \"[unserializable]\";\n }\n if (text.length > MAX_BODY_CHARS) {\n return text.slice(0, MAX_BODY_CHARS) + \"…[truncated]\";\n }\n return redacted;\n}\n\nexport function newLogId(): string {\n const uuid = (globalThis as any)?.crypto?.randomUUID;\n if (typeof uuid === \"function\") return uuid.call((globalThis as any).crypto);\n return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;\n}\n","import type { NetworkLogEntry } from \"./network-log.js\";\n\nexport interface ShipperConfig {\n baseUrl: string;\n apiKey: string;\n fetchImpl: typeof fetch;\n sdk: string;\n applicationId?: string;\n environment?: string;\n batchSize?: number;\n flushIntervalMs?: number;\n}\n\nconst DEFAULT_BATCH = 25;\nconst DEFAULT_FLUSH_MS = 5000;\n\nexport class NetworkLogShipper {\n private readonly cfg: ShipperConfig;\n private queue: NetworkLogEntry[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(cfg: ShipperConfig) {\n this.cfg = cfg;\n }\n\n start(): void {\n if (this.timer) return;\n const interval = this.cfg.flushIntervalMs ?? DEFAULT_FLUSH_MS;\n this.timer = setInterval(() => {\n void this.flush();\n }, interval);\n (this.timer as any)?.unref?.();\n }\n\n add(entry: NetworkLogEntry): void {\n this.queue.push({\n ...entry,\n sdk: this.cfg.sdk,\n applicationId: entry.applicationId ?? this.cfg.applicationId,\n environment: entry.environment ?? this.cfg.environment,\n });\n if (this.queue.length >= (this.cfg.batchSize ?? DEFAULT_BATCH)) {\n void this.flush();\n }\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n const logs = this.queue;\n this.queue = [];\n try {\n await this.cfg.fetchImpl(`${this.cfg.baseUrl}/v1/network-logs`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.cfg.apiKey,\n },\n body: JSON.stringify({ logs }),\n });\n } catch {\n /* empty */\n }\n }\n\n stop(): void {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n void this.flush();\n }\n}\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\";\nimport {\n newLogId,\n previewBody,\n redactHeaders,\n type NetworkLogger,\n} from \"./network-log.js\";\nimport { NetworkLogShipper } from \"./network-shipper.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 onNetworkLog?: NetworkLogger;\n logBodies?: boolean;\n redactFields?: string[];\n shipNetworkLogs?: boolean;\n applicationId?: string;\n environment?: string;\n}\n\nexport interface SimplrClient {\n check(input: CheckInput): Promise<CheckResult>;\n ingestLogs(deviceId: string, logs: EdgeLogEntry[]): Promise<unknown>;\n flushNetworkLogs(): Promise<void>;\n close(): void;\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 const logBodies = config.logBodies ?? config.shipNetworkLogs;\n const redactFields = config.redactFields;\n\n let shipper: NetworkLogShipper | undefined;\n if (config.shipNetworkLogs) {\n shipper = new NetworkLogShipper({\n baseUrl,\n apiKey: config.apiKey,\n fetchImpl,\n sdk: \"express\",\n applicationId: config.applicationId,\n environment: config.environment,\n });\n shipper.start();\n }\n\n const userLog = config.onNetworkLog;\n const onNetworkLog: NetworkLogger | undefined =\n shipper || userLog\n ? (entry) => {\n shipper?.add(entry);\n userLog?.(entry);\n }\n : undefined;\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\n const url = `${baseUrl}${path}`;\n const requestHeaders = { \"Content-Type\": \"application/json\", \"X-API-Key\": config.apiKey };\n const startedAt = Date.now();\n const log = onNetworkLog\n ? {\n id: newLogId(),\n source: \"backend\" as const,\n timestamp: new Date(startedAt).toISOString(),\n method,\n url,\n requestHeaders: redactHeaders(requestHeaders),\n requestBody: logBodies ? previewBody(body, redactFields) : undefined,\n }\n : null;\n\n const emit = (extra: Record<string, unknown>) => {\n if (!log || !onNetworkLog) return;\n try {\n onNetworkLog({ ...log, durationMs: Date.now() - startedAt, ...extra });\n } catch {\n /* empty */\n }\n };\n\n try {\n const res = await fetchImpl(url, {\n method,\n headers: requestHeaders,\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\n emit({\n status: res.status,\n statusText: res.statusText,\n ok: res.ok,\n responseHeaders: redactHeaders(res.headers),\n responseBody: logBodies ? previewBody(parsed ?? text, redactFields) : undefined,\n });\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 emit({ ok: false, error: `timed out after ${timeoutMs}ms` });\n throw new SimplrError(`Request to ${path} timed out after ${timeoutMs}ms`, 0, null);\n }\n emit({ ok: false, error: err instanceof Error ? err.message : \"Network error\" });\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 flushNetworkLogs: () => shipper?.flush() ?? Promise.resolve(),\n close: () => shipper?.stop(),\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 onNetworkLog: options.onNetworkLog,\n logBodies: options.logBodies,\n redactFields: options.redactFields,\n shipNetworkLogs: options.shipNetworkLogs,\n applicationId: options.applicationId,\n environment: options.environment,\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"],"mappings":"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","names":[]}
package/dist/next.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GuardOptions } from './types-CmzpR5S4.cjs';
2
- export { C as CheckResult } from './types-CmzpR5S4.cjs';
1
+ import { G as GuardOptions } from './types-YIpI-xG_.cjs';
2
+ export { C as CheckResult } from './types-YIpI-xG_.cjs';
3
3
 
4
4
  type NextRequestLike = {
5
5
  headers: {
package/dist/next.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GuardOptions } from './types-CmzpR5S4.js';
2
- export { C as CheckResult } from './types-CmzpR5S4.js';
1
+ import { G as GuardOptions } from './types-YIpI-xG_.js';
2
+ export { C as CheckResult } from './types-YIpI-xG_.js';
3
3
 
4
4
  type NextRequestLike = {
5
5
  headers: {
package/dist/next.js CHANGED
@@ -1,3 +1,147 @@
1
+ // src/network-log.ts
2
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
3
+ "authorization",
4
+ "cookie",
5
+ "set-cookie",
6
+ "x-api-key",
7
+ "x-auth-token",
8
+ "x-csrf-token",
9
+ "x-xsrf-token"
10
+ ]);
11
+ var SENSITIVE_KEY_PARTS = [
12
+ "password",
13
+ "passwd",
14
+ "secret",
15
+ "token",
16
+ "api_key",
17
+ "apikey",
18
+ "authorization",
19
+ "auth",
20
+ "credential",
21
+ "private_key",
22
+ "card",
23
+ "cardnumber",
24
+ "pan",
25
+ "cvv",
26
+ "cvc",
27
+ "ssn",
28
+ "pin",
29
+ "otp"
30
+ ];
31
+ var MAX_REDACT_DEPTH = 8;
32
+ var MAX_BODY_CHARS = 1e4;
33
+ function isSensitiveKey(key, extraKeys) {
34
+ const lower = key.toLowerCase();
35
+ if (extraKeys.some((k) => lower === k.toLowerCase())) return true;
36
+ return SENSITIVE_KEY_PARTS.some((part) => lower.includes(part));
37
+ }
38
+ function redactDeep(value, extraKeys, depth = 0) {
39
+ if (depth >= MAX_REDACT_DEPTH) return "[truncated]";
40
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, extraKeys, depth + 1));
41
+ if (value && typeof value === "object") {
42
+ const out = {};
43
+ for (const [key, val] of Object.entries(value)) {
44
+ out[key] = isSensitiveKey(key, extraKeys) ? "[REDACTED]" : redactDeep(val, extraKeys, depth + 1);
45
+ }
46
+ return out;
47
+ }
48
+ return value;
49
+ }
50
+ function redactHeaders(headers) {
51
+ if (!headers) return void 0;
52
+ const out = {};
53
+ const set = (key, value) => {
54
+ out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
55
+ };
56
+ if (typeof headers.forEach === "function" && !Array.isArray(headers)) {
57
+ headers.forEach((value, key) => set(key, value));
58
+ } else {
59
+ for (const [key, value] of Object.entries(headers)) {
60
+ set(key, String(value));
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ function previewBody(raw, redactFields = []) {
66
+ if (raw === void 0 || raw === null) return void 0;
67
+ let value = raw;
68
+ if (typeof raw === "string") {
69
+ try {
70
+ value = JSON.parse(raw);
71
+ } catch {
72
+ return raw.length > MAX_BODY_CHARS ? raw.slice(0, MAX_BODY_CHARS) + "\u2026[truncated]" : raw;
73
+ }
74
+ }
75
+ const redacted = redactDeep(value, redactFields);
76
+ let text;
77
+ try {
78
+ text = JSON.stringify(redacted);
79
+ } catch {
80
+ return "[unserializable]";
81
+ }
82
+ if (text.length > MAX_BODY_CHARS) {
83
+ return text.slice(0, MAX_BODY_CHARS) + "\u2026[truncated]";
84
+ }
85
+ return redacted;
86
+ }
87
+ function newLogId() {
88
+ const uuid = globalThis?.crypto?.randomUUID;
89
+ if (typeof uuid === "function") return uuid.call(globalThis.crypto);
90
+ return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
91
+ }
92
+
93
+ // src/network-shipper.ts
94
+ var DEFAULT_BATCH = 25;
95
+ var DEFAULT_FLUSH_MS = 5e3;
96
+ var NetworkLogShipper = class {
97
+ cfg;
98
+ queue = [];
99
+ timer = null;
100
+ constructor(cfg) {
101
+ this.cfg = cfg;
102
+ }
103
+ start() {
104
+ if (this.timer) return;
105
+ const interval = this.cfg.flushIntervalMs ?? DEFAULT_FLUSH_MS;
106
+ this.timer = setInterval(() => {
107
+ void this.flush();
108
+ }, interval);
109
+ this.timer?.unref?.();
110
+ }
111
+ add(entry) {
112
+ this.queue.push({
113
+ ...entry,
114
+ sdk: this.cfg.sdk,
115
+ applicationId: entry.applicationId ?? this.cfg.applicationId,
116
+ environment: entry.environment ?? this.cfg.environment
117
+ });
118
+ if (this.queue.length >= (this.cfg.batchSize ?? DEFAULT_BATCH)) {
119
+ void this.flush();
120
+ }
121
+ }
122
+ async flush() {
123
+ if (this.queue.length === 0) return;
124
+ const logs = this.queue;
125
+ this.queue = [];
126
+ try {
127
+ await this.cfg.fetchImpl(`${this.cfg.baseUrl}/v1/network-logs`, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ "X-API-Key": this.cfg.apiKey
132
+ },
133
+ body: JSON.stringify({ logs })
134
+ });
135
+ } catch {
136
+ }
137
+ }
138
+ stop() {
139
+ if (this.timer) clearInterval(this.timer);
140
+ this.timer = null;
141
+ void this.flush();
142
+ }
143
+ };
144
+
1
145
  // src/client.ts
2
146
  var DEFAULT_BASE_URL = "https://api.simplr.sh";
3
147
  var SimplrError = class extends Error {
@@ -20,13 +164,51 @@ function createClient(config) {
20
164
  "Simplr: no global fetch available \u2014 use Node 18+ or pass `fetch` in options"
21
165
  );
22
166
  }
167
+ const logBodies = config.logBodies ?? config.shipNetworkLogs;
168
+ const redactFields = config.redactFields;
169
+ let shipper;
170
+ if (config.shipNetworkLogs) {
171
+ shipper = new NetworkLogShipper({
172
+ baseUrl,
173
+ apiKey: config.apiKey,
174
+ fetchImpl,
175
+ sdk: "express",
176
+ applicationId: config.applicationId,
177
+ environment: config.environment
178
+ });
179
+ shipper.start();
180
+ }
181
+ const userLog = config.onNetworkLog;
182
+ const onNetworkLog = shipper || userLog ? (entry) => {
183
+ shipper?.add(entry);
184
+ userLog?.(entry);
185
+ } : void 0;
23
186
  async function apiRequest(method, path, body) {
24
187
  const controller = new AbortController();
25
188
  const timer = setTimeout(() => controller.abort(), timeoutMs);
189
+ const url = `${baseUrl}${path}`;
190
+ const requestHeaders = { "Content-Type": "application/json", "X-API-Key": config.apiKey };
191
+ const startedAt = Date.now();
192
+ const log = onNetworkLog ? {
193
+ id: newLogId(),
194
+ source: "backend",
195
+ timestamp: new Date(startedAt).toISOString(),
196
+ method,
197
+ url,
198
+ requestHeaders: redactHeaders(requestHeaders),
199
+ requestBody: logBodies ? previewBody(body, redactFields) : void 0
200
+ } : null;
201
+ const emit = (extra) => {
202
+ if (!log || !onNetworkLog) return;
203
+ try {
204
+ onNetworkLog({ ...log, durationMs: Date.now() - startedAt, ...extra });
205
+ } catch {
206
+ }
207
+ };
26
208
  try {
27
- const res = await fetchImpl(`${baseUrl}${path}`, {
209
+ const res = await fetchImpl(url, {
28
210
  method,
29
- headers: { "Content-Type": "application/json", "X-API-Key": config.apiKey },
211
+ headers: requestHeaders,
30
212
  body: body !== void 0 ? JSON.stringify(body) : void 0,
31
213
  signal: controller.signal
32
214
  });
@@ -37,6 +219,13 @@ function createClient(config) {
37
219
  } catch {
38
220
  parsed = text;
39
221
  }
222
+ emit({
223
+ status: res.status,
224
+ statusText: res.statusText,
225
+ ok: res.ok,
226
+ responseHeaders: redactHeaders(res.headers),
227
+ responseBody: logBodies ? previewBody(parsed ?? text, redactFields) : void 0
228
+ });
40
229
  if (!res.ok) {
41
230
  const message = parsed && (parsed.message || parsed.error) || `Simplr API error ${res.status}`;
42
231
  throw new SimplrError(message, res.status, parsed);
@@ -45,8 +234,10 @@ function createClient(config) {
45
234
  } catch (err) {
46
235
  if (err instanceof SimplrError) throw err;
47
236
  if (err instanceof Error && err.name === "AbortError") {
237
+ emit({ ok: false, error: `timed out after ${timeoutMs}ms` });
48
238
  throw new SimplrError(`Request to ${path} timed out after ${timeoutMs}ms`, 0, null);
49
239
  }
240
+ emit({ ok: false, error: err instanceof Error ? err.message : "Network error" });
50
241
  throw new SimplrError(err instanceof Error ? err.message : "Network error", 0, null);
51
242
  } finally {
52
243
  clearTimeout(timer);
@@ -54,7 +245,9 @@ function createClient(config) {
54
245
  }
55
246
  return {
56
247
  check: (input) => apiRequest("POST", "/v1/check", input),
57
- ingestLogs: (deviceId, logs) => apiRequest("POST", "/v1/edge/logs", { device_id: deviceId, logs })
248
+ ingestLogs: (deviceId, logs) => apiRequest("POST", "/v1/edge/logs", { device_id: deviceId, logs }),
249
+ flushNetworkLogs: () => shipper?.flush() ?? Promise.resolve(),
250
+ close: () => shipper?.stop()
58
251
  };
59
252
  }
60
253
 
@@ -121,7 +314,13 @@ function createGuard(options) {
121
314
  apiKey: options.apiKey,
122
315
  baseUrl: options.baseUrl,
123
316
  timeoutMs: options.timeoutMs,
124
- fetch: options.fetch
317
+ fetch: options.fetch,
318
+ onNetworkLog: options.onNetworkLog,
319
+ logBodies: options.logBodies,
320
+ redactFields: options.redactFields,
321
+ shipNetworkLogs: options.shipNetworkLogs,
322
+ applicationId: options.applicationId,
323
+ environment: options.environment
125
324
  });
126
325
  const extract = options.extract ?? defaultExtract;
127
326
  const shouldBlock = normalizeBlock(options.block);