@layers/amba-web 4.0.8 → 4.0.10

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.
@@ -24,28 +24,46 @@ async function getCoreRuntime() {
24
24
  /**
25
25
  * Browser localStorage adapter for SDK identity persistence.
26
26
  *
27
- * The SDK runtime currently re-seeds an `anonymous_id` on every page load.
28
- * This adapter is the documented home for storage-prefix handling so
29
- * customers can override the localStorage key prefix used by the
30
- * (eventual) identity cache.
27
+ * Browsers can deny or throw on localStorage access (privacy mode, sandboxed
28
+ * iframes, quota exhaustion). Persistence is best-effort: storage failures
29
+ * must never turn a successful auth/network operation into an SDK failure.
31
30
  */
32
31
  var LocalStoragePersistence = class {
33
32
  constructor(prefix) {
34
33
  this.prefix = prefix;
35
34
  }
36
- /** Read everything we'd want to hydrate at init. No-op until wired through. */
37
- hydrate() {}
35
+ get storage() {
36
+ try {
37
+ return globalThis.localStorage ?? null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ get available() {
43
+ return this.storage !== null;
44
+ }
38
45
  read(key) {
39
- if (typeof localStorage === "undefined") return null;
40
- return localStorage.getItem(`${this.prefix}:${key}`);
46
+ const storage = this.storage;
47
+ if (!storage) return null;
48
+ try {
49
+ return storage.getItem(`${this.prefix}:${key}`);
50
+ } catch {
51
+ return null;
52
+ }
41
53
  }
42
54
  write(key, value) {
43
- if (typeof localStorage === "undefined") return;
44
- localStorage.setItem(`${this.prefix}:${key}`, value);
55
+ const storage = this.storage;
56
+ if (!storage) return;
57
+ try {
58
+ storage.setItem(`${this.prefix}:${key}`, value);
59
+ } catch {}
45
60
  }
46
61
  delete(key) {
47
- if (typeof localStorage === "undefined") return;
48
- localStorage.removeItem(`${this.prefix}:${key}`);
62
+ const storage = this.storage;
63
+ if (!storage) return;
64
+ try {
65
+ storage.removeItem(`${this.prefix}:${key}`);
66
+ } catch {}
49
67
  }
50
68
  };
51
69
 
@@ -1342,11 +1360,10 @@ const DEFAULT_API_URL = "https://api.amba.dev";
1342
1360
  /**
1343
1361
  * Default host that serves deployed functions. `Amba.functions.invoke(name)`
1344
1362
  * targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
1345
- * `functionsBaseUrl` override is configured. The `<projectId>` label is the
1346
- * server-resolved id (from `diagnostics.ping`), which the function router
1347
- * accepts directly.
1363
+ * `functionsBaseUrl` override is configured.
1348
1364
  */
1349
1365
  const DEFAULT_FUNCTIONS_HOST = "fn.amba.host";
1366
+ const PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1350
1367
  /**
1351
1368
  * Internal state — one instance per page. Created lazily by `Amba.configure()`.
1352
1369
  */
@@ -1368,15 +1385,73 @@ let _configuredProjectId = null;
1368
1385
  let _configuredApiUrl = DEFAULT_API_URL;
1369
1386
  let _configuredClientKey = null;
1370
1387
  let _currentSessionToken = null;
1388
+ let _currentRefreshToken = null;
1389
+ let _currentSessionExpiresAt = null;
1390
+ let _currentUser = null;
1391
+ const BROWSER_IDENTITY_VERSION = 1;
1392
+ const DEFAULT_SESSION_TTL_MS = 3600 * 1e3;
1393
+ function identityStorageKey(projectId) {
1394
+ return `identity:v${BROWSER_IDENTITY_VERSION}:project:${encodeURIComponent(projectId)}`;
1395
+ }
1396
+ function isNonEmptyString(value) {
1397
+ return typeof value === "string" && value.trim().length > 0;
1398
+ }
1399
+ function containsControlCharacter(value) {
1400
+ return Array.from(value).some((char) => {
1401
+ const code = char.charCodeAt(0);
1402
+ return code <= 31 || code === 127;
1403
+ });
1404
+ }
1405
+ /** Read + validate a project-bound identity snapshot. Corrupt or cross-project
1406
+ * data is removed instead of being handed to the core. */
1407
+ function readPersistedIdentity(persistence, projectId) {
1408
+ const key = identityStorageKey(projectId);
1409
+ const raw = persistence.read(key);
1410
+ if (!raw) return null;
1411
+ try {
1412
+ const value = JSON.parse(raw);
1413
+ const hasTokens = value.session_token != null || value.refresh_token != null;
1414
+ if (value.version !== BROWSER_IDENTITY_VERSION || value.project_id !== projectId || !isNonEmptyString(value.anonymous_id) || hasTokens && !isNonEmptyString(value.app_user_id) || value.session_token != null && !isNonEmptyString(value.refresh_token) || value.session_token != null && !isNonEmptyString(value.session_token) || value.refresh_token != null && !isNonEmptyString(value.refresh_token) || value.expires_at != null && (!isNonEmptyString(value.expires_at) || !Number.isFinite(Date.parse(value.expires_at)))) throw new Error("invalid identity snapshot");
1415
+ const expiresAt = isNonEmptyString(value.expires_at) ? Date.parse(value.expires_at) : NaN;
1416
+ const sessionExpired = value.session_token != null && Number.isFinite(expiresAt) && expiresAt <= Date.now();
1417
+ return {
1418
+ version: BROWSER_IDENTITY_VERSION,
1419
+ project_id: projectId,
1420
+ anonymous_id: value.anonymous_id,
1421
+ app_user_id: isNonEmptyString(value.app_user_id) ? value.app_user_id : null,
1422
+ session_token: sessionExpired ? null : value.session_token ?? null,
1423
+ refresh_token: value.refresh_token ?? null,
1424
+ expires_at: sessionExpired ? null : value.expires_at ?? null,
1425
+ user: value.user && typeof value.user === "object" && isNonEmptyString(value.user.id) ? value.user : null
1426
+ };
1427
+ } catch {
1428
+ persistence.delete(key);
1429
+ return null;
1430
+ }
1431
+ }
1432
+ function writeIdentitySnapshot() {
1433
+ if (!_persistence?.available || !_configuredProjectId || !coreInstance) return;
1434
+ const snapshot = {
1435
+ version: BROWSER_IDENTITY_VERSION,
1436
+ project_id: _configuredProjectId,
1437
+ anonymous_id: coreInstance.anonymousId(),
1438
+ app_user_id: coreInstance.appUserId() ?? null,
1439
+ session_token: _currentSessionToken,
1440
+ refresh_token: _currentRefreshToken,
1441
+ expires_at: _currentSessionExpiresAt,
1442
+ user: _currentUser
1443
+ };
1444
+ _persistence.write(identityStorageKey(_configuredProjectId), JSON.stringify(snapshot));
1445
+ }
1371
1446
  /**
1372
1447
  * Functions invocation state. `_configuredFunctionsBaseUrl` is the optional
1373
- * override from `Amba.configure({ functionsBaseUrl })`. When it's null, the
1374
- * default-host path resolves the project id once via `diagnostics.ping` and
1375
- * caches it in `_resolvedFunctionsProjectId` so subsequent invokes skip the
1376
- * round-trip. Both reset on every `configure()`.
1448
+ * override from `Amba.configure({ functionsBaseUrl })`. The default-host path
1449
+ * caches its resolved project id and shares an in-flight diagnostics fallback
1450
+ * across concurrent invokes. All three reset on every `configure()`.
1377
1451
  */
1378
1452
  let _configuredFunctionsBaseUrl = null;
1379
1453
  let _resolvedFunctionsProjectId = null;
1454
+ let _functionsProjectIdInflight = null;
1380
1455
  /**
1381
1456
  * Offline-resilience state. `_persistence` is the localStorage-backed store
1382
1457
  * captured at configure-time; `_offlineQueue` is lazily built the first time
@@ -1444,7 +1519,7 @@ async function replayOutboxOp(op) {
1444
1519
  switch (op.kind) {
1445
1520
  case "collection.insert":
1446
1521
  await coreCall((c) => c.collectionsInsert(op.collection, encodeJson({
1447
- ...op.payload ?? {},
1522
+ ...op.payload,
1448
1523
  idempotency_key: op.idempotencyKey
1449
1524
  })));
1450
1525
  return;
@@ -1565,12 +1640,12 @@ function toWireTransactionOp(op) {
1565
1640
  return wire;
1566
1641
  }
1567
1642
  function snapshotSession(user) {
1568
- if (!user) return null;
1643
+ if (!user || !_currentSessionToken) return null;
1569
1644
  return {
1570
- sessionToken: "",
1571
- refreshToken: "",
1645
+ sessionToken: _currentSessionToken,
1646
+ refreshToken: _currentRefreshToken ?? "",
1572
1647
  user,
1573
- expiresAt: ""
1648
+ expiresAt: _currentSessionExpiresAt ?? ""
1574
1649
  };
1575
1650
  }
1576
1651
  function notifyAuthSubscribers(session) {
@@ -1581,7 +1656,11 @@ function notifyAuthSubscribers(session) {
1581
1656
  }
1582
1657
  }
1583
1658
  async function notifyFromAuthResult(result) {
1584
- if (result?.session_token) _currentSessionToken = result.session_token;
1659
+ _currentSessionToken = result.session_token;
1660
+ _currentRefreshToken = result.refresh_token;
1661
+ _currentSessionExpiresAt = result.expires_at && Number.isFinite(Date.parse(result.expires_at)) ? result.expires_at : new Date(Date.now() + DEFAULT_SESSION_TTL_MS).toISOString();
1662
+ _currentUser = result.user;
1663
+ writeIdentitySnapshot();
1585
1664
  notifyAuthSubscribers(snapshotSession(result.user));
1586
1665
  }
1587
1666
  /**
@@ -1628,22 +1707,156 @@ function buildAiStreamRequest(path, request) {
1628
1707
  /**
1629
1708
  * Resolve the full invocation URL for a deployed function.
1630
1709
  *
1631
- * With `functionsBaseUrl` configured we POST to `<base>/<name>` verbatim
1632
- * (custom domains / non-prod). Otherwise we resolve the server-side project
1633
- * id once (via `diagnostics.ping`, cached) and target
1634
- * `https://<projectId>.fn.amba.host/<name>` — the function router accepts the
1635
- * project id as the host label directly.
1710
+ * Resolution checks `functionsBaseUrl` first, then uses a UUID-shaped
1711
+ * configured project id directly, and finally falls back to a deduped
1712
+ * `diagnostics.ping` for the authoritative server-side project id. The
1713
+ * resolved default-host id is cached for subsequent invokes.
1636
1714
  */
1637
1715
  async function resolveFunctionUrl(name) {
1638
1716
  if (_configuredFunctionsBaseUrl) return `${_configuredFunctionsBaseUrl.replace(/\/$/, "")}/${encodeURIComponent(name)}`;
1639
- if (!_resolvedFunctionsProjectId) {
1640
- const ping = await Amba.diagnostics.ping();
1641
- if (!ping.ok || !ping.server_project_id) throw new AmbaApiError("FUNCTIONS_HOST_UNRESOLVED", "Could not resolve the functions host for this project. Pass `functionsBaseUrl` to Amba.configure() to invoke functions explicitly.", ping);
1642
- _resolvedFunctionsProjectId = ping.server_project_id;
1717
+ return `https://${await resolveFunctionsProjectId()}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
1718
+ }
1719
+ async function resolveFunctionsProjectId() {
1720
+ if (_resolvedFunctionsProjectId) return _resolvedFunctionsProjectId;
1721
+ if (_configuredProjectId && PROJECT_ID_PATTERN.test(_configuredProjectId)) {
1722
+ _resolvedFunctionsProjectId = _configuredProjectId.toLowerCase();
1723
+ return _resolvedFunctionsProjectId;
1643
1724
  }
1644
- return `https://${_resolvedFunctionsProjectId}.${DEFAULT_FUNCTIONS_HOST}/${encodeURIComponent(name)}`;
1725
+ if (!_functionsProjectIdInflight) {
1726
+ const configuredProjectId = _configuredProjectId;
1727
+ const resolution = (async () => {
1728
+ const ping = await Amba.diagnostics.ping();
1729
+ if (!ping.ok || !ping.server_project_id) throw new AmbaApiError("FUNCTIONS_HOST_UNRESOLVED", "Could not resolve the functions host for this project. Pass `functionsBaseUrl` to Amba.configure() to invoke functions explicitly.", ping);
1730
+ if (_configuredProjectId === configuredProjectId && !_configuredFunctionsBaseUrl) _resolvedFunctionsProjectId = ping.server_project_id;
1731
+ return ping.server_project_id;
1732
+ })();
1733
+ _functionsProjectIdInflight = resolution;
1734
+ resolution.then(() => {
1735
+ if (_functionsProjectIdInflight === resolution) _functionsProjectIdInflight = null;
1736
+ }, () => {
1737
+ if (_functionsProjectIdInflight === resolution) _functionsProjectIdInflight = null;
1738
+ });
1739
+ }
1740
+ return _functionsProjectIdInflight;
1645
1741
  }
1646
1742
  /**
1743
+ * Immutable browser collection scope for a server-verified group context.
1744
+ * Each method carries its own `groupId` into the Rust request path; no mutable
1745
+ * global header state is used, so parallel group A/group B calls cannot leak.
1746
+ *
1747
+ * Group-scoped writes deliberately bypass the global offline outbox. Replaying
1748
+ * a queued group mutation without its membership context would be unsafe;
1749
+ * callers receive the real network outcome and can retry in the same scope.
1750
+ */
1751
+ var AmbaGroupScopedClient = class AmbaGroupScopedClient {
1752
+ groupId;
1753
+ constructor(groupId) {
1754
+ const normalized = groupId.trim();
1755
+ if (!normalized || containsControlCharacter(normalized)) throw new AmbaApiError("VALIDATION_ERROR", "Amba.asGroup: groupId must be non-empty and contain no control characters");
1756
+ this.groupId = normalized;
1757
+ }
1758
+ asGroup(groupId) {
1759
+ return new AmbaGroupScopedClient(groupId);
1760
+ }
1761
+ inGroup(groupId) {
1762
+ return this.asGroup(groupId);
1763
+ }
1764
+ collection(name) {
1765
+ const groupId = this.groupId;
1766
+ return new CollectionSubscription({
1767
+ baseUrl: _configuredApiUrl,
1768
+ authHeaders: () => {
1769
+ const headers = realtimeAuthHeaders();
1770
+ return headers ? {
1771
+ ...headers,
1772
+ "X-Group-Context": groupId
1773
+ } : null;
1774
+ }
1775
+ }, name);
1776
+ }
1777
+ collections = {
1778
+ find: async (name, options = {}) => await coreCall((c) => c.collectionsFind(name, encodeJson(options), this.groupId)),
1779
+ findOne: async (name, id) => await coreCall((c) => c.collectionsFindOne(name, id, this.groupId)),
1780
+ insert: async (name, row) => await coreCall((c) => c.collectionsInsert(name, encodeJson(row), this.groupId)),
1781
+ update: async (name, id, set) => await coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set), this.groupId)),
1782
+ delete: async (name, id) => await coreCall((c) => c.collectionsDelete(name, id, this.groupId)),
1783
+ upsert: async (name, row, options) => await coreCall((c) => c.collectionsUpsert(name, encodeJson(row), encodeJson({
1784
+ on_conflict: options.onConflict,
1785
+ conflict_target: options.conflictTarget
1786
+ }), this.groupId)),
1787
+ deleteWhere: async (name, filter) => await coreCall((c) => c.collectionsDeleteWhere(name, encodeJson(filter), this.groupId)),
1788
+ restore: async (name, id) => await coreCall((c) => c.collectionsRestore(name, id, this.groupId)),
1789
+ transaction: async (operations) => {
1790
+ return (await coreCall((c) => c.collectionsTransaction(encodeJson(operations.map(toWireTransactionOp)), this.groupId))).data;
1791
+ },
1792
+ findNearest: async (name, options) => await coreCall((c) => c.collectionsFindNearest(name, encodeJson(options), this.groupId)),
1793
+ count: async (name, filter) => await coreCall((c) => c.collectionsCount(name, filter ? encodeJson(filter) : void 0, this.groupId)),
1794
+ optimisticInsert: async (name, row, view) => withOptimistic({
1795
+ apply: view.apply,
1796
+ mutate: () => coreCall((c) => c.collectionsInsert(name, encodeJson(row), this.groupId)),
1797
+ rollback: view.rollback
1798
+ }),
1799
+ optimisticUpdate: async (name, id, set, view) => withOptimistic({
1800
+ apply: view.apply,
1801
+ mutate: () => coreCall((c) => c.collectionsUpdate(name, id, encodeJson(set), this.groupId)),
1802
+ rollback: view.rollback
1803
+ }),
1804
+ optimisticDelete: async (name, id, view) => withOptimistic({
1805
+ apply: view.apply,
1806
+ mutate: () => coreCall((c) => c.collectionsDelete(name, id, this.groupId)),
1807
+ rollback: view.rollback
1808
+ }),
1809
+ where: {
1810
+ eq: (column, value) => ({
1811
+ column,
1812
+ op: "eq",
1813
+ value
1814
+ }),
1815
+ ne: (column, value) => ({
1816
+ column,
1817
+ op: "ne",
1818
+ value
1819
+ }),
1820
+ gt: (column, value) => ({
1821
+ column,
1822
+ op: "gt",
1823
+ value
1824
+ }),
1825
+ gte: (column, value) => ({
1826
+ column,
1827
+ op: "gte",
1828
+ value
1829
+ }),
1830
+ lt: (column, value) => ({
1831
+ column,
1832
+ op: "lt",
1833
+ value
1834
+ }),
1835
+ lte: (column, value) => ({
1836
+ column,
1837
+ op: "lte",
1838
+ value
1839
+ }),
1840
+ in: (column, values) => ({
1841
+ column,
1842
+ op: "in",
1843
+ value: values
1844
+ }),
1845
+ isNull: (column) => ({
1846
+ column,
1847
+ op: "is_null"
1848
+ }),
1849
+ isNotNull: (column) => ({
1850
+ column,
1851
+ op: "is_not_null"
1852
+ }),
1853
+ and: (...filters) => ({ and: filters }),
1854
+ or: (...filters) => ({ or: filters }),
1855
+ not: (filter) => ({ not: filter })
1856
+ }
1857
+ };
1858
+ };
1859
+ /**
1647
1860
  * Top-level SDK namespace.
1648
1861
  *
1649
1862
  * Stateless from the JS side — all state lives in the SDK runtime. Calling
@@ -1658,21 +1871,10 @@ const Amba = {
1658
1871
  const { initCore } = await getCoreRuntime();
1659
1872
  const explicitWasmInput = config.wasmModule ?? config.wasmUrl;
1660
1873
  if (explicitWasmInput !== void 0) await initCore({ module_or_path: explicitWasmInput });
1661
- else if (typeof process !== "undefined" && !!(process.versions && process.versions.node)) {
1662
- const [{ readFile }, { fileURLToPath, pathToFileURL }, { createRequire }] = await Promise.all([
1663
- import("node:fs/promises"),
1664
- import("node:url"),
1665
- import("node:module")
1666
- ]);
1667
- const resolveFn = import.meta.resolve;
1668
- let wasmJsUrl;
1669
- if (resolveFn) wasmJsUrl = await resolveFn("@layers/amba-core/wasm");
1670
- else wasmJsUrl = pathToFileURL(createRequire(import.meta.url).resolve("@layers/amba-core/wasm")).href;
1671
- await initCore({ module_or_path: await readFile(fileURLToPath(wasmJsUrl)) });
1672
- } else await initCore();
1874
+ else await initCore();
1673
1875
  }
1674
1876
  const persistence = new LocalStoragePersistence(config.storagePrefix ?? "amba");
1675
- persistence.hydrate();
1877
+ const restoredIdentity = readPersistedIdentity(persistence, config.projectId);
1676
1878
  let nextCore;
1677
1879
  try {
1678
1880
  const { wasmClass } = await getCoreRuntime();
@@ -1685,6 +1887,13 @@ const Amba = {
1685
1887
  consent_required: config.consentRequired,
1686
1888
  debug: config.debug
1687
1889
  }), globalThis.fetch.bind(globalThis));
1890
+ if (restoredIdentity) nextCore.hydrateIdentity(encodeJson({
1891
+ anonymous_id: restoredIdentity.anonymous_id,
1892
+ app_user_id: restoredIdentity.app_user_id,
1893
+ session_token: restoredIdentity.session_token,
1894
+ refresh_token: restoredIdentity.refresh_token,
1895
+ expires_at: restoredIdentity.expires_at
1896
+ }));
1688
1897
  } catch (err) {
1689
1898
  throw toAmbaApiError(err);
1690
1899
  }
@@ -1694,8 +1903,15 @@ const Amba = {
1694
1903
  _configuredProjectId = config.projectId;
1695
1904
  _configuredApiUrl = config.apiUrl ?? DEFAULT_API_URL;
1696
1905
  _configuredClientKey = config.clientKey;
1906
+ _currentSessionToken = nextCore.isAuthenticated() ? restoredIdentity?.session_token ?? null : null;
1907
+ _currentRefreshToken = restoredIdentity?.refresh_token ?? null;
1908
+ _currentSessionExpiresAt = nextCore.isAuthenticated() ? restoredIdentity?.expires_at ?? null : null;
1909
+ _currentUser = nextCore.isAuthenticated() ? restoredIdentity?.user ?? null : null;
1697
1910
  _configuredFunctionsBaseUrl = config.functionsBaseUrl ?? null;
1698
1911
  _resolvedFunctionsProjectId = null;
1912
+ _functionsProjectIdInflight = null;
1913
+ if (!_configuredFunctionsBaseUrl && !PROJECT_ID_PATTERN.test(config.projectId)) resolveFunctionsProjectId().catch(() => {});
1914
+ writeIdentitySnapshot();
1699
1915
  },
1700
1916
  get projectId() {
1701
1917
  if (!_configuredProjectId) throw new AmbaApiError("NOT_INITIALIZED", "amba SDK not configured. Call Amba.configure({ projectId, clientKey }) at app start before any other API.");
@@ -1848,6 +2064,10 @@ const Amba = {
1848
2064
  signOut: async (rotateAnonymousId = false) => {
1849
2065
  await coreCall((c) => c.signOut(rotateAnonymousId));
1850
2066
  _currentSessionToken = null;
2067
+ _currentRefreshToken = null;
2068
+ _currentSessionExpiresAt = null;
2069
+ _currentUser = null;
2070
+ writeIdentitySnapshot();
1851
2071
  notifyAuthSubscribers(null);
1852
2072
  },
1853
2073
  refresh: async () => {
@@ -1856,11 +2076,17 @@ const Amba = {
1856
2076
  return r;
1857
2077
  },
1858
2078
  me: async () => {
1859
- return await coreCall((c) => c.me());
2079
+ const user = await coreCall((c) => c.me());
2080
+ _currentUser = user;
2081
+ writeIdentitySnapshot();
2082
+ return user;
1860
2083
  },
1861
2084
  getSession: async () => {
1862
2085
  if (!requireCore().isAuthenticated()) return null;
1863
- return snapshotSession(await coreCall((c) => c.me()));
2086
+ const user = await coreCall((c) => c.me());
2087
+ _currentUser = user;
2088
+ writeIdentitySnapshot();
2089
+ return snapshotSession(user);
1864
2090
  },
1865
2091
  getAnonymousId: async () => {
1866
2092
  return requireCore().anonymousId();
@@ -1891,6 +2117,14 @@ const Amba = {
1891
2117
  await coreCall((c) => c.sessionsRevoke(sessionId));
1892
2118
  }
1893
2119
  },
2120
+ asGroup: (groupId) => {
2121
+ requireCore();
2122
+ return new AmbaGroupScopedClient(groupId);
2123
+ },
2124
+ inGroup: (groupId) => {
2125
+ requireCore();
2126
+ return new AmbaGroupScopedClient(groupId);
2127
+ },
1894
2128
  collection: (name) => new CollectionSubscription({
1895
2129
  baseUrl: _configuredApiUrl,
1896
2130
  authHeaders: realtimeAuthHeaders
@@ -2229,7 +2463,7 @@ const Amba = {
2229
2463
  const method = options?.method ?? "POST";
2230
2464
  const headers = {
2231
2465
  "X-Api-Key": _configuredClientKey,
2232
- ...options?.headers ?? {}
2466
+ ...options?.headers
2233
2467
  };
2234
2468
  if (_currentSessionToken) headers["Authorization"] = `Bearer ${_currentSessionToken}`;
2235
2469
  let requestBody;
@@ -2440,12 +2674,12 @@ function getCoreWasm() {
2440
2674
  * `verify-sdk-version-coherence` GH action gates every publish on the
2441
2675
  * pair agreeing (and matching the `web-v<X.Y.Z>` tag when tag-triggered).
2442
2676
  */
2443
- const SDK_VERSION = "4.0.8";
2677
+ const SDK_VERSION = "4.0.10";
2444
2678
 
2445
2679
  //#endregion
2446
2680
  //#region src/browser.ts
2447
2681
  setCoreRuntime(init, AmbaCoreWasm);
2448
2682
 
2449
2683
  //#endregion
2450
- export { Amba, AmbaApiError, CollectionSubscription, ConversationSubscription, GamificationSubscription, SDK_VERSION, getCoreWasm, withOptimistic };
2684
+ export { Amba, AmbaApiError, AmbaGroupScopedClient, CollectionSubscription, ConversationSubscription, GamificationSubscription, SDK_VERSION, getCoreWasm, withOptimistic };
2451
2685
  //# sourceMappingURL=index.browser.js.map