@neetru/sdk 3.0.2 → 3.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +554 -530
  2. package/dist/auth.cjs +219 -54
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.d.cts +1 -2
  5. package/dist/auth.d.ts +1 -2
  6. package/dist/auth.mjs +219 -54
  7. package/dist/auth.mjs.map +1 -1
  8. package/dist/catalog.cjs +50 -2
  9. package/dist/catalog.cjs.map +1 -1
  10. package/dist/catalog.d.cts +1 -2
  11. package/dist/catalog.d.ts +1 -2
  12. package/dist/catalog.mjs +50 -2
  13. package/dist/catalog.mjs.map +1 -1
  14. package/dist/checkout.cjs +52 -3
  15. package/dist/checkout.cjs.map +1 -1
  16. package/dist/checkout.d.cts +1 -2
  17. package/dist/checkout.d.ts +1 -2
  18. package/dist/checkout.mjs +52 -3
  19. package/dist/checkout.mjs.map +1 -1
  20. package/dist/db-react.cjs +18 -6
  21. package/dist/db-react.cjs.map +1 -1
  22. package/dist/db-react.d.cts +35 -13
  23. package/dist/db-react.d.ts +35 -13
  24. package/dist/db-react.mjs +18 -6
  25. package/dist/db-react.mjs.map +1 -1
  26. package/dist/db.cjs +164 -56
  27. package/dist/db.cjs.map +1 -1
  28. package/dist/db.d.cts +1 -2
  29. package/dist/db.d.ts +1 -2
  30. package/dist/db.mjs +164 -56
  31. package/dist/db.mjs.map +1 -1
  32. package/dist/entitlements.cjs +50 -2
  33. package/dist/entitlements.cjs.map +1 -1
  34. package/dist/entitlements.d.cts +1 -2
  35. package/dist/entitlements.d.ts +1 -2
  36. package/dist/entitlements.mjs +50 -2
  37. package/dist/entitlements.mjs.map +1 -1
  38. package/dist/errors.cjs.map +1 -1
  39. package/dist/errors.d.cts +3 -1
  40. package/dist/errors.d.ts +3 -1
  41. package/dist/errors.mjs.map +1 -1
  42. package/dist/firestore-compat.cjs +411 -0
  43. package/dist/firestore-compat.cjs.map +1 -0
  44. package/dist/firestore-compat.d.cts +319 -0
  45. package/dist/firestore-compat.d.ts +319 -0
  46. package/dist/firestore-compat.mjs +383 -0
  47. package/dist/firestore-compat.mjs.map +1 -0
  48. package/dist/index.cjs +266 -66
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +9 -4
  51. package/dist/index.d.ts +9 -4
  52. package/dist/index.mjs +265 -67
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/mocks.cjs.map +1 -1
  55. package/dist/mocks.d.cts +1 -2
  56. package/dist/mocks.d.ts +1 -2
  57. package/dist/mocks.mjs.map +1 -1
  58. package/dist/notifications.cjs +50 -2
  59. package/dist/notifications.cjs.map +1 -1
  60. package/dist/notifications.d.cts +1 -2
  61. package/dist/notifications.d.ts +1 -2
  62. package/dist/notifications.mjs +50 -2
  63. package/dist/notifications.mjs.map +1 -1
  64. package/dist/react.cjs +268 -2
  65. package/dist/react.cjs.map +1 -1
  66. package/dist/react.d.cts +135 -3
  67. package/dist/react.d.ts +135 -3
  68. package/dist/react.mjs +266 -3
  69. package/dist/react.mjs.map +1 -1
  70. package/dist/support.cjs +50 -2
  71. package/dist/support.cjs.map +1 -1
  72. package/dist/support.d.cts +1 -2
  73. package/dist/support.d.ts +1 -2
  74. package/dist/support.mjs +50 -2
  75. package/dist/support.mjs.map +1 -1
  76. package/dist/telemetry.cjs +65 -4
  77. package/dist/telemetry.cjs.map +1 -1
  78. package/dist/telemetry.d.cts +1 -2
  79. package/dist/telemetry.d.ts +1 -2
  80. package/dist/telemetry.mjs +65 -4
  81. package/dist/telemetry.mjs.map +1 -1
  82. package/dist/{types-D7zVkO3n.d.ts → types-B3XFHD4A.d.ts} +640 -2
  83. package/dist/{types-Dc4bjrrt.d.cts → types-DePdSU3P.d.cts} +640 -2
  84. package/dist/usage.cjs +50 -2
  85. package/dist/usage.cjs.map +1 -1
  86. package/dist/usage.d.cts +1 -2
  87. package/dist/usage.d.ts +1 -2
  88. package/dist/usage.mjs +50 -2
  89. package/dist/usage.mjs.map +1 -1
  90. package/dist/webhooks.cjs +83 -2
  91. package/dist/webhooks.cjs.map +1 -1
  92. package/dist/webhooks.d.cts +1 -2
  93. package/dist/webhooks.d.ts +1 -2
  94. package/dist/webhooks.mjs +83 -3
  95. package/dist/webhooks.mjs.map +1 -1
  96. package/package.json +156 -151
  97. package/dist/collection-ref-BDdfD87k.d.cts +0 -581
  98. package/dist/collection-ref-BDdfD87k.d.ts +0 -581
