@securitydept/client 0.3.0-beta.2 → 0.3.0-beta.5

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 (57) hide show
  1. package/README.md +6 -0
  2. package/dist/create-Bm-XRA3G.mjs +1573 -0
  3. package/dist/create-Bm-XRA3G.mjs.map +1 -0
  4. package/dist/index-BT-u3qNI.d.mts +133 -0
  5. package/dist/index-Ch8lrE44.d.mts +625 -0
  6. package/dist/index-D5mW18cM.d.mts +1648 -0
  7. package/dist/index.d.mts +3 -353
  8. package/dist/index.mjs +1124 -362
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/router-BJ8LqNJ1.mjs +245 -0
  11. package/dist/router-BJ8LqNJ1.mjs.map +1 -0
  12. package/dist/rx/index.d.mts +69 -0
  13. package/dist/rx/index.mjs +2 -0
  14. package/dist/rx-BoB32Dzg.mjs +1106 -0
  15. package/dist/rx-BoB32Dzg.mjs.map +1 -0
  16. package/dist/server/index.d.mts +21 -0
  17. package/dist/server/index.mjs +29 -0
  18. package/dist/server/index.mjs.map +1 -0
  19. package/dist/test/index.d.mts +102 -0
  20. package/dist/test/index.mjs +270 -0
  21. package/dist/test/index.mjs.map +1 -0
  22. package/dist/uri-string-BfnIT4gJ.mjs +298 -0
  23. package/dist/uri-string-BfnIT4gJ.mjs.map +1 -0
  24. package/dist/web/index.d.mts +82 -336
  25. package/dist/web/index.mjs +328 -337
  26. package/dist/web/index.mjs.map +1 -1
  27. package/dist/webext/index.d.mts +74 -0
  28. package/dist/webext/index.mjs +229 -0
  29. package/dist/webext/index.mjs.map +1 -0
  30. package/package.json +28 -21
  31. package/dist/auth-coordination/index.d.mts +0 -198
  32. package/dist/auth-coordination/index.mjs +0 -107
  33. package/dist/auth-coordination/index.mjs.map +0 -1
  34. package/dist/events/index.d.mts +0 -2
  35. package/dist/events/index.mjs +0 -2
  36. package/dist/events-BUlfsRfl.mjs +0 -127
  37. package/dist/events-BUlfsRfl.mjs.map +0 -1
  38. package/dist/fetch-transport-Bd5RyWj2.d.mts +0 -339
  39. package/dist/fetch-transport-DtnSRWAH.mjs +0 -631
  40. package/dist/fetch-transport-DtnSRWAH.mjs.map +0 -1
  41. package/dist/index-C_wgl6dW.d.mts +0 -102
  42. package/dist/index-DbYSFHDe.d.mts +0 -39
  43. package/dist/persistence/index.d.mts +0 -3
  44. package/dist/persistence/index.mjs +0 -2
  45. package/dist/persistence/web/index.d.mts +0 -16
  46. package/dist/persistence/web/index.mjs +0 -51
  47. package/dist/persistence/web/index.mjs.map +0 -1
  48. package/dist/persistence-CSB8770k.mjs +0 -108
  49. package/dist/persistence-CSB8770k.mjs.map +0 -1
  50. package/dist/planner-host-CPyGNyQV.d.mts +0 -190
  51. package/dist/planner-host-CV38bMSn.mjs +0 -195
  52. package/dist/planner-host-CV38bMSn.mjs.map +0 -1
  53. package/dist/types-C59fcM5W.d.mts +0 -52
  54. package/dist/types-DQc088ZP.d.mts +0 -32
  55. package/dist/web-router/index.d.mts +0 -259
  56. package/dist/web-router/index.mjs +0 -395
  57. package/dist/web-router/index.mjs.map +0 -1
