@incodetech/tri-sdk 0.1.1 → 0.1.2-dev-20260528

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.
Files changed (47) hide show
  1. package/dist/bundle-analysis.html +4950 -0
  2. package/dist/chunks/index-BAwek9Id.cjs +489 -0
  3. package/dist/chunks/index-BAwek9Id.cjs.map +1 -0
  4. package/dist/chunks/index-BBnHO5vA.js +477 -0
  5. package/dist/chunks/index-BBnHO5vA.js.map +1 -0
  6. package/dist/chunks/index-BMVAKptw.cjs +432 -0
  7. package/dist/chunks/index-BMVAKptw.cjs.map +1 -0
  8. package/dist/chunks/index-B_sPgtA6.cjs +489 -0
  9. package/dist/chunks/index-B_sPgtA6.cjs.map +1 -0
  10. package/dist/chunks/index-BlclLEsV.js +420 -0
  11. package/dist/chunks/index-BlclLEsV.js.map +1 -0
  12. package/dist/chunks/index-CECrIW-A.js +420 -0
  13. package/dist/chunks/index-CECrIW-A.js.map +1 -0
  14. package/dist/chunks/index-DUZ-lBzx.js +477 -0
  15. package/dist/chunks/index-DUZ-lBzx.js.map +1 -0
  16. package/dist/chunks/index-SQXEe_Yi.cjs +432 -0
  17. package/dist/chunks/index-SQXEe_Yi.cjs.map +1 -0
  18. package/dist/chunks/index-kx-dpbK_.cjs +432 -0
  19. package/dist/chunks/index-kx-dpbK_.cjs.map +1 -0
  20. package/dist/chunks/index-uSwsY9-F.js +420 -0
  21. package/dist/chunks/index-uSwsY9-F.js.map +1 -0
  22. package/dist/chunks/start-BnvikGDT.cjs +5059 -0
  23. package/dist/chunks/start-BnvikGDT.cjs.map +1 -0
  24. package/dist/chunks/start-CMqC8j2A.cjs +5561 -0
  25. package/dist/chunks/start-CMqC8j2A.cjs.map +1 -0
  26. package/dist/chunks/start-CkLlbFOM.cjs +5554 -0
  27. package/dist/chunks/start-CkLlbFOM.cjs.map +1 -0
  28. package/dist/chunks/start-CwAEBqvW.js +5057 -0
  29. package/dist/chunks/start-CwAEBqvW.js.map +1 -0
  30. package/dist/chunks/start-CymdUpX5.js +5060 -0
  31. package/dist/chunks/start-CymdUpX5.js.map +1 -0
  32. package/dist/chunks/start-DTFRIfeO.js +5552 -0
  33. package/dist/chunks/start-DTFRIfeO.js.map +1 -0
  34. package/dist/chunks/start-Dpgr8IbP.js +5559 -0
  35. package/dist/chunks/start-Dpgr8IbP.js.map +1 -0
  36. package/dist/chunks/start-Dtnxf7k4.cjs +5554 -0
  37. package/dist/chunks/start-Dtnxf7k4.cjs.map +1 -0
  38. package/dist/chunks/start-DwQtOigF.cjs +5062 -0
  39. package/dist/chunks/start-DwQtOigF.cjs.map +1 -0
  40. package/dist/chunks/start-MKWGpbIO.js +5552 -0
  41. package/dist/chunks/start-MKWGpbIO.js.map +1 -0
  42. package/dist/index.d.ts +2 -2
  43. package/dist/tri.cjs.js +1 -1
  44. package/dist/tri.esm.js +1 -1
  45. package/dist/tri.umd.js +156 -591
  46. package/dist/tri.umd.js.map +1 -1
  47. package/package.json +1 -1
package/dist/tri.umd.js CHANGED
@@ -106,7 +106,7 @@
106
106
  */
107
107
  const ENV_TO_BASE_API_URL_LOOKUP = {
108
108
  STAGE_K8S: 'https://falcon-ingest-k8s.stage.incodetest.com',
109
- SAAS: 'http://localhost:8080',
109
+ SAAS: '',
110
110
  STAGE: '',
111
111
  DEMO: '',
112
112
  DEMO_K8S: ''
@@ -267,6 +267,58 @@
267
267
  }
268
268
  }
269
269
 
