@simplr-ai/node 1.0.0 → 1.1.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.js CHANGED
@@ -31,7 +31,7 @@ async function apiRequest(cfg, method, path, body) {
31
31
  method,
32
32
  headers: {
33
33
  "Content-Type": "application/json",
34
- "X-API-Key": cfg.apiKey
34
+ ...cfg.authHeaders
35
35
  },
36
36
  body: body !== void 0 ? JSON.stringify(body) : void 0,
37
37
  signal: controller.signal
@@ -112,6 +112,402 @@ var EdgeResource = class {
112
112
  }
113
113
  };
114
114
 
115
+ // src/flags.ts
116
+ function murmurHash3(input, seed = 0) {
117
+ let h1 = seed;
118
+ const c1 = 3432918353;
119
+ const c2 = 461845907;
120
+ for (let i = 0; i < input.length; i++) {
121
+ let k1 = input.charCodeAt(i);
122
+ k1 = Math.imul(k1, c1);
123
+ k1 = k1 << 15 | k1 >>> 17;
124
+ k1 = Math.imul(k1, c2);
125
+ h1 ^= k1;
126
+ h1 = h1 << 13 | h1 >>> 19;
127
+ h1 = Math.imul(h1, 5) + 3864292196;
128
+ }
129
+ h1 ^= input.length;
130
+ h1 ^= h1 >>> 16;
131
+ h1 = Math.imul(h1, 2246822507);
132
+ h1 ^= h1 >>> 13;
133
+ h1 = Math.imul(h1, 3266489909);
134
+ h1 ^= h1 >>> 16;
135
+ return h1 >>> 0;
136
+ }
137
+ function matchRule(rule, attributes) {
138
+ const actual = attributes[rule.attribute];
139
+ switch (rule.op) {
140
+ case "eq":
141
+ return String(actual) === rule.value;
142
+ case "neq":
143
+ return String(actual) !== rule.value;
144
+ case "contains":
145
+ return String(actual ?? "").includes(rule.value);
146
+ default:
147
+ return false;
148
+ }
149
+ }
150
+ var SimplrFlags = class {
151
+ cfg;
152
+ environment;
153
+ refreshIntervalMs;
154
+ flags = {};
155
+ defaultUserId;
156
+ timer = null;
157
+ ready = false;
158
+ constructor(options) {
159
+ if (!options?.publicKey) throw new Error("SimplrFlags: `publicKey` is required");
160
+ this.cfg = {
161
+ authHeaders: { "X-API-Key": options.publicKey },
162
+ baseUrl: (options.baseUrl || "https://api.simplr.sh").replace(/\/+$/, ""),
163
+ timeoutMs: options.timeoutMs ?? 15e3,
164
+ fetchImpl: options.fetch ?? globalThis.fetch
165
+ };
166
+ this.environment = options.environment;
167
+ this.refreshIntervalMs = options.refreshIntervalMs ?? 6e4;
168
+ }
169
+ /** Fetch the flag config once and start the background refresh. */
170
+ async initialize() {
171
+ await this.refresh();
172
+ this.ready = true;
173
+ if (this.refreshIntervalMs > 0) {
174
+ this.timer = setInterval(() => {
175
+ void this.refresh();
176
+ }, this.refreshIntervalMs);
177
+ this.timer?.unref?.();
178
+ }
179
+ }
180
+ /** Set the default identifier used for bucketing when none is passed to isEnabled. */
181
+ setUser(userId) {
182
+ this.defaultUserId = userId;
183
+ }
184
+ /** Re-fetch the flag config (counts as one billable request). */
185
+ async refresh() {
186
+ const path = this.environment ? `/v1/flags?environment=${encodeURIComponent(this.environment)}` : "/v1/flags";
187
+ try {
188
+ const content = await apiRequest(this.cfg, "GET", path);
189
+ const list = content?.flags || [];
190
+ const map = {};
191
+ for (const f of list) map[f.key] = f;
192
+ this.flags = map;
193
+ } catch {
194
+ }
195
+ }
196
+ /** Evaluate a flag locally. Deterministic per user; no network call. */
197
+ isEnabled(key, ctx = {}) {
198
+ const f = this.flags[key];
199
+ if (!f || !f.enabled) return false;
200
+ const uid = ctx.userId || this.defaultUserId || "anonymous";
201
+ if (f.target_user_ids?.includes(uid)) return true;
202
+ if (ctx.attributes && f.rules?.length && f.rules.some((r) => matchRule(r, ctx.attributes))) {
203
+ return true;
204
+ }
205
+ if (f.rollout_percentage >= 100) return true;
206
+ if (f.rollout_percentage <= 0) return false;
207
+ return murmurHash3(`${key}:${uid}`) % 100 < f.rollout_percentage;
208
+ }
209
+ getAll() {
210
+ return { ...this.flags };
211
+ }
212
+ isReady() {
213
+ return this.ready;
214
+ }
215
+ /** Stop the background refresh timer. */
216
+ dispose() {
217
+ if (this.timer) clearInterval(this.timer);
218
+ this.timer = null;
219
+ }
220
+ };
221
+
222
+ // src/profiles.ts
223
+ var SimplrProfiles = class {
224
+ constructor(cfg) {
225
+ this.cfg = cfg;
226
+ }
227
+ cfg;
228
+ /**
229
+ * Identify a user — creates or updates an anonymous profile and (optionally)
230
+ * links a device fingerprint. POST /v1/profiles.
231
+ */
232
+ identify(externalId, options) {
233
+ const { profileType, fingerprintHash, ...rest } = options ?? {};
234
+ const body = {
235
+ external_id: externalId,
236
+ profile_type: profileType || "customer",
237
+ ...rest
238
+ };
239
+ if (fingerprintHash) body.fingerprint_hash = fingerprintHash;
240
+ return apiRequest(this.cfg, "POST", "/v1/profiles", body);
241
+ }
242
+ /** Submit an order for real-time fraud scoring. POST /v1/orders. */
243
+ submitOrder(order) {
244
+ return apiRequest(this.cfg, "POST", "/v1/orders", order);
245
+ }
246
+ /** Get the risk profile for a user. GET /v1/profiles/{externalId}. */
247
+ getProfileRisk(externalId) {
248
+ return apiRequest(
249
+ this.cfg,
250
+ "GET",
251
+ `/v1/profiles/${encodeURIComponent(externalId)}`
252
+ );
253
+ }
254
+ /** Report a profile as fraud or legitimate. POST /v1/profiles/{externalId}/outcome. */
255
+ async reportOutcome(externalId, outcome) {
256
+ await apiRequest(
257
+ this.cfg,
258
+ "POST",
259
+ `/v1/profiles/${encodeURIComponent(externalId)}/outcome`,
260
+ { outcome }
261
+ );
262
+ }
263
+ };
264
+
265
+ // src/rum.ts
266
+ var DEFAULT_BATCH_SIZE = 30;
267
+ var DEFAULT_FLUSH_INTERVAL = 1e4;
268
+ var DEFAULT_ENDPOINT = "/v1/rum/events";
269
+ function genId() {
270
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
271
+ }
272
+ var SimplrRUM = class {
273
+ constructor(cfg) {
274
+ this.cfg = cfg;
275
+ }
276
+ cfg;
277
+ config = null;
278
+ initialized = false;
279
+ queue = [];
280
+ timer = null;
281
+ flushing = false;
282
+ sessionId = null;
283
+ currentViewId = null;
284
+ userId;
285
+ userAttributes;
286
+ globalAttributes = {};
287
+ batchSize = DEFAULT_BATCH_SIZE;
288
+ endpoint = DEFAULT_ENDPOINT;
289
+ /** Initialize the SDK, start a session, and begin the flush timer. */
290
+ initialize(config) {
291
+ if (this.initialized) return;
292
+ this.config = config;
293
+ this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
294
+ this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
295
+ this.sessionId = genId();
296
+ this.initialized = true;
297
+ this.trackEvent("session_start");
298
+ const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
299
+ if (interval > 0) {
300
+ this.timer = setInterval(() => {
301
+ void this.flush();
302
+ }, interval);
303
+ this.timer?.unref?.();
304
+ }
305
+ }
306
+ isInitialized() {
307
+ return this.initialized;
308
+ }
309
+ /** Associate subsequent events with a user. */
310
+ setUser(userId, attributes) {
311
+ this.userId = userId;
312
+ this.userAttributes = attributes;
313
+ }
314
+ clearUser() {
315
+ this.userId = void 0;
316
+ this.userAttributes = void 0;
317
+ }
318
+ addAttribute(key, value) {
319
+ this.globalAttributes[key] = value;
320
+ }
321
+ removeAttribute(key) {
322
+ delete this.globalAttributes[key];
323
+ }
324
+ /** Track a screen/page view. */
325
+ trackView(name, attributes) {
326
+ if (!this.initialized) return;
327
+ this.currentViewId = genId();
328
+ this.trackEvent("view", {
329
+ view: { id: this.currentViewId, name },
330
+ attributes
331
+ });
332
+ }
333
+ /** Track a user action. */
334
+ trackAction(name, attributes) {
335
+ if (!this.initialized) return;
336
+ this.trackEvent("action", { action: { name, type: "custom" }, attributes });
337
+ }
338
+ /** Track an error. */
339
+ trackError(error, attributes) {
340
+ if (!this.initialized) return;
341
+ const data = error instanceof Error ? { message: error.message, stack: error.stack, type: error.constructor.name } : error;
342
+ this.trackEvent("error", { error: data, attributes });
343
+ }
344
+ /** Emit a log line. */
345
+ log(level, message, attributes) {
346
+ if (!this.initialized) return;
347
+ this.trackEvent("log", { log: { level, message }, attributes });
348
+ }
349
+ trackEvent(type, data) {
350
+ if (!this.initialized || !this.sessionId) return;
351
+ const event = {
352
+ type,
353
+ timestamp: Date.now(),
354
+ sessionId: this.sessionId,
355
+ viewId: this.currentViewId || void 0,
356
+ userId: this.userId,
357
+ applicationId: this.config.applicationId,
358
+ applicationVersion: this.config?.applicationVersion,
359
+ environment: this.config?.environment,
360
+ userAttributes: this.userAttributes,
361
+ globalAttributes: Object.keys(this.globalAttributes).length > 0 ? this.globalAttributes : void 0,
362
+ ...data
363
+ };
364
+ this.queue.push(event);
365
+ if (this.queue.length >= this.batchSize) void this.flush();
366
+ }
367
+ /** Flush queued events to POST /v1/rum/events. */
368
+ async flush() {
369
+ if (this.flushing || this.queue.length === 0) return;
370
+ this.flushing = true;
371
+ const events = this.queue;
372
+ this.queue = [];
373
+ try {
374
+ await apiRequest(this.cfg, "POST", this.endpoint, {
375
+ events,
376
+ sentAt: Date.now()
377
+ });
378
+ } catch {
379
+ this.queue = [...events, ...this.queue];
380
+ } finally {
381
+ this.flushing = false;
382
+ }
383
+ }
384
+ /** End the session, flush remaining events, and stop the timer. */
385
+ async stopSession() {
386
+ if (!this.initialized) return;
387
+ this.trackEvent("session_end");
388
+ await this.flush();
389
+ if (this.timer) {
390
+ clearInterval(this.timer);
391
+ this.timer = null;
392
+ }
393
+ this.initialized = false;
394
+ }
395
+ getSessionId() {
396
+ return this.sessionId;
397
+ }
398
+ getViewId() {
399
+ return this.currentViewId;
400
+ }
401
+ };
402
+
403
+ // src/ai.ts
404
+ function mapDelegation(d) {
405
+ return {
406
+ delegationId: d.delegation_id,
407
+ endUserId: d.end_user_id,
408
+ bindingMode: d.binding_mode,
409
+ status: d.status,
410
+ expiresAt: d.expires_at,
411
+ useCount: d.use_count,
412
+ lastUsedAt: d.last_used_at,
413
+ createdAt: d.created_at
414
+ };
415
+ }
416
+ var SimplrAI = class {
417
+ constructor(cfg) {
418
+ this.cfg = cfg;
419
+ }
420
+ cfg;
421
+ /** Create a new AI delegation token for a user. POST /v1/ai/delegations. */
422
+ async createDelegation(options) {
423
+ const content = await apiRequest(this.cfg, "POST", "/v1/ai/delegations", {
424
+ end_user_id: options.userId,
425
+ end_user_email: options.email,
426
+ binding: options.binding || "any_location",
427
+ expires_in_days: options.expiresInDays || 7,
428
+ session_id: options.sessionId,
429
+ fingerprint_hash: options.fingerprintHash
430
+ });
431
+ const d = content.delegation;
432
+ return {
433
+ token: d.token,
434
+ delegationId: d.delegation_id,
435
+ expiresAt: d.expires_at,
436
+ bindingMode: d.binding_mode
437
+ };
438
+ }
439
+ /** Validate (introspect) an AI delegation token. POST /v1/ai/validate. */
440
+ async validate(token, options) {
441
+ try {
442
+ const content = await apiRequest(this.cfg, "POST", "/v1/ai/validate", {
443
+ token,
444
+ fingerprint_hash: options?.fingerprintHash,
445
+ ai_provider: options?.aiProvider,
446
+ action: options?.action
447
+ });
448
+ return {
449
+ valid: true,
450
+ sessionType: content.session_type,
451
+ endUserId: content.end_user_id,
452
+ delegation: content.delegation ? {
453
+ delegationId: content.delegation.delegation_id,
454
+ bindingMode: content.delegation.binding_mode,
455
+ expiresAt: content.delegation.expires_at,
456
+ useCount: content.delegation.use_count
457
+ } : void 0
458
+ };
459
+ } catch (err) {
460
+ return { valid: false, error: err instanceof Error ? err.message : "Validation failed" };
461
+ }
462
+ }
463
+ /** Revoke a delegation. POST /v1/ai/delegations/{id}/revoke. */
464
+ async revoke(delegationId, reason) {
465
+ await apiRequest(
466
+ this.cfg,
467
+ "POST",
468
+ `/v1/ai/delegations/${encodeURIComponent(delegationId)}/revoke`,
469
+ { reason }
470
+ );
471
+ }
472
+ /** List delegations, optionally filtered by user. GET /v1/ai/delegations. */
473
+ async list(userId) {
474
+ const path = userId ? `/v1/ai/delegations?end_user_id=${encodeURIComponent(userId)}` : "/v1/ai/delegations";
475
+ const content = await apiRequest(this.cfg, "GET", path);
476
+ return (content.delegations || []).map(mapDelegation);
477
+ }
478
+ /** Get a single delegation. GET /v1/ai/delegations/{id}. */
479
+ async get(delegationId) {
480
+ const content = await apiRequest(
481
+ this.cfg,
482
+ "GET",
483
+ `/v1/ai/delegations/${encodeURIComponent(delegationId)}`
484
+ );
485
+ return mapDelegation(content.delegation);
486
+ }
487
+ /** Get delegation statistics. GET /v1/ai/stats. */
488
+ async stats() {
489
+ const content = await apiRequest(this.cfg, "GET", "/v1/ai/stats");
490
+ const s = content.stats;
491
+ return {
492
+ totalDelegations: s.total_delegations,
493
+ activeDelegations: s.active_delegations,
494
+ totalUses: s.total_uses,
495
+ delegationsByBinding: {
496
+ verifiedDevice: s.delegations_by_binding.verified_device,
497
+ anyLocation: s.delegations_by_binding.any_location
498
+ }
499
+ };
500
+ }
501
+ /** Revoke all delegations for a user (e.g. on logout). POST /v1/ai/revoke-all. */
502
+ async revokeAllForUser(userId, reason) {
503
+ const content = await apiRequest(this.cfg, "POST", "/v1/ai/revoke-all", {
504
+ end_user_id: userId,
505
+ reason
506
+ });
507
+ return content.revoked_count;
508
+ }
509
+ };
510
+
115
511
  // src/webhooks.ts