@@ -1,382 +1,373 @@
1
- import { S as ClientErrorKind, T as UserRecovery, a as normalizeAbortError, h as createRuntime, i as createCancellationTokenFromAbortSignal, l as fromEventPattern, n as createFetchTransport, o as fromAbortSignal, r as createAbortSignalBridge, s as fromStorageEvent, t as FetchTransportRedirectKind, x as ClientError } from "../fetch-transport-DtnSRWAH.mjs";
2
- //#region src/web/cross-tab-sync.ts
1
+ import { A as RxEventSubject, G as UserRecovery, H as ClientErrorKind, V as ClientError, h as createAsyncSchedulerWithTimestampProvider, k as RxEventStream } from "../rx-BoB32Dzg.mjs";
2
+ import { F as PopupAttachFailureReason, G as validateWithSchemaSync, H as throwValidationClientError, I as PopupClientWindowHandle, J as PAGE_LIFECYCLE_TRAIT_TOKEN, K as PopupErrorCode, L as PopupServerWindowHandle, P as POPUP_TRAIT_TOKEN, T as ROUTER_TRAIT_TOKEN, U as validateTraitInput, at as createProviderIfTokenMissing, c as PERSISTENT_STORAGE_TRAIT_TOKEN, d as StorageChangeEventOrigin, ot as getSecuritydeptProviderToken, st as notMissingProvider, t as createFoundationEnvironment, u as SESSION_STORAGE_TRAIT_TOKEN, w as TIME_TRAIT_TOKEN } from "../create-Bm-XRA3G.mjs";
3
+ import { a as createPageLifecycleForNativeWeb, i as createRouterForNativeWeb, n as WebLegacyRouter, o as PageResumeTriggerKind, r as WebNavigationRouter, s as createPageResumeSource, t as NativeWebRouter } from "../router-BJ8LqNJ1.mjs";
4
+ import { filter, from, fromEventPattern, map, merge, take, timer } from "rxjs";
5
+ import { type } from "arktype";
6
+ //#region src/web/events/from-storage.ts
3
7
  /**
4
- * Create a cross-tab sync listener for a specific localStorage key.
5
- *
6
- * The browser `storage` event fires on ALL tabs EXCEPT the one that made the
7
- * change. This makes it a natural primitive for cross-tab state notification.
8
- *
9
- * @example
10
- * ```ts
11
- * const sync = createCrossTabSync({
12
- * key: "auth:v1",
13
- * onSync: ({ newValue }) => {
14
- * if (newValue) {
15
- * // Another tab updated auth state — reconcile
16
- * controller.restoreFromPersistence();
17
- * } else {
18
- * // Another tab cleared auth state — log out
19
- * controller.clearState({ clearPersisted: false });
20
- * }
21
- * },
22
- * });
23
- * // Later:
24
- * sync.dispose();
25
- * ```
8
+ * Adapt browser `storage` events into an event stream.
26
9
  */
