@securitydept/client 0.2.0-beta.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.
- package/dist/auth-coordination/index.d.mts +198 -0
- package/dist/auth-coordination/index.mjs +107 -0
- package/dist/auth-coordination/index.mjs.map +1 -0
- package/dist/fetch-transport-CMG4tfDu.d.mts +368 -0
- package/dist/fetch-transport-DtnSRWAH.mjs +631 -0
- package/dist/fetch-transport-DtnSRWAH.mjs.map +1 -0
- package/dist/index-XkwmYaJF.d.mts +39 -0
- package/dist/index.d.mts +426 -0
- package/dist/index.mjs +661 -0
- package/dist/index.mjs.map +1 -0
- package/dist/persistence/index.d.mts +3 -0
- package/dist/persistence/index.mjs +2 -0
- package/dist/persistence/web/index.d.mts +16 -0
- package/dist/persistence/web/index.mjs +51 -0
- package/dist/persistence/web/index.mjs.map +1 -0
- package/dist/persistence-Db2CIFbx.mjs +108 -0
- package/dist/persistence-Db2CIFbx.mjs.map +1 -0
- package/dist/planner-host-CcEpcHws.mjs +195 -0
- package/dist/planner-host-CcEpcHws.mjs.map +1 -0
- package/dist/planner-host-DUZCkLj3.d.mts +190 -0
- package/dist/types-DKluXsWS.d.mts +52 -0
- package/dist/web/index.d.mts +329 -0
- package/dist/web/index.mjs +292 -0
- package/dist/web/index.mjs.map +1 -0
- package/dist/web-router/index.d.mts +259 -0
- package/dist/web-router/index.mjs +395 -0
- package/dist/web-router/index.mjs.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,292 @@
|
|
|
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
|
|
3
|
+
/**
|
|
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
|
+
* ```
|
|
26
|
+
*/
|
|
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
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/web/popup.ts
|
|
52
|
+
/** Stable error codes for popup-related failures. */
|
|
53
|
+
const PopupErrorCode = {
|
|
54
|
+
Blocked: "popup.blocked",
|
|
55
|
+
Closed: "popup.closed_by_user",
|
|
56
|
+
Timeout: "popup.relay_timeout",
|
|
57
|
+
RelayError: "popup.relay_error",
|
|
58
|
+
Internal: "popup.internal"
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Compute the `features` string for `window.open()`, centered on screen.
|
|
62
|
+
*/
|
|
63
|
+
function computePopupFeatures(options = {}) {
|
|
64
|
+
const width = options.width ?? 500;
|
|
65
|
+
const height = options.height ?? 600;
|
|
66
|
+
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`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Open a popup window to the given URL.
|
|
70
|
+
*
|
|
71
|
+
* Throws a `ClientError` with code `popup.blocked` if the browser
|
|
72
|
+
* blocks the popup.
|
|
73
|
+
*/
|
|
74
|
+
function openPopupWindow(url, options) {
|
|
75
|
+
const features = computePopupFeatures(options);
|
|
76
|
+
const target = options?.target ?? "_blank";
|
|
77
|
+
const win = window.open(url, target, features);
|
|
78
|
+
if (!win || win.closed) throw new ClientError({
|
|
79
|
+
kind: ClientErrorKind.Authorization,
|
|
80
|
+
code: PopupErrorCode.Blocked,
|
|
81
|
+
message: "Popup window was blocked by the browser. Please allow popups for this site.",
|
|
82
|
+
recovery: UserRecovery.Retry,
|
|
83
|
+
source: "popup"
|
|
84
|
+
});
|
|
85
|
+
return { window: win };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Wait for a relay message from a popup callback page.
|
|
89
|
+
*
|
|
90
|
+
* The popup callback page should call `relayPopupCallback()` to post
|
|
91
|
+
* the callback URL back to the opener.
|
|
92
|
+
*
|
|
93
|
+
* Resolves with the callback payload (URL or fragment string).
|
|
94
|
+
* Rejects with a `ClientError` if:
|
|
95
|
+
* - the popup is closed without relaying (popup.closed_by_user)
|
|
96
|
+
* - the timeout expires (popup.relay_timeout)
|
|
97
|
+
* - the relay message contains an error (popup.relay_error)
|
|
98
|
+
*/
|
|
99
|
+
function waitForPopupRelay(options) {
|
|
100
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
101
|
+
const expectedOrigin = options.expectedOrigin ?? window.location.origin;
|
|
102
|
+
const pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
let settled = false;
|
|
105
|
+
let timeoutId;
|
|
106
|
+
let pollId;
|
|
107
|
+
function cleanup() {
|
|
108
|
+
settled = true;
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
clearInterval(pollId);
|
|
111
|
+
window.removeEventListener("message", onMessage);
|
|
112
|
+
}
|
|
113
|
+
function onMessage(event) {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
if (event.origin !== expectedOrigin) return;
|
|
116
|
+
const data = event.data;
|
|
117
|
+
if (!data || data.type !== "securitydept:popup_callback") return;
|
|
118
|
+
cleanup();
|
|
119
|
+
if (data.error) {
|
|
120
|
+
reject(new ClientError({
|
|
121
|
+
kind: ClientErrorKind.Protocol,
|
|
122
|
+
code: PopupErrorCode.RelayError,
|
|
123
|
+
message: `Popup callback relay error: ${data.error}`,
|
|
124
|
+
source: "popup"
|
|
125
|
+
}));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
resolve(data.payload);
|
|
129
|
+
}
|
|
130
|
+
window.addEventListener("message", onMessage);
|
|
131
|
+
pollId = setInterval(() => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
if (options.popup.window.closed) {
|
|
134
|
+
cleanup();
|
|
135
|
+
reject(new ClientError({
|
|
136
|
+
kind: ClientErrorKind.Authorization,
|
|
137
|
+
code: PopupErrorCode.Closed,
|
|
138
|
+
message: "Popup window was closed before completing the login flow.",
|
|
139
|
+
recovery: UserRecovery.RestartFlow,
|
|
140
|
+
source: "popup"
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
}, pollIntervalMs);
|
|
144
|
+
timeoutId = setTimeout(() => {
|
|
145
|
+
if (settled) return;
|
|
146
|
+
cleanup();
|
|
147
|
+
try {
|
|
148
|
+
options.popup.window.close();
|
|
149
|
+
} catch {}
|
|
150
|
+
reject(new ClientError({
|
|
151
|
+
kind: ClientErrorKind.Timeout,
|
|
152
|
+
code: PopupErrorCode.Timeout,
|
|
153
|
+
message: `Popup login timed out after ${timeoutMs}ms.`,
|
|
154
|
+
recovery: UserRecovery.Retry,
|
|
155
|
+
source: "popup"
|
|
156
|
+
}));
|
|
157
|
+
}, timeoutMs);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Relay the popup callback result back to the opener window.
|
|
162
|
+
*
|
|
163
|
+
* Call this function from the popup callback page to send the callback
|
|
164
|
+
* URL/fragment back to the parent window that initiated the popup login.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```html
|
|
168
|
+
* <script type="module">
|
|
169
|
+
* import { relayPopupCallback } from "@securitydept/client/web";
|
|
170
|
+
* relayPopupCallback({ payload: window.location.href });
|
|
171
|
+
* <\/script>
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
function relayPopupCallback(options = {}) {
|
|
175
|
+
const opener = window.opener;
|
|
176
|
+
if (!opener) return;
|
|
177
|
+
const targetOrigin = options.targetOrigin ?? window.location.origin;
|
|
178
|
+
const message = {
|
|
179
|
+
type: "securitydept:popup_callback",
|
|
180
|
+
payload: options.payload ?? window.location.href,
|
|
181
|
+
error: options.error
|
|
182
|
+
};
|
|
183
|
+
opener.postMessage(message, targetOrigin);
|
|
184
|
+
if (options.closeAfterRelay !== false) window.close();
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/web/runtime.ts
|
|
188
|
+
/**
|
|
189
|
+
* Create a `ClientRuntime` for browser/Web-capable environments.
|
|
190
|
+
*/
|
|
191
|
+
function createWebRuntime(overrides = {}) {
|
|
192
|
+
const { fetchTransport, transport, ...runtime } = overrides;
|
|
193
|
+
return createRuntime({
|
|
194
|
+
...runtime,
|
|
195
|
+
transport: transport ?? createFetchTransport(fetchTransport)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/web/visibility.ts
|
|
200
|
+
/** Visibility state snapshot passed to the callback. */
|
|
201
|
+
const VisibilityState = {
|
|
202
|
+
Visible: "visible",
|
|
203
|
+
Hidden: "hidden"
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Subscribe to browser document visibility changes.
|
|
207
|
+
*
|
|
208
|
+
* Built on {@link fromEventPattern} so it follows the same Subscription
|
|
209
|
+
* contract. Returns a {@link Subscription} handle for cleanup.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* const sub = fromVisibilityChange({
|
|
214
|
+
* callback: (state) => {
|
|
215
|
+
* if (state === VisibilityState.Visible) {
|
|
216
|
+
* scheduleRefresh();
|
|
217
|
+
* }
|
|
218
|
+
* },
|
|
219
|
+
* });
|
|
220
|
+
* // Cleanup:
|
|
221
|
+
* sub.unsubscribe();
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
function fromVisibilityChange(options) {
|
|
225
|
+
const doc = options.document ?? globalThis.document;
|
|
226
|
+
return fromEventPattern({
|
|
227
|
+
addHandler: (handler) => {
|
|
228
|
+
doc.addEventListener("visibilitychange", handler);
|
|
229
|
+
},
|
|
230
|
+
removeHandler: (handler) => {
|
|
231
|
+
doc.removeEventListener("visibilitychange", handler);
|
|
232
|
+
},
|
|
233
|
+
callback: () => {
|
|
234
|
+
const state = doc.visibilityState === "visible" ? VisibilityState.Visible : VisibilityState.Hidden;
|
|
235
|
+
options.callback(state);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/web/visibility-reconciler.ts
|
|
241
|
+
/**
|
|
242
|
+
* Create a visibility reconciler that triggers a callback when the page
|
|
243
|
+
* transitions from hidden → visible.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* const reconciler = createVisibilityReconciler({
|
|
248
|
+
* onReconcile: () => {
|
|
249
|
+
* // Re-check token freshness or sync cross-tab state
|
|
250
|
+
* if (controller.snapshot?.tokens.accessTokenExpiresAt) {
|
|
251
|
+
* scheduleRefreshIfExpired();
|
|
252
|
+
* }
|
|
253
|
+
* },
|
|
254
|
+
* });
|
|
255
|
+
* // Later:
|
|
256
|
+
* reconciler.dispose();
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
function createVisibilityReconciler(options) {
|
|
260
|
+
const throttleMs = options.throttleMs ?? 5e3;
|
|
261
|
+
const getNow = options.now ?? (() => Date.now());
|
|
262
|
+
let lastReconcileAt = 0;
|
|
263
|
+
let reconcileCount = 0;
|
|
264
|
+
const doc = options.document ?? globalThis.document;
|
|
265
|
+
let previousState = doc.visibilityState === "visible" ? VisibilityState.Visible : VisibilityState.Hidden;
|
|
266
|
+
const subscription = fromVisibilityChange({
|
|
267
|
+
callback: (state) => {
|
|
268
|
+
if (state === VisibilityState.Visible && previousState === VisibilityState.Hidden) {
|
|
269
|
+
const now = getNow();
|
|
270
|
+
if (now - lastReconcileAt >= throttleMs) {
|
|
271
|
+
lastReconcileAt = now;
|
|
272
|
+
reconcileCount++;
|
|
273
|
+
options.onReconcile();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
previousState = state;
|
|
277
|
+
},
|
|
278
|
+
document: doc
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
dispose() {
|
|
282
|
+
subscription.unsubscribe();
|
|
283
|
+
},
|
|
284
|
+
get reconcileCount() {
|
|
285
|
+
return reconcileCount;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
export { FetchTransportRedirectKind, PopupErrorCode, VisibilityState, computePopupFeatures, createAbortSignalBridge, createCancellationTokenFromAbortSignal, createCrossTabSync, createFetchTransport, createVisibilityReconciler, createWebRuntime, fromAbortSignal, fromStorageEvent, fromVisibilityChange, normalizeAbortError, openPopupWindow, relayPopupCallback, waitForPopupRelay };
|
|
291
|
+
|
|
292
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/web/cross-tab-sync.ts","../../src/web/popup.ts","../../src/web/runtime.ts","../../src/web/visibility.ts","../../src/web/visibility-reconciler.ts"],"sourcesContent":["import { fromStorageEvent } from \"./input-sources\";\n\n// Cross-tab auth state sync — minimal baseline using storage events\n//\n// Provides a minimal, composable cross-tab sync mechanism for auth state.\n// When another tab writes to the auth persistence key (via localStorage),\n// this listener detects the change and notifies the current tab so it can\n// reconcile its in-memory state.\n//\n// This is intentionally thin — it uses the browser `storage` event which\n// fires on OTHER tabs when localStorage is modified. The reconciliation\n// callback is the adopter's responsibility (e.g. re-read from persistence,\n// compare with in-memory state, and update).\n//\n// Architecture boundary:\n// - This module does NOT own the persistence store.\n// - It only listens for cross-tab mutations and notifies via callback.\n// - The adopter wires this to their AuthMaterialController.\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Callback invoked when a cross-tab storage change is detected for the watched key. */\nexport type CrossTabSyncCallback = (event: {\n\t/** The new value written by the other tab, or null if the key was removed. */\n\tnewValue: string | null;\n\t/** The old value before the other tab's write. */\n\toldValue: string | null;\n}) => void;\n\n/** Options for {@link createCrossTabSync}. */\nexport interface CreateCrossTabSyncOptions {\n\t/** The localStorage key to watch for cross-tab changes. */\n\tkey: string;\n\n\t/** Called when another tab modifies the watched key. */\n\tonSync: CrossTabSyncCallback;\n\n\t/**\n\t * Override the window target (primarily for testing).\n\t * Defaults to `globalThis`.\n\t */\n\ttarget?: {\n\t\taddEventListener(type: string, listener: EventListener): void;\n\t\tremoveEventListener(type: string, listener: EventListener): void;\n\t};\n}\n\n/** Handle returned by {@link createCrossTabSync}. */\nexport interface CrossTabSync {\n\t/** Stop listening for cross-tab storage events. */\n\tdispose(): void;\n\t/** Number of times a sync event has been received. */\n\treadonly syncCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Create a cross-tab sync listener for a specific localStorage key.\n *\n * The browser `storage` event fires on ALL tabs EXCEPT the one that made the\n * change. This makes it a natural primitive for cross-tab state notification.\n *\n * @example\n * ```ts\n * const sync = createCrossTabSync({\n * key: \"auth:v1\",\n * onSync: ({ newValue }) => {\n * if (newValue) {\n * // Another tab updated auth state — reconcile\n * controller.restoreFromPersistence();\n * } else {\n * // Another tab cleared auth state — log out\n * controller.clearState({ clearPersisted: false });\n * }\n * },\n * });\n * // Later:\n * sync.dispose();\n * ```\n */\nexport function createCrossTabSync(\n\toptions: CreateCrossTabSyncOptions,\n): CrossTabSync {\n\tconst target = options.target ?? globalThis;\n\tlet syncCount = 0;\n\tconst subscription = fromStorageEvent({\n\t\ttarget,\n\t\tcallback: (storageEvent) => {\n\t\t\t// Only react to changes on our watched key.\n\t\t\tif (storageEvent.key !== options.key) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsyncCount++;\n\t\t\toptions.onSync({\n\t\t\t\tnewValue: storageEvent.newValue,\n\t\t\t\toldValue: storageEvent.oldValue,\n\t\t\t});\n\t\t},\n\t});\n\n\treturn {\n\t\tdispose() {\n\t\t\tsubscription.unsubscribe();\n\t\t},\n\t\tget syncCount() {\n\t\t\treturn syncCount;\n\t\t},\n\t};\n}\n","// Popup shared infrastructure — browser-only helpers\n//\n// Provides cross-mode popup window management for login flows.\n// Owned by @securitydept/client/web, consumed by backend-oidc-mode\n// and frontend-oidc-mode popup login entries.\n\nimport { ClientError, ClientErrorKind, UserRecovery } from \"../errors/index\";\n\n// ---------------------------------------------------------------------------\n// Error codes (stable contract)\n// ---------------------------------------------------------------------------\n\n/** Stable error codes for popup-related failures. */\nexport const PopupErrorCode = {\n\t/** The browser blocked the popup window from opening. */\n\tBlocked: \"popup.blocked\",\n\t/** The popup was closed by the user before completing the flow. */\n\tClosed: \"popup.closed_by_user\",\n\t/** The relay message was not received within the timeout. */\n\tTimeout: \"popup.relay_timeout\",\n\t/** The relay message contained an error from the callback page. */\n\tRelayError: \"popup.relay_error\",\n\t/** An unexpected error occurred during popup lifecycle. */\n\tInternal: \"popup.internal\",\n} as const;\n\nexport type PopupErrorCode =\n\t(typeof PopupErrorCode)[keyof typeof PopupErrorCode];\n\n// ---------------------------------------------------------------------------\n// Popup features\n// ---------------------------------------------------------------------------\n\n/**\n * Options for computing the popup window features string.\n */\nexport interface PopupFeaturesOptions {\n\t/** Width in pixels (default: 500). */\n\twidth?: number;\n\t/** Height in pixels (default: 600). */\n\theight?: number;\n}\n\n/**\n * Compute the `features` string for `window.open()`, centered on screen.\n */\nexport function computePopupFeatures(\n\toptions: PopupFeaturesOptions = {},\n): string {\n\tconst width = options.width ?? 500;\n\tconst height = options.height ?? 600;\n\tconst left = Math.max(\n\t\t0,\n\t\tMath.round(window.screenX + (window.innerWidth - width) / 2),\n\t);\n\tconst top = Math.max(\n\t\t0,\n\t\tMath.round(window.screenY + (window.innerHeight - height) / 2),\n\t);\n\n\treturn `width=${width},height=${height},left=${left},top=${top},popup=yes,toolbar=no,menubar=no`;\n}\n\n// ---------------------------------------------------------------------------\n// Popup open + blocked detection\n// ---------------------------------------------------------------------------\n\n/**\n * Result of opening a popup window.\n */\nexport interface PopupWindowHandle {\n\t/** The opened window reference. */\n\twindow: Window;\n}\n\n/**\n * Open a popup window to the given URL.\n *\n * Throws a `ClientError` with code `popup.blocked` if the browser\n * blocks the popup.\n */\nexport function openPopupWindow(\n\turl: string,\n\toptions?: PopupFeaturesOptions & { target?: string },\n): PopupWindowHandle {\n\tconst features = computePopupFeatures(options);\n\tconst target = options?.target ?? \"_blank\";\n\n\tconst win = window.open(url, target, features);\n\n\tif (!win || win.closed) {\n\t\tthrow new ClientError({\n\t\t\tkind: ClientErrorKind.Authorization,\n\t\t\tcode: PopupErrorCode.Blocked,\n\t\t\tmessage:\n\t\t\t\t\"Popup window was blocked by the browser. Please allow popups for this site.\",\n\t\t\trecovery: UserRecovery.Retry,\n\t\t\tsource: \"popup\",\n\t\t});\n\t}\n\n\treturn { window: win };\n}\n\n// ---------------------------------------------------------------------------\n// PostMessage relay\n// ---------------------------------------------------------------------------\n\n/**\n * Expected shape of a relay message from the popup callback page.\n */\nexport interface PopupRelayMessage {\n\t/** Discriminator for the message type. */\n\ttype: \"securitydept:popup_callback\";\n\t/** The callback URL or fragment received by the popup. */\n\tpayload: string;\n\t/** Optional error from the callback page. */\n\terror?: string;\n}\n\n/**\n * Options for waiting for a popup relay message.\n */\nexport interface WaitForPopupRelayOptions {\n\t/** The popup window handle to monitor. */\n\tpopup: PopupWindowHandle;\n\t/** Maximum time to wait in milliseconds (default: 120000 = 2 minutes). */\n\ttimeoutMs?: number;\n\t/** Expected origin for the relay message (default: current origin). */\n\texpectedOrigin?: string;\n\t/** Poll interval for checking if the popup is closed (default: 500ms). */\n\tpollIntervalMs?: number;\n}\n\n/**\n * Wait for a relay message from a popup callback page.\n *\n * The popup callback page should call `relayPopupCallback()` to post\n * the callback URL back to the opener.\n *\n * Resolves with the callback payload (URL or fragment string).\n * Rejects with a `ClientError` if:\n * - the popup is closed without relaying (popup.closed_by_user)\n * - the timeout expires (popup.relay_timeout)\n * - the relay message contains an error (popup.relay_error)\n */\nexport function waitForPopupRelay(\n\toptions: WaitForPopupRelayOptions,\n): Promise<string> {\n\tconst timeoutMs = options.timeoutMs ?? 120_000;\n\tconst expectedOrigin = options.expectedOrigin ?? window.location.origin;\n\tconst pollIntervalMs = options.pollIntervalMs ?? 500;\n\n\treturn new Promise<string>((resolve, reject) => {\n\t\tlet settled = false;\n\t\tlet timeoutId: ReturnType<typeof setTimeout>;\n\t\tlet pollId: ReturnType<typeof setInterval>;\n\n\t\tfunction cleanup() {\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timeoutId);\n\t\t\tclearInterval(pollId);\n\t\t\twindow.removeEventListener(\"message\", onMessage);\n\t\t}\n\n\t\tfunction onMessage(event: MessageEvent) {\n\t\t\tif (settled) return;\n\t\t\tif (event.origin !== expectedOrigin) return;\n\n\t\t\tconst data = event.data as PopupRelayMessage | undefined;\n\t\t\tif (!data || data.type !== \"securitydept:popup_callback\") return;\n\n\t\t\tcleanup();\n\n\t\t\tif (data.error) {\n\t\t\t\treject(\n\t\t\t\t\tnew ClientError({\n\t\t\t\t\t\tkind: ClientErrorKind.Protocol,\n\t\t\t\t\t\tcode: PopupErrorCode.RelayError,\n\t\t\t\t\t\tmessage: `Popup callback relay error: ${data.error}`,\n\t\t\t\t\t\tsource: \"popup\",\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tresolve(data.payload);\n\t\t}\n\n\t\t// Listen for relay messages.\n\t\twindow.addEventListener(\"message\", onMessage);\n\n\t\t// Poll for popup closure.\n\t\tpollId = setInterval(() => {\n\t\t\tif (settled) return;\n\t\t\tif (options.popup.window.closed) {\n\t\t\t\tcleanup();\n\t\t\t\treject(\n\t\t\t\t\tnew ClientError({\n\t\t\t\t\t\tkind: ClientErrorKind.Authorization,\n\t\t\t\t\t\tcode: PopupErrorCode.Closed,\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\"Popup window was closed before completing the login flow.\",\n\t\t\t\t\t\trecovery: UserRecovery.RestartFlow,\n\t\t\t\t\t\tsource: \"popup\",\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}, pollIntervalMs);\n\n\t\t// Timeout.\n\t\ttimeoutId = setTimeout(() => {\n\t\t\tif (settled) return;\n\t\t\tcleanup();\n\t\t\ttry {\n\t\t\t\toptions.popup.window.close();\n\t\t\t} catch {\n\t\t\t\t// Best-effort close.\n\t\t\t}\n\t\t\treject(\n\t\t\t\tnew ClientError({\n\t\t\t\t\tkind: ClientErrorKind.Timeout,\n\t\t\t\t\tcode: PopupErrorCode.Timeout,\n\t\t\t\t\tmessage: `Popup login timed out after ${timeoutMs}ms.`,\n\t\t\t\t\trecovery: UserRecovery.Retry,\n\t\t\t\t\tsource: \"popup\",\n\t\t\t\t}),\n\t\t\t);\n\t\t}, timeoutMs);\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// Popup callback relay (for use in popup callback pages)\n// ---------------------------------------------------------------------------\n\n/**\n * Options for relaying the callback from the popup window back to the opener.\n */\nexport interface RelayPopupCallbackOptions {\n\t/** The callback payload (URL or fragment) to relay. */\n\tpayload?: string;\n\t/** An error message to relay instead of a successful payload. */\n\terror?: string;\n\t/** Whether to close the popup after relaying (default: true). */\n\tcloseAfterRelay?: boolean;\n\t/** Target origin for the postMessage (default: current origin). */\n\ttargetOrigin?: string;\n}\n\n/**\n * Relay the popup callback result back to the opener window.\n *\n * Call this function from the popup callback page to send the callback\n * URL/fragment back to the parent window that initiated the popup login.\n *\n * @example\n * ```html\n * <script type=\"module\">\n * import { relayPopupCallback } from \"@securitydept/client/web\";\n * relayPopupCallback({ payload: window.location.href });\n * </script>\n * ```\n */\nexport function relayPopupCallback(\n\toptions: RelayPopupCallbackOptions = {},\n): void {\n\tconst opener = window.opener;\n\tif (!opener) {\n\t\treturn;\n\t}\n\n\tconst targetOrigin = options.targetOrigin ?? window.location.origin;\n\tconst message: PopupRelayMessage = {\n\t\ttype: \"securitydept:popup_callback\",\n\t\tpayload: options.payload ?? window.location.href,\n\t\terror: options.error,\n\t};\n\n\topener.postMessage(message, targetOrigin);\n\n\tif (options.closeAfterRelay !== false) {\n\t\twindow.close();\n\t}\n}\n","import { createRuntime } from \"../runtime/create-runtime\";\nimport type { ClientRuntime } from \"../runtime/types\";\nimport {\n\tcreateFetchTransport,\n\ttype FetchTransportOptions,\n} from \"../transport/fetch-transport\";\n\nexport interface CreateWebRuntimeOptions\n\textends Partial<Omit<ClientRuntime, \"transport\">> {\n\ttransport?: ClientRuntime[\"transport\"];\n\tfetchTransport?: FetchTransportOptions;\n}\n\n/**\n * Create a `ClientRuntime` for browser/Web-capable environments.\n */\nexport function createWebRuntime(\n\toverrides: CreateWebRuntimeOptions = {},\n): ClientRuntime {\n\tconst { fetchTransport, transport, ...runtime } = overrides;\n\treturn createRuntime({\n\t\t...runtime,\n\t\ttransport: transport ?? createFetchTransport(fetchTransport),\n\t});\n}\n","// Browser visibility change adapter — built on fromEventPattern.\n//\n// Adapts the browser `visibilitychange` event into the SDK's Subscription\n// model, built on the foundation fromEventPattern helper.\n\nimport { fromEventPattern, type Subscription } from \"../scheduling/helpers\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Visibility state snapshot passed to the callback. */\nexport const VisibilityState = {\n\tVisible: \"visible\",\n\tHidden: \"hidden\",\n} as const;\n\nexport type VisibilityState =\n\t(typeof VisibilityState)[keyof typeof VisibilityState];\n\n/** Options for {@link fromVisibilityChange}. */\nexport interface FromVisibilityChangeOptions {\n\t/**\n\t * Callback invoked when the document visibility state changes.\n\t *\n\t * The callback receives the new visibility state (\"visible\" or \"hidden\").\n\t */\n\tcallback: (state: VisibilityState) => void;\n\t/**\n\t * Override the document target (primarily for testing).\n\t * Defaults to `globalThis.document`.\n\t */\n\tdocument?: Pick<\n\t\tDocument,\n\t\t\"addEventListener\" | \"removeEventListener\" | \"visibilityState\"\n\t>;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to browser document visibility changes.\n *\n * Built on {@link fromEventPattern} so it follows the same Subscription\n * contract. Returns a {@link Subscription} handle for cleanup.\n *\n * @example\n * ```ts\n * const sub = fromVisibilityChange({\n * callback: (state) => {\n * if (state === VisibilityState.Visible) {\n * scheduleRefresh();\n * }\n * },\n * });\n * // Cleanup:\n * sub.unsubscribe();\n * ```\n */\nexport function fromVisibilityChange(\n\toptions: FromVisibilityChangeOptions,\n): Subscription {\n\tconst doc = options.document ?? globalThis.document;\n\n\treturn fromEventPattern<Event>({\n\t\taddHandler: (handler) => {\n\t\t\tdoc.addEventListener(\"visibilitychange\", handler);\n\t\t},\n\t\tremoveHandler: (handler) => {\n\t\t\tdoc.removeEventListener(\"visibilitychange\", handler);\n\t\t},\n\t\tcallback: () => {\n\t\t\tconst state: VisibilityState =\n\t\t\t\tdoc.visibilityState === \"visible\"\n\t\t\t\t\t? VisibilityState.Visible\n\t\t\t\t\t: VisibilityState.Hidden;\n\t\t\toptions.callback(state);\n\t\t},\n\t});\n}\n","// Visibility reconciler — cross-tab / visibility lifecycle hardening baseline\n//\n// Provides a minimal, composable integration between the browser visibility\n// change adapter and the auth material lifecycle. When a tab transitions from\n// hidden → visible, the reconciler triggers a user-supplied reconciliation\n// callback (e.g. check token freshness, re-validate with server, or sync\n// cross-tab state).\n//\n// This is the SDK-side baseline that real projects (e.g. outposts) can wire\n// directly to their mode client's refresh or state-check logic.\n\nimport type { Subscription } from \"../scheduling/helpers\";\nimport type { FromVisibilityChangeOptions } from \"./visibility\";\nimport { fromVisibilityChange, VisibilityState } from \"./visibility\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Callback invoked when the page becomes visible and reconciliation is needed. */\nexport type ReconcileCallback = () => void | Promise<void>;\n\n/** Options for {@link createVisibilityReconciler}. */\nexport interface CreateVisibilityReconcilerOptions {\n\t/**\n\t * Called when the document transitions from hidden → visible.\n\t * The reconciler only fires on the hidden→visible edge; repeated\n\t * visible→visible events are suppressed.\n\t */\n\tonReconcile: ReconcileCallback;\n\n\t/**\n\t * Minimum interval (ms) between reconciliation triggers.\n\t * Prevents rapid-fire reconciliation when the user toggles tabs quickly.\n\t * Defaults to 5000ms (5 seconds).\n\t */\n\tthrottleMs?: number;\n\n\t/**\n\t * Override the document target (primarily for testing).\n\t * Defaults to `globalThis.document`.\n\t */\n\tdocument?: FromVisibilityChangeOptions[\"document\"];\n\n\t/**\n\t * Clock override for testing.\n\t */\n\tnow?: () => number;\n}\n\n/** Handle returned by {@link createVisibilityReconciler}. */\nexport interface VisibilityReconciler {\n\t/** Stop listening for visibility changes. */\n\tdispose(): void;\n\t/** Number of times reconciliation has been triggered. */\n\treadonly reconcileCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Create a visibility reconciler that triggers a callback when the page\n * transitions from hidden → visible.\n *\n * @example\n * ```ts\n * const reconciler = createVisibilityReconciler({\n * onReconcile: () => {\n * // Re-check token freshness or sync cross-tab state\n * if (controller.snapshot?.tokens.accessTokenExpiresAt) {\n * scheduleRefreshIfExpired();\n * }\n * },\n * });\n * // Later:\n * reconciler.dispose();\n * ```\n */\nexport function createVisibilityReconciler(\n\toptions: CreateVisibilityReconcilerOptions,\n): VisibilityReconciler {\n\tconst throttleMs = options.throttleMs ?? 5000;\n\tconst getNow = options.now ?? (() => Date.now());\n\n\tlet lastReconcileAt = 0;\n\tlet reconcileCount = 0;\n\t// Initialize from the real document state so that tabs opened in background\n\t// (initial hidden) will correctly trigger on their first visible transition.\n\tconst doc = options.document ?? globalThis.document;\n\tlet previousState: VisibilityState =\n\t\tdoc.visibilityState === \"visible\"\n\t\t\t? VisibilityState.Visible\n\t\t\t: VisibilityState.Hidden;\n\n\tconst subscription: Subscription = fromVisibilityChange({\n\t\tcallback: (state) => {\n\t\t\t// Only fire on the hidden → visible edge.\n\t\t\tif (\n\t\t\t\tstate === VisibilityState.Visible &&\n\t\t\t\tpreviousState === VisibilityState.Hidden\n\t\t\t) {\n\t\t\t\tconst now = getNow();\n\t\t\t\tif (now - lastReconcileAt >= throttleMs) {\n\t\t\t\t\tlastReconcileAt = now;\n\t\t\t\t\treconcileCount++;\n\t\t\t\t\toptions.onReconcile();\n\t\t\t\t}\n\t\t\t}\n\t\t\tpreviousState = state;\n\t\t},\n\t\tdocument: doc,\n\t});\n\n\treturn {\n\t\tdispose() {\n\t\t\tsubscription.unsubscribe();\n\t\t},\n\t\tget reconcileCount() {\n\t\t\treturn reconcileCount;\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { i as PlannerHost, s as RequirementsClientSetComposition, t as AuthGuardClientOption } from "../planner-host-DUZCkLj3.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/web-router/navigation-adapter.d.ts
|
|
4
|
+
declare const NavigationAdapterKind: {
|
|
5
|
+
readonly NavigationApi: "navigation-api";
|
|
6
|
+
readonly History: "history";
|
|
7
|
+
};
|
|
8
|
+
type NavigationAdapterKind = (typeof NavigationAdapterKind)[keyof typeof NavigationAdapterKind];
|
|
9
|
+
declare const NavigationCause: {
|
|
10
|
+
readonly Initial: "initial";
|
|
11
|
+
readonly Push: "push";
|
|
12
|
+
readonly Replace: "replace";
|
|
13
|
+
readonly Traverse: "traverse";
|
|
14
|
+
};
|
|
15
|
+
type NavigationCause = (typeof NavigationCause)[keyof typeof NavigationCause];
|
|
16
|
+
interface NavigationIntent {
|
|
17
|
+
readonly url: URL;
|
|
18
|
+
readonly cause: NavigationCause;
|
|
19
|
+
readonly replace: boolean;
|
|
20
|
+
readonly state: unknown;
|
|
21
|
+
/** Abort this navigation before it commits. */
|
|
22
|
+
preventDefault(): void;
|
|
23
|
+
/** Cancel the current intent and navigate to the given URL instead. */
|
|
24
|
+
redirect(url: string | URL, options?: {
|
|
25
|
+
replace?: boolean;
|
|
26
|
+
}): void;
|
|
27
|
+
}
|
|
28
|
+
interface NavigationCommit {
|
|
29
|
+
readonly url: URL;
|
|
30
|
+
}
|
|
31
|
+
interface NavigateOptions {
|
|
32
|
+
/** When true, replace the current history entry instead of pushing. */
|
|
33
|
+
replace?: boolean;
|
|
34
|
+
/** Serializable state to attach to the history entry. */
|
|
35
|
+
state?: unknown;
|
|
36
|
+
/**
|
|
37
|
+
* Arbitrary per-navigation metadata. Passed through to `NavigateEvent.info`
|
|
38
|
+
* when the Navigation API adapter is active; ignored by the History
|
|
39
|
+
* adapter (no wire support).
|
|
40
|
+
*/
|
|
41
|
+
info?: unknown;
|
|
42
|
+
}
|
|
43
|
+
interface NavigationAdapter {
|
|
44
|
+
readonly kind: NavigationAdapterKind;
|
|
45
|
+
currentUrl(): URL;
|
|
46
|
+
navigate(url: string | URL, options?: NavigateOptions): Promise<void>;
|
|
47
|
+
back(): void;
|
|
48
|
+
forward(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Called *before* a navigation commits, so listeners can cancel or
|
|
51
|
+
* redirect. Both adapters forward synchronous and asynchronous handlers
|
|
52
|
+
* (adapters await the intent being fully processed before committing).
|
|
53
|
+
*/
|
|
54
|
+
onBeforeNavigate(listener: (intent: NavigationIntent) => void | Promise<void>): () => void;
|
|
55
|
+
/** Called *after* a navigation commits. */
|
|
56
|
+
onNavigate(listener: (commit: NavigationCommit) => void): () => void;
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
|
59
|
+
declare function createNavigationApiAdapter(): NavigationAdapter;
|
|
60
|
+
interface CreateHistoryAdapterOptions {
|
|
61
|
+
/**
|
|
62
|
+
* When true (default), the adapter captures clicks on in-document
|
|
63
|
+
* `<a href>` elements and routes them through `navigate()` so guards
|
|
64
|
+
* can intercept. Disable to handle anchor navigation manually.
|
|
65
|
+
*/
|
|
66
|
+
captureAnchorClicks?: boolean;
|
|
67
|
+
}
|
|
68
|
+
declare function createHistoryAdapter(options?: CreateHistoryAdapterOptions): NavigationAdapter;
|
|
69
|
+
interface CreateNavigationAdapterOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Force a specific adapter kind. Useful for tests that need to exercise
|
|
72
|
+
* the fallback path in a Chromium environment that ships the Navigation
|
|
73
|
+
* API.
|
|
74
|
+
* @default "auto"
|
|
75
|
+
*/
|
|
76
|
+
prefer?: "auto" | NavigationAdapterKind;
|
|
77
|
+
/**
|
|
78
|
+
* Options forwarded to {@link createHistoryAdapter} when the history
|
|
79
|
+
* adapter is selected.
|
|
80
|
+
*/
|
|
81
|
+
history?: CreateHistoryAdapterOptions;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Pick the best available navigation backend. Prefers the Web Navigation
|
|
85
|
+
* API when `window.navigation` is defined; falls back to the History API
|
|
86
|
+
* otherwise.
|
|
87
|
+
*/
|
|
88
|
+
declare function createNavigationAdapter(options?: CreateNavigationAdapterOptions): NavigationAdapter;
|
|
89
|
+
/**
|
|
90
|
+
* Quick capability probe — true when the Web Navigation API is present.
|
|
91
|
+
*/
|
|
92
|
+
declare function isNavigationApiAvailable(): boolean;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/web-router/web-router.d.ts
|
|
95
|
+
interface WebRouteContext {
|
|
96
|
+
/** The URL being navigated to. */
|
|
97
|
+
url: URL;
|
|
98
|
+
/** Intent metadata. Null for programmatic `activate()` calls. */
|
|
99
|
+
intent: NavigationIntent | null;
|
|
100
|
+
}
|
|
101
|
+
type WebRouteMatcher = string | RegExp | ((url: URL) => Record<string, string> | null | undefined);
|
|
102
|
+
interface WebRouteDefinition<TData = unknown> {
|
|
103
|
+
/** Opaque identifier. */
|
|
104
|
+
id: string;
|
|
105
|
+
/**
|
|
106
|
+
* Matcher for this route segment. Required on leaves — parents that
|
|
107
|
+
* only carry metadata for children may omit `match`.
|
|
108
|
+
*
|
|
109
|
+
* A string is treated as a path-with-params pattern
|
|
110
|
+
* (`"/users/:id/profile"` → `{ id: "123" }` for `/users/123/profile`).
|
|
111
|
+
* A RegExp uses `exec(pathname)` and passes named/indexed groups
|
|
112
|
+
* through as params. A function returns the parsed params or null.
|
|
113
|
+
*/
|
|
114
|
+
match?: WebRouteMatcher;
|
|
115
|
+
/**
|
|
116
|
+
* Auth requirements declared at this route segment. The router walks
|
|
117
|
+
* the matched route chain from root to leaf and aggregates every
|
|
118
|
+
* segment's requirements via {@link extractFullRouteRequirements}
|
|
119
|
+
* before asking the planner-host to evaluate.
|
|
120
|
+
*/
|
|
121
|
+
requirements?: readonly AuthGuardClientOption[];
|
|
122
|
+
/**
|
|
123
|
+
* Composition strategy with the parent segment in the matched chain.
|
|
124
|
+
*
|
|
125
|
+
* - `merge` (default): parent requirements are kept, child requirements
|
|
126
|
+
* append/override by `requirementId`.
|
|
127
|
+
* - `inherit`: child declares no additional requirements; parent set is
|
|
128
|
+
* used as-is.
|
|
129
|
+
* - `replace`: parent requirements are discarded entirely; this child
|
|
130
|
+
* contributes the full effective set (use for "public" subtrees of an
|
|
131
|
+
* otherwise protected parent).
|
|
132
|
+
*/
|
|
133
|
+
composition?: RequirementsClientSetComposition;
|
|
134
|
+
/**
|
|
135
|
+
* Nested child routes. When present, `match` on the parent is optional
|
|
136
|
+
* and is not consulted for matching leaves — only the leaves' matchers
|
|
137
|
+
* participate in URL resolution. The chain from root to leaf is what
|
|
138
|
+
* drives requirement aggregation.
|
|
139
|
+
*/
|
|
140
|
+
children?: readonly WebRouteDefinition<TData>[];
|
|
141
|
+
/** Arbitrary data attached to matched routes. */
|
|
142
|
+
data?: TData;
|
|
143
|
+
}
|
|
144
|
+
interface WebRouteMatch<TData = unknown> {
|
|
145
|
+
/** The leaf route that matched. */
|
|
146
|
+
readonly route: WebRouteDefinition<TData>;
|
|
147
|
+
readonly url: URL;
|
|
148
|
+
readonly params: Record<string, string>;
|
|
149
|
+
/**
|
|
150
|
+
* The complete chain from root ancestor → leaf. A flat route without
|
|
151
|
+
* children produces a single-element chain.
|
|
152
|
+
*/
|
|
153
|
+
readonly chain: readonly WebRouteDefinition<TData>[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Sugar for declaring a route segment. Enforces a consistent shape so
|
|
157
|
+
* adopter code reads like the Angular / TanStack helpers without forcing
|
|
158
|
+
* any particular DI or decorator style.
|
|
159
|
+
*/
|
|
160
|
+
declare function defineWebRoute<TData = unknown>(definition: WebRouteDefinition<TData>): WebRouteDefinition<TData>;
|
|
161
|
+
interface FullRouteRequirementsOptions {
|
|
162
|
+
/**
|
|
163
|
+
* Default composition for segments that do not set their own
|
|
164
|
+
* `composition` field. Defaults to `merge` to match the Angular helper
|
|
165
|
+
* and the TanStack adapter.
|
|
166
|
+
*/
|
|
167
|
+
defaultComposition?: RequirementsClientSetComposition;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Aggregate every segment's `requirements` across the matched chain into
|
|
171
|
+
* a single {@link AuthGuardClientOption} list, honouring each segment's
|
|
172
|
+
* composition strategy (`inherit` / `merge` / `replace`).
|
|
173
|
+
*
|
|
174
|
+
* This is the extraction counter-part to the nested `WebRouteDefinition`
|
|
175
|
+
* tree — the router calls it once per matched navigation and forwards the
|
|
176
|
+
* resulting flat list to the planner-host.
|
|
177
|
+
*/
|
|
178
|
+
declare function extractFullRouteRequirements<TData = unknown>(chain: readonly WebRouteDefinition<TData>[], options?: FullRouteRequirementsOptions): AuthGuardClientOption[];
|
|
179
|
+
interface CreateWebRouterOptions<TData = unknown> {
|
|
180
|
+
/**
|
|
181
|
+
* Initial routes. Nested `children` are supported: the router
|
|
182
|
+
* flattens the declared tree into matchable leaves and reconstructs
|
|
183
|
+
* the root→leaf chain on every match for full-route aggregation.
|
|
184
|
+
*
|
|
185
|
+
* More routes can be added via `router.addRoute(...)` at runtime.
|
|
186
|
+
*/
|
|
187
|
+
routes?: readonly WebRouteDefinition<TData>[];
|
|
188
|
+
/** Planner-host for evaluating aggregated route requirements. */
|
|
189
|
+
plannerHost?: PlannerHost;
|
|
190
|
+
/**
|
|
191
|
+
* Options for the underlying navigation adapter. When omitted the
|
|
192
|
+
* router auto-selects the Navigation API if available.
|
|
193
|
+
*/
|
|
194
|
+
navigationAdapter?: CreateNavigationAdapterOptions | NavigationAdapter;
|
|
195
|
+
/**
|
|
196
|
+
* Called after every committed navigation. Prefer `onNavigate` for
|
|
197
|
+
* reactivity; this option exists for convenience.
|
|
198
|
+
*/
|
|
199
|
+
onNavigate?: (commit: NavigationCommit) => void;
|
|
200
|
+
/**
|
|
201
|
+
* Default requirement composition strategy applied when a route
|
|
202
|
+
* segment does not declare its own `composition`. Defaults to `merge`
|
|
203
|
+
* (the Angular / TanStack default).
|
|
204
|
+
*/
|
|
205
|
+
defaultComposition?: RequirementsClientSetComposition;
|
|
206
|
+
}
|
|
207
|
+
interface WebRouter<TData = unknown> {
|
|
208
|
+
readonly adapter: NavigationAdapter;
|
|
209
|
+
/** Current URL (from the underlying adapter). */
|
|
210
|
+
currentUrl(): URL;
|
|
211
|
+
/** Current matched route (or null if no route matches). */
|
|
212
|
+
currentMatch(): WebRouteMatch<TData> | null;
|
|
213
|
+
/** Register an additional root-level route at runtime. */
|
|
214
|
+
addRoute(route: WebRouteDefinition<TData>): () => void;
|
|
215
|
+
/** All currently registered root-level routes. */
|
|
216
|
+
routes(): readonly WebRouteDefinition<TData>[];
|
|
217
|
+
/**
|
|
218
|
+
* Match a URL against the registered routes. Returns the first match in
|
|
219
|
+
* registration order, or `null` if none match. When a match is found,
|
|
220
|
+
* {@link WebRouteMatch.chain} contains the root→leaf route chain so
|
|
221
|
+
* adopters can feed it to {@link extractFullRouteRequirements}.
|
|
222
|
+
*/
|
|
223
|
+
match(url: URL): WebRouteMatch<TData> | null;
|
|
224
|
+
/**
|
|
225
|
+
* Compute the effective aggregated requirements for a given match,
|
|
226
|
+
* honouring every segment's composition strategy.
|
|
227
|
+
*/
|
|
228
|
+
extractRequirements(match: WebRouteMatch<TData>): AuthGuardClientOption[];
|
|
229
|
+
/** Programmatically navigate. */
|
|
230
|
+
navigate(url: string | URL, options?: {
|
|
231
|
+
replace?: boolean;
|
|
232
|
+
state?: unknown;
|
|
233
|
+
}): Promise<void>;
|
|
234
|
+
back(): void;
|
|
235
|
+
forward(): void;
|
|
236
|
+
/** Subscribe to committed navigations. */
|
|
237
|
+
onNavigate(listener: (commit: NavigationCommit) => void): () => void;
|
|
238
|
+
/** Tear down event listeners on the underlying adapter. */
|
|
239
|
+
destroy(): void;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Create a {@link WebRouter} wired to the selected navigation backend and
|
|
243
|
+
* optionally to a planner-host for aggregated route requirements evaluation.
|
|
244
|
+
*
|
|
245
|
+
* The router listens for pre-commit navigation intents, matches the
|
|
246
|
+
* destination URL against the registered route tree, aggregates the full
|
|
247
|
+
* root→leaf chain's requirements (respecting each segment's composition
|
|
248
|
+
* strategy), and asks `plannerHost.evaluate()` to rule on the complete
|
|
249
|
+
* candidate set. Depending on the host's decision it either:
|
|
250
|
+
*
|
|
251
|
+
* - lets the navigation commit (all requirements authenticated);
|
|
252
|
+
* - redirects to the URL returned by `onUnauthenticated` (when a candidate
|
|
253
|
+
* picks a redirect); or
|
|
254
|
+
* - cancels the navigation (when `onUnauthenticated` returns `false`).
|
|
255
|
+
*/
|
|
256
|
+
declare function createWebRouter<TData = unknown>(options?: CreateWebRouterOptions<TData>): WebRouter<TData>;
|
|
257
|
+
//#endregion
|
|
258
|
+
export { type CreateHistoryAdapterOptions, type CreateNavigationAdapterOptions, type CreateWebRouterOptions, type FullRouteRequirementsOptions, type NavigateOptions, type NavigationAdapter, NavigationAdapterKind, type NavigationCause, type NavigationCommit, type NavigationIntent, RequirementsClientSetComposition, type WebRouteContext, type WebRouteDefinition, type WebRouteMatch, type WebRouteMatcher, type WebRouter, createHistoryAdapter, createNavigationAdapter, createNavigationApiAdapter, createWebRouter, defineWebRoute, extractFullRouteRequirements, isNavigationApiAvailable };
|
|
259
|
+
//# sourceMappingURL=index.d.mts.map
|