270
+ /**
271
+ * @module sdk/internal/collection-status
272
+ *
273
+ * @description
274
+ * Preflight check — queries the backend to decide whether the SDK should load
275
+ * its full manager bundle for this session.
276
+ *
277
+ * ### Sanctioned `fetch` exception
278
+ * CLAUDE.md prohibits calling `fetch` outside `src/transport/`. This module is
279
+ * the **only** approved exception outside transport, analogous to the History
280
+ * API wrap in the navigation collector. The preflight runs before the transport
281
+ * layer exists (the lazy chunk hasn't loaded yet), so it must call fetch
282
+ * directly. `_fetch` is captured at module-evaluation time to survive host-side
283
+ * patching of `globalThis.fetch` (same hazard model as `lib/http/http.ts`).
284
+ *
285
+ * @internal
286
+ */
287
+ const _fetch$1 = globalThis.fetch.bind(globalThis);
288
+ const TIMEOUT_MS = 10000;
289
+ /**
290
+ * Queries `GET {apiURL}/api/v1/events/collection-status` and returns whether the backend has
291
+ * collection enabled for this API key.
292
+ *
293
+ * Returns `false` on any network error, non-OK response, or timeout so that a
294
+ * hung or unreachable backend silently disables the SDK rather than blocking the
295
+ * host application.
296
+ */
297
+ async function getCollectionStatus(apiURL, apiKey) {
298
+ const controller = new AbortController();
299
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
300
+ try {
301
+ const base = apiURL.replace(/\/$/, '');
302
+ const response = await _fetch$1(`${base}/api/v1/events/collection-status`, {
303
+ method: 'GET',
304
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
305
+ signal: controller.signal
306
+ });
307
+ if (!response.ok) {
308
+ return false;
309
+ }
310
+ const data = (await response.json());
311
+ return Boolean(data.eventsCollectionEnabled);
312
+ }
313
+ catch (error) {
314
+ logger.warn('CollectionStatus', 'failed to load collection status', error);
315
+ return false;
316
+ }
317
+ finally {
318
+ clearTimeout(timer);
319
+ }
320
+ }
321
+
270
322
  /**
271
323
  * Singleton entry-point for the TRI SDK.
272
324
  *
@@ -327,7 +379,7 @@
327
379
  }
328
380
  this.startState = 'starting';
329
381
  const generation = ++this.startGeneration;
330
- void this.runStart(config, generation);
382
+ void this._start(config, generation);
331
383
  }
332
384
  /**
333
385
  * @description Stops all collectors, flushes pending events, and tears down
@@ -359,12 +411,12 @@
359
411
  * event silently (with a dev warning) if called before `start` completes.
360
412
  */
361
413
  reportEvent(name, metadata) {
362
- if (this.shouldEarlyEscapeReport()) {
414
+ if (this._shouldEarlyEscapeReport()) {
363
415
  return;
364
416
  }
365
417
  void safeAsync(() => this.eventReportManager.report(name, metadata));
366
418
  }
367
- async runStart(config, generation) {
419
+ async _start(config, generation) {
368
420
  if (typeof document === 'undefined') {
369
421
  logger.fatal('error', 'Start', 'start() called in a non-browser environment. The SDK will not start.');
370
422
  this.startState = 'idle';
@@ -377,10 +429,15 @@
377
429
  this.startState = 'idle';
378
430
  return;
379
431
  }
432
+ const enabled = await getCollectionStatus(preflight.baseApiURL, preflight.apiKey);
380
433
  // Bail if stop() or a concurrent start() invalidated this run.
381
434
  if (this.startGeneration !== generation) {
382
435
  return;
383
436
  }
437
+ if (!enabled) {
438
+ this.startState = 'idle';
439
+ return;
440
+ }
384
441
  // Flip DEBUG only after the SDK is confirmed to start.
385
442
  logger.DEBUG = preflight.debug;
386
443
  // Dynamically import the lazy chunk with a hard timeout so a slow/dead
@@ -433,7 +490,7 @@
433
490
  * no-op. Guards against calling before the SDK is fully started, or when
434
491
  * the `eventReportManager` is unexpectedly absent.
435
492
  */
436
- shouldEarlyEscapeReport() {
493
+ _shouldEarlyEscapeReport() {
437
494
  if (this.startState !== 'started') {
438
495
  logger.warn('ReportEvent', 'reportEvent() called before start() completed.');
439
496
  return true;
@@ -482,6 +539,58 @@
482
539
  return navigator.locks.request(lockName, { mode: 'exclusive' }, async () => safeAsync(fn));
483
540
  }
484
541
 
542
+ function createStorage(storageId) {
543
+ function hasStorage() {
544
+ return (globalThis.window &&
545
+ globalThis.window[storageId] &&
546
+ typeof globalThis.window[storageId] !== 'undefined');
547
+ }
548
+ function get(key) {
549
+ if (!hasStorage())
550
+ return null;
551
+ try {
552
+ const raw = window[storageId].getItem(key) ?? 'null';
553
+ if (raw === 'null')
554
+ return null;
555
+ return JSON.parse(raw);
556
+ }
557
+ catch {
558
+ return null;
559
+ }
560
+ }
561
+ function set(key, value) {
562
+ if (!hasStorage())
563
+ return;
564
+ safeSync(() => window[storageId].setItem(key, JSON.stringify(value)));
565
+ }
566
+ function remove(key) {
567
+ if (!hasStorage())
568
+ return;
569
+ safeSync(() => window[storageId].removeItem(key));
570
+ }
571
+ function key(index) {
572
+ if (!hasStorage())
573
+ return null;
574
+ return window[storageId].key(index);
575
+ }
576
+ function length() {
577
+ if (!hasStorage())
578
+ return 0;
579
+ return window[storageId].length;
580
+ }
581
+ return {
582
+ get,
583
+ set,
584
+ remove,
585
+ key,
586
+ get length() {
587
+ return length();
588
+ }
589
+ };
590
+ }
591
+ const LocalStorage = createStorage('localStorage');
592
+ const SessionStorage = createStorage('sessionStorage');
593
+
485
594
  const KEY_TOKEN = 'activity_token';
486
595
  const KEY_ACTIVITY_ID = 'activity_id';
487
596
  const KEY_ACTIVITY_STATUS = 'activity_status';
@@ -508,11 +617,13 @@
508
617
  }
509
618
  /* Doesn't have a lock because it is called from the lifecycle manager on pagehide, if there is a lock, this won't fire */
510
619
  async stop() {
620
+ /** clear persisted activity state from storage - don't invalidate in-memory state
621
+ * because token has to survive for api.stopActivity() call, because api auth middleware uses activity.getToken() */
622
+ this._updatePersistedActivityState(null);
511
623
  this.startPromise = null;
512
624
  if (this.token) {
513
625
  await this.context.stopActivity();
514
626
  }
515
- await this._invalidate();
516
627
  }
517
628
  /* despite facading the this.start() method, this method is still needed
518
629
  if we decide to have separated endpoint for the token refresh */
@@ -526,7 +637,7 @@
526
637
  this.activityStatus === 'ACTIVE') {
527
638
  return;
528
639
  }
529
- const stored = await this._load();
640
+ const stored = this._load();
530
641
  const didOtherTabRefresh = stored &&
531
642
  stored.token !== tokenAtRequestTime &&
532
643
  !jwt.isExpired(stored.token) &&
@@ -535,7 +646,7 @@
535
646
  this._updateActivityState(stored);
536
647
  return;
537
648
  }
538
- await this._invalidate();
649
+ this._invalidate();
539
650
  await this._resolveStartActivity();
540
651
  });
