@securitydept/client 0.3.0-beta.3 → 0.3.0-beta.6

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 (60) hide show
  1. package/README.md +6 -0
  2. package/dist/create-99Ksgl_O.mjs +952 -0
  3. package/dist/create-99Ksgl_O.mjs.map +1 -0
  4. package/dist/handle-TnxwtQ4T.mjs +634 -0
  5. package/dist/handle-TnxwtQ4T.mjs.map +1 -0
  6. package/dist/index-Cg_iY2yY.d.mts +608 -0
  7. package/dist/index-DmIapvr-.d.mts +1669 -0
  8. package/dist/index-O1A3CdBV.d.mts +119 -0
  9. package/dist/index.d.mts +3 -353
  10. package/dist/index.mjs +1127 -364
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/router-DUh3bhLy.mjs +245 -0
  13. package/dist/router-DUh3bhLy.mjs.map +1 -0
  14. package/dist/rx/index.d.mts +68 -0
  15. package/dist/rx/index.mjs +2 -0
  16. package/dist/rx-B3OU17wu.mjs +1105 -0
  17. package/dist/rx-B3OU17wu.mjs.map +1 -0
  18. package/dist/server/index.d.mts +20 -0
  19. package/dist/server/index.mjs +29 -0
  20. package/dist/server/index.mjs.map +1 -0
  21. package/dist/test/index.d.mts +101 -0
  22. package/dist/test/index.mjs +270 -0
  23. package/dist/test/index.mjs.map +1 -0
  24. package/dist/uri-string-BPNbIMgv.mjs +300 -0
  25. package/dist/uri-string-BPNbIMgv.mjs.map +1 -0
  26. package/dist/web/index.d.mts +93 -363
  27. package/dist/web/index.mjs +335 -428
  28. package/dist/web/index.mjs.map +1 -1
  29. package/dist/webext/index.d.mts +73 -0
  30. package/dist/webext/index.mjs +231 -0
  31. package/dist/webext/index.mjs.map +1 -0
  32. package/package.json +28 -21
  33. package/dist/auth-coordination/index.d.mts +0 -198
  34. package/dist/auth-coordination/index.mjs +0 -107
  35. package/dist/auth-coordination/index.mjs.map +0 -1
  36. package/dist/client-environment-CaumNmir.mjs +0 -812
  37. package/dist/client-environment-CaumNmir.mjs.map +0 -1
  38. package/dist/client-environment-CdHtVxKW.d.mts +0 -401
  39. package/dist/events/index.d.mts +0 -2
  40. package/dist/events/index.mjs +0 -2
  41. package/dist/events-BUlfsRfl.mjs +0 -127
  42. package/dist/events-BUlfsRfl.mjs.map +0 -1
  43. package/dist/index-C4xlKhG7.d.mts +0 -102
  44. package/dist/index-COUt5x9Z.d.mts +0 -39
  45. package/dist/persistence/index.d.mts +0 -3
  46. package/dist/persistence/index.mjs +0 -2
  47. package/dist/persistence/web/index.d.mts +0 -16
  48. package/dist/persistence/web/index.mjs +0 -2
  49. package/dist/persistence-CSB8770k.mjs +0 -108
  50. package/dist/persistence-CSB8770k.mjs.map +0 -1
  51. package/dist/planner-host-BTWie0lo.mjs +0 -197
  52. package/dist/planner-host-BTWie0lo.mjs.map +0 -1
  53. package/dist/planner-host-DN9dRsQx.d.mts +0 -190
  54. package/dist/types-BJqUdH2e.d.mts +0 -32
  55. package/dist/types-BwcnUGNl.d.mts +0 -52
  56. package/dist/web-DQmMqZnR.mjs +0 -51
  57. package/dist/web-DQmMqZnR.mjs.map +0 -1
  58. package/dist/web-router/index.d.mts +0 -259
  59. package/dist/web-router/index.mjs +0 -395
  60. package/dist/web-router/index.mjs.map +0 -1