116
512
  var webhooks_exports = {};
117
513
  __export(webhooks_exports, {
@@ -166,6 +562,91 @@ function constructEvent(payload, header, secret, options = {}) {
166
562
  }
167
563
  var webhooks = { verify, constructEvent };
168
564
 
565
+ // src/admin.ts
566
+ function qs(params) {
567
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
568
+ if (!entries.length) return "";
569
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
570
+ }
571
+ var UsageApi = class {
572
+ constructor(cfg) {
573
+ this.cfg = cfg;
574
+ }
575
+ cfg;
576
+ /** Aggregate usage stats for an org. */
577
+ stats(orgId) {
578
+ return apiRequest(this.cfg, "GET", `/v1/usage/stats${qs({ org_id: orgId })}`);
579
+ }
580
+ /** Raw usage logs for an org. */
581
+ logs(orgId, params = {}) {
582
+ return apiRequest(this.cfg, "GET", `/v1/usage/logs${qs({ org_id: orgId, ...params })}`);
583
+ }
584
+ /** Billing usage breakdown (per-service totals + estimated cost). */
585
+ billing(orgId) {
586
+ return apiRequest(this.cfg, "GET", `/v1/billing/usage${qs({ org_id: orgId })}`);
587
+ }
588
+ };
589
+ var FlagsAdminApi = class {
590
+ constructor(cfg) {
591
+ this.cfg = cfg;
592
+ }
593
+ cfg;
594
+ list(orgId, environment) {
595
+ return apiRequest(this.cfg, "GET", `/v1/feature-flags${qs({ org_id: orgId, environment })}`);
596
+ }
597
+ get(orgId, id) {
598
+ return apiRequest(this.cfg, "GET", `/v1/feature-flags/${id}${qs({ org_id: orgId })}`);
599
+ }
600
+ create(orgId, data) {
601
+ return apiRequest(this.cfg, "POST", "/v1/feature-flags", { org_id: orgId, ...data });
602
+ }
603
+ update(orgId, id, data) {
604
+ return apiRequest(this.cfg, "PATCH", `/v1/feature-flags/${id}`, { org_id: orgId, ...data });
605
+ }
606
+ remove(orgId, id) {
607
+ return apiRequest(this.cfg, "DELETE", `/v1/feature-flags/${id}${qs({ org_id: orgId })}`);
608
+ }
609
+ history(orgId, id, params = {}) {
610
+ return apiRequest(
611
+ this.cfg,
612
+ "GET",
613
+ `/v1/feature-flags/${id}/history${qs({ org_id: orgId, ...params })}`
614
+ );
615
+ }
616
+ };
617
+ var RumApi = class {
618
+ constructor(cfg) {
619
+ this.cfg = cfg;
620
+ }
621
+ cfg;
622
+ overview(orgId, params = {}) {
623
+ return apiRequest(this.cfg, "GET", `/v1/rum/overview${qs({ org_id: orgId, ...params })}`);
624
+ }
625
+ sessions(orgId, params = {}) {
626
+ return apiRequest(this.cfg, "GET", `/v1/rum/sessions${qs({ org_id: orgId, ...params })}`);
627
+ }
628
+ };
629
+ var SimplrAdmin = class {
630
+ usage;
631
+ flags;
632
+ rum;
633
+ constructor(options) {
634
+ if (!options?.token) throw new Error("SimplrAdmin: `token` is required");
635
+ const cfg = {
636
+ authHeaders: { Authorization: `Bearer ${options.token}` },
637
+ baseUrl: (options.baseUrl || "https://api.simplr.sh").replace(/\/+$/, ""),
638
+ timeoutMs: options.timeoutMs ?? 15e3,
639
+ fetchImpl: options.fetch ?? globalThis.fetch
640
+ };
641
+ if (typeof cfg.fetchImpl !== "function") {
642
+ throw new Error("SimplrAdmin: no global fetch available \u2014 use Node 18+ or pass `fetch`");
643
+ }
644
+ this.usage = new UsageApi(cfg);
645
+ this.flags = new FlagsAdminApi(cfg);
646
+ this.rum = new RumApi(cfg);
647
+ }
648
+ };
649
+
169
650
  // src/index.ts
170
651
  var DEFAULT_BASE_URL = "https://api.simplr.sh";
171
652
  var Simplr = class {
@@ -173,12 +654,19 @@ var Simplr = class {
173
654
  orders;
174
655
  phone;
175
656
  edge;
657
+ /** Anonymous user profiles + order fraud monitoring. */
658
+ profiles;
659
+ /** Real User Monitoring — batched events to /v1/rum/events. */
660
+ rum;
661
+ /** AI delegation — OAuth-like AI authentication. */
662
+ ai;
176
663
  /** Webhook signature helpers (no network). */
177
664
  webhooks = webhooks_exports;
665
+ _flags;
178
666
  constructor(options) {
179
667
  if (!options?.apiKey) throw new Error("Simplr: `apiKey` is required");
180
668
  this.cfg = {
181
- apiKey: options.apiKey,
669
+ authHeaders: { "X-API-Key": options.apiKey },
182
670
  baseUrl: (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, ""),
183
671
  timeoutMs: options.timeoutMs ?? 15e3,
184
672
  fetchImpl: options.fetch ?? globalThis.fetch
@@ -189,6 +677,30 @@ var Simplr = class {
189
677
  this.orders = new OrdersResource(this.cfg);
190
678
  this.phone = new PhoneResource(this.cfg);
191
679
  this.edge = new EdgeResource(this.cfg);
680
+ this.profiles = new SimplrProfiles(this.cfg);
681
+ this.rum = new SimplrRUM(this.cfg);
682
+ this.ai = new SimplrAI(this.cfg);
683
+ if (options.publicKey) {
684
+ this._flags = new SimplrFlags({
685
+ publicKey: options.publicKey,
686
+ environment: options.environment,
687
+ baseUrl: this.cfg.baseUrl,
688
+ timeoutMs: this.cfg.timeoutMs,
689
+ fetch: this.cfg.fetchImpl
690
+ });
691
+ }
692
+ }
693
+ /**
694
+ * Server-side feature flags. Requires a `publicKey` in the constructor options
695
+ * (flag config is read with the public key). Call `simplr.flags.initialize()` once.
696
+ */
697
+ get flags() {
698
+ if (!this._flags) {
699
+ throw new Error(
700
+ "Simplr.flags requires a `publicKey` \u2014 pass it to `new Simplr({ apiKey, publicKey })`."
701
+ );
702
+ }
703
+ return this._flags;
192
704
  }
193
705
  /** Run an identity/fraud check. Provide any of email, phone, device, behavior. */
194
706
  check(input) {
@@ -202,7 +714,12 @@ var Simplr = class {
202
714
  var src_default = Simplr;
203
715
  export {
204
716
  Simplr,
717
+ SimplrAI,
718
+ SimplrAdmin,
205
719
  SimplrError,
720
+ SimplrFlags,
721
+ SimplrProfiles,
722
+ SimplrRUM,
206
723
  WebhookVerificationError,
207
724
  constructEvent as constructWebhookEvent,
208
725
  src_default as default,