541
652
  }
@@ -558,7 +669,7 @@
558
669
  */
559
670
  async _resolveStartActivity() {
560
671
  /* 2. try to load from persistent storage */
561
- const stored = await this._load();
672
+ const stored = this._load();
562
673
  const shouldUsePersisted = stored &&
563
674
  !jwt.isExpired(stored.token) &&
564
675
  stored.activityStatus === 'ACTIVE';
@@ -569,7 +680,7 @@
569
680
  if (stored) {
570
681
  // Stale — drop it before we fetch fresh, so a partial save below
571
682
  // doesn't leave the old token behind.
572
- await this._invalidate();
683
+ this._invalidate();
573
684
  }
574
685
  /* 3. if no token or token is expired, start a new activity */
575
686
  const result = await this.context.startActivity();
@@ -582,18 +693,18 @@
582
693
  return;
583
694
  const newActivityState = result.data;
584
695
  this._updateActivityState(newActivityState);
585
- await this._save(newActivityState);
586
- }
587
- async _load() {
588
- const [token, activityId, activityStatus] = await Promise.all([
589
- this.context.db.get(KEY_TOKEN),
590
- this.context.db.get(KEY_ACTIVITY_ID),
591
- this.context.db.get(KEY_ACTIVITY_STATUS)
592
- ]);
696
+ this._save(newActivityState);
697
+ }
698
+ _load() {
699
+ const [token, activityId, activityStatus] = [
700
+ LocalStorage.get(KEY_TOKEN),
701
+ LocalStorage.get(KEY_ACTIVITY_ID),
702
+ LocalStorage.get(KEY_ACTIVITY_STATUS)
703
+ ];
593
704
  if (!token || !activityId || !activityStatus) {
594
705
  // Partial state — clean up the orphan so future loads start fresh.
595
706
  if (token || activityId || activityStatus) {
596
- await this._invalidate();
707
+ this._invalidate();
597
708
  }
598
709
  return null;
599
710
  }
@@ -603,21 +714,26 @@
603
714
  activityStatus: activityStatus
604
715
  };
605
716
  }