@@ -1,472 +1,379 @@
1
- import { B as ClientErrorKind, C as normalizeAbortError, D as fromEventPattern, S as createCancellationTokenFromAbortSignal, T as fromStorageEvent, U as UserRecovery, _ as requireDefaultPageLocationHistoryCapability, a as assertResolveFromEnvironment, b as createFetchTransport, c as createBrowserWorkerClientEnvironment, d as deriveClientEnvironment, f as readDefaultPageLocationCapability, g as requireDefaultPageLocationCapability, h as readPageLocationHistoryCapability, i as assertResolveEnvironment, l as createServiceWorkerClientEnvironment, m as readPageLocationCapability, n as assertPageLocationCapability, o as createBrowserExtensionBackgroundClientEnvironment, p as readDefaultPageLocationHistoryCapability, r as assertPageLocationHistoryCapability, s as createBrowserPageClientEnvironment, t as ClientEnvironmentPreset, u as createWebClientEnvironment, v as requirePageClientEnvironment, w as fromAbortSignal, x as createAbortSignalBridge, y as FetchTransportRedirectKind, z as ClientError } from "../client-environment-CaumNmir.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-B3OU17wu.mjs";
2
+ import { n as PopupServerWindowHandle, o as PopupErrorCode, t as PopupClientWindowHandle } from "../handle-TnxwtQ4T.mjs";
3
+ import { B as validateWithSchemaSync, F as PopupAttachFailureReason, L as throwValidationClientError, P as POPUP_TRAIT_TOKEN, Q as notMissingProvider, R as validateTraitInput, T as ROUTER_TRAIT_TOKEN, V as PAGE_LIFECYCLE_TRAIT_TOKEN, X as createProviderIfTokenMissing, Z as getSecuritydeptProviderToken, c as PERSISTENT_STORAGE_TRAIT_TOKEN, d as StorageChangeEventOrigin, t as createFoundationEnvironment, u as SESSION_STORAGE_TRAIT_TOKEN, w as TIME_TRAIT_TOKEN } from "../create-99Ksgl_O.mjs";
4
+ import { a as createPageLifecycleForNativeWeb, i as createRouterForNativeWeb, n as WebLegacyRouter, o as PageResumeTriggerKind, r as WebNavigationRouter, s as createPageResumeSource, t as NativeWebRouter } from "../router-DUh3bhLy.mjs";
5
+ import { filter, from, fromEventPattern, map, merge, take, timer } from "rxjs";
6
+ import { type } from "arktype";
7
+ //#region src/web/events/from-storage.ts
3
8
  /**
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
- * ```
9
+ * Adapt browser `storage` events into an event stream.
26
10
  */
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
- };
11
+ function fromStorageEvent(options) {
12
+ const { storageEventTarget } = options;
13
+ return RxEventStream.fromObservableInput(fromEventPattern((handler) => {
14
+ storageEventTarget.addEventListener("storage", handler);
15
+ }, (handler) => {
16
+ storageEventTarget.removeEventListener("storage", handler);
17
+ }));
49
18
  }
50
19
  //#endregion