package/dist/auth.cjs CHANGED
@@ -53,8 +53,47 @@ var init_idempotency = __esm({
53
53
  // src/http.ts
54
54
  var http_exports = {};
55
55
  __export(http_exports, {
56
+ checkCoreVersionCompat: () => checkCoreVersionCompat,
56
57
  httpRequest: () => httpRequest
57
58
  });
59
+ function stripRevision(v) {
60
+ return v.split("+")[0] ?? v;
61
+ }
62
+ function compareCalVer(a, b) {
63
+ const partsA = stripRevision(a).split(".").map(Number);
64
+ const partsB = stripRevision(b).split(".").map(Number);
65
+ const len = Math.max(partsA.length, partsB.length);
66
+ for (let i = 0; i < len; i++) {
67
+ const pa = partsA[i] ?? 0;
68
+ const pb = partsB[i] ?? 0;
69
+ if (pa !== pb) return pa - pb;
70
+ }
71
+ return 0;
72
+ }
73
+ function getSdkMajor() {
74
+ try {
75
+ const ver = globalThis.__NEETRU_SDK_VERSION__;
76
+ if (ver) return parseInt(ver.split(".")[0] ?? "3", 10);
77
+ } catch {
78
+ }
79
+ return 3;
80
+ }
81
+ function checkCoreVersionCompat(coreVersion) {
82
+ if (!coreVersion) return;
83
+ const sdkMajor = getSdkMajor();
84
+ const minCore = COMPAT_MATRIX[sdkMajor];
85
+ if (!minCore) return;
86
+ const stripped = stripRevision(coreVersion);
87
+ if (compareCalVer(stripped, minCore) < 0) {
88
+ const key = `${sdkMajor}:${stripped}`;
89
+ if (!_warnedCoreTooOld.has(key)) {
90
+ _warnedCoreTooOld.add(key);
91
+ console.warn(
92
+ `[Neetru SDK] Aten\xE7\xE3o: Core vers\xE3o "${coreVersion}" \xE9 anterior ao m\xEDnimo recomendado "${minCore}" para SDK major ${sdkMajor}. Alguns recursos podem n\xE3o funcionar corretamente. Atualize o Core para a vers\xE3o mais recente.`
93
+ );
94
+ }
95
+ }
96
+ }
58
97
  function backoffMs(attempt) {
59
98
  const base = 200 * Math.pow(4, attempt);
60
99
  const jitter = base * 0.2 * (Math.random() * 2 - 1);
@@ -63,11 +102,11 @@ function backoffMs(attempt) {
63
102
  function parseRetryAfter(value) {
64
103
  if (!value) return null;
65
104
  const secs = Number(value);
66
- if (Number.isFinite(secs) && secs >= 0) return Math.round(secs * 1e3);
105
+ if (Number.isFinite(secs) && secs >= 0) return Math.min(Math.round(secs * 1e3), MAX_RETRY_AFTER_MS);
67
106
  const dateMs = Date.parse(value);
68
107
  if (Number.isFinite(dateMs)) {
69
108
  const delta = dateMs - Date.now();
70
- if (delta > 0) return delta;
109
+ if (delta > 0) return Math.min(delta, MAX_RETRY_AFTER_MS);
71
110
  }
72
111
  return null;
73
112
  }
@@ -78,6 +117,7 @@ function statusToCode(status) {
78
117
  if (status === 401) return "unauthorized";
79
118
  if (status === 403) return "forbidden";
80
119
  if (status === 404) return "not_found";
120
+ if (status === 409) return "conflict";
81
121
  if (status === 422 || status === 400) return "validation_failed";
82
122
  if (status === 429) return "rate_limited";
83
123
  if (status >= 500) return "server_error";
@@ -149,6 +189,8 @@ async function httpRequest(config, opts) {
149
189
  }
150
190
  const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-correlation-id") ?? void 0;
151
191
  if (res.ok) {
192
+ const coreVersion = res.headers.get("x-neetru-version");
193
+ if (coreVersion) checkCoreVersionCompat(coreVersion);
152
194
  const parsed = await safeJson(res);
153
195
  return parsed;
154
196
  }
@@ -177,11 +219,17 @@ async function httpRequest(config, opts) {
177
219
  }
178
220
  throw lastError ?? new NeetruError("unknown", "unexpected httpRequest exit");
179
221
  }
180
- var DEFAULT_RETRIES_READ, DEFAULT_RETRIES_WRITE, READ_METHODS, RETRYABLE_CODES;
222
+ var COMPAT_MATRIX, _warnedCoreTooOld, DEFAULT_RETRIES_READ, DEFAULT_RETRIES_WRITE, READ_METHODS, RETRYABLE_CODES, MAX_RETRY_AFTER_MS;
181
223
  var init_http = __esm({
182
224
  "src/http.ts"() {
183
225
  init_errors();
184
226
  init_idempotency();
227
+ COMPAT_MATRIX = {
228
+ // SDK major 3.x requer Core ≥ 2026.05.1 (suporte a X-Neetru-Version header,
229
+ // runTransaction write-after-read, verifyWebhookRequest, webhooks 3.0+)
230
+ 3: "2026.05.1"
231
+ };
232
+ _warnedCoreTooOld = /* @__PURE__ */ new Set();
185
233
  DEFAULT_RETRIES_READ = 2;
186
234
  DEFAULT_RETRIES_WRITE = 0;
187
235
  READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
@@ -190,6 +238,7 @@ var init_http = __esm({
190
238
  "server_error",
191
239
  "network_error"
192
240
  ]);
241
+ MAX_RETRY_AFTER_MS = 6e4;
193
242
  }
194
243
  });
195
244
 
@@ -891,11 +940,11 @@ var TRACK_MAX_QUEUE = 50;
891
940
  function createTelemetryNamespace(config) {
892
941
  const queue = [];
893
942
  let flushTimer = null;
943
+ let _inflightDrain = null;
894
944
  async function drainQueue() {
895
945
  if (queue.length === 0) return;
896
946
  const batch = queue.splice(0, queue.length);
897
- flushTimer = null;
898
- await Promise.allSettled(
947
+ const work = Promise.allSettled(
899
948
  batch.map(async (ev) => {
900
949
  try {
901
950
  const body = { name: ev.name };
@@ -917,6 +966,14 @@ function createTelemetryNamespace(config) {
917
966
  }
918
967
  })
919
968
  );
969
+ _inflightDrain = work.then(() => {
970
+ _inflightDrain = null;
971
+ });
972
+ await work;
973
+ flushTimer = null;
974
+ if (queue.length > 0) {
975
+ scheduleFlush();
976
+ }
920
977
  }
921
978
  function scheduleFlush() {
922
979
  if (flushTimer) return;
@@ -979,6 +1036,10 @@ function createTelemetryNamespace(config) {
979
1036
  if (input.name.length > 128) return;
980
1037
  queue.push(input);
981
1038
  if (queue.length >= TRACK_MAX_QUEUE) {
1039
+ if (flushTimer) {
1040
+ clearTimeout(flushTimer);
1041
+ flushTimer = null;
1042
+ }
982
1043
  void drainQueue();
983
1044
  } else {
984
1045
  scheduleFlush();
@@ -993,6 +1054,7 @@ function createTelemetryNamespace(config) {
993
1054
  clearTimeout(flushTimer);
994
1055
  flushTimer = null;
995
1056
  }
1057
+ if (_inflightDrain) await _inflightDrain;
996
1058
  await drainQueue();
997
1059
  },
998
1060
  /**
@@ -1481,6 +1543,31 @@ var LocalStore = class {
1481
1543
  this.db?.close();
1482
1544
  this.db = null;
1483
1545
  }
1546
+ /**
1547
+ * Apaga completamente o banco IndexedDB e o recria do zero.
1548
+ *
1549
+ * Ao contrário de `close()` + `open()` (que reabre o banco existente sem
1550
+ * apagar dados), `clearAll()` chama `indexedDB.deleteDatabase(dbName)` e
1551
+ * só então chama `open()` — garantindo que nenhum documento, mutação ou
1552
+ * metadado sobrevive.
1553
+ *
1554
+ * BUG fix: `clearCache()` em `NeetruDbDocumentsImpl` chamava close+open,
1555
+ * que reabre o mesmo banco. `clearAll()` é o método correto.
1556
+ */
1557
+ async clearAll() {
1558
+ if (this.db !== null) {
1559
+ this.db.close();
1560
+ this.db = null;
1561
+ }
1562
+ await new Promise((resolve, reject) => {
1563
+ const req = indexedDB.deleteDatabase(this.dbName);
1564
+ req.onsuccess = () => resolve();
1565
+ req.onerror = () => reject(req.error);
1566
+ req.onblocked = () => {
1567
+ };
1568
+ });
1569
+ await this.open();
1570
+ }
1484
1571
  assertOpen() {
1485
1572
  if (this.db === null) {
1486
1573
  throw new Error("LocalStore: banco n\xE3o est\xE1 aberto. Chame open() primeiro.");
@@ -1688,6 +1775,12 @@ var MemoryStore = class {
1688
1775
  async open() {
1689
1776
  }
1690
1777
  async close() {
1778
+ }
1779
+ /**
1780
+ * Apaga todos os dados em memória — equivalente ao `indexedDB.deleteDatabase`
1781
+ * que `LocalStore.clearAll()` executa em ambientes com IndexedDB.
1782
+ */
1783
+ async clearAll() {
1691
1784
  this._docs.clear();
1692
1785
  this._mutations.clear();
1693
1786
  this._meta.clear();
@@ -2043,7 +2136,60 @@ var MutationQueue = class {
2043
2136
  }
2044
2137
  };
2045
2138
 
2139
+ // src/db/field-value.ts
2140
+ var NEETRU_SENTINEL_KEY = "__neetru__";
2141
+ function isFieldSentinel(v) {
2142
+ return typeof v === "object" && v !== null && NEETRU_SENTINEL_KEY in v && typeof v[NEETRU_SENTINEL_KEY] === "string";
2143
+ }
2144
+ function isServerTimestampSentinel(v) {
2145
+ return isFieldSentinel(v) && v.__neetru__ === "serverTimestamp";
2146
+ }
2147
+ function isIncrementSentinel(v) {
2148
+ return isFieldSentinel(v) && v.__neetru__ === "increment" && typeof v.operand === "number" && Number.isFinite(v.operand);
2149
+ }
2150
+ function resolveSentinelLocally(value, baseValue) {
2151
+ if (isServerTimestampSentinel(value)) {
2152
+ return Date.now();
2153
+ }
2154
+ if (isIncrementSentinel(value)) {
2155
+ const base = typeof baseValue === "number" && Number.isFinite(baseValue) ? baseValue : 0;
2156
+ return base + value.operand;
2157
+ }
2158
+ return value;
2159
+ }
2160
+ function resolveSentinelsLocally(data, existing) {
2161
+ let touched = false;
2162
+ const out = {};
2163
+ for (const [k, v] of Object.entries(data)) {
2164
+ if (isFieldSentinel(v)) {
2165
+ touched = true;
2166
+ out[k] = resolveSentinelLocally(v, existing?.[k]);
2167
+ } else {
2168
+ out[k] = v;
2169
+ }
2170
+ }
2171
+ return touched ? out : data;
2172
+ }
2173
+ function hasFieldSentinel(data) {
2174
+ for (const v of Object.values(data)) {
2175
+ if (isFieldSentinel(v)) return true;
2176
+ }
2177
+ return false;
2178
+ }
2179
+
2046
2180
  // src/db/offline/sync-engine.ts
2181
+ function hasSentinels(payload) {
2182
+ return payload !== null && hasFieldSentinel(payload);
2183
+ }
2184
+ function sanitizeSentinelPayload(payload) {
2185
+ if (payload === null) return null;
2186
+ if (!hasFieldSentinel(payload)) return payload;
2187
+ const out = {};
2188
+ for (const [k, v] of Object.entries(payload)) {
2189
+ if (!isFieldSentinel(v)) out[k] = v;
2190
+ }
2191
+ return out;
2192
+ }
2047
2193
  var SyncEngine = class {
2048
2194
  _store;
2049
2195
  _queue;
@@ -2149,6 +2295,8 @@ var SyncEngine = class {
2149
2295
  const phase1Ok = await this._phase1Push();
2150
2296
  if (!phase1Ok) {
2151
2297
  this._updateState({ status: "idle" });
2298
+ this._refreshPendingWrites().catch(() => {
2299
+ });
2152
2300
  this._syncing = false;
2153
2301
  return;
2154
2302
  }
@@ -2251,11 +2399,16 @@ var SyncEngine = class {
2251
2399
  }
2252
2400
  return;
2253
2401
  }
2254
- const newData = mut.payload ?? (existing?.data ?? {});
2402
+ const literalPayload = sanitizeSentinelPayload(mut.payload);
2403
+ const newData = literalPayload ?? (existing?.data ?? {});
2255
2404
  const updatedDoc = {
2256
2405
  collection,
2257
2406
  id: docId,
2258
- data: mut.op === "update" ? { ...existing?.data ?? {}, ...newData } : newData,
2407
+ data: mut.op === "update" ? { ...existing?.data ?? {}, ...newData } : (
2408
+ // add/set: se o payload tinha sentinels, o cache otimista (existing.data)
2409
+ // é a fonte de verdade até o pull; senão usa o payload literal.
2410
+ hasSentinels(mut.payload) ? { ...existing?.data ?? {}, ...newData } : newData
2411
+ ),
2259
2412
  meta: {
2260
2413
  serverVersion,
2261
2414
  updatedAtServer: serverTimestamp,
@@ -2530,7 +2683,7 @@ var SyncEngine = class {
2530
2683
  ownerId: localDoc.meta.ownerId
2531
2684
  }
2532
2685
  };
2533
- const pendingMuts = await this._queue.drain();
2686
+ const pendingMuts = await this._queue.listAll();
2534
2687
  const docMut = pendingMuts.find(
2535
2688
  (m) => m.collection === serverDoc.collection && m.docId === serverDoc.id
2536
2689
  );
@@ -3337,43 +3490,6 @@ var TabCoordinator = class {
3337
3490
 
3338
3491
  // src/db/collection-ref.ts
3339
3492
  init_db_errors();
3340
-
3341
- // src/db/field-value.ts
3342
- var NEETRU_SENTINEL_KEY = "__neetru__";
3343
- function isFieldSentinel(v) {
3344
- return typeof v === "object" && v !== null && NEETRU_SENTINEL_KEY in v && typeof v[NEETRU_SENTINEL_KEY] === "string";
3345
- }
3346
- function isServerTimestampSentinel(v) {
3347
- return isFieldSentinel(v) && v.__neetru__ === "serverTimestamp";
3348
- }
3349
- function isIncrementSentinel(v) {
3350
- return isFieldSentinel(v) && v.__neetru__ === "increment" && typeof v.operand === "number" && Number.isFinite(v.operand);
3351
- }
3352
- function resolveSentinelLocally(value, baseValue) {
3353
- if (isServerTimestampSentinel(value)) {
3354
- return Date.now();
3355
- }
3356
- if (isIncrementSentinel(value)) {
3357
- const base = typeof baseValue === "number" && Number.isFinite(baseValue) ? baseValue : 0;
3358
- return base + value.operand;
3359
- }
3360
- return value;
3361
- }
3362
- function resolveSentinelsLocally(data, existing) {
3363
- let touched = false;
3364
- const out = {};
3365
- for (const [k, v] of Object.entries(data)) {
3366
- if (isFieldSentinel(v)) {
3367
- touched = true;
3368
- out[k] = resolveSentinelLocally(v, existing?.[k]);
3369
- } else {
3370
- out[k] = v;
3371
- }
3372
- }
3373
- return touched ? out : data;
3374
- }
3375
-
3376
- // src/db/collection-ref.ts
3377
3493
  function isIndexedDBUnavailable() {
3378
3494
  return typeof indexedDB === "undefined" || typeof window === "undefined";
3379
3495
  }
@@ -3921,8 +4037,13 @@ var NeetruDbDocumentsImpl = class {
3921
4037
  return this._engine.flush();
3922
4038
  }
3923
4039
  async clearCache() {
3924
- await this._store.close();
3925
- await this._store.open();
4040
+ const storeWithClear = this._store;
4041
+ if (typeof storeWithClear.clearAll === "function") {
4042
+ await storeWithClear.clearAll();
4043
+ } else {
4044
+ await this._store.close();
4045
+ await this._store.open();
4046
+ }
3926
4047
  }
3927
4048
  async getConflicts() {
3928
4049
  return this._store.listConflicts();
@@ -4809,7 +4930,8 @@ function createHttpCheckoutNamespace(config) {
4809
4930
  method: "POST",
4810
4931
  path: "/api/v1/checkout/intents",
4811
4932
  body,
4812
- requireAuth: true
4933
+ requireAuth: true,
4934
+ idempotencyKey: true
4813
4935
  });
4814
4936
  const result = parseStartResponse(raw);
4815
4937
  const shouldRedirect = input.autoRedirect !== false;
@@ -5564,6 +5686,7 @@ function createOidcAuthNamespace(config) {
5564
5686
  const STORAGE_KEY = "neetru_id_token";
5565
5687
  const listeners = /* @__PURE__ */ new Set();
5566
5688
  let cachedUser = null;
5689
+ let _expiryTimerId = null;
5567
5690
  function getStorage() {
5568
5691
  try {
5569
5692
  return typeof globalThis.localStorage !== "undefined" ? globalThis.localStorage : null;
@@ -5586,6 +5709,9 @@ function createOidcAuthNamespace(config) {
5586
5709
  function tokenToUser(token) {
5587
5710
  const payload = decodeJwtPayload(token);
5588
5711
  if (!payload || typeof payload.sub !== "string") return null;
5712
+ if (typeof payload.exp === "number" && payload.exp * 1e3 < Date.now()) {
5713
+ return null;
5714
+ }
5589
5715
  return {
5590
5716
  uid: payload.sub,
5591
5717
  email: typeof payload.email === "string" ? payload.email : "",
@@ -5599,12 +5725,28 @@ function createOidcAuthNamespace(config) {
5599
5725
  };
5600
5726
  }
5601
5727
  function loadCachedUser() {
5602
- if (cachedUser) return cachedUser;
5728
+ if (cachedUser) {
5729
+ const exp = cachedUser.exp;
5730
+ if (typeof exp === "number" && exp * 1e3 < Date.now()) {
5731
+ cachedUser = null;
5732
+ const storage2 = getStorage();
5733
+ storage2?.removeItem(STORAGE_KEY);
5734
+ notify();
5735
+ return null;
5736
+ }
5737
+ return cachedUser;
5738
+ }
5603
5739
  const storage = getStorage();
5604
5740
  if (!storage) return null;
5605
5741
  const token = storage.getItem(STORAGE_KEY);
5606
5742
  if (!token) return null;
5607
- cachedUser = tokenToUser(token);
5743
+ const user = tokenToUser(token);
5744
+ if (!user) {
5745
+ storage.removeItem(STORAGE_KEY);
5746
+ return null;
5747
+ }
5748
+ cachedUser = user;
5749
+ scheduleExpiryTimer(user);
5608
5750
  return cachedUser;
5609
5751
  }
5610
5752
  function notify() {
@@ -5615,6 +5757,23 @@ function createOidcAuthNamespace(config) {
5615
5757
  }
5616
5758
  }
5617
5759
  }
5760
+ function cancelExpiryTimer() {
5761
+ if (_expiryTimerId !== null) {
5762
+ clearTimeout(_expiryTimerId);
5763
+ _expiryTimerId = null;
5764
+ }
5765
+ }
5766
+ function scheduleExpiryTimer(user) {
5767
+ cancelExpiryTimer();
5768
+ const exp = user.exp;
5769
+ if (typeof exp !== "number") return;
5770
+ const msUntilExpiry = exp * 1e3 - Date.now();
5771
+ if (msUntilExpiry <= 0) return;
5772
+ _expiryTimerId = setTimeout(() => {
5773
+ _expiryTimerId = null;
5774
+ loadCachedUser();
5775
+ }, msUntilExpiry + 1);
5776
+ }
5618
5777
  return {
5619
5778
  async signIn(options) {
5620
5779
  if (typeof globalThis.location !== "undefined" && typeof globalThis.location.assign === "function") {
@@ -5642,18 +5801,24 @@ function createOidcAuthNamespace(config) {
5642
5801
  },
5643
5802
  async signOut() {
5644
5803
  const storage = getStorage();
5804
+ const storedToken = storage?.getItem(STORAGE_KEY) ?? null;
5645
5805
  if (storage) storage.removeItem(STORAGE_KEY);
5806
+ cancelExpiryTimer();
5646
5807
  cachedUser = null;
5647
5808
  const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5648
5809
  const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5649
5810
  const revokeUrl = `${idpOrigin}/api/v1/oauth/revoke`;
5811
+ const revokeBody = storedToken ? JSON.stringify({ token: storedToken }) : null;
5650
5812
  let revokeError = null;
5651
5813
  for (let attempt = 0; attempt < 2; attempt++) {
5652
5814
  try {
5653
- await config.fetch(revokeUrl, {
5654
- method: "POST",
5655
- headers: { "content-type": "application/json" }
5656
- });
5815
+ if (revokeBody) {
5816
+ await config.fetch(revokeUrl, {
5817
+ method: "POST",
5818
+ headers: { "content-type": "application/json" },
5819
+ body: revokeBody
5820
+ });
5821
+ }
5657
5822
  revokeError = null;
5658
5823
  break;
5659
5824
  } catch (err) {