606
- async _save(data) {
607
- await Promise.all([
608
- this.context.db.set(KEY_TOKEN, data.token ?? ''),
609
- this.context.db.set(KEY_ACTIVITY_ID, data.activityId),
610
- this.context.db.set(KEY_ACTIVITY_STATUS, data.activityStatus)
611
- ]);
612
- }
613
- async _invalidate() {
614
- await Promise.all([
615
- this.context.db.remove(KEY_TOKEN),
616
- this.context.db.remove(KEY_ACTIVITY_ID),
617
- this.context.db.remove(KEY_ACTIVITY_STATUS)
618
- ]);
717
+ _save(data) {
718
+ LocalStorage.set(KEY_TOKEN, data.token);
719
+ LocalStorage.set(KEY_ACTIVITY_ID, data.activityId);
720
+ LocalStorage.set(KEY_ACTIVITY_STATUS, data.activityStatus);
721
+ }
722
+ _invalidate() {
723
+ this._updatePersistedActivityState(null);
619
724
  this._updateActivityState(null);
620
725
  }
726
+ _updatePersistedActivityState(data) {
727
+ if (!data) {
728
+ LocalStorage.remove(KEY_TOKEN);
729
+ LocalStorage.remove(KEY_ACTIVITY_ID);
730
+ LocalStorage.remove(KEY_ACTIVITY_STATUS);
731
+ return;
732
+ }
733
+ LocalStorage.set(KEY_TOKEN, data.token);
734
+ LocalStorage.set(KEY_ACTIVITY_ID, data.activityId);
735
+ LocalStorage.set(KEY_ACTIVITY_STATUS, data.activityStatus);
736
+ }
621
737
  _updateActivityState(data) {
622
738
  if (!data) {
623
739
  this.token = null;
@@ -645,7 +761,7 @@
645
761
  */
646
762
  const WIRE = {
647
763
  /** Injected at build time from package.json `version` via @rollup/plugin-replace. */
648
- sdkVersion: "0.1.1",
764
+ sdkVersion: "0.1.2-dev-20260528",
649
765
  /** Injected at build time via @rollup/plugin-replace. Bump when the wire schema changes. */
650
766
  schemaVersion: "0.1.0",
651
767
  sdkPlatform: 'web',
@@ -1198,8 +1314,11 @@
1198
1314
  /**
1199
1315
  * Returns whether a failed `HttpResult` is worth retrying.
1200
1316
  *
1201
- * - `network` / `timeout`: transient transport issue.
1202
- * - `server` / `rate_limited`: backend asked us to back off.
1317
+ * - `timeout` / `server` / `rate_limited`: transient or back-pressure
1318
+ * failure — the backend may recover; retry with backoff.
1319
+ * - `network`: connection-level failure. Intentionally non-retryable: the
1320
+ * `onConnectionChange()` offline path preserves the queue and re-arms the
1321
+ * drain when connectivity returns, making a separate retry redundant.
1203
1322
  * - `abort`: deliberate cancellation; retrying defeats the cancel.
1204
1323
  * - `client` / `parse` / `serialize`: our bug; the next attempt will
1205
1324
  * produce the same result. Drop the batch.
@@ -1228,58 +1347,6 @@
1228
1347
  }
1229
1348
  }
1230
1349
 
1231
- function createStorage(storageId) {
1232
- function hasStorage() {
1233
- return (globalThis.window &&
1234
- globalThis.window[storageId] &&
1235
- typeof globalThis.window[storageId] !== 'undefined');
1236
- }
1237
- function get(key) {
1238
- if (!hasStorage())
1239
- return null;
1240
- try {
1241
- const raw = window[storageId].getItem(key) ?? 'null';
1242
- if (raw === 'null')
1243
- return null;
1244
- return JSON.parse(raw);
1245
- }
1246
- catch {
1247
- return null;
1248
- }
1249
- }
1250
- function set(key, value) {
1251
- if (!hasStorage())
1252
- return;
1253
- safeSync(() => window[storageId].setItem(key, JSON.stringify(value)));
1254
- }
1255
- function remove(key) {
1256
- if (!hasStorage())
1257
- return;
1258
- safeSync(() => window[storageId].removeItem(key));
1259
- }
1260
- function key(index) {
1261
- if (!hasStorage())
1262
- return null;
1263
- return window[storageId].key(index);
1264
- }
1265
- function length() {
1266
- if (!hasStorage())
1267
- return 0;
1268
- return window[storageId].length;
1269
- }
1270
- return {
1271
- get,
1272
- set,
1273
- remove,
1274
- key,
1275
- get length() {
1276
- return length();
1277
- }
1278
- };
1279
- }
1280
- const LocalStorage = createStorage('localStorage');
1281
- const SessionStorage = createStorage('sessionStorage');
1282
-
1283
1350
  /**
1284
1351
  * @module storage-event-channel
1285
1352
  *
@@ -4291,7 +4358,7 @@
4291
4358
  }
4292
4359
  /** @description Stops an activity and returns a result — authenticated endpoint */
4293
4360
  async function stopActivity() {
4294
- return authMiddleware.withAuth((token) => http.post('/stop-activity', { token, keepalive: true }), { refreshable: false });
4361
+ return authMiddleware.withAuth((token) => http.post('/end-activity', { token, keepalive: true }), { refreshable: false });
4295
4362
  }
4296
4363
  /** @description Ingests a batch of events — authenticated endpoint */
4297
4364
  async function ingest(payload, keepalive) {
@@ -4368,507 +4435,6 @@
4368
4435
  }
4369
4436
  }
4370
4437
 
4371
- /**
4372
- * Internal RFC6265-compliant cookie utility.
4373
- *
4374
- * Encoding is intentionally bit-for-bit compatible with js-cookie v3.0.7 so
4375
- * that cookies written by earlier SDK versions (which used js-cookie directly)
4376
- * continue to decode correctly after upgrading to this implementation.
4377
- *
4378
- * The module is internal-only and must not be re-exported from src/index.ts.
4379
- *
4380
- * @module
4381
- */
4382
- /**
4383
- * Encodes a cookie value using the js-cookie v3 RFC6265 scheme:
4384
- * 1. `encodeURIComponent` percent-encodes everything except `A–Z a–z 0–9 - _ . ! ~ * ' ( )`.
4385
- * 2. A second pass un-escapes the RFC6265 cookie-octet whitelist characters
4386
- * that are safe to transmit without encoding:
4387
- * `# $ & + / : < = > ? @ [ ] ^ \` { | }`.
4388
- * 3. Semicolons (`;`) remain percent-encoded because they act as the cookie
4389
- * attribute separator and must not appear unescaped in a value.
4390
- */
4391
- function encodeValue(value) {
4392
- return encodeURIComponent(value).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/gi, decodeURIComponent);
4393
- }
4394
- /**
4395
- * Encodes a cookie name using the js-cookie v3 RFC6265 scheme:
4396
- * 1. `encodeURIComponent` percent-encodes the name.
4397
- * 2. A second pass un-escapes the RFC6265 cookie-name token whitelist:
4398
- * `# $ & + ^ \` |`.
4399
- * 3. `(` and `)` are left as `%28` / `%29`; raw parentheses are disallowed in
4400
- * cookie names per the spec, and js-cookie explicitly re-encodes them via
4401
- * the deprecated global `escape()` which produces the same result.
4402
- */
4403
- function encodeName(name) {
4404
- return encodeURIComponent(name)
4405
- .replace(/%(2[346B]|5E|60|7C)/gi, decodeURIComponent)
4406
- .replace(/[()]/g, (char) => (char === '(' ? '%28' : '%29'));
4407
- }
4408
- /**
4409
- * Decodes a cookie value using the js-cookie v3 scheme.
4410
- *
4411
- * Strips a surrounding pair of double quotes (some Set-Cookie implementations
4412
- * wrap values in quotes), then replaces runs of percent-encoded bytes with
4413
- * their decoded form. Mirrors js-cookie's per-sequence try/catch fallback so
4414
- * that malformed percent-sequences in cookies written by other parties degrade
4415
- * to the raw string rather than throwing.
4416
- */
4417
- function decodeValue(raw) {
4418
- let value = raw;
4419
- if (value[0] === '"') {
4420
- value = value.slice(1, -1);
4421
- }
4422
- return value.replace(/(%[\dA-F]{2})+/gi, (sequence) => {
4423
- try {
4424
- return decodeURIComponent(sequence);
4425
- }
4426
- catch {
4427
- return sequence;
4428
- }
4429
- });
4430
- }
4431
- /**
4432
- * Serialises `CookieAttributes` into the `; Key=Value` fragment appended to
4433
- * the `name=value` pair written to `document.cookie`.
4434
- */
4435
- function serializeAttributes(attributes) {
4436
- let result = '';
4437
- if (attributes.expires !== undefined) {
4438
- let expiresDate;
4439
- if (attributes.expires instanceof Date) {
4440
- expiresDate = attributes.expires;
4441
- }
4442
- else {
4443
- expiresDate = new Date(Date.now() + attributes.expires * 864e5);
4444
- }
4445
- result += '; Expires=' + expiresDate.toUTCString();
4446
- }
4447
- if (attributes.path !== undefined) {
4448
- result += '; Path=' + attributes.path;
4449
- }
4450
- if (attributes.domain !== undefined) {
4451
- result += '; Domain=' + attributes.domain;
4452
- }
4453
- if (attributes.sameSite !== undefined) {
4454
- const capitalised = attributes.sameSite.charAt(0).toUpperCase() +
4455
- attributes.sameSite.slice(1);
4456
- result += '; SameSite=' + capitalised;
4457
- }
4458
- if (attributes.secure === true) {
4459
- result += '; Secure';
4460
- }
4461
- return result;
4462
- }
4463
- /**
4464
- * Reads a cookie by name from `document.cookie`.
4465
- *
4466
- * Decoding mirrors js-cookie v3 exactly:
4467
- * - Segments are split on `'; '`.
4468
- * - Each segment is split on the **first** `=`.
4469
- * - The name is decoded with plain `decodeURIComponent`.
4470
- * - The value is decoded with the js-cookie percent-sequence approach (see
4471
- * `decodeValue`).
4472
- *
4473
- * Returns `undefined` when the cookie is absent or when `document` is not
4474
- * available in the current environment.
4475
- */
4476
- function getCookie(name) {
4477
- if (typeof document === 'undefined')
4478
- return undefined;
4479
- const cookies = document.cookie ? document.cookie.split('; ') : [];
4480
- for (const cookie of cookies) {
4481
- const separatorIndex = cookie.indexOf('=');
4482
- if (separatorIndex === -1)
4483
- continue;
4484
- const rawName = cookie.slice(0, separatorIndex);
4485
- let decodedName;
4486
- try {
4487
- decodedName = decodeURIComponent(rawName);
4488
- }
4489
- catch {
4490
- decodedName = rawName;
4491
- }
4492
- if (decodedName === name) {
4493
- const rawValue = cookie.slice(separatorIndex + 1);
4494
- return decodeValue(rawValue);
4495
- }
4496
- }
4497
- return undefined;
4498
- }
4499
- /**
4500
- * Writes a cookie to `document.cookie`.
4501
- *
4502
- * Name and value encoding mirrors js-cookie v3 exactly (see `encodeName` and
4503
- * `encodeValue`). No-ops silently when `document` is not available.
4504
- */
4505
- function setCookie(name, value, attributes = {}) {
4506
- if (typeof document === 'undefined')
4507
- return;
4508
- document.cookie =
4509
- encodeName(name) +
4510
- '=' +
4511
- encodeValue(value) +
4512
- serializeAttributes(attributes);
4513
- }
4514
- /**
4515
- * Removes a cookie by expiring it one day in the past.
4516
- *
4517
- * Mirrors js-cookie v3's removal approach (`expires: -1`). The `path` and
4518
- * `domain` attributes must match those used when the cookie was written;
4519
- * mismatches silently leave the cookie in place.
4520
- */
4521
- function removeCookie(name, attributes = {}) {
4522
- setCookie(name, '', { ...attributes, expires: -1 });
4523
- }
4524
-
4525
- /**
4526
- * Hard cap on cookie size. Browsers vary slightly (the spec is 4096 bytes
4527
- * including name, value, and attributes), but ~4000 leaves headroom for
4528
- * the attribute serialization.
4529
- */
4530
- const COOKIE_BYTE_BUDGET = 4000;
4531
- /**
4532
- * Cookie storage adapter backed by `document.cookie`.
4533
- *
4534
- * Cookies are durable across tabs and survive localStorage clears, but they're
4535
- * capped at ~4KB per cookie and ~50 cookies per domain — large values are
4536
- * rejected rather than silently truncated so the orchestrator can degrade
4537
- * gracefully.
4538
- *
4539
- * Encoding is bit-for-bit compatible with js-cookie v3 (RFC6265) so that
4540
- * cookies written by earlier SDK versions continue to read correctly.
4541
- */
4542
- class CookieStorage {
4543
- constructor(options = {}) {
4544
- this.name = 'cookies';
4545
- this.attrs = CookieStorage.getCookieAttributes(options);
4546
- if (options.domain)
4547
- this.attrs.domain = options.domain;
4548
- }
4549
- /**
4550
- * `navigator.cookieEnabled` returns true even in some embedded contexts
4551
- * where third-party cookie writes silently fail, so we round-trip a probe.
4552
- */
4553
- async isAvailable() {
4554
- if (typeof document === 'undefined' ||
4555
- (typeof navigator !== 'undefined' && navigator.cookieEnabled === false))
4556
- return false;
4557
- try {
4558
- const probeKey = `__immortal_probe_${generateUUID()}__`;
4559
- safeSync(() => setCookie(probeKey, '1', { path: '/' }));
4560
- const ok = safeSync(() => getCookie(probeKey)) === '1';
4561
- safeSync(() => removeCookie(probeKey, { path: '/' }));
4562
- return ok;
4563
- }
4564
- catch {
4565
- return false;
4566
- }
4567
- }
4568
- async get(key) {
4569
- const value = safeSync(() => getCookie(key));
4570
- return value === undefined ? null : value;
4571
- }
4572
- async set(key, value) {
4573
- if (key.length + value.length > COOKIE_BYTE_BUDGET) {
4574
- throw new Error(`CookieStorage: combined key+value size (${key.length + value.length}) ` +
4575
- `exceeds budget (${COOKIE_BYTE_BUDGET}) for key "${key}"`);
4576
- }
4577
- safeSync(() => setCookie(key, value, this.attrs));
4578
- }
4579
- async remove(key) {
4580
- // Path must match the original write to actually clear the cookie.
4581
- safeSync(() => removeCookie(key, {
4582
- path: this.attrs.path,
4583
- domain: this.attrs.domain,
4584
- }));
4585
- }
4586
- static getCookieAttributes(options) {
4587
- return {
4588
- expires: options.expires ?? 365,
4589
- path: options.path ?? '/',
4590
- sameSite: options.sameSite ?? 'lax',
4591
- secure: options.secure ?? false,
4592
- };
4593
- }
4594
- }
4595
-
4596
- /**
4597
- * Adapter over IndexedDB. Single object store keyed by string, value is
4598
- * a string. The DB handle is opened lazily on first operation and cached
4599
- * on the instance — failed opens are not cached so the next call retries.
4600
- *
4601
- * Transaction completion (not just request success) is awaited for writes,
4602
- * since `request.onsuccess` can fire before a transaction has actually
4603
- * committed in some browsers.
4604
- */
4605
- class IndexedDBStorage {
4606
- constructor(options = {}) {
4607
- this.name = 'indexedDB';
4608
- this.dbPromise = null;
4609
- this.dbName = options.dbName ?? 'tri:_immortal_db';
4610
- this.storeName = options.storeName ?? 'tri:kv';
4611
- this.version = options.version ?? 1;
4612
- }
4613
- async isAvailable() {
4614
- if (typeof indexedDB === 'undefined')
4615
- return false;
4616
- try {
4617
- await this.open();
4618
- return true;
4619
- }
4620
- catch {
4621
- return false;
4622
- }
4623
- }
4624
- async get(key) {
4625
- const result = await this.tx('readonly', (s) => s.get(key));
4626
- if (result === undefined || result === null)
4627
- return null;
4628
- return typeof result === 'string' ? result : String(result);
4629
- }
4630
- async set(key, value) {
4631
- await this.tx('readwrite', (s) => s.put(value, key));
4632
- }
4633
- async remove(key) {
4634
- await this.tx('readwrite', (s) => s.delete(key));
4635
- }
4636
- open() {
4637
- if (this.dbPromise)
4638
- return this.dbPromise;
4639
- const promise = new Promise((resolve, reject) => {
4640
- if (typeof indexedDB === 'undefined') {
4641
- reject(new Error('IndexedDB is not available in this environment'));
4642
- return;
4643
- }
4644
- const req = indexedDB.open(this.dbName, this.version);
4645
- req.onerror = () => reject(req.error);
4646
- req.onblocked = () => reject(new Error('IndexedDB open blocked'));
4647
- req.onsuccess = () => resolve(req.result);
4648
- req.onupgradeneeded = () => {
4649
- const db = req.result;
4650
- if (!db.objectStoreNames.contains(this.storeName)) {
4651
- db.createObjectStore(this.storeName);
4652
- }
4653
- };
4654
- });
4655
- // Don't cache a rejected promise — let the next call retry from scratch.
4656
- promise.catch(() => {
4657
- if (this.dbPromise === promise)
4658
- this.dbPromise = null;
4659
- });
4660
- this.dbPromise = promise;
4661
- return promise;
4662
- }
4663
- async tx(mode, fn) {
4664
- return this.open().then((db) => new Promise((resolve, reject) => {
4665
- const tx = db.transaction(this.storeName, mode);
4666
- const objectStore = tx.objectStore(this.storeName);
4667
- const req = fn(objectStore);
4668
- let result;
4669
- req.onsuccess = () => {
4670
- result = req.result;
4671
- };
4672
- req.onerror = () => reject(req.error);
4673
- tx.oncomplete = () => resolve(result);
4674
- tx.onerror = () => reject(tx.error);
4675
- tx.onabort = () => reject(tx.error ?? new Error('IndexedDB transaction aborted'));
4676
- }));
4677
- }
4678
- }
4679
-
4680
- /**
4681
- * Adapter for the Web Storage API (localStorage / sessionStorage).
4682
- * The underlying Storage instance is injected via constructor so the same
4683
- * class can serve both — pass `window.localStorage` or `window.sessionStorage`.
4684
- */
4685
- class WebStorage {
4686
- constructor(storage, name) {
4687
- this.storage = storage;
4688
- this.name = name;
4689
- }
4690
- /**
4691
- * Probe via write/read/delete of a sentinel key. Catches:
4692
- * - Safari private mode (throws on setItem)
4693
- * - Storage disabled by user / policy
4694
- * - Quota-exhausted state
4695
- * `typeof Storage !== 'undefined'` alone is unreliable.
4696
- */
4697
- async isAvailable() {
4698
- try {
4699
- const probeKey = `__immortal_probe_${generateUUID()}__`;
4700
- safeSync(() => this.storage.setItem(probeKey, '1'));
4701
- const ok = safeSync(() => this.storage.getItem(probeKey)) === '1';
4702
- safeSync(() => this.storage.removeItem(probeKey));
4703
- return ok;
4704
- }
4705
- catch {
4706
- return false;
4707
- }
4708
- }
4709
- async get(key) {
4710
- return safeSync(() => this.storage.getItem(key)) ?? null;
4711
- }
4712
- async set(key, value) {
4713
- safeSync(() => this.storage.setItem(key, value));
4714
- }
4715
- async remove(key) {
4716
- safeSync(() => this.storage.removeItem(key));
4717
- }
4718
- }
4719
-
4720
- /**
4721
- * Redundant key-value store fanning out across multiple storage backends.
4722
- *
4723
- * - `set` / `remove` fan out to every available backend in parallel; per-store
4724
- * failures are logged via `onError` but do not fail the operation.
4725
- * - `get` reads from all backends in parallel and resolves divergence by
4726
- * majority vote, with non-null preferred on ties (a present value is
4727
- * stronger evidence than absence) and earlier-configured stores winning
4728
- * further ties.
4729
- * - After every read, any backend whose value differs from the winner is
4730
- * healed via a corrective write (fire-and-forget — does not block the read).
4731
- */
4732
- let ImmortalDB$1 = class ImmortalDB {
4733
- constructor(options = {}) {
4734
- this._stores = [];
4735
- this._initialized = false;
4736
- this._initPromise = null;
4737
- this._configured = options.stores ?? ImmortalDB._defaultStores();
4738
- this._keyPrefix = options.keyPrefix ?? '_tri_immortal_';
4739
- this._onError = options.onError ?? (() => { });
4740
- }
4741
- /** Names of backends that passed availability probing. Useful for diagnostics. */
4742
- get activeStores() {
4743
- return this._stores.map((s) => s.name);
4744
- }
4745
- /**
4746
- * Probe configured backends in parallel and retain only the live ones.
4747
- * Idempotent: subsequent calls are no-ops; concurrent calls share the
4748
- * same promise so probes only run once.
4749
- */
4750
- async init() {
4751
- if (this._initialized)
4752
- return;
4753
- if (this._initPromise)
4754
- return this._initPromise;
4755
- this._initPromise = (async () => {
4756
- const probes = await Promise.allSettled(this._configured.map(async (store) => ({
4757
- store,
4758
- ok: await store.isAvailable()
4759
- })));
4760
- this._stores = probes.reduce((acc, p) => {
4761
- if (p.status === 'fulfilled' && p.value.ok) {
4762
- acc.push(p.value.store);
4763
- }
4764
- return acc;
4765
- }, []);
4766
- if (this._stores.length === 0 && true) {
4767
- throw new Error('ImmortalDB: no storage backends are available');
4768
- }
4769
- this._initialized = true;
4770
- })();
4771
- try {
4772
- await this._initPromise;
4773
- }
4774
- finally {
4775
- this._initPromise = null;
4776
- }
4777
- }
4778
- async get(key) {
4779
- await this._ensureInitialized();
4780
- const namespacedKey = this._namespace(key);
4781
- const reads = await Promise.allSettled(this._stores.map((s) => s.get(namespacedKey)));
4782
- // Map each store to its observed value, treating rejections as "missing".
4783
- const observations = reads.map((r, i) => {
4784
- if (r.status === 'fulfilled')
4785
- return { store: this._stores[i], value: r.value };
4786
- this._onError(this._stores[i].name, 'get', r.reason);
4787
- return { store: this._stores[i], value: null };
4788
- });
4789
- const winner = ImmortalDB._pickWinner(observations);
4790
- // Self-heal divergent backends. Fire-and-forget — don't block the read.
4791
- for (const { store, value } of observations) {
4792
- if (value === winner)
4793
- continue;
4794
- winner === null
4795
- ? store
4796
- .remove(namespacedKey)
4797
- .catch((e) => this._onError(store.name, 'heal-remove', e))
4798
- : store
4799
- .set(namespacedKey, winner)
4800
- .catch((e) => this._onError(store.name, 'heal-set', e));
4801
- }
4802
- return winner;
4803
- }
4804
- async set(key, value) {
4805
- await this._ensureInitialized();
4806
- const namespacedKey = this._namespace(key);
4807
- const writes = await Promise.allSettled(this._stores.map((s) => s.set(namespacedKey, value)));
4808
- writes.forEach((w, i) => {
4809
- if (w.status === 'rejected')
4810
- this._onError(this._stores[i].name, 'set', w.reason);
4811
- });
4812
- }
4813
- async remove(key) {
4814
- await this._ensureInitialized();
4815
- const namespacedKey = this._namespace(key);
4816
- const removes = await Promise.allSettled(this._stores.map((s) => s.remove(namespacedKey)));
4817
- removes.forEach((r, i) => {
4818
- if (r.status === 'rejected')
4819
- this._onError(this._stores[i].name, 'remove', r.reason);
4820
- });
4821
- }
4822
- _namespace(key) {
4823
- return `${this._keyPrefix}${key}`;
4824
- }
4825
- async _ensureInitialized() {
4826
- if (!this._initialized)
4827
- await this.init();
4828
- }
4829
- /**
4830
- * Pick the canonical value from a set of observations.
4831
- * Rules:
4832
- * 1. Highest vote count wins.
4833
- * 2. On tie between null and non-null, prefer non-null
4834
- * (a present value is stronger evidence than absence).
4835
- * 3. On further tie, the earliest-configured store wins
4836
- * (deterministic, predictable from `stores` ordering).
4837
- */
4838
- static _pickWinner(observations) {
4839
- const counts = new Map();
4840
- for (const { value } of observations) {
4841
- counts.set(value, (counts.get(value) ?? 0) + 1);
4842
- }
4843
- const maxCount = Math.max(...counts.values());
4844
- const tied = observations.filter((o) => counts.get(o.value) === maxCount);
4845
- // Prefer the first non-null observation among the tied; fall back to null.
4846
- const firstNonNull = tied.find((o) => o.value !== null);
4847
- return firstNonNull ? firstNonNull.value : null;
4848
- }
4849
- static _defaultStores() {
4850
- const stores = [];
4851
- if (typeof window !== 'undefined') {
4852
- try {
4853
- if (window.localStorage)
4854
- stores.push(new WebStorage(window.localStorage, 'localStorage'));
4855
- }
4856
- catch {
4857
- // Accessing window.localStorage can throw in some sandboxed contexts.
4858
- }
4859
- }
4860
- if (typeof document !== 'undefined') {
4861
- stores.push(new CookieStorage());
4862
- }
4863
- if (typeof indexedDB !== 'undefined') {
4864
- stores.push(new IndexedDBStorage());
4865
- }
4866
- return stores;
4867
- }
4868
- };
4869
-
4870
- const ImmortalDB = new ImmortalDB$1();
4871
-
4872
4438
  const INTERACTIVE_TAGS = new Set([
4873
4439
  'A',
4874
4440
  'BUTTON',
@@ -5883,8 +5449,7 @@
5883
5449
  /** Activity Manager */
5884
5450
  activityManagerRef = new ActivityManager({
5885
5451
  startActivity: () => falconServiceManagerRef.api.startActivity(),
5886
- stopActivity: () => falconServiceManagerRef.api.stopActivity(),
5887
- db: ImmortalDB
5452
+ stopActivity: () => falconServiceManagerRef.api.stopActivity()
5888
5453
  });
5889
5454
  await activityManagerRef.start();
5890
5455
  /** Transport Manager */