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