51
- //#region src/web/environment-service.ts
52
- function createEnvironmentServiceState() {
53
- return {
54
- generation: 0,
55
- hasValue: false,
56
- value: null,
57
- hasError: false,
58
- error: void 0,
59
- pending: null
60
- };
61
- }
62
- /**
63
- * Framework-neutral environment resolver with layered lazy materialization.
64
- *
65
- * `resolve*()` coalesces concurrent work into a single pending promise.
66
- * `read*()` is Suspense-compatible: pending resolutions throw the same promise,
67
- * rejections throw the cached error, and fulfilled layers return the value.
68
- *
69
- * `reset()` invalidates all cached layers. In-flight promises may still settle
70
- * for existing awaiters, but their results are discarded and never repopulate
71
- * the service cache after a reset.
72
- */
73
- var ClientEnvironmentService = class {
74
- createClientEnvironmentImpl;
75
- createWebEnvironmentImpl;
76
- createPageEnvironmentImpl;
77
- clientState = createEnvironmentServiceState();
78
- webState = createEnvironmentServiceState();
79
- pageState = createEnvironmentServiceState();
80
- constructor(options = {}) {
81
- this.createClientEnvironmentImpl = options.createClientEnvironment ?? (() => createWebClientEnvironment());
82
- this.createWebEnvironmentImpl = options.createWebEnvironment ?? ((clientEnvironment) => clientEnvironment);
83
- this.createPageEnvironmentImpl = options.createPageEnvironment ?? ((webEnvironment) => createBrowserPageClientEnvironment({ ...deriveClientEnvironment(webEnvironment) }));
84
- }
85
- resolveClientEnvironment() {
86
- return this.resolveState(this.clientState, () => this.createClientEnvironmentImpl());
87
- }
88
- resolveWebEnvironment() {
89
- return this.resolveState(this.webState, async () => this.createWebEnvironmentImpl(await this.resolveClientEnvironment()));
90
- }
91
- resolvePageEnvironment() {
92
- return this.resolveState(this.pageState, async () => this.createPageEnvironmentImpl(await this.resolveWebEnvironment()));
93
- }
94
- readClientEnvironment() {
95
- return this.readState(this.clientState, () => this.resolveClientEnvironment());
96
- }
97
- readWebEnvironment() {
98
- return this.readState(this.webState, () => this.resolveWebEnvironment());
99
- }
100
- readPageEnvironment() {
101
- return this.readState(this.pageState, () => this.resolvePageEnvironment());
102
- }
103
- reset() {
104
- this.resetState(this.clientState);
105
- this.resetState(this.webState);
106
- this.resetState(this.pageState);
107
- }
108
- resolveState(state, materialize) {
109
- if (state.hasValue) return Promise.resolve(state.value);
110
- if (state.hasError) return Promise.reject(state.error);
111
- if (state.pending) return state.pending;
112
- const generation = state.generation;
113
- const pending = Promise.resolve().then(materialize).then((value) => {
114
- if (state.generation === generation) {
115
- state.value = value;
116
- state.hasValue = true;
117
- state.hasError = false;
118
- state.error = void 0;
119
- state.pending = null;
120
- }
121
- return value;
122
- }, (error) => {
123
- if (state.generation === generation) {
124
- state.hasError = true;
125
- state.error = error;
126
- state.pending = null;
127
- }
128
- throw error;
129
- });
130
- state.pending = pending;
131
- return pending;
132
- }
133
- readState(state, resolve) {
134
- if (state.hasValue) return state.value;
135
- if (state.hasError) throw state.error;
136
- throw resolve();
137
- }
138
- resetState(state) {
139
- state.generation += 1;
140
- state.hasValue = false;
141
- state.value = null;
142
- state.hasError = false;
143
- state.error = void 0;
144
- state.pending = null;
145
- }
146
- };
147
- //#endregion
148
- //#region src/web/page-resume-reconciler.ts
149
- const PageResumeTriggerKind = {
150
- Visibility: "visibility",
151
- PageShow: "pageshow",
152
- Focus: "focus",
153
- Online: "online"
154
- };
155
- function createPageResumeReconciler(options) {
156
- const throttleMs = options.throttleMs ?? 5e3;
157
- const getNow = options.now ?? (() => Date.now());
158
- const doc = options.document ?? maybeDocument();
159
- const win = options.window ?? maybeWindow();
160
- let previousVisibilityState = doc?.visibilityState ?? "visible";
161
- let lastReconcileAt = 0;
162
- let reconcileCount = 0;
163
- let disposed = false;
164
- const cleanups = [];
165
- const reconcile = (event) => {
166
- if (disposed) return;
167
- const now = getNow();
168
- if (now - lastReconcileAt < throttleMs) return;
169
- lastReconcileAt = now;
170
- reconcileCount += 1;
171
- try {
172
- const result = options.onReconcile(event);
173
- if (isPromiseLike(result)) result.catch((error) => options.onError?.(error, event));
174
- } catch (error) {
175
- options.onError?.(error, event);
176
- }
20
+ //#region src/web/popup.ts
21
+ const PopupForNativeWebCreateOptionsSchema = type({ window: {
22
+ open: "Function",
23
+ addEventListener: "Function",
24
+ removeEventListener: "Function"
25
+ } });
26
+ const PopupForNativeWebUnavailableProbeSchema = type({ window: "null | undefined" });
27
+ function createPopupForNativeWeb(options) {
28
+ const { validators, time, ...createOptions } = options;
29
+ const resolvedCreateOptions = {
30
+ window: globalThis.window ?? null,
31
+ ...createOptions
177
32
  };
178
- if (doc) {
179
- const handler = () => {
180
- const nextVisibilityState = doc.visibilityState;
181
- if (nextVisibilityState === "visible" && previousVisibilityState !== "visible") reconcile({ trigger: PageResumeTriggerKind.Visibility });
182
- previousVisibilityState = nextVisibilityState;
183
- };
184
- doc.addEventListener("visibilitychange", handler);
185
- cleanups.push(() => doc.removeEventListener("visibilitychange", handler));
186
- }
187
- if (win) {
188
- const pageshowHandler = (event) => {
189
- reconcile({
190
- trigger: PageResumeTriggerKind.PageShow,
191
- persisted: Boolean(event.persisted)
192
- });
193
- };
194
- const focusHandler = () => {
195
- reconcile({ trigger: PageResumeTriggerKind.Focus });
196
- };
197
- const onlineHandler = () => {
198
- reconcile({ trigger: PageResumeTriggerKind.Online });
199
- };
200
- win.addEventListener("pageshow", pageshowHandler);
201
- win.addEventListener("focus", focusHandler);
202
- win.addEventListener("online", onlineHandler);
203
- cleanups.push(() => win.removeEventListener("pageshow", pageshowHandler));
204
- cleanups.push(() => win.removeEventListener("focus", focusHandler));
205
- cleanups.push(() => win.removeEventListener("online", onlineHandler));
206
- }
33
+ if (validateWithSchemaSync(PopupForNativeWebUnavailableProbeSchema, resolvedCreateOptions).success) return null;
34
+ validateTraitInput({
35
+ value: resolvedCreateOptions,
36
+ bundledSchema: PopupForNativeWebCreateOptionsSchema,
37
+ validator: validators?.popup,
38
+ onInvalid: (failure) => throwValidationClientError({
39
+ code: "web.popup.invalid_native_web_popup_options",
40
+ source: "web",
41
+ messagePrefix: "createPopupForNativeWeb could not validate popupForNativeWebCreateOptions",
42
+ failure
43
+ })
44
+ });
45
+ const nativeWebWindow = resolvedCreateOptions.window;
207
46
  return {
208
- dispose() {
209
- if (disposed) return;
210
- disposed = true;
211
- for (const cleanup of cleanups.splice(0)) cleanup();
47
+ open(url, openOptions) {
48
+ return createPopupClientWindowHandleForNativeWeb({
49
+ url,
50
+ time,
51
+ window: nativeWebWindow,
52
+ ...openOptions
53
+ });
212
54
  },
213
- get reconcileCount() {
214
- return reconcileCount;
55
+ attach() {
56
+ return attachPopupWindowToOpenerForNativeWeb({
57
+ time,
58
+ window: nativeWebWindow
59
+ });
215
60
  }
216
61
  };
217
62
  }
218
- function maybeDocument() {
219
- const documentTarget = globalThis.document;
220
- return isPageResumeDocumentTarget(documentTarget) ? documentTarget : null;
221
- }
222
- function maybeWindow() {
223
- const windowTarget = globalThis.window;
224
- return isPageResumeWindowTarget(windowTarget) ? windowTarget : null;
225
- }
226
- function isPageResumeDocumentTarget(value) {
227
- return typeof value === "object" && value !== null && "visibilityState" in value && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
228
- }
229
- function isPageResumeWindowTarget(value) {
230
- return typeof value === "object" && value !== null && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
231
- }
232
- function isPromiseLike(value) {
233
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
234
- }
235
- //#endregion
236
- //#region src/web/popup.ts
237
- /** Stable error codes for popup-related failures. */
238
- const PopupErrorCode = {
239
- /** The browser blocked the popup window from opening. */
240
- Blocked: "popup.blocked",
241
- /** The popup was closed by the user before completing the flow. */
242
- Closed: "popup.closed_by_user",
243
- /** The relay message was not received within the timeout. */
244
- Timeout: "popup.relay_timeout",
245
- /** The relay message contained an error from the callback page. */
246
- RelayError: "popup.relay_error",
247
- /** An unexpected error occurred during popup lifecycle. */
248
- Internal: "popup.internal"
249
- };
250
- /**
251
- * Compute the `features` string for `window.open()`, centered on screen.
252
- */
253
- function computePopupFeatures(options = {}) {
63
+ function computePopupFeatures(options = {}, windowLike = globalThis) {
254
64
  const width = options.width ?? 500;
255
65
  const height = options.height ?? 600;
256
- 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`;
66
+ 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`;
257
67
  }
258
- /**
259
- * Open a popup window to the given URL.
260
- *
261
- * Throws a `ClientError` with code `popup.blocked` if the browser
262
- * blocks the popup.
263
- */
264
- function openPopupWindow(url, options) {
265
- const features = computePopupFeatures(options);
266
- const target = options?.target ?? "_blank";
267
- const win = window.open(url, target, features);
268
- if (!win || win.closed) throw new ClientError({
68
+ function createPopupClientWindowHandleForNativeWeb(options) {
69
+ const hostWindow = options.window;
70
+ const features = computePopupFeatures(options, hostWindow);
71
+ const target = options.target ?? "_blank";
72
+ const popupWindow = hostWindow.open(options.url, target, features);
73
+ if (!popupWindow || popupWindow.closed) throw new ClientError({
269
74
  kind: ClientErrorKind.Authorization,
270
75
  code: PopupErrorCode.Blocked,
271
76
  message: "Popup window was blocked by the browser. Please allow popups for this site.",
272
77
  recovery: UserRecovery.Retry,
273
78
  source: "popup"
274
79
  });
275
- return { window: win };
276
- }
277
- /**
278
- * Wait for a relay message from a popup callback page.
279
- *
280
- * The popup callback page should call `relayPopupCallback()` to post
281
- * the callback URL back to the opener.
282
- *
283
- * Resolves with the callback payload (URL or fragment string).
284
- * Rejects with a `ClientError` if:
285
- * - the popup is closed without relaying (popup.closed_by_user)
286
- * - the timeout expires (popup.relay_timeout)
287
- * - the relay message contains an error (popup.relay_error)
288
- */
289
- function waitForPopupRelay(options) {
290
- const timeoutMs = options.timeoutMs ?? 12e4;
291
- const expectedOrigin = options.expectedOrigin ?? window.location.origin;
292
- const pollIntervalMs = options.pollIntervalMs ?? 500;
293
- return new Promise((resolve, reject) => {
294
- let settled = false;
295
- let timeoutId;
296
- let pollId;
297
- function cleanup() {
298
- settled = true;
299
- clearTimeout(timeoutId);
300
- clearInterval(pollId);
301
- window.removeEventListener("message", onMessage);
302
- }
303
- function onMessage(event) {
304
- if (settled) return;
305
- if (event.origin !== expectedOrigin) return;
306
- const data = event.data;
307
- if (!data || data.type !== "securitydept:popup_callback") return;
308
- cleanup();
309
- if (data.error) {
310
- reject(new ClientError({
311
- kind: ClientErrorKind.Protocol,
312
- code: PopupErrorCode.RelayError,
313
- message: `Popup callback relay error: ${data.error}`,
314
- source: "popup"
315
- }));
316
- return;
317
- }
318
- resolve(data.payload);
319
- }
320
- window.addEventListener("message", onMessage);
321
- pollId = setInterval(() => {
322
- if (settled) return;
323
- if (options.popup.window.closed) {
324
- cleanup();
325
- reject(new ClientError({
326
- kind: ClientErrorKind.Authorization,
327
- code: PopupErrorCode.Closed,
328
- message: "Popup window was closed before completing the login flow.",
329
- recovery: UserRecovery.RestartFlow,
330
- source: "popup"
331
- }));
332
- }
333
- }, pollIntervalMs);
334
- timeoutId = setTimeout(() => {
335
- if (settled) return;
336
- cleanup();
337
- try {
338
- options.popup.window.close();
339
- } catch {}
340
- reject(new ClientError({
341
- kind: ClientErrorKind.Timeout,
342
- code: PopupErrorCode.Timeout,
343
- message: `Popup login timed out after ${timeoutMs}ms.`,
344
- recovery: UserRecovery.Retry,
345
- source: "popup"
346
- }));
347
- }, timeoutMs);
80
+ const resolvedBaseHref = hostWindow.location?.href ?? "http://localhost";
81
+ const expectedOrigin = options.expectedOrigin ?? new URL(options.url, resolvedBaseHref).origin;
82
+ const incoming = RxEventStream.fromObservableInput(fromEventPattern((handler) => {
83
+ hostWindow.addEventListener("message", handler);
84
+ }, (handler) => {
85
+ hostWindow.removeEventListener("message", handler);
86
+ }).pipe(filter((event) => event.origin === expectedOrigin && event.source === popupWindow), map((event) => event.data)));
87
+ const outgoing = createPostMessageSubject((message) => {
88
+ popupWindow.postMessage(message, expectedOrigin);
89
+ });
90
+ const closedStream = RxEventStream.fromObservableInput(timer(options.pollIntervalMs ?? 500, options.pollIntervalMs ?? 500, createAsyncSchedulerWithTimestampProvider(options.time)).pipe(filter(() => popupWindow.closed), map(() => true), take(1)));
91
+ return new PopupClientWindowHandle({
92
+ close() {
93
+ popupWindow.close();
94
+ },
95
+ messaging: {
96
+ incoming,
97
+ outgoing
98
+ },
99
+ time: options.time,
100
+ closedStream
348
101
  });
349
102
  }
350
- /**
351
- * Relay the popup callback result back to the opener window.
352
- *
353
- * Call this function from the popup callback page to send the callback
354
- * URL/fragment back to the parent window that initiated the popup login.
355
- *
356
- * @example
357
- * ```html
358
- * <script type="module">
359
- * import { relayPopupCallback } from "@securitydept/client/web";
360
- * relayPopupCallback({ payload: window.location.href });
361
- * <\/script>
362
- * ```
363
- */
364
- function relayPopupCallback(options = {}) {
365
- const opener = window.opener;
366
- if (!opener) return;
367
- const targetOrigin = options.targetOrigin ?? window.location.origin;
368
- const message = {
369
- type: "securitydept:popup_callback",
370
- payload: options.payload ?? window.location.href,
371
- error: options.error
103
+ function attachPopupWindowToOpenerForNativeWeb(options) {
104
+ const currentWindow = options.window;
105
+ const opener = currentWindow.opener;
106
+ if (!opener) return {
107
+ kind: "failure",
108
+ reason: PopupAttachFailureReason.MissingOpener,
109
+ error: new ClientError({
110
+ kind: ClientErrorKind.Configuration,
111
+ message: "Current page is not attached to a popup opener and cannot attach popup messaging.",
112
+ recovery: UserRecovery.RestartFlow,
113
+ source: "popup"
114
+ })
115
+ };
116
+ const allowedParentOrigin = currentWindow.location?.origin ?? void 0;
117
+ if (!allowedParentOrigin) return {
118
+ kind: "failure",
119
+ reason: PopupAttachFailureReason.MissingOrigin,
120
+ error: new ClientError({
121
+ kind: ClientErrorKind.Configuration,
122
+ message: "Current page has no origin information and cannot attach popup messaging.",
123
+ recovery: UserRecovery.RestartFlow,
124
+ source: "popup"
125
+ })
126
+ };
127
+ const incoming = RxEventStream.fromObservableInput(fromEventPattern((handler) => {
128
+ currentWindow.addEventListener("message", handler);
129
+ }, (handler) => {
130
+ currentWindow.removeEventListener("message", handler);
131
+ }).pipe(filter((event) => event.origin === allowedParentOrigin && event.source === opener), map((event) => event.data)));
132
+ const outgoing = createPostMessageSubject((message) => {
133
+ opener.postMessage(message, allowedParentOrigin);
134
+ });
135
+ return {
136
+ kind: "success",
137
+ handle: new PopupServerWindowHandle({
138
+ close() {
139
+ currentWindow.close();
140
+ },
141
+ messaging: {
142
+ incoming,
143
+ outgoing
144
+ }
145
+ })
372
146
  };
373
- opener.postMessage(message, targetOrigin);
374
- if (options.closeAfterRelay !== false) setTimeout(() => {
375
- window.close();
376
- }, 0);
147
+ }
148
+ function createPostMessageSubject(send) {
149
+ const outgoing = new RxEventSubject();
150
+ outgoing.subscribe({ next(message) {
151
+ send(message);
152
+ } });
153
+ return outgoing;
377
154
  }
378
155
  //#endregion
379
- //#region src/web/visibility.ts
380
- /** Visibility state snapshot passed to the callback. */
381
- const VisibilityState = {
382
- Visible: "visible",
383
- Hidden: "hidden"
384
- };
385
- /**
386
- * Subscribe to browser document visibility changes.
387
- *
388
- * Built on {@link fromEventPattern} so it follows the same Subscription
389
- * contract. Returns a {@link Subscription} handle for cleanup.
390
- *
391
- * @example
392
- * ```ts
393
- * const sub = fromVisibilityChange({
394
- * callback: (state) => {
395
- * if (state === VisibilityState.Visible) {
396
- * scheduleRefresh();
397
- * }
398
- * },
399
- * });
400
- * // Cleanup:
401
- * sub.unsubscribe();
402
- * ```
403
- */
404
- function fromVisibilityChange(options) {
405
- const doc = options.document ?? globalThis.document;
406
- return fromEventPattern({
407
- addHandler: (handler) => {
408
- doc.addEventListener("visibilitychange", handler);
156
+ //#region src/web/storage.ts
157
+ const NativeWebStorageLikeSchema = type({
158
+ getItem: "Function",
159
+ setItem: "Function",
160
+ removeItem: "Function"
161
+ });
162
+ const StorageEventTargetSchema = type({
163
+ addEventListener: "Function",
164
+ removeEventListener: "Function"
165
+ });
166
+ const StorageForNativeWebCreateOptionsSchema = type({
167
+ storage: NativeWebStorageLikeSchema,
168
+ storageEventTarget: StorageEventTargetSchema.or("null").or("undefined"),
169
+ prefix: "string | undefined",
170
+ validatorKey: "'persistentStorage' | 'sessionStorage'"
171
+ });
172
+ const StorageForNativeWebUnavailableProbeSchema = type({
173
+ storage: "null | undefined",
174
+ storageEventTarget: "unknown",
175
+ prefix: "string | undefined",
176
+ validatorKey: "'persistentStorage' | 'sessionStorage'"
177
+ });
178
+ const DEFAULT_PERSISTENT_STORAGE_PREFIX = "securitydept.web.client:";
179
+ const DEFAULT_SESSION_STORAGE_PREFIX = "securitydept.web.client:";
180
+ function createStorageForNativeWeb(options) {
181
+ const { validators, ...createOptions } = options;
182
+ const global = globalThis;
183
+ const resolvedCreateOptions = {
184
+ storage: options.validatorKey === "persistentStorage" ? global.localStorage ?? null : global.sessionStorage ?? null,
185
+ ...createOptions,
186
+ storageEventTarget: createOptions.storageEventTarget === void 0 ? typeof global.addEventListener === "function" && typeof global.removeEventListener === "function" ? global : null : createOptions.storageEventTarget,
187
+ prefix: createOptions.prefix ?? (options.validatorKey === "persistentStorage" ? DEFAULT_PERSISTENT_STORAGE_PREFIX : DEFAULT_SESSION_STORAGE_PREFIX)
188
+ };
189
+ if (validateWithSchemaSync(StorageForNativeWebUnavailableProbeSchema, resolvedCreateOptions).success) return null;
190
+ validateTraitInput({
191
+ value: resolvedCreateOptions,
192
+ bundledSchema: StorageForNativeWebCreateOptionsSchema,
193
+ validator: validators?.[options.validatorKey],
194
+ onInvalid: (failure) => throwValidationClientError({
195
+ code: "web.storage.invalid_native_web_storage_options",
196
+ source: "web",
197
+ messagePrefix: `createStorageForNativeWeb could not validate ${options.validatorKey}ForNativeWebCreateOptions`,
198
+ failure
199
+ })
200
+ });
201
+ const storage = resolvedCreateOptions.storage;
202
+ const prefix = resolvedCreateOptions.prefix;
203
+ const localStorageEvent = new RxEventSubject();
204
+ 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) => ({
205
+ origin: StorageChangeEventOrigin.External,
206
+ key: event.key === null ? null : event.key.slice(prefix.length),
207
+ oldValue: event.oldValue,
208
+ newValue: event.newValue
209
+ }))) : null;
210
+ return {
211
+ storageEvent: RxEventStream.fromObservableInput(externalStorageEvent ? merge(localStorageEvent, externalStorageEvent) : localStorageEvent),
212
+ get(key) {
213
+ return storage.getItem(prefix + key);
409
214
  },
410
- removeHandler: (handler) => {
411
- doc.removeEventListener("visibilitychange", handler);
215
+ set(key, value) {
216
+ const storageKey = prefix + key;
217
+ const oldValue = storage.getItem(storageKey);
218
+ storage.setItem(storageKey, value);
219
+ localStorageEvent.next({
220
+ origin: StorageChangeEventOrigin.Local,
221
+ key,
222
+ oldValue,
223
+ newValue: value
224
+ });
225
+ },
226
+ take(key) {
227
+ const storageKey = prefix + key;
228
+ const value = storage.getItem(storageKey);
229
+ if (value !== null) {
230
+ storage.removeItem(storageKey);
231
+ localStorageEvent.next({
232
+ origin: StorageChangeEventOrigin.Local,
233
+ key,
234
+ oldValue: value,
235
+ newValue: null
236
+ });
237
+ }
238
+ return value;
412
239
  },
413
- callback: () => {
414
- const state = doc.visibilityState === "visible" ? VisibilityState.Visible : VisibilityState.Hidden;
415
- options.callback(state);
240
+ remove(key) {
241
+ const storageKey = prefix + key;
242
+ const oldValue = storage.getItem(storageKey);
243
+ storage.removeItem(storageKey);
244
+ if (oldValue !== null) localStorageEvent.next({
245
+ origin: StorageChangeEventOrigin.Local,
246
+ key,
247
+ oldValue,
248
+ newValue: null
249
+ });
416
250
  }
251
+ };
252
+ }
253
+ function createPersistentStorageForNativeWeb(options) {
254
+ return createStorageForNativeWeb({
255
+ ...options,
256
+ validatorKey: "persistentStorage"
257
+ });
258
+ }
259
+ function createSessionStorageForNativeWeb(options) {
260
+ return createStorageForNativeWeb({
261
+ ...options,
262
+ validatorKey: "sessionStorage"
263
+ });
264
+ }
265
+ //#endregion
266
+ //#region src/web/environment.ts
267
+ function createEnvironmentForNativeWeb(options) {
268
+ const userProviders = options.providers ?? [];
269
+ const providers = [...createNativeWebTraitProviders(options, new Set(userProviders.map((provider) => getSecuritydeptProviderToken(provider)))), ...userProviders];
270
+ return createFoundationEnvironment({
271
+ providers,
272
+ transport: options.transport,
273
+ transportForStdFetchCreateOptions: options.transportForStdFetchCreateOptions,
274
+ time: options.time,
275
+ timeForStdCreateOptions: options.timeForStdCreateOptions,
276
+ realmStorage: options.realmStorage,
277
+ idleCallback: options.idleCallback,
278
+ span: options.span,
279
+ spanCreateOptions: options.spanCreateOptions,
280
+ tracing: options.tracing,
281
+ tracingCreateOptions: options.tracingCreateOptions,
282
+ validators: options.validators
417
283
  });
418
284
  }
285
+ function createNativeWebTraitProviders(options, externalProviderTokens) {
286
+ return [
287
+ createProviderIfTokenMissing(externalProviderTokens, ROUTER_TRAIT_TOKEN, () => createNativeWebTraitUnit({
288
+ provide: ROUTER_TRAIT_TOKEN,
289
+ value: options.router,
290
+ createValue: () => createRouterForNativeWeb({
291
+ ...options.routerForNativeWebCreateOptions,
292
+ validators: options.validators
293
+ })
294
+ })),
295
+ createProviderIfTokenMissing(externalProviderTokens, PAGE_LIFECYCLE_TRAIT_TOKEN, () => createNativeWebTraitUnit({
296
+ provide: PAGE_LIFECYCLE_TRAIT_TOKEN,
297
+ value: options.pageLifecycle,
298
+ createValue: () => createPageLifecycleForNativeWeb({
299
+ ...options.pageLifecycleForNativeWebCreateOptions,
300
+ validators: options.validators
301
+ })
302
+ })),
303
+ createProviderIfTokenMissing(externalProviderTokens, POPUP_TRAIT_TOKEN, () => createNativeWebTraitUnit({
304
+ provide: POPUP_TRAIT_TOKEN,
305
+ value: options.popup,
306
+ createValue: (time) => createPopupForNativeWeb({
307
+ time,
308
+ ...options.popupForNativeWebCreateOptions,
309
+ validators: options.validators
310
+ }),
311
+ deps: [TIME_TRAIT_TOKEN]
312
+ })),
313
+ createProviderIfTokenMissing(externalProviderTokens, PERSISTENT_STORAGE_TRAIT_TOKEN, () => createNativeWebTraitUnit({
314
+ provide: PERSISTENT_STORAGE_TRAIT_TOKEN,
315
+ value: options.persistentStorage,
316
+ createValue: () => createPersistentStorageForNativeWeb({
317
+ ...options.persistentStorageForNativeWebCreateOptions,
318
+ validators: options.validators
319
+ })
320
+ })),
321
+ createProviderIfTokenMissing(externalProviderTokens, SESSION_STORAGE_TRAIT_TOKEN, () => createNativeWebTraitUnit({
322
+ provide: SESSION_STORAGE_TRAIT_TOKEN,
323
+ value: options.sessionStorage,
324
+ createValue: () => createSessionStorageForNativeWeb({
325
+ ...options.sessionStorageForNativeWebCreateOptions,
326
+ validators: options.validators
327
+ })
328
+ }))
329
+ ].filter(notMissingProvider);
330
+ }
331
+ function createNativeWebTraitUnit(options) {
332
+ if (options.value !== void 0) return {
333
+ provide: options.provide,
334
+ useValue: options.value
335
+ };
336
+ return {
337
+ provide: options.provide,
338
+ useFactory: options.createValue,
339
+ deps: options.deps
340
+ };
341
+ }
419
342
  //#endregion
420
- //#region src/web/visibility-reconciler.ts
343
+ //#region src/web/sync/cross-tab-sync.ts
421
344
  /**
422
- * Create a visibility reconciler that triggers a callback when the page
423
- * transitions from hidden → visible.
345
+ * Create a cross-tab sync listener for a specific localStorage key.
346
+ *
347
+ * The browser `storage` event fires on ALL tabs EXCEPT the one that made the
348
+ * change. This makes it a natural primitive for cross-tab state notification.
424
349
  *
425
350
  * @example
426
351
  * ```ts
427
- * const reconciler = createVisibilityReconciler({
428
- * onReconcile: () => {
429
- * // Re-check token freshness or sync cross-tab state
430
- * if (controller.snapshot?.tokens.accessTokenExpiresAt) {
431
- * scheduleRefreshIfExpired();
352
+ * const subscription = createCrossTabSync({
353
+ * key: "auth:v1",
354
+ * storageEventTarget: window,
355
+ * }).subscribe({
356
+ * next: ({ newValue }) => {
357
+ * if (newValue) {
358
+ * // Another tab updated auth state — reconcile
359
+ * client.restorePersistedState();
360
+ * } else {
361
+ * // Another tab cleared auth state — log out
362
+ * controller.clearState({ persistPolicy: "skip" });
432
363
  * }
433
364
  * },
434
365
  * });
435
366
  * // Later:
436
- * reconciler.dispose();
367
+ * subscription.unsubscribe();
437
368
  * ```
438
369
  */
439
- function createVisibilityReconciler(options) {
440
- const throttleMs = options.throttleMs ?? 5e3;
441
- const getNow = options.now ?? (() => Date.now());
442
- let lastReconcileAt = 0;
443
- let reconcileCount = 0;
444
- const doc = options.document ?? globalThis.document;
445
- let previousState = doc.visibilityState === "visible" ? VisibilityState.Visible : VisibilityState.Hidden;
446
- const subscription = fromVisibilityChange({
447
- callback: (state) => {
448
- if (state === VisibilityState.Visible && previousState === VisibilityState.Hidden) {
449
- const now = getNow();
450
- if (now - lastReconcileAt >= throttleMs) {
451
- lastReconcileAt = now;
452
- reconcileCount++;
453
- options.onReconcile();
454
- }
455
- }
456
- previousState = state;
457
- },
458
- document: doc
459
- });
460
- return {
461
- dispose() {
462
- subscription.unsubscribe();
463
- },
464
- get reconcileCount() {
465
- return reconcileCount;
466
- }
467
- };
370
+ function createCrossTabSync(options) {
371
+ return RxEventStream.fromObservableInput(fromStorageEvent({ storageEventTarget: options.storageEventTarget })).pipe(filter((storageEvent) => storageEvent.key === options.key), map((storageEvent) => ({
372
+ newValue: storageEvent.newValue,
373
+ oldValue: storageEvent.oldValue
374
+ })));
468
375
  }
469
376
  //#endregion
470
- export { ClientEnvironmentPreset, ClientEnvironmentService, FetchTransportRedirectKind, PageResumeTriggerKind, PopupErrorCode, VisibilityState, assertPageLocationCapability, assertPageLocationHistoryCapability, assertResolveEnvironment, assertResolveFromEnvironment, computePopupFeatures, createAbortSignalBridge, createBrowserExtensionBackgroundClientEnvironment, createBrowserPageClientEnvironment, createBrowserWorkerClientEnvironment, createCancellationTokenFromAbortSignal, createCrossTabSync, createFetchTransport, createPageResumeReconciler, createServiceWorkerClientEnvironment, createVisibilityReconciler, createWebClientEnvironment, deriveClientEnvironment, fromAbortSignal, fromStorageEvent, fromVisibilityChange, normalizeAbortError, openPopupWindow, readDefaultPageLocationCapability, readDefaultPageLocationHistoryCapability, readPageLocationCapability, readPageLocationHistoryCapability, relayPopupCallback, requireDefaultPageLocationCapability, requireDefaultPageLocationHistoryCapability, requirePageClientEnvironment, waitForPopupRelay };
377
+ export { NativeWebRouter, PageResumeTriggerKind, WebLegacyRouter, WebNavigationRouter, computePopupFeatures, createCrossTabSync, createEnvironmentForNativeWeb, createPageLifecycleForNativeWeb, createPageResumeSource, createPersistentStorageForNativeWeb, createPopupForNativeWeb, createRouterForNativeWeb, createSessionStorageForNativeWeb, createStorageForNativeWeb, fromStorageEvent };
471
378
 
472
379
  //# sourceMappingURL=index.mjs.map