@simplr-ai/react-native 1.1.0 → 1.2.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/index.cjs CHANGED
@@ -20,6 +20,98 @@ var SimplrError = class _SimplrError extends Error {
20
20
  }
21
21
  };
22
22
 
23
+ // src/network-log.ts
24
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
25
+ "authorization",
26
+ "cookie",
27
+ "set-cookie",
28
+ "x-api-key",
29
+ "x-auth-token",
30
+ "x-csrf-token",
31
+ "x-xsrf-token"
32
+ ]);
33
+ var SENSITIVE_KEY_PARTS = [
34
+ "password",
35
+ "passwd",
36
+ "secret",
37
+ "token",
38
+ "api_key",
39
+ "apikey",
40
+ "authorization",
41
+ "auth",
42
+ "credential",
43
+ "private_key",
44
+ "card",
45
+ "cardnumber",
46
+ "pan",
47
+ "cvv",
48
+ "cvc",
49
+ "ssn",
50
+ "pin",
51
+ "otp"
52
+ ];
53
+ var MAX_REDACT_DEPTH = 8;
54
+ var MAX_BODY_CHARS = 1e4;
55
+ function isSensitiveKey(key, extraKeys) {
56
+ const lower = key.toLowerCase();
57
+ if (extraKeys.some((k) => lower === k.toLowerCase())) return true;
58
+ return SENSITIVE_KEY_PARTS.some((part) => lower.includes(part));
59
+ }
60
+ function redactDeep(value, extraKeys, depth = 0) {
61
+ if (depth >= MAX_REDACT_DEPTH) return "[truncated]";
62
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, extraKeys, depth + 1));
63
+ if (value && typeof value === "object") {
64
+ const out = {};
65
+ for (const [key, val] of Object.entries(value)) {
66
+ out[key] = isSensitiveKey(key, extraKeys) ? "[REDACTED]" : redactDeep(val, extraKeys, depth + 1);
67
+ }
68
+ return out;
69
+ }
70
+ return value;
71
+ }
72
+ function redactHeaders(headers) {
73
+ if (!headers) return void 0;
74
+ const out = {};
75
+ const set = (key, value) => {
76
+ out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
77
+ };
78
+ if (typeof headers.forEach === "function" && !Array.isArray(headers)) {
79
+ headers.forEach((value, key) => set(key, value));
80
+ } else {
81
+ for (const [key, value] of Object.entries(headers)) {
82
+ set(key, String(value));
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ function previewBody(raw, redactFields = []) {
88
+ if (raw === void 0 || raw === null) return void 0;
89
+ let value = raw;
90
+ if (typeof raw === "string") {
91
+ try {
92
+ value = JSON.parse(raw);
93
+ } catch {
94
+ return raw.length > MAX_BODY_CHARS ? raw.slice(0, MAX_BODY_CHARS) + "\u2026[truncated]" : raw;
95
+ }
96
+ }
97
+ const redacted = redactDeep(value, redactFields);
98
+ let text;
99
+ try {
100
+ text = JSON.stringify(redacted);
101
+ } catch {
102
+ return "[unserializable]";
103
+ }
104
+ if (text.length > MAX_BODY_CHARS) {
105
+ return text.slice(0, MAX_BODY_CHARS) + "\u2026[truncated]";
106
+ }
107
+ return redacted;
108
+ }
109
+ function newLogId() {
110
+ const uuid = globalThis?.crypto?.randomUUID;
111
+ if (typeof uuid === "function") return uuid.call(globalThis.crypto);
112
+ return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
113
+ }
114
+
23
115
  // src/http.ts
24
116
  async function apiRequest(cfg, method, path, body) {
25
117
  const fetchImpl = cfg.fetchImpl ?? globalThis.fetch;
@@ -28,13 +120,32 @@ async function apiRequest(cfg, method, path, body) {
28
120
  }
29
121
  const controller = new AbortController();
30
122
  const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
123
+ const url = `${cfg.baseUrl}${path}`;
124
+ const requestHeaders = {
125
+ "Content-Type": "application/json",
126
+ ...cfg.authHeaders
127
+ };
128
+ const startedAt = Date.now();
129
+ const log = cfg.onNetworkLog ? {
130
+ id: newLogId(),
131
+ source: "frontend",
132
+ timestamp: new Date(startedAt).toISOString(),
133
+ method,
134
+ url,
135
+ requestHeaders: redactHeaders(requestHeaders),
136
+ requestBody: cfg.logBodies ? previewBody(body, cfg.redactFields) : void 0
137
+ } : null;
138
+ const emit = (extra) => {
139
+ if (!log || !cfg.onNetworkLog) return;
140
+ try {
141
+ cfg.onNetworkLog({ ...log, durationMs: Date.now() - startedAt, ...extra });
142
+ } catch {
143
+ }
144
+ };
31
145
  try {
32
- const res = await fetchImpl(`${cfg.baseUrl}${path}`, {
146
+ const res = await fetchImpl(url, {
33
147
  method,
34
- headers: {
35
- "Content-Type": "application/json",
36
- ...cfg.authHeaders
37
- },
148
+ headers: requestHeaders,
38
149
  body: body !== void 0 ? JSON.stringify(body) : void 0,
39
150
  signal: controller.signal
40
151
  });
@@ -45,6 +156,13 @@ async function apiRequest(cfg, method, path, body) {
45
156
  } catch {
46
157
  parsed = text;
47
158
  }
159
+ emit({
160
+ status: res.status,
161
+ statusText: res.statusText,
162
+ ok: res.ok,
163
+ responseHeaders: redactHeaders(res.headers),
164
+ responseBody: cfg.logBodies ? previewBody(parsed ?? text, cfg.redactFields) : void 0
165
+ });
48
166
  if (!res.ok) {
49
167
  const message = parsed && (parsed.message || parsed.error) || `Simplr API error ${res.status}`;
50
168
  throw new SimplrError(message, res.status, parsed);
@@ -53,8 +171,10 @@ async function apiRequest(cfg, method, path, body) {
53
171
  } catch (err) {
54
172
  if (err instanceof SimplrError) throw err;
55
173
  if (err instanceof Error && err.name === "AbortError") {
174
+ emit({ ok: false, error: `timed out after ${cfg.timeoutMs}ms` });
56
175
  throw new SimplrError(`Request to ${path} timed out after ${cfg.timeoutMs}ms`, 0, null);
57
176
  }
177
+ emit({ ok: false, error: err instanceof Error ? err.message : "Network error" });
58
178
  throw new SimplrError(err instanceof Error ? err.message : "Network error", 0, null);
59
179
  } finally {
60
180
  clearTimeout(timer);
@@ -64,6 +184,57 @@ function normalizeBaseUrl(url) {
64
184
  return url.replace(/\/+$/, "");
65
185
  }
66
186
 
187
+ // src/network-shipper.ts
188
+ var DEFAULT_BATCH = 25;
189
+ var DEFAULT_FLUSH_MS = 5e3;
190
+ var NetworkLogShipper = class {
191
+ constructor(cfg) {
192
+ this.queue = [];
193
+ this.timer = null;
194
+ this.cfg = cfg;
195
+ }
196
+ start() {
197
+ if (this.timer) return;
198
+ const interval = this.cfg.flushIntervalMs ?? DEFAULT_FLUSH_MS;
199
+ this.timer = setInterval(() => {
200
+ void this.flush();
201
+ }, interval);
202
+ this.timer?.unref?.();
203
+ }
204
+ add(entry) {
205
+ this.queue.push({
206
+ ...entry,
207
+ sdk: this.cfg.sdk,
208
+ applicationId: entry.applicationId ?? this.cfg.applicationId,
209
+ environment: entry.environment ?? this.cfg.environment
210
+ });
211
+ if (this.queue.length >= (this.cfg.batchSize ?? DEFAULT_BATCH)) {
212
+ void this.flush();
213
+ }
214
+ }
215
+ async flush() {
216
+ if (this.queue.length === 0) return;
217
+ const logs = this.queue;
218
+ this.queue = [];
219
+ try {
220
+ await this.cfg.fetchImpl(`${this.cfg.baseUrl}/v1/network-logs`, {
221
+ method: "POST",
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ "X-API-Key": this.cfg.apiKey
225
+ },
226
+ body: JSON.stringify({ logs })
227
+ });
228
+ } catch {
229
+ }
230
+ }
231
+ stop() {
232
+ if (this.timer) clearInterval(this.timer);
233
+ this.timer = null;
234
+ void this.flush();
235
+ }
236
+ };
237
+
67
238
  // src/hash.ts
68
239
  function murmurHash3(input, seed = 0) {
69
240
  let h1 = seed;
@@ -391,6 +562,9 @@ var SimplrProfiles = class {
391
562
  this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL;
392
563
  if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
393
564
  this.fetchImpl = config.fetchImpl;
565
+ this.onNetworkLog = config.onNetworkLog;
566
+ this.logBodies = config.logBodies;
567
+ this.redactFields = config.redactFields;
394
568
  this.configured = true;
395
569
  return this;
396
570
  }
@@ -411,7 +585,10 @@ var SimplrProfiles = class {
411
585
  authHeaders: { "X-API-Key": this.apiKey },
412
586
  baseUrl: this.baseUrl,
413
587
  timeoutMs: this.timeoutMs,
414
- fetchImpl: this.fetchImpl
588
+ fetchImpl: this.fetchImpl,
589
+ onNetworkLog: this.onNetworkLog,
590
+ logBodies: this.logBodies,
591
+ redactFields: this.redactFields
415
592
  };
416
593
  }
417
594
  /** Create or update an anonymous profile and link the current device. */
@@ -639,6 +816,9 @@ var SimplrAI = class {
639
816
  this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL3;
640
817
  if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
641
818
  this.fetchImpl = config.fetchImpl;
819
+ this.onNetworkLog = config.onNetworkLog;
820
+ this.logBodies = config.logBodies;
821
+ this.redactFields = config.redactFields;
642
822
  this.configured = true;
643
823
  return this;
644
824
  }
@@ -652,7 +832,10 @@ var SimplrAI = class {
652
832
  authHeaders: { "X-API-Key": this.apiKey },
653
833
  baseUrl: this.baseUrl,
654
834
  timeoutMs: this.timeoutMs,
655
- fetchImpl: this.fetchImpl
835
+ fetchImpl: this.fetchImpl,
836
+ onNetworkLog: this.onNetworkLog,
837
+ logBodies: this.logBodies,
838
+ redactFields: this.redactFields
656
839
  };
657
840
  }
658
841
  /** Create a new AI delegation token for a user. POST /v1/ai/delegations. */
@@ -775,12 +958,36 @@ var SimplrFraud = class {
775
958
  this.baseUrl = config.baseUrl ? normalizeBaseUrl(config.baseUrl) : DEFAULT_BASE_URL4;
776
959
  if (config.timeoutMs !== void 0) this.timeoutMs = config.timeoutMs;
777
960
  this.fetchImpl = config.fetchImpl;
961
+ this.logBodies = config.logBodies ?? config.shipNetworkLogs;
962
+ this.redactFields = config.redactFields;
778
963
  this.configured = true;
964
+ this.shipper?.stop();
965
+ this.shipper = void 0;
966
+ if (config.shipNetworkLogs) {
967
+ this.shipper = new NetworkLogShipper({
968
+ baseUrl: this.baseUrl,
969
+ apiKey: this.apiKey,
970
+ fetchImpl: this.fetchImpl ?? globalThis.fetch,
971
+ sdk: "react-native",
972
+ applicationId: config.applicationId,
973
+ environment: config.environment
974
+ });
975
+ this.shipper.start();
976
+ }
977
+ const shipper = this.shipper;
978
+ const userLog = config.onNetworkLog;
979
+ this.onNetworkLog = shipper || userLog ? (entry) => {
980
+ shipper?.add(entry);
981
+ userLog?.(entry);
982
+ } : void 0;
779
983
  const sub = {
780
984
  apiKey: this.apiKey,
781
985
  baseUrl: this.baseUrl,
782
986
  timeoutMs: this.timeoutMs,
783
- fetchImpl: this.fetchImpl
987
+ fetchImpl: this.fetchImpl,
988
+ onNetworkLog: this.onNetworkLog,
989
+ logBodies: this.logBodies,
990
+ redactFields: this.redactFields
784
991
  };
785
992
  this.profiles.configure(sub);
786
993
  this.ai.configure(sub);
@@ -799,9 +1006,20 @@ var SimplrFraud = class {
799
1006
  authHeaders: { "X-API-Key": this.apiKey },
800
1007
  baseUrl: this.baseUrl,
801
1008
  timeoutMs: this.timeoutMs,
802
- fetchImpl: this.fetchImpl
1009
+ fetchImpl: this.fetchImpl,
1010
+ onNetworkLog: this.onNetworkLog,
1011
+ logBodies: this.logBodies,
1012
+ redactFields: this.redactFields
803
1013
  };
804
1014
  }
1015
+ /** Flush any queued network logs to /v1/network-logs. */
1016
+ flushNetworkLogs() {
1017
+ return this.shipper?.flush() ?? Promise.resolve();
1018
+ }
1019
+ /** Stop the network-log shipper and flush remaining logs. */
1020
+ close() {
1021
+ this.shipper?.stop();
1022
+ }
805
1023
  // --- Biometrics ---------------------------------------------------------
806
1024
  /** Start collecting touch biometrics and reset the form timer. */
807
1025
  startTracking() {
@@ -987,6 +1205,7 @@ var simplrFlags = new SimplrFlags();
987
1205
  // src/index.ts
988
1206
  var src_default = SimplrFraud;
989
1207
 
1208
+ exports.NetworkLogShipper = NetworkLogShipper;
990
1209
  exports.SimplrAI = SimplrAI;
991
1210
  exports.SimplrError = SimplrError;
992
1211
  exports.SimplrFlags = SimplrFlags;