27
- function createCrossTabSync(options) {
28
- const target = options.target ?? globalThis;
29
- let syncCount = 0;
30
- const subscription = fromStorageEvent({
31
- target,
32
- callback: (storageEvent) => {
33
- if (storageEvent.key !== options.key) return;
34
- syncCount++;
35
- options.onSync({
36
- newValue: storageEvent.newValue,
37
- oldValue: storageEvent.oldValue
38
- });
39
- }
40
- });
41
- return {
42
- dispose() {
43
- subscription.unsubscribe();
44
- },
45
- get syncCount() {
46
- return syncCount;
47
- }
48
- };
10
+ function fromStorageEvent(options) {
11
+ const { storageEventTarget } = options;
12
+ return RxEventStream.fromObservableInput(fromEventPattern((handler) => {
13
+ storageEventTarget.addEventListener("storage", handler);
14
+ }, (handler) => {
15
+ storageEventTarget.removeEventListener("storage", handler);
16
+ }));
49
17
  }
50
18
  //#endregion
51
- //#region src/web/page-resume-reconciler.ts
52
- const PageResumeTriggerKind = {
53
- Visibility: "visibility",
54
- PageShow: "pageshow",
55
- Focus: "focus",
56
- Online: "online"
57
- };
58
- function createPageResumeReconciler(options) {
59
- const throttleMs = options.throttleMs ?? 5e3;
60
- const getNow = options.now ?? (() => Date.now());
61
- const doc = options.document ?? maybeDocument();
62
- const win = options.window ?? maybeWindow();
63
- let previousVisibilityState = doc?.visibilityState ?? "visible";
64
- let lastReconcileAt = 0;
65
- let reconcileCount = 0;
66
- let disposed = false;
67
- const cleanups = [];
68
- const reconcile = (event) => {
69
- if (disposed) return;
70
- const now = getNow();
71
- if (now - lastReconcileAt < throttleMs) return;
72
- lastReconcileAt = now;
73
- reconcileCount += 1;
74
- try {
75
- const result = options.onReconcile(event);
76
- if (isPromiseLike(result)) result.catch((error) => options.onError?.(error, event));
77
- } catch (error) {
78
- options.onError?.(error, event);
79
- }
19
+ //#region src/web/popup.ts
20
+ const PopupForNativeWebCreateOptionsSchema = type({ window: {
21
+ open: "Function",
22
+ addEventListener: "Function",
23
+ removeEventListener: "Function"
24
+ } });
25
+ const PopupForNativeWebUnavailableProbeSchema = type({ window: "null | undefined" });
26
+ function createPopupForNativeWeb(options) {
27
+ const { validators, time, ...createOptions } = options;
28
+ const resolvedCreateOptions = {
29
+ window: globalThis.window ?? null,
30
+ ...createOptions
80
31
  };
81
- if (doc) {
82
- const handler = () => {
83
- const nextVisibilityState = doc.visibilityState;
84
- if (nextVisibilityState === "visible" && previousVisibilityState !== "visible") reconcile({ trigger: PageResumeTriggerKind.Visibility });
85
- previousVisibilityState = nextVisibilityState;
86
- };
87
- doc.addEventListener("visibilitychange", handler);
88
- cleanups.push(() => doc.removeEventListener("visibilitychange", handler));
89
- }
90
- if (win) {
91
- const pageshowHandler = (event) => {
92
- reconcile({
93
- trigger: PageResumeTriggerKind.PageShow,
94
- persisted: Boolean(event.persisted)
95
- });
96
- };
97
- const focusHandler = () => {
98
- reconcile({ trigger: PageResumeTriggerKind.Focus });
99
- };
100
- const onlineHandler = () => {
101
- reconcile({ trigger: PageResumeTriggerKind.Online });
102
- };
103
- win.addEventListener("pageshow", pageshowHandler);
104
- win.addEventListener("focus", focusHandler);
105
- win.addEventListener("online", onlineHandler);
106
- cleanups.push(() => win.removeEventListener("pageshow", pageshowHandler));
107
- cleanups.push(() => win.removeEventListener("focus", focusHandler));
108
- cleanups.push(() => win.removeEventListener("online", onlineHandler));
109
- }
32
+ if (validateWithSchemaSync(PopupForNativeWebUnavailableProbeSchema, resolvedCreateOptions).success) return null;
33
+ validateTraitInput({
34
+ value: resolvedCreateOptions,
35
+ bundledSchema: PopupForNativeWebCreateOptionsSchema,
36
+ validator: validators?.popup,
37
+ onInvalid: (failure) => throwValidationClientError({
38
+ code: "web.popup.invalid_native_web_popup_options",
39
+ source: "web",
40
+ messagePrefix: "createPopupForNativeWeb could not validate popupForNativeWebCreateOptions",
41
+ failure
42
+ })
43
+ });
44
+ const nativeWebWindow = resolvedCreateOptions.window;
110
45
  return {
111
- dispose() {
112
- if (disposed) return;
113
- disposed = true;
114
- for (const cleanup of cleanups.splice(0)) cleanup();
46
+ open(url, openOptions) {
47
+ return createPopupClientWindowHandleForNativeWeb({
48
+ url,
49
+ time,
50
+ window: nativeWebWindow,
51
+ ...openOptions
52
+ });
115
53
  },
116
- get reconcileCount() {
117
- return reconcileCount;
54
+ attach() {
55
+ return attachPopupWindowToOpenerForNativeWeb({
56
+ time,
57
+ window: nativeWebWindow
58
+ });
118
59
  }
119
60
  };
120
61
  }
121
- function maybeDocument() {
122
- const documentTarget = globalThis.document;
123
- return isPageResumeDocumentTarget(documentTarget) ? documentTarget : null;
124
- }
125
- function maybeWindow() {
126
- const windowTarget = globalThis.window;
127
- return isPageResumeWindowTarget(windowTarget) ? windowTarget : null;
128
- }
129
- function isPageResumeDocumentTarget(value) {
130
- return typeof value === "object" && value !== null && "visibilityState" in value && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
131
- }
132
- function isPageResumeWindowTarget(value) {
133
- return typeof value === "object" && value !== null && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
134
- }
135
- function isPromiseLike(value) {
136
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
137
- }
138
- //#endregion
139
- //#region src/web/popup.ts
140
- /** Stable error codes for popup-related failures. */
141
- const PopupErrorCode = {
142
- Blocked: "popup.blocked",
143
- Closed: "popup.closed_by_user",
144
- Timeout: "popup.relay_timeout",
145
- RelayError: "popup.relay_error",
146
- Internal: "popup.internal"
147
- };
148
- /**
149
- * Compute the `features` string for `window.open()`, centered on screen.
150
- */
151
- function computePopupFeatures(options = {}) {
62
+ function computePopupFeatures(options = {}, windowLike = globalThis) {
152
63
  const width = options.width ?? 500;
153
64
  const height = options.height ?? 600;
154
- return `width=${width},height=${height},left=${Math.max(0, Math.round(window.screenX + (window.innerWidth - width) / 2))},top=${Math.max(0, Math.round(window.screenY + (window.innerHeight - height) / 2))},popup=yes,toolbar=no,menubar=no`;
65
+ return `width=${width},height=${height},left=${Math.max(0, Math.round((windowLike.screenX ?? 0) + ((windowLike.innerWidth ?? width) - width) / 2))},top=${Math.max(0, Math.round((windowLike.screenY ?? 0) + ((windowLike.innerHeight ?? height) - height) / 2))},popup=yes,toolbar=no,menubar=no`;
155
66
  }
156
- /**
157
- * Open a popup window to the given URL.
158
- *
159
- * Throws a `ClientError` with code `popup.blocked` if the browser
160
- * blocks the popup.
161
- */
162
- function openPopupWindow(url, options) {
163
- const features = computePopupFeatures(options);
164
- const target = options?.target ?? "_blank";
165
- const win = window.open(url, target, features);
166
- if (!win || win.closed) throw new ClientError({
67
+ function createPopupClientWindowHandleForNativeWeb(options) {
68
+ const hostWindow = options.window;
69
+ const features = computePopupFeatures(options, hostWindow);
70
+ const target = options.target ?? "_blank";
71
+ const popupWindow = hostWindow.open(options.url, target, features);
72
+ if (!popupWindow || popupWindow.closed) throw new ClientError({
167
73
  kind: ClientErrorKind.Authorization,
168
74
  code: PopupErrorCode.Blocked,
169
75
  message: "Popup window was blocked by the browser. Please allow popups for this site.",
170
76
  recovery: UserRecovery.Retry,
171
77
  source: "popup"
172
78
  });
173
- return { window: win };
174
- }
175
- /**
176
- * Wait for a relay message from a popup callback page.
177
- *
178
- * The popup callback page should call `relayPopupCallback()` to post
179
- * the callback URL back to the opener.
180
- *
181
- * Resolves with the callback payload (URL or fragment string).
182
- * Rejects with a `ClientError` if:
183
- * - the popup is closed without relaying (popup.closed_by_user)
184
- * - the timeout expires (popup.relay_timeout)
185
- * - the relay message contains an error (popup.relay_error)
186
- */
187
- function waitForPopupRelay(options) {
188
- const timeoutMs = options.timeoutMs ?? 12e4;
189
- const expectedOrigin = options.expectedOrigin ?? window.location.origin;
190
- const pollIntervalMs = options.pollIntervalMs ?? 500;
191
- return new Promise((resolve, reject) => {
192
- let settled = false;
193
- let timeoutId;
194
- let pollId;
195
- function cleanup() {
196
- settled = true;
197
- clearTimeout(timeoutId);
198
- clearInterval(pollId);
199
- window.removeEventListener("message", onMessage);
200
- }
201
- function onMessage(event) {
202
- if (settled) return;
203
- if (event.origin !== expectedOrigin) return;
204
- const data = event.data;
205
- if (!data || data.type !== "securitydept:popup_callback") return;
206
- cleanup();
207
- if (data.error) {
208
- reject(new ClientError({
209
- kind: ClientErrorKind.Protocol,
210
- code: PopupErrorCode.RelayError,
211
- message: `Popup callback relay error: ${data.error}`,
212
- source: "popup"
213
- }));
214
- return;
215
- }
216
- resolve(data.payload);
217
- }
218
- window.addEventListener("message", onMessage);
219
- pollId = setInterval(() => {
220
- if (settled) return;
221
- if (options.popup.window.closed) {
222
- cleanup();
223
- reject(new ClientError({
224
- kind: ClientErrorKind.Authorization,
225
- code: PopupErrorCode.Closed,
226
- message: "Popup window was closed before completing the login flow.",
227
- recovery: UserRecovery.RestartFlow,
228
- source: "popup"
229
- }));
230
- }
231
- }, pollIntervalMs);
232
- timeoutId = setTimeout(() => {
233
- if (settled) return;
234
- cleanup();
235
- try {
236
- options.popup.window.close();
237
- } catch {}
238
- reject(new ClientError({
239
- kind: ClientErrorKind.Timeout,
240
- code: PopupErrorCode.Timeout,
241
- message: `Popup login timed out after ${timeoutMs}ms.`,
242
- recovery: UserRecovery.Retry,
243
- source: "popup"
244
- }));
245
- }, timeoutMs);
79
+ const resolvedBaseHref = hostWindow.location?.href ?? "http://localhost";
80
+ const expectedOrigin = options.expectedOrigin ?? new URL(options.url, resolvedBaseHref).origin;
81
+ const incoming = RxEventStream.fromObservableInput(fromEventPattern((handler) => {
82
+ hostWindow.addEventListener("message", handler);
83
+ }, (handler) => {
84
+ hostWindow.removeEventListener("message", handler);
85
+ }).pipe(filter((event) => event.origin === expectedOrigin && event.source === popupWindow), map((event) => event.data)));
86
+ const outgoing = createPostMessageSubject((message) => {
87
+ popupWindow.postMessage(message, expectedOrigin);
88
+ });
89
+ const closedStream = RxEventStream.fromObservableInput(timer(options.pollIntervalMs ?? 500, options.pollIntervalMs ?? 500, createAsyncSchedulerWithTimestampProvider(options.time)).pipe(filter(() => popupWindow.closed), map(() => true), take(1)));
90
+ return new PopupClientWindowHandle({
91
+ close() {
92
+ popupWindow.close();
93
+ },
94
+ messaging: {
95
+ incoming,
96
+ outgoing
97
+ },
98
+ time: options.time,
99
+ closedStream
246
100
  });
247
101
  }
248
- /**
249
- * Relay the popup callback result back to the opener window.
250
- *
251
- * Call this function from the popup callback page to send the callback
252
- * URL/fragment back to the parent window that initiated the popup login.
253
- *
254
- * @example
255
- * ```html
256
- * <script type="module">
257
- * import { relayPopupCallback } from "@securitydept/client/web";
258
- * relayPopupCallback({ payload: window.location.href });
259
- * <\/script>
260
- * ```
261
- */
262
- function relayPopupCallback(options = {}) {
263
- const opener = window.opener;
264
- if (!opener) return;
265
- const targetOrigin = options.targetOrigin ?? window.location.origin;
266
- const message = {
267
- type: "securitydept:popup_callback",
268
- payload: options.payload ?? window.location.href,
269
- error: options.error
102
+ function attachPopupWindowToOpenerForNativeWeb(options) {
103
+ const currentWindow = options.window;
104
+ const opener = currentWindow.opener;
105
+ if (!opener) return {
106
+ kind: "failure",
107
+ reason: PopupAttachFailureReason.MissingOpener,
108
+ error: new ClientError({
109
+ kind: ClientErrorKind.Configuration,
110
+ message: "Current page is not attached to a popup opener and cannot attach popup messaging.",
111
+ recovery: UserRecovery.RestartFlow,
112
+ source: "popup"
113
+ })
114
+ };
115
+ const allowedParentOrigin = currentWindow.location?.origin ?? void 0;
116
+ if (!allowedParentOrigin) return {
117
+ kind: "failure",
118
+ reason: PopupAttachFailureReason.MissingOrigin,
119
+ error: new ClientError({
120
+ kind: ClientErrorKind.Configuration,
121
+ message: "Current page has no origin information and cannot attach popup messaging.",
122
+ recovery: UserRecovery.RestartFlow,
123
+ source: "popup"
124
+ })
125
+ };
126
+ return {
127
+ kind: "success",
128
+ handle: new PopupServerWindowHandle({
129
+ close() {
130
+ currentWindow.close();
131
+ },
132
+ messaging: {
133
+ incoming: RxEventStream.fromObservableInput(fromEventPattern((handler) => {
134
+ currentWindow.addEventListener("message", handler);
135
+ }, (handler) => {
136
+ currentWindow.removeEventListener("message", handler);
137
+ }).pipe(filter((event) => event.origin === allowedParentOrigin && event.source === opener), map((event) => event.data))),
138
+ outgoing: createPostMessageSubject((message) => {
139
+ opener.postMessage(message, allowedParentOrigin);
140
+ })
141
+ }
142
+ })
270
143
  };
271
- opener.postMessage(message, targetOrigin);
272
- if (options.closeAfterRelay !== false) setTimeout(() => {
273
- window.close();
274
- }, 0);
275
144
  }
276
- //#endregion
277
- //#region src/web/runtime.ts
278
- /**
279
- * Create a `ClientRuntime` for browser/Web-capable environments.
280
- */
281
- function createWebRuntime(overrides = {}) {
282
- const { fetchTransport, transport, ...runtime } = overrides;
283
- return createRuntime({
284
- ...runtime,
285
- transport: transport ?? createFetchTransport(fetchTransport)
286
- });
145
+ function createPostMessageSubject(send) {
146
+ const outgoing = new RxEventSubject();
147
+ outgoing.subscribe({ next(message) {
148
+ send(message);
149
+ } });
150
+ return outgoing;
287
151
  }
288
152
  //#endregion
289
- //#region src/web/visibility.ts
290
- /** Visibility state snapshot passed to the callback. */
291
- const VisibilityState = {
292
- Visible: "visible",
293
- Hidden: "hidden"
294
- };
295
- /**
296
- * Subscribe to browser document visibility changes.
297
- *
298
- * Built on {@link fromEventPattern} so it follows the same Subscription
299
- * contract. Returns a {@link Subscription} handle for cleanup.
300
- *
301
- * @example
302
- * ```ts
303
- * const sub = fromVisibilityChange({
304
- * callback: (state) => {
305
- * if (state === VisibilityState.Visible) {
306
- * scheduleRefresh();
307
- * }
308
- * },
309
- * });
310
- * // Cleanup:
311
- * sub.unsubscribe();
312
- * ```
313
- */
314
- function fromVisibilityChange(options) {
315
- const doc = options.document ?? globalThis.document;
316
- return fromEventPattern({
317
- addHandler: (handler) => {
318
- doc.addEventListener("visibilitychange", handler);
153
+ //#region src/web/storage.ts
154
+ const StorageForNativeWebCreateOptionsSchema = type({
155
+ storage: type({
156
+ getItem: "Function",
157
+ setItem: "Function",
158
+ removeItem: "Function"
159
+ }),
160
+ storageEventTarget: type({
161
+ addEventListener: "Function",
162
+ removeEventListener: "Function"
163
+ }).or("null").or("undefined"),
164
+ prefix: "string | undefined",
165
+ validatorKey: "'persistentStorage' | 'sessionStorage'"
166
+ });
167
+ const StorageForNativeWebUnavailableProbeSchema = type({
168
+ storage: "null | undefined",
169
+ storageEventTarget: "unknown",
170
+ prefix: "string | undefined",
171
+ validatorKey: "'persistentStorage' | 'sessionStorage'"
172
+ });
173
+ const DEFAULT_PERSISTENT_STORAGE_PREFIX = "securitydept.web.client:";
174
+ const DEFAULT_SESSION_STORAGE_PREFIX = "securitydept.web.client:";
175
+ function createStorageForNativeWeb(options) {
176
+ const { validators, ...createOptions } = options;
177
+ const global = globalThis;
178
+ const resolvedCreateOptions = {
179
+ storage: options.validatorKey === "persistentStorage" ? global.localStorage ?? null : global.sessionStorage ?? null,
180
+ ...createOptions,
181
+ storageEventTarget: createOptions.storageEventTarget === void 0 ? typeof global.addEventListener === "function" && typeof global.removeEventListener === "function" ? global : null : createOptions.storageEventTarget,
182
+ prefix: createOptions.prefix ?? (options.validatorKey === "persistentStorage" ? DEFAULT_PERSISTENT_STORAGE_PREFIX : DEFAULT_SESSION_STORAGE_PREFIX)
183
+ };
184
+ if (validateWithSchemaSync(StorageForNativeWebUnavailableProbeSchema, resolvedCreateOptions).success) return null;
185
+ validateTraitInput({
186
+ value: resolvedCreateOptions,
187
+ bundledSchema: StorageForNativeWebCreateOptionsSchema,
188
+ validator: validators?.[options.validatorKey],
189
+ onInvalid: (failure) => throwValidationClientError({
190
+ code: "web.storage.invalid_native_web_storage_options",
191
+ source: "web",
192
+ messagePrefix: `createStorageForNativeWeb could not validate ${options.validatorKey}ForNativeWebCreateOptions`,
193
+ failure
194
+ })
195
+ });
196
+ const storage = resolvedCreateOptions.storage;
197
+ const prefix = resolvedCreateOptions.prefix;
198
+ const localStorageEvent = new RxEventSubject();
199
+ const externalStorageEvent = resolvedCreateOptions.storageEventTarget ? from(fromStorageEvent({ storageEventTarget: resolvedCreateOptions.storageEventTarget })).pipe(filter((event) => (event.storageArea === null || event.storageArea === storage) && (event.key === null || event.key.startsWith(prefix))), map((event) => ({
200
+ origin: StorageChangeEventOrigin.External,
201
+ key: event.key === null ? null : event.key.slice(prefix.length),
202
+ oldValue: event.oldValue,
203
+ newValue: event.newValue
204
+ }))) : null;
205
+ return {
206
+ storageEvent: RxEventStream.fromObservableInput(externalStorageEvent ? merge(localStorageEvent, externalStorageEvent) : localStorageEvent),
207
+ get(key) {
208
+ return storage.getItem(prefix + key);
319
209
  },
320
- removeHandler: (handler) => {
321
- doc.removeEventListener("visibilitychange", handler);
210
+ set(key, value) {
211
+ const storageKey = prefix + key;
212
+ const oldValue = storage.getItem(storageKey);
213
+ storage.setItem(storageKey, value);
214
+ localStorageEvent.next({
215
+ origin: StorageChangeEventOrigin.Local,
216
+ key,
217
+ oldValue,
218
+ newValue: value
219
+ });
322
220
  },
323
- callback: () => {
324
- const state = doc.visibilityState === "visible" ? VisibilityState.Visible : VisibilityState.Hidden;
325
- options.callback(state);
221
+ take(key) {
222
+ const storageKey = prefix + key;
223
+ const value = storage.getItem(storageKey);
224
+ if (value !== null) {
225
+ storage.removeItem(storageKey);
226
+ localStorageEvent.next({
227
+ origin: StorageChangeEventOrigin.Local,
228
+ key,
229
+ oldValue: value,
230
+ newValue: null
231
+ });
232
+ }
233
+ return value;
234
+ },
235
+ remove(key) {
236
+ const storageKey = prefix + key;
237
+ const oldValue = storage.getItem(storageKey);
238
+ storage.removeItem(storageKey);
239
+ if (oldValue !== null) localStorageEvent.next({
240
+ origin: StorageChangeEventOrigin.Local,
241
+ key,
242
+ oldValue,
243
+ newValue: null
244
+ });
326
245
  }
246
+ };
247
+ }
248
+ function createPersistentStorageForNativeWeb(options) {
249
+ return createStorageForNativeWeb({
250
+ ...options,
251
+ validatorKey: "persistentStorage"
252
+ });
253
+ }
254
+ function createSessionStorageForNativeWeb(options) {
255
+ return createStorageForNativeWeb({
256
+ ...options,
257
+ validatorKey: "sessionStorage"
327
258
  });
328
259
  }
329
260
  //#endregion
330
- //#region src/web/visibility-reconciler.ts
261
+ //#region src/web/environment.ts
262
+ function createEnvironmentForNativeWeb(options) {
263
+ const userProviders = options.providers ?? [];
264
+ return createFoundationEnvironment({
265
+ providers: [...createNativeWebTraitProviders(options, new Set(userProviders.map((provider) => getSecuritydeptProviderToken(provider)))), ...userProviders],
266
+ transport: options.transport,
267
+ transportForStdFetchCreateOptions: options.transportForStdFetchCreateOptions,
268
+ time: options.time,
269
+ timeForStdCreateOptions: options.timeForStdCreateOptions,
270
+ realmStorage: options.realmStorage,
271
+ idleCallback: options.idleCallback,
272
+ span: options.span,
273
+ spanCreateOptions: options.spanCreateOptions,
274
+ tracing: options.tracing,
275
+ tracingCreateOptions: options.tracingCreateOptions,
276
+ validators: options.validators
277
+ });
278
+ }
279
+ function createNativeWebTraitProviders(options, externalProviderTokens) {
280
+ return [
281
+ createProviderIfTokenMissing(externalProviderTokens, ROUTER_TRAIT_TOKEN, () => createNativeWebTraitUnit({
282
+ provide: ROUTER_TRAIT_TOKEN,
283
+ value: options.router,
284
+ createValue: () => createRouterForNativeWeb({
285
+ ...options.routerForNativeWebCreateOptions,
286
+ validators: options.validators
287
+ })
288
+ })),
289
+ createProviderIfTokenMissing(externalProviderTokens, PAGE_LIFECYCLE_TRAIT_TOKEN, () => createNativeWebTraitUnit({
290
+ provide: PAGE_LIFECYCLE_TRAIT_TOKEN,
291
+ value: options.pageLifecycle,
292
+ createValue: () => createPageLifecycleForNativeWeb({
293
+ ...options.pageLifecycleForNativeWebCreateOptions,
294
+ validators: options.validators
295
+ })
296
+ })),
297
+ createProviderIfTokenMissing(externalProviderTokens, POPUP_TRAIT_TOKEN, () => createNativeWebTraitUnit({
298
+ provide: POPUP_TRAIT_TOKEN,
299
+ value: options.popup,
300
+ createValue: (time) => createPopupForNativeWeb({
301
+ time,
302
+ ...options.popupForNativeWebCreateOptions,
303
+ validators: options.validators
304
+ }),
305
+ deps: [TIME_TRAIT_TOKEN]
306
+ })),
307
+ createProviderIfTokenMissing(externalProviderTokens, PERSISTENT_STORAGE_TRAIT_TOKEN, () => createNativeWebTraitUnit({
308
+ provide: PERSISTENT_STORAGE_TRAIT_TOKEN,
309
+ value: options.persistentStorage,
310
+ createValue: () => createPersistentStorageForNativeWeb({
311
+ ...options.persistentStorageForNativeWebCreateOptions,
312
+ validators: options.validators
313
+ })
314
+ })),
315
+ createProviderIfTokenMissing(externalProviderTokens, SESSION_STORAGE_TRAIT_TOKEN, () => createNativeWebTraitUnit({
316
+ provide: SESSION_STORAGE_TRAIT_TOKEN,
317
+ value: options.sessionStorage,
318
+ createValue: () => createSessionStorageForNativeWeb({
319
+ ...options.sessionStorageForNativeWebCreateOptions,
320
+ validators: options.validators
321
+ })
322
+ }))
323
+ ].filter(notMissingProvider);
324
+ }
325
+ function createNativeWebTraitUnit(options) {
326
+ if (options.value !== void 0) return {
327
+ provide: options.provide,
328
+ useValue: options.value
329
+ };
330
+ return {
331
+ provide: options.provide,
332
+ useFactory: options.createValue,
333
+ deps: options.deps
334
+ };
335
+ }
336
+ //#endregion
337
+ //#region src/web/sync/cross-tab-sync.ts
331
338
  /**
332
- * Create a visibility reconciler that triggers a callback when the page
333
- * transitions from hidden → visible.
339
+ * Create a cross-tab sync listener for a specific localStorage key.
340
+ *
341
+ * The browser `storage` event fires on ALL tabs EXCEPT the one that made the
342
+ * change. This makes it a natural primitive for cross-tab state notification.
334
343
  *
335
344
  * @example
336
345
  * ```ts
337
- * const reconciler = createVisibilityReconciler({
338
- * onReconcile: () => {
339
- * // Re-check token freshness or sync cross-tab state
340
- * if (controller.snapshot?.tokens.accessTokenExpiresAt) {
341
- * scheduleRefreshIfExpired();
346
+ * const subscription = createCrossTabSync({
347
+ * key: "auth:v1",
348
+ * storageEventTarget: window,
349
+ * }).subscribe({
350
+ * next: ({ newValue }) => {
351
+ * if (newValue) {
352
+ * // Another tab updated auth state — reconcile
353
+ * client.restorePersistedState();
354
+ * } else {
355
+ * // Another tab cleared auth state — log out
356
+ * controller.clearState({ persistPolicy: "skip" });
342
357
  * }
343
358
  * },
344
359
  * });
345
360
  * // Later:
346
- * reconciler.dispose();
361
+ * subscription.unsubscribe();
347
362
  * ```
348
363
  */
349
- function createVisibilityReconciler(options) {
350
- const throttleMs = options.throttleMs ?? 5e3;
351
- const getNow = options.now ?? (() => Date.now());
352
- let lastReconcileAt = 0;
353
- let reconcileCount = 0;
354
- const doc = options.document ?? globalThis.document;
355
- let previousState = doc.visibilityState === "visible" ? VisibilityState.Visible : VisibilityState.Hidden;
356
- const subscription = fromVisibilityChange({
357
- callback: (state) => {
358
- if (state === VisibilityState.Visible && previousState === VisibilityState.Hidden) {
359
- const now = getNow();
360
- if (now - lastReconcileAt >= throttleMs) {
361
- lastReconcileAt = now;
362
- reconcileCount++;
363
- options.onReconcile();
364
- }
365
- }
366
- previousState = state;
367
- },
368
- document: doc
369
- });
370
- return {
371
- dispose() {
372
- subscription.unsubscribe();
373
- },
374
- get reconcileCount() {
375
- return reconcileCount;
376
- }
377
- };
364
+ function createCrossTabSync(options) {
365
+ return RxEventStream.fromObservableInput(fromStorageEvent({ storageEventTarget: options.storageEventTarget })).pipe(filter((storageEvent) => storageEvent.key === options.key), map((storageEvent) => ({
366
+ newValue: storageEvent.newValue,
367
+ oldValue: storageEvent.oldValue
368
+ })));
378
369
  }
379
370
  //#endregion
380
- export { FetchTransportRedirectKind, PageResumeTriggerKind, PopupErrorCode, VisibilityState, computePopupFeatures, createAbortSignalBridge, createCancellationTokenFromAbortSignal, createCrossTabSync, createFetchTransport, createPageResumeReconciler, createVisibilityReconciler, createWebRuntime, fromAbortSignal, fromStorageEvent, fromVisibilityChange, normalizeAbortError, openPopupWindow, relayPopupCallback, waitForPopupRelay };
371
+ export { NativeWebRouter, PageResumeTriggerKind, WebLegacyRouter, WebNavigationRouter, computePopupFeatures, createCrossTabSync, createEnvironmentForNativeWeb, createPageLifecycleForNativeWeb, createPageResumeSource, createPersistentStorageForNativeWeb, createPopupForNativeWeb, createRouterForNativeWeb, createSessionStorageForNativeWeb, createStorageForNativeWeb, fromStorageEvent };
381
372
 
382
373
  //# sourceMappingURL=index.mjs.map