@simplr-ai/react-native 1.1.0 → 1.2.1

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,37 @@ 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
+ const logSelf = !!config.logSelfCalls;
962
+ this.logBodies = config.logBodies ?? (config.shipNetworkLogs && logSelf);
963
+ this.redactFields = config.redactFields;
778
964
  this.configured = true;
965
+ this.shipper?.stop();
966
+ this.shipper = void 0;
967
+ if (config.shipNetworkLogs && logSelf) {
968
+ this.shipper = new NetworkLogShipper({
969
+ baseUrl: this.baseUrl,
970
+ apiKey: this.apiKey,
971
+ fetchImpl: this.fetchImpl ?? globalThis.fetch,
972
+ sdk: "react-native",
973
+ applicationId: config.applicationId,
974
+ environment: config.environment
975
+ });
976
+ this.shipper.start();
977
+ }
978
+ const shipper = this.shipper;
979
+ const userLog = config.onNetworkLog;
980
+ this.onNetworkLog = shipper || userLog ? (entry) => {
981
+ shipper?.add(entry);
982
+ userLog?.(entry);
983
+ } : void 0;
779
984
  const sub = {
780
985
  apiKey: this.apiKey,
781
986
  baseUrl: this.baseUrl,
782
987
  timeoutMs: this.timeoutMs,
783
- fetchImpl: this.fetchImpl
988
+ fetchImpl: this.fetchImpl,
989
+ onNetworkLog: this.onNetworkLog,
990
+ logBodies: this.logBodies,
991
+ redactFields: this.redactFields
784
992
  };
785
993
  this.profiles.configure(sub);
786
994
  this.ai.configure(sub);
@@ -799,9 +1007,20 @@ var SimplrFraud = class {
799
1007
  authHeaders: { "X-API-Key": this.apiKey },
800
1008
  baseUrl: this.baseUrl,
801
1009
  timeoutMs: this.timeoutMs,
802
- fetchImpl: this.fetchImpl
1010
+ fetchImpl: this.fetchImpl,
1011
+ onNetworkLog: this.onNetworkLog,
1012
+ logBodies: this.logBodies,
1013
+ redactFields: this.redactFields
803
1014
  };
804
1015
  }
1016
+ /** Flush any queued network logs to /v1/network-logs. */
1017
+ flushNetworkLogs() {
1018
+ return this.shipper?.flush() ?? Promise.resolve();
1019
+ }
1020
+ /** Stop the network-log shipper and flush remaining logs. */
1021
+ close() {
1022
+ this.shipper?.stop();
1023
+ }
805
1024
  // --- Biometrics ---------------------------------------------------------
806
1025
  /** Start collecting touch biometrics and reset the form timer. */
807
1026
  startTracking() {
@@ -987,6 +1206,7 @@ var simplrFlags = new SimplrFlags();
987
1206
  // src/index.ts
988
1207
  var src_default = SimplrFraud;
989
1208
 
1209
+ exports.NetworkLogShipper = NetworkLogShipper;
990
1210
  exports.SimplrAI = SimplrAI;
991
1211
  exports.SimplrError = SimplrError;
992
1212
  exports.SimplrFlags = SimplrFlags;