@securitydept/client 0.3.0-beta.1 → 0.3.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +6 -0
  2. package/dist/create-i5VyYgN6.mjs +989 -0
  3. package/dist/create-i5VyYgN6.mjs.map +1 -0
  4. package/dist/handle-DwMgqe7R.mjs +638 -0
  5. package/dist/handle-DwMgqe7R.mjs.map +1 -0
  6. package/dist/index-Bo1BtjZH.d.mts +132 -0
  7. package/dist/index-CFAJ969E.d.mts +674 -0
  8. package/dist/index-CbeZ3AfB.d.mts +1680 -0
  9. package/dist/index.d.mts +3 -353
  10. package/dist/index.mjs +1196 -401
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/router-at5bTdJQ.mjs +245 -0
  13. package/dist/router-at5bTdJQ.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-DM6Ouv-m.mjs +1119 -0
  17. package/dist/rx-DM6Ouv-m.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 +82 -337
  27. package/dist/web/index.mjs +334 -337
  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/events/index.d.mts +0 -2
  37. package/dist/events/index.mjs +0 -2
  38. package/dist/events-BUlfsRfl.mjs +0 -127
  39. package/dist/events-BUlfsRfl.mjs.map +0 -1
  40. package/dist/fetch-transport-Bd5RyWj2.d.mts +0 -339
  41. package/dist/fetch-transport-DtnSRWAH.mjs +0 -631
  42. package/dist/fetch-transport-DtnSRWAH.mjs.map +0 -1
  43. package/dist/index-C_wgl6dW.d.mts +0 -102
  44. package/dist/index-DbYSFHDe.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 -51
  49. package/dist/persistence/web/index.mjs.map +0 -1
  50. package/dist/persistence-CSB8770k.mjs +0 -108
  51. package/dist/persistence-CSB8770k.mjs.map +0 -1
  52. package/dist/planner-host-CPyGNyQV.d.mts +0 -190
  53. package/dist/planner-host-CV38bMSn.mjs +0 -195
  54. package/dist/planner-host-CV38bMSn.mjs.map +0 -1
  55. package/dist/types-C59fcM5W.d.mts +0 -52
  56. package/dist/types-DQc088ZP.d.mts +0 -32
  57. package/dist/web-router/index.d.mts +0 -259
  58. package/dist/web-router/index.mjs +0 -395
  59. package/dist/web-router/index.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,69 +1,274 @@
1
- import { C as ClientErrorSource, S as ClientErrorKind, T as UserRecovery, _ as createDefaultClock, b as OperationTraceEventType, c as PromiseSettlementKind, d as fromSignal, f as interval, g as createDefaultScheduler, h as createRuntime, l as fromEventPattern, m as timer, p as scheduleAt, t as FetchTransportRedirectKind, u as fromPromise, v as createOperationTracer, w as ErrorPresentationTone, x as ClientError, y as LogLevel } from "./fetch-transport-DtnSRWAH.mjs";
2
- import { _ as createEventStream, a as filter, b as fromRxObservable, c as merge, d as shareReplay, f as switchMap, g as withLatestFromSignal, h as throttleTime, i as exhaustMap, l as pipe, m as tap, n as concatMap, o as finalize, p as takeUntil, r as debounceTime, s as map, t as EventSourceKind, u as share, v as createReplaySubject, x as toRxObservable, y as createSubject } from "./events-BUlfsRfl.mjs";
3
- import { i as createJsonCodec, n as createEphemeralFlowStore, r as createKeyedEphemeralFlowStore, t as createInMemoryRecordStore } from "./persistence-CSB8770k.mjs";
4
- //#region src/cancellation/cancellation-token.ts
5
- var CancellationToken = class {
6
- _isCancelled = false;
7
- _reason;
8
- _listeners = /* @__PURE__ */ new Set();
9
- get isCancellationRequested() {
10
- return this._isCancelled;
11
- }
12
- get reason() {
13
- return this._reason;
1
+ import { A as RxEventSubject, B as withDisposableStack, E as RxComputedSignal, F as SYMBOL_DISPOSE, G as ErrorPresentationTone, H as ClientError, I as SimpleDisposableStack, K as UserRecovery, L as StdDisposableStack, M as isInteropObservableTrait, N as SYMBOL_ASYNC_DISPOSE, O as RxEventReplaySubject, P as SYMBOL_DISPOSABLE_STACK, R as createDisposableStack, U as ClientErrorKind, V as CLIENT_ERROR_SPAN_ATTRIBUTE_PROVIDER_ID, W as ClientErrorSource, a as ResourceSnapshotUpdateKind, c as whenResourceSnapshotValue, d as ResourceErrorCode, f as CancellationToken, i as rxResource, j as SYMBOL_OBSERVABLE, k as RxEventStream, l as ResourceStatus, m as isCancellationTokenTrait, n as mapResource, o as flattenResourceSnapshot, p as createCancellationTokenSource, r as resourceFromSnapshots, s as reduceResourceSnapshot, u as ResourceError, z as injectDisposableStackFrom } from "./rx-DM6Ouv-m.mjs";
2
+ import { a as createJsonRpcServerAndClient, c as createSignal, d as readErrorPresentationDescriptor, f as SpanSharedAttributeName, i as createJsonRpcServer, l as readonlySignal, n as PopupServerWindowHandle, o as PopupErrorCode, r as createJsonRpcClient, s as readPopupErrorPresentationDescriptor, t as PopupClientWindowHandle, u as formatClientErrorContext } from "./handle-DwMgqe7R.mjs";
3
+ import { $ as notMissingProvider, A as SECURITYDEPT_COMPAT_FRAGMENT_VERSION, B as validateWithSchema, C as SPAN_TRAIT_TOKEN, D as RouterNavigationIntent, E as ROUTER_TRAIT_TOKEN, F as POPUP_TRAIT_TOKEN, G as INJECTOR_TOKEN, H as PAGE_LIFECYCLE_TRAIT_TOKEN, I as PopupAttachFailureReason, J as toRuntimeInjector, K as PARENT_INJECTOR_TOKEN, L as formatValidationFailure, M as isCompatFragmentBlock, N as parseCompatFragment, O as RouterNavigationMode, P as takeCompatFragment, Q as getSecuritydeptProviderToken, R as throwValidationClientError, S as createRootSpan, T as TIME_TRAIT_TOKEN, U as EventSourceKind, V as validateWithSchemaSync, W as isEventStreamTrait, X as SecuritydeptInjectionToken, Y as SecurityDeptOptional, Z as createProviderIfTokenMissing, _ as createTimeForStd, a as TRACING_TRAIT_TOKEN, b as normalizeAbortError, c as createInMemoryRecordStore, d as SESSION_STORAGE_TRAIT_TOKEN, f as StorageChangeEventOrigin, g as TransportErrorCode, h as TRANSPORT_TRAIT_TOKEN, i as TRACING_SPAN_ATTRIBUTE_PROVIDER_ID, j as appendOrReplaceCompatFragment, k as takeCompatFragmentFromRouter, l as PERSISTENT_STORAGE_TRAIT_TOKEN, m as createBaseTransportForStdFetch, n as ENVIRONMENT_TOKEN, o as TracingLevel, p as FetchTransportRedirectKind, q as SecuritydeptInjector, r as OperationTraceEventType, s as createTracing, t as createFoundationEnvironment, u as REALM_STORAGE_TRAIT_TOKEN, v as abortSignalToCancellationToken, w as IDLE_CALLBACK_TRAIT_TOKEN, x as abortSignalToEventStream, y as cancellationTokenToAbortSignal, z as validateTraitInput } from "./create-i5VyYgN6.mjs";
4
+ import { i as UriString, n as UriReferenceString, r as UriRelativeString, t as UriParseError } from "./uri-string-BPNbIMgv.mjs";
5
+ import { v7 } from "uuid";
6
+ import { inject as inject$1, runInInjectionContext as runInInjectionContext$1 } from "injection-js";
7
+ import { EMPTY, NEVER, ReplaySubject, filter, from, merge, take, takeUntil } from "rxjs";
8
+ import { type } from "arktype";
9
+ import { isPromise } from "es-toolkit/predicate";
10
+ //#region src/auth-coordination/contract.ts
11
+ var StaticAttrsAuthRequirement = class StaticAttrsAuthRequirement {
12
+ id;
13
+ label;
14
+ attributes;
15
+ constructor(id, label, attributes) {
16
+ this.id = id;
17
+ this.label = label;
18
+ this.attributes = attributes;
19
+ Object.freeze(this);
14
20
  }
15
- /** @internal — called by `CancellationTokenSource`. */
16
- _cancel(reason) {
17
- if (this._isCancelled) return;
18
- this._isCancelled = true;
19
- this._reason = reason;
20
- for (const listener of this._listeners) listener(reason);
21
- this._listeners.clear();
21
+ static create(input) {
22
+ return new StaticAttrsAuthRequirement(input?.id ?? v7(), input?.label, input?.attributes ? Object.freeze(input.attributes) : void 0);
22
23
  }
23
- onCancellationRequested(listener) {
24
- if (this._isCancelled) {
25
- listener(this._reason);
26
- return { dispose() {} };
27
- }
28
- this._listeners.add(listener);
29
- return { dispose: () => {
30
- this._listeners.delete(listener);
31
- } };
24
+ static createList(inputs) {
25
+ return inputs.map((input) => StaticAttrsAuthRequirement.create(input));
32
26
  }
33
- throwIfCancellationRequested() {
34
- if (this._isCancelled) throw new ClientError({
35
- kind: ClientErrorKind.Cancelled,
36
- message: "Operation was cancelled",
37
- cause: this._reason
38
- });
27
+ };
28
+ /** Outcome of resolving a single requirement. */
29
+ const ResolutionStatus = {
30
+ Fulfilled: "fulfilled",
31
+ Failed: "failed",
32
+ Skipped: "skipped"
33
+ };
34
+ /** Overall plan status. */
35
+ const PlanStatus = {
36
+ /** There are still pending requirements. */
37
+ Pending: "pending",
38
+ /** All requirements have been resolved. */
39
+ Settled: "settled"
40
+ };
41
+ /** Error thrown when a planner contract is violated (e.g. duplicate ids). */
42
+ var RequirementPlannerError = class extends Error {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = "RequirementPlannerError";
39
46
  }
40
47
  };
48
+ function isRouteStateSnapshotTrait(value) {
49
+ return typeof value === "object" && value !== null && "url" in value;
50
+ }
51
+ function isRouteBehaviourContextExtra(value) {
52
+ return typeof value === "object" && value !== null && "routeState" in value && isRouteStateSnapshotTrait(value.routeState);
53
+ }
54
+ /** Outcome of a pipeline step or run. */
55
+ const PipelineOutcome = {
56
+ /** No candidates remain — the plan is satisfied. */
57
+ Settled: "settled",
58
+ /** A candidate was resolved as fulfilled; more may remain. */
59
+ Resolved: "resolved",
60
+ /** A candidate's handler blocked progress. */
61
+ Blocked: "blocked",
62
+ /** A candidate's handler requested a redirect. */
63
+ Redirect: "redirect"
64
+ };
41
65
  /**
42
- * Create a `CancellationTokenSource` that produces a cooperative cancellation token.
66
+ * How a route segment's requirements compose with the inherited parent chain.
67
+ *
68
+ * - `inherit` — keep the parent requirements unchanged (segment adds nothing)
69
+ * - `merge` — inherit parent requirements, then append/override with the
70
+ * segment's own (same-id entries replace the parent's)
71
+ * - `replace` — discard the parent chain, use only the segment's requirements
43
72
  */
44
- function createCancellationTokenSource() {
45
- const ct = new CancellationToken();
46
- let disposed = false;
47
- return {
48
- get token() {
49
- return ct;
50
- },
51
- cancel(reason) {
52
- if (!disposed) ct._cancel(reason);
53
- },
54
- dispose() {
55
- if (!disposed) {
56
- disposed = true;
57
- ct._cancel(new ClientError({
58
- kind: ClientErrorKind.Cancelled,
59
- message: "Disposed"
60
- }));
61
- }
73
+ const RequirementsComposition = {
74
+ Inherit: "inherit",
75
+ Merge: "merge",
76
+ Replace: "replace"
77
+ };
78
+ /**
79
+ * Apply a route requirements declaration onto an effective requirement map.
80
+ *
81
+ * Reuse `effective` across a route chain so merge/replace fold in O(total
82
+ * requirements) without rebuilding parent state each segment. When omitted, a
83
+ * fresh map is created for one-shot composition.
84
+ */
85
+ function resolveEffectiveRequirements(child, effective = /* @__PURE__ */ new Map()) {
86
+ switch (child.composition) {
87
+ case RequirementsComposition.Inherit: return effective;
88
+ case RequirementsComposition.Merge:
89
+ for (const requirement of child.requirements) effective.set(requirement.id, requirement);
90
+ return effective;
91
+ case RequirementsComposition.Replace:
92
+ effective.clear();
93
+ for (const requirement of child.requirements) effective.set(requirement.id, requirement);
94
+ return effective;
95
+ default: {
96
+ const _exhaustive = child.composition;
97
+ throw new RequirementPlannerError(`Unknown requirements composition: ${String(_exhaustive)}`);
62
98
  }
99
+ }
100
+ }
101
+ //#endregion
102
+ //#region src/errors/error-attributes.ts
103
+ /** Extract secret-safe diagnostic attributes without copying runtime messages. */
104
+ function describeError(error) {
105
+ if (error instanceof ClientError) return {
106
+ errorName: error.name,
107
+ errorKind: error.kind,
108
+ errorCode: error.code,
109
+ recovery: error.recovery
63
110
  };
111
+ return { errorName: error instanceof Error ? error.name : typeof error };
112
+ }
113
+ //#endregion
114
+ //#region src/compat/base64.ts
115
+ const Base64CompatErrorCode = {
116
+ EncoderUnavailable: "compat.base64.encoder_unavailable",
117
+ DecoderUnavailable: "compat.base64.decoder_unavailable"
118
+ };
119
+ function encodeUint8ArrayToBase64(value) {
120
+ const toBase64 = globalThis.Uint8Array.prototype.toBase64;
121
+ if (typeof toBase64 === "function") return toBase64.call(value);
122
+ if (typeof globalThis.btoa === "function") {
123
+ let binary = "";
124
+ for (const byte of value) binary += String.fromCharCode(byte);
125
+ return globalThis.btoa(binary);
126
+ }
127
+ const BufferConstructor = Reflect.get(globalThis, "Buffer");
128
+ if (typeof BufferConstructor?.from === "function") return BufferConstructor.from(value).toString("base64");
129
+ throw new ClientError({
130
+ kind: ClientErrorKind.Configuration,
131
+ code: Base64CompatErrorCode.EncoderUnavailable,
132
+ message: "The current realm does not provide a Base64 encoder.",
133
+ source: "compat.base64"
134
+ });
135
+ }
136
+ function decodeBase64ToUint8Array(value) {
137
+ const Uint8ArrayConstructor = globalThis.Uint8Array;
138
+ if (typeof Uint8ArrayConstructor.fromBase64 === "function") return Uint8ArrayConstructor.fromBase64(value);
139
+ if (typeof globalThis.atob === "function") {
140
+ const binary = globalThis.atob(value);
141
+ const bytes = new Uint8Array(binary.length);
142
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
143
+ return bytes;
144
+ }
145
+ const BufferConstructor = Reflect.get(globalThis, "Buffer");
146
+ if (typeof BufferConstructor?.from === "function") return Uint8ArrayConstructor.from(BufferConstructor.from(value, "base64"));
147
+ throw new ClientError({
148
+ kind: ClientErrorKind.Configuration,
149
+ code: Base64CompatErrorCode.DecoderUnavailable,
150
+ message: "The current realm does not provide a Base64 decoder.",
151
+ source: "compat.base64"
152
+ });
153
+ }
154
+ //#endregion
155
+ //#region src/compat/generator.ts
156
+ async function* promisesToRacedAsyncGenerator(promises) {
157
+ const pending = new Map(promises.map((promise, index) => [index, promise.then((value) => ({
158
+ value,
159
+ index
160
+ }))]));
161
+ while (pending.size > 0) {
162
+ const next = await Promise.race(pending.values());
163
+ pending.delete(next.index);
164
+ yield next.value;
165
+ }
166
+ }
167
+ //#endregion
168
+ //#region src/injection/context.ts
169
+ function inject(token, options) {
170
+ if (options?.optional === true) return inject$1(token, { optional: true });
171
+ return inject$1(token);
172
+ }
173
+ function tryInjectInInjectionContext(token, options) {
174
+ try {
175
+ if (options?.optional === true) return inject(token, { optional: true });
176
+ return inject(token);
177
+ } catch (error) {
178
+ if (isMissingInjectionContextError(error)) return null;
179
+ throw error;
180
+ }
181
+ }
182
+ function runInInjectionContext(injector, fn) {
183
+ return runInInjectionContext$1(toRuntimeInjector(injector), fn);
184
+ }
185
+ function isMissingInjectionContextError(error) {
186
+ return error instanceof Error && error.message.includes("can only be used within an injection context");
64
187
  }
65
188
  //#endregion
66
- //#region src/cancellation/linked-cancellation-token.ts
189
+ //#region src/injection/destroy.ts
190
+ var SecuritydeptDestroyRef = class {
191
+ [SYMBOL_DISPOSE]() {
192
+ this.dispose();
193
+ }
194
+ };
195
+ var ManagedSecuritydeptDestroyRef = class extends SecuritydeptDestroyRef {
196
+ destroyedSignal = createSignal(false);
197
+ get destroyed() {
198
+ return this.destroyedSignal.get();
199
+ }
200
+ onDestroy(callback) {
201
+ if (this.destroyedSignal.get()) {
202
+ callback();
203
+ return () => void 0;
204
+ }
205
+ const subscription = from(this.destroyedSignal).pipe(filter((destroyed) => destroyed), take(1)).subscribe({ next: callback });
206
+ return () => {
207
+ subscription.unsubscribe();
208
+ };
209
+ }
210
+ dispose() {
211
+ if (this.destroyedSignal.get()) return;
212
+ this.destroyedSignal.set(true);
213
+ }
214
+ destroy() {
215
+ this.dispose();
216
+ }
217
+ };
218
+ function createSecuritydeptDestroyRef() {
219
+ return new ManagedSecuritydeptDestroyRef();
220
+ }
221
+ //#endregion
222
+ //#region src/validation/schemas/uri.ts
223
+ const UriStringSchema = type("string").narrow((value, context) => UriString.tryParse(value) == null ? context.reject("an absolute URI string") : true);
224
+ const BaseURIStringSchema = UriStringSchema;
225
+ const UriRelativeStringSchema = type("string").narrow((value, context) => UriRelativeString.tryParse(value) == null ? context.reject("a relative URI reference string") : true);
226
+ const UriReferenceStringSchema = type("string").narrow((value, context) => UriReferenceString.tryParse(value) == null ? context.reject("a URI reference string") : true);
227
+ //#endregion
228
+ //#region src/cancellation/linked.ts
229
+ var LinkedCancellationToken = class {
230
+ sources;
231
+ destoryed$ = new ReplaySubject(1);
232
+ token;
233
+ constructor(sources) {
234
+ this.sources = sources;
235
+ if (sources.length === 0) this.token = new CancellationToken();
236
+ if (sources.length === 1) this.token = sources[0];
237
+ for (const source of sources) if (source.isCancellationRequested) {
238
+ this.token = source;
239
+ return;
240
+ }
241
+ const newToken = new CancellationToken();
242
+ merge(...sources).pipe(take(1), takeUntil(this.destoryed$)).subscribe(({ reason }) => {
243
+ newToken._cancel(reason);
244
+ });
245
+ this.token = newToken;
246
+ }
247
+ get isCancellationRequested() {
248
+ return this.token.isCancellationRequested;
249
+ }
250
+ get cancellationError() {
251
+ return this.token.cancellationError;
252
+ }
253
+ get reason() {
254
+ return this.token.reason;
255
+ }
256
+ onCancellationRequested(listener) {
257
+ return this.token.onCancellationRequested(listener);
258
+ }
259
+ throwIfCancellationRequested() {
260
+ this.token.throwIfCancellationRequested();
261
+ }
262
+ dispose() {
263
+ this.destoryed$.next(true);
264
+ }
265
+ [SYMBOL_DISPOSE]() {
266
+ this.dispose();
267
+ }
268
+ [SYMBOL_OBSERVABLE]() {
269
+ return this.token[SYMBOL_OBSERVABLE]();
270
+ }
271
+ };
67
272
  /**
68
273
  * Create a cancellation token that fires when ANY of the given source tokens
69
274
  * is cancelled.
@@ -79,418 +284,1008 @@ function createCancellationTokenSource() {
79
284
  * await transport.execute({ ..., cancellationToken: linked });
80
285
  */
81
286
  function createLinkedCancellationToken(...sources) {
82
- if (sources.length === 0) return createCancellationTokenSource().token;
83
- if (sources.length === 1) return sources[0];
84
- for (const source of sources) if (source.isCancellationRequested) return source;
85
- const linked = createCancellationTokenSource();
86
- const subscriptions = sources.map((source) => source.onCancellationRequested((reason) => linked.cancel(reason)));
87
- linked.token.onCancellationRequested(() => {
88
- for (const sub of subscriptions) sub.dispose();
89
- });
90
- return linked.token;
287
+ return new LinkedCancellationToken(sources.filter((s) => !!s));
91
288
  }
92
289
  //#endregion
93
- //#region src/errors/presentation-descriptor.ts
94
- const DEFAULT_RECOVERY_LABELS = {
95
- [UserRecovery.None]: "",
96
- [UserRecovery.Retry]: "Try again",
97
- [UserRecovery.RestartFlow]: "Restart flow",
98
- [UserRecovery.Reauthenticate]: "Sign in again",
99
- [UserRecovery.ContactSupport]: "Contact support"
100
- };
101
- const POPUP_PRESENTATIONS = {
102
- "popup.blocked": {
103
- title: "Popup was blocked",
104
- description: "The browser blocked the popup window before the login flow could start. Allow popups for this site, then try again.",
105
- tone: ErrorPresentationTone.Warning
106
- },
107
- "popup.closed_by_user": {
108
- title: "Popup login was closed",
109
- description: "The popup window was closed before the OIDC provider returned a callback. Start the login flow again to continue.",
110
- tone: ErrorPresentationTone.Warning
111
- },
112
- "popup.relay_timeout": {
113
- title: "Popup relay timed out",
114
- description: "The popup callback never relayed back to the opener before the timeout window expired.",
115
- tone: ErrorPresentationTone.Warning
116
- },
117
- "popup.relay_error": {
118
- title: "Popup relay failed",
119
- description: "The popup callback page returned an error instead of a callback payload.",
120
- tone: ErrorPresentationTone.Danger
121
- }
122
- };
123
- function readErrorPresentationDescriptor(error, options = {}) {
124
- if (error instanceof ClientError) return describeClientErrorPresentation(error, options);
125
- const clientErrorLike = coerceClientErrorLike(error);
126
- if (clientErrorLike) return describeClientErrorLikePresentation(clientErrorLike, options);
127
- if (error instanceof Error) return {
128
- code: null,
129
- kind: null,
130
- title: options.fallbackTitle ?? "Operation failed",
131
- description: error.message || options.fallbackDescription || "An unexpected error prevented the operation from completing.",
132
- recovery: UserRecovery.None,
133
- retryable: false,
134
- tone: ErrorPresentationTone.Danger,
135
- primaryAction: null
136
- };
137
- return {
138
- code: null,
139
- kind: null,
140
- title: options.fallbackTitle ?? "Operation failed",
141
- description: options.fallbackDescription ?? "An unexpected error prevented the operation from completing.",
142
- recovery: UserRecovery.None,
143
- retryable: false,
144
- tone: ErrorPresentationTone.Danger,
145
- primaryAction: null
146
- };
147
- }
148
- function describeClientErrorPresentation(error, options) {
149
- return describeClientErrorLikePresentation({
150
- code: error.code,
151
- kind: error.kind,
152
- message: error.message,
153
- recovery: error.recovery,
154
- retryable: error.retryable,
155
- source: error.source,
156
- presentation: error.presentation
157
- }, options);
158
- }
159
- function describeClientErrorLikePresentation(error, options) {
160
- const popupPresentation = POPUP_PRESENTATIONS[error.code];
161
- const title = popupPresentation?.title ?? readClientErrorTitle(error);
162
- const description = popupPresentation?.description ?? error.presentation?.message ?? error.message ?? options.fallbackDescription ?? "The operation could not complete.";
163
- return {
164
- code: error.code,
165
- kind: error.kind,
166
- source: error.source,
167
- title,
168
- description,
169
- recovery: error.recovery,
170
- retryable: error.retryable,
171
- tone: popupPresentation?.tone ?? readClientErrorTone(error),
172
- primaryAction: readPrimaryAction(error.recovery, options)
173
- };
290
+ //#region src/signals/computed.ts
291
+ /**
292
+ * Create a lazy computed signal.
293
+ *
294
+ * Dependencies are collected dynamically while `compute()` runs.
295
+ */
296
+ function createComputed(compute, options) {
297
+ return RxComputedSignal.computed(compute, options);
174
298
  }
175
- function readClientErrorTitle(error) {
176
- if (error.kind === ClientErrorKind.Unauthenticated) return "Authentication required";
177
- if (error.kind === ClientErrorKind.Unauthorized) return "Access denied";
178
- if (error.kind === ClientErrorKind.Cancelled) return "Operation cancelled";
179
- if (error.kind === ClientErrorKind.Timeout) return "Request timed out";
180
- if (error.kind === ClientErrorKind.Configuration) return "Configuration error";
181
- if (error.kind === ClientErrorKind.Validation) return "Validation failed";
182
- if (error.kind === ClientErrorKind.Storage) return "Browser storage failed";
183
- if (error.kind === ClientErrorKind.Authorization && error.recovery === UserRecovery.RestartFlow) return "Flow needs to restart";
184
- if (error.kind === ClientErrorKind.Transport) return "Network request failed";
185
- if (error.kind === ClientErrorKind.Server) return "Server request failed";
186
- if (error.kind === ClientErrorKind.Protocol) return "Request failed";
187
- return "Operation failed";
188
- }
189
- function readClientErrorTone(error) {
190
- if (error.kind === ClientErrorKind.Cancelled) return ErrorPresentationTone.Neutral;
191
- if (error.kind === ClientErrorKind.Unauthenticated || error.kind === ClientErrorKind.Timeout || error.kind === ClientErrorKind.Transport || error.kind === ClientErrorKind.Server || error.recovery === UserRecovery.Retry || error.recovery === UserRecovery.RestartFlow || error.recovery === UserRecovery.Reauthenticate) return ErrorPresentationTone.Warning;
192
- return ErrorPresentationTone.Danger;
193
- }
194
- function coerceClientErrorLike(error) {
195
- if (typeof error !== "object" || error === null) return null;
196
- const candidate = error;
197
- if (typeof candidate.message !== "string") return null;
198
- return {
199
- code: typeof candidate.code === "string" ? candidate.code : null,
200
- kind: typeof candidate.kind === "string" ? candidate.kind : null,
201
- message: candidate.message,
202
- recovery: typeof candidate.recovery === "string" ? candidate.recovery : UserRecovery.None,
203
- retryable: candidate.retryable === true,
204
- source: typeof candidate.source === "string" ? candidate.source : void 0,
205
- presentation: isErrorPresentation(candidate.presentation) ? candidate.presentation : void 0
206
- };
299
+ //#endregion
300
+ //#region src/signals/resource.ts
301
+ function createResource(options) {
302
+ return rxResource({
303
+ ...options,
304
+ stream: (context) => from(options.stream(context))
305
+ });
207
306
  }
208
- function isErrorPresentation(value) {
209
- return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string" && typeof value.recovery === "string";
307
+ //#endregion
308
+ //#region src/router/metadata.ts
309
+ /** Route-data key holding the SecurityDept metadata blob for a segment. */
310
+ const SECURITYDEPT_ROUTE_METADATA_KEY = "__securitydept__";
311
+ /** Read SecurityDept route metadata from a route `data` bag. */
312
+ function readSecuritydeptRouteMetadata(data) {
313
+ const raw = data?.[SECURITYDEPT_ROUTE_METADATA_KEY];
314
+ if (raw === void 0 || raw === null) return;
315
+ return raw;
210
316
  }
211
- function readPrimaryAction(recovery, options) {
212
- if (recovery === UserRecovery.None) return null;
213
- const label = options.recoveryLabels?.[recovery] ?? DEFAULT_RECOVERY_LABELS[recovery];
214
- if (!label) return null;
215
- return {
216
- recovery,
217
- label,
218
- href: options.recoveryLinks?.[recovery] ?? null
317
+ /** Merge SecurityDept route metadata into a route `data` bag. */
318
+ function writeSecuritydeptRouteMetadata(base, patch) {
319
+ const data = { ...base ?? {} };
320
+ const previous = data["__securitydept__"] ?? {};
321
+ data[SECURITYDEPT_ROUTE_METADATA_KEY] = {
322
+ ...previous,
323
+ ...patch
219
324
  };
325
+ return data;
220
326
  }
221
327
  //#endregion
222
- //#region src/identity/principal.ts
223
- function readClaimsBag(value) {
224
- return typeof value === "object" && value !== null ? value : void 0;
225
- }
226
- function readOptionalString(value) {
227
- return typeof value === "string" ? value : void 0;
328
+ //#region src/std/transport/authorization-interceptor.ts
329
+ function createAuthorizationInterceptedFetch(fetchImpl, options) {
330
+ return async (input, init) => {
331
+ if (!await options.predicate(input, init)) return fetchImpl(input, init);
332
+ const authorization = "whenValue" in options.authorization ? await options.authorization.whenValue() : options.authorization.get();
333
+ if (!authorization) return fetchImpl(input, init);
334
+ const headers = new Headers(typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
335
+ if (init?.headers) new Headers(init.headers).forEach((value, key) => {
336
+ headers.set(key, value);
337
+ });
338
+ headers.set("authorization", authorization);
339
+ return fetchImpl(input, {
340
+ ...init,
341
+ headers
342
+ });
343
+ };
228
344
  }
229
- function resolveDisplayName(displayName, subject) {
230
- if (typeof displayName === "string" && displayName.trim().length > 0) return displayName;
231
- return subject;
345
+ //#endregion
346
+ //#region src/tracing/operation-runner.ts
347
+ var OperationSpan = class OperationSpan {
348
+ span;
349
+ _environment;
350
+ _target;
351
+ constructor(span, _environment, _target) {
352
+ this.span = span;
353
+ this._environment = _environment;
354
+ this._target = _target;
355
+ }
356
+ static start(options) {
357
+ const span = options.span.fork({
358
+ mutable: true,
359
+ idFactory: options.idFactory,
360
+ attributes: { [SpanSharedAttributeName.OperationName]: options.name }
361
+ });
362
+ if (options.traceAttributes) span.setAttributes(options.traceAttributes, { providerId: TRACING_SPAN_ATTRIBUTE_PROVIDER_ID });
363
+ const operationSpan = new OperationSpan(span, options.environment, options.target);
364
+ operationSpan.recordStarted();
365
+ return operationSpan;
366
+ }
367
+ setTraceAttributes(attributes) {
368
+ this.span.setAttributes(attributes, { providerId: TRACING_SPAN_ATTRIBUTE_PROVIDER_ID });
369
+ }
370
+ addEvent(name, fields) {
371
+ this._record(OperationTraceEventType.Event, TracingLevel.Info, {
372
+ eventName: name,
373
+ ...fields ?? {}
374
+ });
375
+ }
376
+ recordError(error, fields) {
377
+ this._record(OperationTraceEventType.Error, TracingLevel.Error, {
378
+ ...describeError(error),
379
+ ...fields ?? {}
380
+ });
381
+ }
382
+ recordStarted() {
383
+ this._record(OperationTraceEventType.Started, TracingLevel.Info);
384
+ }
385
+ recordEnded(outcome) {
386
+ this._record(OperationTraceEventType.Ended, outcome === "succeeded" ? TracingLevel.Info : TracingLevel.Error, { outcome });
387
+ }
388
+ _record(name, level, fields) {
389
+ this._environment.tracing.record({
390
+ name,
391
+ at: this._environment.time.now(),
392
+ span: this.span,
393
+ level,
394
+ target: this._target,
395
+ fields
396
+ });
397
+ }
398
+ };
399
+ function runOperation(options) {
400
+ const operationSpan = OperationSpan.start(options);
401
+ try {
402
+ const result = options.execute(operationSpan);
403
+ if (isPromise(result)) return result.then((value) => completeOperation(operationSpan, value), (error) => failOperation(operationSpan, error, options.clientErrorFromUnknown));
404
+ return completeOperation(operationSpan, result);
405
+ } catch (error) {
406
+ return failOperation(operationSpan, error, options.clientErrorFromUnknown);
407
+ }
232
408
  }
233
- function normalizeAuthenticatedPrincipal(input) {
234
- if (typeof input.subject !== "string" || input.subject.trim().length === 0) return null;
235
- return {
236
- subject: input.subject,
237
- displayName: resolveDisplayName(input.displayName, input.subject),
238
- picture: readOptionalString(input.picture),
239
- issuer: readOptionalString(input.issuer),
240
- claims: readClaimsBag(input.claims)
241
- };
409
+ function completeOperation(operationSpan, result) {
410
+ operationSpan.recordEnded("succeeded");
411
+ return result;
242
412
  }
243
- function normalizeAuthenticatedPrincipalWire(input) {
244
- if (typeof input.subject !== "string" || input.subject.trim().length === 0) return null;
245
- return {
246
- subject: input.subject,
247
- displayName: resolveDisplayName(input.display_name, input.subject),
248
- picture: readOptionalString(input.picture),
249
- issuer: readOptionalString(input.issuer),
250
- claims: readClaimsBag(input.claims)
251
- };
413
+ function failOperation(operationSpan, error, clientErrorFromUnknown) {
414
+ const clientError = clientErrorFromUnknown ? clientErrorFromUnknown(error, { span: operationSpan.span }) : error;
415
+ if (clientError instanceof ClientError) clientError.captureSpanContext(operationSpan.span);
416
+ operationSpan.recordError(clientError);
417
+ operationSpan.recordEnded("failed");
418
+ throw clientError;
252
419
  }
253
- function projectAuthenticatedPrincipal(options) {
254
- const principal = options.principal;
255
- const subject = principal?.subject ?? options.fallbackSubject ?? "context.anonymous";
256
- return {
257
- subject,
258
- displayName: principal?.displayName?.trim() || options.fallbackDisplayName || subject,
259
- picture: principal?.picture,
260
- issuer: principal?.issuer ?? options.fallbackIssuer,
261
- claims: principal?.claims ?? options.fallbackClaims
420
+ //#endregion
421
+ //#region src/tracing/operation-method.ts
422
+ function defineInstrumentMethodDecorator(factory) {
423
+ return (...factoryArgs) => (target, context) => {
424
+ const methodName = String(context.name);
425
+ const resolve = factory({
426
+ factoryArgs,
427
+ methodName
428
+ });
429
+ return function(...args) {
430
+ const resolvedOptions = resolve.call(this, {
431
+ args,
432
+ methodName
433
+ });
434
+ return runOperation({
435
+ environment: resolvedOptions.environment,
436
+ span: resolvedOptions.span,
437
+ name: resolvedOptions.name ?? methodName,
438
+ target: resolvedOptions.target,
439
+ traceAttributes: resolvedOptions.traceAttributes,
440
+ idFactory: resolvedOptions.idFactory,
441
+ clientErrorFromUnknown: resolvedOptions.clientErrorFromUnknown,
442
+ execute: (span) => target.call(this, ...args, span)
443
+ });
444
+ };
262
445
  };
263
446
  }
264
447
  //#endregion
265
- //#region src/logging/console-logger.ts
266
- /** Simple console-based logger implementation. */
267
- function createConsoleLogger(scope) {
268
- return { log(entry) {
269
- const label = `[${entry.scope ?? scope ?? "securitydept"}]`;
270
- switch (entry.level) {
448
+ //#region src/tracing/subscriber/console.ts
449
+ var ConsoleTracingSubscriber = class {
450
+ record(event) {
451
+ const label = `[${event.target}] ${event.name}`;
452
+ const details = {
453
+ ...event.fields ?? {},
454
+ spanAttributes: event.span.getRootToNodeAttributes({ providerId: TRACING_SPAN_ATTRIBUTE_PROVIDER_ID })
455
+ };
456
+ switch (event.level) {
271
457
  case "debug":
272
- console.debug(label, entry.message, entry.attributes ?? "");
458
+ console.debug(label, details);
273
459
  break;
274
460
  case "info":
275
- console.info(label, entry.message, entry.attributes ?? "");
461
+ console.info(label, details);
276
462
  break;
277
463
  case "warn":
278
- console.warn(label, entry.message, entry.attributes ?? "");
279
- break;
280
- case "error":
281
- console.error(label, entry.message, entry.error ?? "", entry.attributes ?? "");
464
+ console.warn(label, details);
282
465
  break;
466
+ case "error": console.error(label, details);
283
467
  }
284
- } };
468
+ }
469
+ };
470
+ function createConsoleTracingSubscriber() {
471
+ return new ConsoleTracingSubscriber();
285
472
  }
286
473
  //#endregion
287
- //#region src/logging/noop-logger.ts
288
- /** No-op logger — default when no logger is provided. */
289
- function createNoopLogger() {
290
- return { log() {} };
291
- }
474
+ //#region src/struct/ring-buffer.ts
475
+ var FixedCapacityRingBuffer = class {
476
+ capacity;
477
+ values;
478
+ head = 0;
479
+ tail = 0;
480
+ _size = 0;
481
+ constructor(capacity) {
482
+ this.capacity = capacity;
483
+ if (!Number.isInteger(capacity) || capacity <= 0) throw new RangeError("Ring buffer capacity must be a positive integer.");
484
+ this.values = new Array(capacity);
485
+ }
486
+ get size() {
487
+ return this._size;
488
+ }
489
+ append(value) {
490
+ this.values[this.tail] = value;
491
+ this.tail = (this.tail + 1) % this.capacity;
492
+ if (this._size === this.capacity) this.head = this.tail;
493
+ else this._size += 1;
494
+ }
495
+ toArray() {
496
+ const snapshot = new Array(this._size);
497
+ for (let index = 0; index < this._size; index += 1) snapshot[index] = this.values[(this.head + index) % this.capacity];
498
+ return snapshot;
499
+ }
500
+ clear() {
501
+ this.values.fill(void 0);
502
+ this.head = 0;
503
+ this.tail = 0;
504
+ this._size = 0;
505
+ }
506
+ };
292
507
  //#endregion
293
- //#region src/logging/trace-timeline-store.ts
294
- function createTraceTimelineStore(limit = 200) {
295
- let nextId = 1;
296
- let entries = [];
297
- const listeners = /* @__PURE__ */ new Set();
298
- function readRecordedAtIso(at) {
299
- if (!Number.isFinite(at)) return "invalid-timestamp";
300
- const date = new Date(at);
301
- const timestamp = date.getTime();
302
- if (!Number.isFinite(timestamp)) return "invalid-timestamp";
303
- return date.toISOString();
304
- }
305
- function notify() {
306
- for (const listener of listeners) listener();
508
+ //#region src/tracing/subscriber/timeline-store.ts
509
+ /**
510
+ * Bounded subscriber-side trace timeline.
511
+ *
512
+ * This store consumes tracing events through `record(event)` and keeps a
513
+ * bounded local timeline for tests and UI surfaces. It is not the tracing
514
+ * runtime itself.
515
+ */
516
+ var TraceTimelineStore = class {
517
+ limit;
518
+ nextId = 1;
519
+ timeline;
520
+ latestEntrySubject = new RxEventSubject();
521
+ constructor(limit = 200) {
522
+ this.limit = limit;
523
+ this.timeline = new FixedCapacityRingBuffer(limit);
307
524
  }
308
- return {
309
- record(event) {
310
- const entry = {
311
- ...event,
312
- id: nextId++,
313
- recordedAtIso: readRecordedAtIso(event.at)
314
- };
315
- entries = [...entries, entry].slice(-limit);
316
- notify();
317
- },
318
- get() {
319
- return entries;
320
- },
321
- subscribe(listener) {
322
- listeners.add(listener);
323
- return () => {
324
- listeners.delete(listener);
325
- };
326
- },
327
- clear() {
328
- if (entries.length === 0) return;
329
- entries = [];
330
- notify();
525
+ get latestEntry() {
526
+ return this.latestEntrySubject;
527
+ }
528
+ get entries() {
529
+ return this.timeline.toArray();
530
+ }
531
+ record(event) {
532
+ const { span, ...eventWithoutSpan } = event;
533
+ const entry = {
534
+ ...eventWithoutSpan,
535
+ id: this.nextId++,
536
+ recordedAtIso: new Date(event.at).toISOString(),
537
+ span: {
538
+ id: span.id,
539
+ ...span.parent ? { parentId: span.parent.id } : {}
540
+ },
541
+ spanAttributes: span.getRootToNodeAttributes({ providerId: TRACING_SPAN_ATTRIBUTE_PROVIDER_ID })
542
+ };
543
+ this.timeline.append(entry);
544
+ this.latestEntrySubject.next(entry);
545
+ }
546
+ clear() {
547
+ if (this.timeline.size > 0) {
548
+ this.timeline.clear();
549
+ this.latestEntrySubject.next(null);
331
550
  }
332
- };
551
+ }
552
+ };
553
+ function createTraceTimelineStore(limit = 200) {
554
+ return new TraceTimelineStore(limit);
333
555
  }
334
556
  //#endregion
335
- //#region src/signals/computed.ts
557
+ //#region src/auth-coordination/planner-host.ts
558
+ const REQUIREMENT_PLANNER_HOST = new SecuritydeptInjectionToken("REQUIREMENT_PLANNER_HOST");
336
559
  /**
337
- * Create a computed signal that derives its value from other signals.
560
+ * Default candidate selector: first unauthenticated candidate as soon as it is
561
+ * discovered.
338
562
  *
339
- * Semantics: lazy + cached.
340
- * - The compute function is NOT called at construction time.
341
- * - `get()` recomputes only when dirty (a dependency changed since last read).
342
- * - Subscribers are notified only when a dependency changes (dirty-flagged),
343
- * and the next `get()` will produce the fresh value.
344
- *
345
- * Dependency subscriptions are permanent for the lifetime of the computed signal.
563
+ * If every requirement is already fulfilled the stream completes and the
564
+ * selector returns `undefined`.
346
565
  */
347
- function createComputed(compute, deps) {
348
- let cached;
349
- let dirty = true;
350
- const listeners = /* @__PURE__ */ new Set();
351
- const markDirty = () => {
352
- if (!dirty) {
353
- dirty = true;
354
- for (const listener of listeners) listener();
355
- }
566
+ function defaultSelectCandidate() {
567
+ return async function selectFirstCandidate(context) {
568
+ const result = await context.candidateList.next();
569
+ return result.done ? void 0 : result.value;
356
570
  };
357
- for (const dep of deps) dep.subscribe(markDirty);
571
+ }
572
+ /**
573
+ * A coordination-scope host that resolves behaviour through a parent chain.
574
+ *
575
+ * Construct via {@link RequirementPlannerHost.fromBehaviour}. Each behaviour
576
+ * field is optional at host construction time; missing required behaviour is
577
+ * resolved from `parent` and fails fast during planner build if no host in the
578
+ * chain provides it.
579
+ *
580
+ * @example
581
+ * ```ts
582
+ * const root = RequirementPlannerHost.fromBehaviour({
583
+ * checkAuthenticated: (req) => isAuthenticated(req),
584
+ * onUnauthenticated: (req) => `/login/${req.id}`,
585
+ * });
586
+ *
587
+ * // Feature scope overrides only the chooser, inheriting the rest.
588
+ * const feature = RequirementPlannerHost.fromBehaviour(
589
+ * { selectCandidate: async (ctx) => showChooser(ctx.candidateList) },
590
+ * { parent: root },
591
+ * );
592
+ * ```
593
+ */
594
+ var RequirementPlannerHost = class RequirementPlannerHost {
595
+ environment;
596
+ parent;
597
+ /** The parent host, or undefined at the root. */
598
+ _behaviour;
599
+ constructor(behaviour, environment, parent) {
600
+ this.environment = environment;
601
+ this.parent = parent;
602
+ this._behaviour = behaviour;
603
+ }
604
+ /** Create a host scope from a partial behaviour and optional parent. */
605
+ static fromBehaviour(behaviour, options = {}) {
606
+ return new RequirementPlannerHost(behaviour, options.environment, options.parent);
607
+ }
608
+ async resolveBehaviourOptionFor(key) {
609
+ return this._behaviour[key] ?? await this.parent?.resolveBehaviourOptionFor(key);
610
+ }
611
+ async resolveBehaviourFor(key, defaultValue) {
612
+ return await this.resolveBehaviourOptionFor(key) ?? await defaultValue();
613
+ }
614
+ async resolveEnvironmentOption() {
615
+ return this.environment ?? await this.parent?.resolveEnvironmentOption();
616
+ }
617
+ async resolveEnvironment(defaultValue) {
618
+ return await this.resolveEnvironmentOption() ?? await defaultValue();
619
+ }
620
+ };
621
+ function provideRequirementPlannerHost(behaviour, options = {}) {
358
622
  return {
359
- get() {
360
- if (dirty) {
361
- cached = compute();
362
- dirty = false;
363
- }
364
- return cached;
623
+ provide: REQUIREMENT_PLANNER_HOST,
624
+ useFactory: (parentInjector, environment) => {
625
+ const parent = options.parent ?? parentInjector?.get(REQUIREMENT_PLANNER_HOST, null) ?? void 0;
626
+ const resolved = typeof behaviour === "function" ? behaviour() : behaviour;
627
+ return RequirementPlannerHost.fromBehaviour(resolved, {
628
+ parent,
629
+ environment: options.environment ?? environment ?? void 0
630
+ });
365
631
  },
366
- subscribe(listener) {
367
- listeners.add(listener);
368
- return () => {
369
- listeners.delete(listener);
370
- };
371
- }
632
+ deps: [[new SecurityDeptOptional(), PARENT_INJECTOR_TOKEN], [new SecurityDeptOptional(), ENVIRONMENT_TOKEN]]
372
633
  };
373
634
  }
635
+ function injectRequirementPlannerHost(options) {
636
+ return options.injector.get(REQUIREMENT_PLANNER_HOST, null);
637
+ }
374
638
  //#endregion
375
- //#region src/signals/signal.ts
639
+ //#region src/auth-coordination/planner/base-planner.ts
376
640
  /**
377
- * Minimal writable signal implementation.
378
- * Snapshot-first: values are immutable snapshots, mutations go through `set()`.
641
+ * Abstract base for all requirement planners.
642
+ *
643
+ * Subclasses supply requirements via {@link buildRequirementList}. Pipeline
644
+ * state lives on {@link RequirementPlan}, produced by {@link buildPlan}.
379
645
  */
380
- function createSignal(initial) {
381
- let current = initial;
382
- const listeners = /* @__PURE__ */ new Set();
383
- return {
384
- get() {
385
- return current;
386
- },
387
- set(value) {
388
- if (Object.is(current, value)) return;
389
- current = value;
390
- for (const listener of listeners) listener();
391
- },
392
- subscribe(listener) {
393
- listeners.add(listener);
394
- return () => {
395
- listeners.delete(listener);
646
+ var BaseRequirementPlanner = class {
647
+ host;
648
+ initialResolutions;
649
+ constructor(host, initialResolutions = []) {
650
+ this.host = host;
651
+ this.initialResolutions = initialResolutions.map((resolution) => Object.freeze({ ...resolution }));
652
+ }
653
+ /**
654
+ * Pipeline step 0: resolve host behaviour and materialize requirements into
655
+ * a {@link RequirementPlan}.
656
+ *
657
+ * Behaviour and the requirement list stay fixed on the returned plan until
658
+ * the next `buildPlan()` call. Resolve host parent chains (and any
659
+ * injection-context capture) here — not between async steps.
660
+ */
661
+ async buildPlan() {
662
+ return {
663
+ planContext: this.planContext,
664
+ environment: await this.host.resolveEnvironment(() => {
665
+ throw new RequirementPlannerError("No environment resolved");
666
+ }),
667
+ checkAuthenticated: await this.host.resolveBehaviourFor("checkAuthenticated", () => {
668
+ throw new RequirementPlannerError("No checkAuthenticated resolved");
669
+ }),
670
+ onUnauthenticated: await this.host.resolveBehaviourFor("onUnauthenticated", () => {
671
+ throw new RequirementPlannerError("No onUnauthenticated behaviour resolved");
672
+ }),
673
+ selectCandidate: await this.host.resolveBehaviourFor("selectCandidate", defaultSelectCandidate),
674
+ requirements: await this.resolveRequirementList(),
675
+ resolutionList: [...this.initialResolutions]
676
+ };
677
+ }
678
+ /**
679
+ * Materialize the effective requirement list (also invoked by {@link buildPlan}).
680
+ */
681
+ async resolveRequirementList() {
682
+ return [...(await this.buildRequirementList()).values()];
683
+ }
684
+ /**
685
+ * Pipeline step 1 — one loop iteration: select a candidate, run
686
+ * `onUnauthenticated`, then reduce `plan` state.
687
+ */
688
+ async runStep(plan) {
689
+ const candidate = await plan.selectCandidate({
690
+ ...plan,
691
+ candidateList: this.createCandidateStream(plan)
692
+ });
693
+ if (!candidate) return { outcome: PipelineOutcome.Settled };
694
+ const action = await plan.onUnauthenticated(candidate, plan);
695
+ return this.reduceFromAction(plan, candidate, action);
696
+ }
697
+ reduceFromAction(plan, candidate, action) {
698
+ if (action === true) {
699
+ this.recordFulfilled(plan, candidate);
700
+ return {
701
+ outcome: PipelineOutcome.Resolved,
702
+ requirement: candidate
396
703
  };
397
704
  }
398
- };
399
- }
705
+ if (action === false) return {
706
+ outcome: PipelineOutcome.Blocked,
707
+ requirement: candidate
708
+ };
709
+ return {
710
+ outcome: PipelineOutcome.Redirect,
711
+ requirement: candidate,
712
+ location: action
713
+ };
714
+ }
715
+ /**
716
+ * Run the full pipeline: {@link buildPlan}, step 1, then loop step 2 until
717
+ * settled or a terminal outcome (`blocked`, `redirect`, or a never-settling
718
+ * action).
719
+ */
720
+ async runUntilSettled() {
721
+ const plan = await this.buildPlan();
722
+ return this.runUntilSettledWithPlan(plan);
723
+ }
724
+ async runUntilSettledWithPlan(plan) {
725
+ while (true) {
726
+ const step = await this.runStep(plan);
727
+ if (step.outcome === PipelineOutcome.Settled) return {
728
+ outcome: PipelineOutcome.Settled,
729
+ resolutions: plan.resolutionList
730
+ };
731
+ if (step.outcome === PipelineOutcome.Resolved) continue;
732
+ if (step.outcome === PipelineOutcome.Blocked) return step;
733
+ if (step.outcome === PipelineOutcome.Redirect) return step;
734
+ }
735
+ }
736
+ async *createCandidateStream(plan) {
737
+ const context = {
738
+ planContext: this.planContext,
739
+ requirements: plan.requirements,
740
+ resolutionList: plan.resolutionList,
741
+ environment: plan.environment
742
+ };
743
+ const checked = plan.requirements.filter((requirement) => !plan.resolutionList.some((resolution) => resolution.requirementId === requirement.id)).map(async (requirement) => {
744
+ return {
745
+ requirement,
746
+ satisfied: await plan.checkAuthenticated(requirement, context)
747
+ };
748
+ });
749
+ for await (const { requirement, satisfied } of promisesToRacedAsyncGenerator(checked)) {
750
+ if (satisfied) {
751
+ this.recordFulfilled(plan, requirement);
752
+ continue;
753
+ }
754
+ yield requirement;
755
+ }
756
+ }
757
+ recordFulfilled(plan, requirement) {
758
+ if (plan.resolutionList.some((resolution) => resolution.requirementId === requirement.id)) return;
759
+ plan.resolutionList.push(Object.freeze({
760
+ requirementId: requirement.id,
761
+ status: ResolutionStatus.Fulfilled
762
+ }));
763
+ }
764
+ };
765
+ //#endregion
766
+ //#region src/auth-coordination/planner/merge-planner.ts
400
767
  /**
401
- * Create a read-only view of a writable signal.
768
+ * A planner whose effective requirements are the ordered union of its source
769
+ * planners. Overlapping ids follow Map semantics (later source wins).
770
+ *
771
+ * @example
772
+ * ```ts
773
+ * const planner = MergeRequirementPlanner.fromPlanners(host, [
774
+ * sessionPlanner,
775
+ * oidcPlanner,
776
+ * ]);
777
+ * ```
402
778
  */
403
- function readonlySignal(signal) {
404
- return {
405
- get: () => signal.get(),
406
- subscribe: (listener) => signal.subscribe(listener)
407
- };
408
- }
779
+ var MergeRequirementPlanner = class MergeRequirementPlanner extends BaseRequirementPlanner {
780
+ sources;
781
+ get planContext() {
782
+ return Object.assign({}, ...this.sources.map((source) => source.planContext));
783
+ }
784
+ constructor(host, sources) {
785
+ super(host);
786
+ this.sources = sources;
787
+ }
788
+ /** Create a merge planner from an ordered list of source planners. */
789
+ static fromPlanners(host, planners) {
790
+ return new MergeRequirementPlanner(host, planners);
791
+ }
792
+ async buildRequirementList() {
793
+ const map = /* @__PURE__ */ new Map();
794
+ for (const source of this.sources) for (const requirement of await source.resolveRequirementList()) map.set(requirement.id, requirement);
795
+ return map;
796
+ }
797
+ };
409
798
  //#endregion
410
- //#region src/validation/index.ts
799
+ //#region src/auth-coordination/planner/route-composition-planner.ts
800
+ /** Fold a matched route chain into its effective requirement list. */
801
+ function foldSegments(segments) {
802
+ const effective = /* @__PURE__ */ new Map();
803
+ for (const segment of segments) resolveEffectiveRequirements({
804
+ composition: segment.composition ?? RequirementsComposition.Merge,
805
+ requirements: segment.requirements
806
+ }, effective);
807
+ return [...effective.values()];
808
+ }
809
+ /** Length of the shared requirement-id prefix between two effective lists. */
810
+ function sharedPrefixLength(previous, next) {
811
+ const limit = Math.min(previous.length, next.length);
812
+ let shared = 0;
813
+ while (shared < limit && previous[shared].id === next[shared].id) shared += 1;
814
+ return shared;
815
+ }
411
816
  /**
412
- * Validate an unknown input against a `@standard-schema`-compatible schema.
817
+ * A planner driven by a matched route chain with composition semantics.
413
818
  *
414
- * This is the foundation-level entry point for SDK validation. It accepts
415
- * any schema implementing `StandardSchemaV1` and returns a discriminated
416
- * `ValidationResult`.
819
+ * Construct via {@link fromRouteSegments} for a matched route chain, or
820
+ * {@link fromRouteTransition} for a transition that preserves shared-prefix
821
+ * resolutions from the previous planner instance.
822
+ */
823
+ var RouteCompositionRequirementPlanner = class RouteCompositionRequirementPlanner extends BaseRequirementPlanner {
824
+ _segments;
825
+ _effective;
826
+ _planContext;
827
+ constructor(host, segments, routeState, preservedResolutions = []) {
828
+ super(host, preservedResolutions);
829
+ this._segments = segments;
830
+ this._effective = foldSegments(segments);
831
+ this._planContext = Object.freeze({ routeState: Object.freeze({ ...routeState }) });
832
+ }
833
+ /** Build a planner from a matched route chain. */
834
+ static fromRouteSegments(host, segments, routeState) {
835
+ return new RouteCompositionRequirementPlanner(host, segments, routeState);
836
+ }
837
+ /**
838
+ * Build a planner for a route transition, preserving resolutions for the
839
+ * shared requirement prefix carried over from `previousPlan`.
840
+ */
841
+ static fromRouteTransition(host, currentSegments, routeState, previousPlanner, previousPlan) {
842
+ const nextEffective = foldSegments(currentSegments);
843
+ const shared = sharedPrefixLength(previousPlanner.effectiveRequirements, nextEffective);
844
+ const sharedIds = new Set(nextEffective.slice(0, shared).map((requirement) => requirement.id));
845
+ const preserved = previousPlan.resolutionList.filter((resolution) => sharedIds.has(resolution.requirementId));
846
+ return new RouteCompositionRequirementPlanner(host, currentSegments, routeState, preserved);
847
+ }
848
+ get planContext() {
849
+ return this._planContext;
850
+ }
851
+ /** The matched route chain backing this planner. */
852
+ get segments() {
853
+ return this._segments;
854
+ }
855
+ /** The folded effective requirement list (composition applied). */
856
+ get effectiveRequirements() {
857
+ return this._effective;
858
+ }
859
+ buildRequirementList() {
860
+ return new Map(this._effective.map((requirement) => [requirement.id, requirement]));
861
+ }
862
+ };
863
+ //#endregion
864
+ //#region src/auth-coordination/planner/static-planner.ts
865
+ /**
866
+ * A planner whose effective requirement list is a fixed array supplied at
867
+ * construction time.
417
868
  *
418
869
  * @example
419
870
  * ```ts
420
- * import { validateWithSchema } from "@securitydept/client";
421
- * import { z } from "zod";
422
- *
423
- * const UserSchema = z.object({ name: z.string() });
424
- * const result = await validateWithSchema(UserSchema, { name: "Alice" });
425
- * if (result.success) {
426
- * console.log(result.value.name);
427
- * } else {
428
- * console.error(result.issues);
429
- * }
871
+ * const planner = StaticRequirementPlanner.fromRequirements(host, [
872
+ * StaticAttrsAuthRequirement.create({ id: "session" }),
873
+ * ] as Readonly<AuthRequirement[]>);
874
+ * const plan = await planner.buildPlan();
875
+ * const step = await planner.runStep(plan);
876
+ * // or: const result = await planner.runUntilSettled();
430
877
  * ```
431
878
  */
432
- async function validateWithSchema(schema, input) {
433
- const result = await schema["~standard"].validate(input);
434
- if ("value" in result) return {
435
- success: true,
436
- value: result.value
879
+ var StaticRequirementPlanner = class StaticRequirementPlanner extends BaseRequirementPlanner {
880
+ staticRequirements;
881
+ get planContext() {
882
+ return {};
883
+ }
884
+ constructor(host, requirements) {
885
+ super(host);
886
+ this.staticRequirements = requirements;
887
+ }
888
+ /** Create a static planner from an ordered requirement list. */
889
+ static fromRequirements(host, requirements) {
890
+ return new StaticRequirementPlanner(host, requirements);
891
+ }
892
+ buildRequirementList() {
893
+ const map = /* @__PURE__ */ new Map();
894
+ for (const requirement of this.staticRequirements) map.set(requirement.id, requirement);
895
+ return map;
896
+ }
897
+ };
898
+ //#endregion
899
+ //#region src/events/create.ts
900
+ /**
901
+ * Create a new event stream from a producer function.
902
+ *
903
+ * The producer receives an observer and should call `next()` to emit values,
904
+ * `error()` to signal an error, and `complete()` to signal completion.
905
+ * It may return a teardown function that will be called on unsubscribe.
906
+ */
907
+ function createEventStream(producer) {
908
+ return new RxEventStream(producer);
909
+ }
910
+ function createEventSubject() {
911
+ return new RxEventSubject();
912
+ }
913
+ function createEventReplaySubject(bufferSize = Infinity) {
914
+ return new RxEventReplaySubject(bufferSize);
915
+ }
916
+ function createEmptyEventStream() {
917
+ return RxEventStream.fromObservableInput(EMPTY);
918
+ }
919
+ function createNeverEventStream() {
920
+ return RxEventStream.fromObservableInput(NEVER);
921
+ }
922
+ //#endregion
923
+ //#region src/events/error.ts
924
+ function isClientErrorEvent(event) {
925
+ if (typeof event !== "object" || event === null) return false;
926
+ if ("error" in event && event.error instanceof ClientError) return true;
927
+ return "payload" in event && typeof event.payload === "object" && event.payload !== null && "error" in event.payload && event.payload.error instanceof ClientError;
928
+ }
929
+ //#endregion
930
+ //#region src/events/interop.ts
931
+ function toEventStream(input) {
932
+ if (isEventStreamTrait(input)) return input;
933
+ return RxEventStream.fromObservableInput(from(input));
934
+ }
935
+ //#endregion
936
+ //#region src/identity/schemas.ts
937
+ const IdentityPrincipalSchema = type({
938
+ subject: "string > 0",
939
+ "displayName?": "string | null | undefined",
940
+ "picture?": "string | null | undefined",
941
+ "issuer?": "string | null | undefined",
942
+ "claims?": "object | null | undefined"
943
+ }).pipe((input) => ({
944
+ subject: input.subject,
945
+ displayName: typeof input.displayName === "string" && input.displayName.trim().length > 0 ? input.displayName : input.subject,
946
+ picture: input.picture ?? void 0,
947
+ issuer: input.issuer ?? void 0,
948
+ claims: input.claims ?? void 0
949
+ }));
950
+ const IdentityPrincipalWireSchema = type({
951
+ subject: "string > 0",
952
+ "display_name?": "string | null | undefined",
953
+ "picture?": "string | null | undefined",
954
+ "issuer?": "string | null | undefined",
955
+ "claims?": "object | null | undefined"
956
+ }).pipe((input) => ({
957
+ subject: input.subject,
958
+ displayName: typeof input.display_name === "string" && input.display_name.trim().length > 0 ? input.display_name : input.subject,
959
+ picture: input.picture ?? void 0,
960
+ issuer: input.issuer ?? void 0,
961
+ claims: input.claims ?? void 0
962
+ }));
963
+ //#endregion
964
+ //#region src/identity/parsers.ts
965
+ function throwIdentityPrincipalParseError(options) {
966
+ throw new ClientError({
967
+ kind: ClientErrorKind.Protocol,
968
+ code: options.code,
969
+ message: `${options.messagePrefix}: ${formatValidationFailure(options.failure) ?? "schema validation failed"}.`,
970
+ source: "identity",
971
+ cause: options.failure.issues
972
+ });
973
+ }
974
+ function parseIdentityPrincipal(input) {
975
+ const result = validateWithSchemaSync(IdentityPrincipalSchema, input);
976
+ if (result.success) return result.value;
977
+ throwIdentityPrincipalParseError({
978
+ code: "identity.invalid_principal",
979
+ messagePrefix: "Identity principal payload is invalid",
980
+ failure: result
981
+ });
982
+ }
983
+ function parseIdentityPrincipalWire(input) {
984
+ const result = validateWithSchemaSync(IdentityPrincipalWireSchema, input);
985
+ if (result.success) return result.value;
986
+ throwIdentityPrincipalParseError({
987
+ code: "identity.invalid_principal_wire",
988
+ messagePrefix: "Identity principal wire payload is invalid",
989
+ failure: result
990
+ });
991
+ }
992
+ function projectIdentityPrincipal(options) {
993
+ const principal = options.principal;
994
+ const subject = principal?.subject ?? options.fallbackSubject ?? "context.anonymous";
995
+ return {
996
+ subject,
997
+ displayName: principal?.displayName?.trim() || options.fallbackDisplayName || subject,
998
+ picture: principal?.picture,
999
+ issuer: principal?.issuer ?? options.fallbackIssuer,
1000
+ claims: principal?.claims ?? options.fallbackClaims
437
1001
  };
1002
+ }
1003
+ //#endregion
1004
+ //#region src/protocol/jwt/error.ts
1005
+ const JwtDecodeErrorCode = {
1006
+ InvalidFormat: "protocol.jwt.invalid_format",
1007
+ InvalidPayload: "protocol.jwt.invalid_payload",
1008
+ InvalidClaim: "protocol.jwt.invalid_claim"
1009
+ };
1010
+ var JwtDecodeError = class extends ClientError {
1011
+ code;
1012
+ constructor(options) {
1013
+ super({
1014
+ kind: ClientErrorKind.Protocol,
1015
+ code: options.code,
1016
+ message: options.message,
1017
+ source: "protocol.jwt",
1018
+ cause: options.cause
1019
+ });
1020
+ this.name = "JwtDecodeError";
1021
+ this.code = options.code;
1022
+ }
1023
+ };
1024
+ //#endregion
1025
+ //#region src/protocol/jwt/decode.ts
1026
+ function decodeJwtPayload(jwt, options = {}) {
1027
+ const label = options.label ?? "JWT";
1028
+ const parts = jwt.split(".");
1029
+ if (parts.length !== 3) throw new JwtDecodeError({
1030
+ code: JwtDecodeErrorCode.InvalidFormat,
1031
+ message: `${label} must use compact JWS serialization`
1032
+ });
1033
+ const claims = parseBase64UrlJsonObject(parts[1], `${label} payload`);
1034
+ validateRegisteredClaimTypes(claims, label);
1035
+ return claims;
1036
+ }
1037
+ function parseBase64UrlJsonObject(value, label) {
1038
+ let parsed;
1039
+ try {
1040
+ parsed = JSON.parse(decodeBase64UrlUtf8(value));
1041
+ } catch (cause) {
1042
+ throw new JwtDecodeError({
1043
+ code: JwtDecodeErrorCode.InvalidPayload,
1044
+ message: `${label} must be base64url-encoded JSON`,
1045
+ cause
1046
+ });
1047
+ }
1048
+ if (!isJsonObject(parsed)) throw new JwtDecodeError({
1049
+ code: JwtDecodeErrorCode.InvalidPayload,
1050
+ message: `${label} must be a JSON object`
1051
+ });
1052
+ return parsed;
1053
+ }
1054
+ function decodeBase64UrlUtf8(value) {
1055
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
1056
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
1057
+ const binary = atob(padded);
1058
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
1059
+ return new TextDecoder().decode(bytes);
1060
+ }
1061
+ function validateRegisteredClaimTypes(claims, label) {
1062
+ if (claims.exp !== void 0 && typeof claims.exp !== "number") throw new JwtDecodeError({
1063
+ code: JwtDecodeErrorCode.InvalidClaim,
1064
+ message: `${label} "exp" claim must be a number`
1065
+ });
1066
+ if (claims.iat !== void 0 && typeof claims.iat !== "number") throw new JwtDecodeError({
1067
+ code: JwtDecodeErrorCode.InvalidClaim,
1068
+ message: `${label} "iat" claim must be a number`
1069
+ });
1070
+ if (claims.nbf !== void 0 && typeof claims.nbf !== "number") throw new JwtDecodeError({
1071
+ code: JwtDecodeErrorCode.InvalidClaim,
1072
+ message: `${label} "nbf" claim must be a number`
1073
+ });
1074
+ if (claims.iss !== void 0 && typeof claims.iss !== "string") throw new JwtDecodeError({
1075
+ code: JwtDecodeErrorCode.InvalidClaim,
1076
+ message: `${label} "iss" claim must be a string`
1077
+ });
1078
+ if (claims.aud !== void 0 && typeof claims.aud !== "string" && !isStringArray(claims.aud)) throw new JwtDecodeError({
1079
+ code: JwtDecodeErrorCode.InvalidClaim,
1080
+ message: `${label} "aud" claim must be a string or string array`
1081
+ });
1082
+ }
1083
+ function isJsonObject(value) {
1084
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1085
+ }
1086
+ function isStringArray(value) {
1087
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
1088
+ }
1089
+ //#endregion
1090
+ //#region src/scheduling/duration.ts
1091
+ /**
1092
+ * Parse a human-readable duration string (for example `5m` or `300s`) into
1093
+ * milliseconds.
1094
+ */
1095
+ function parseDurationToMs(duration) {
1096
+ const match = duration.match(/^(\d+(?:\.\d+)?)\s*(s|ms|m|h)$/);
1097
+ if (!match) return 0;
1098
+ const value = Number(match[1]);
1099
+ switch (match[2]) {
1100
+ case "ms": return value;
1101
+ case "s": return value * 1e3;
1102
+ case "m": return value * 60 * 1e3;
1103
+ case "h": return value * 60 * 60 * 1e3;
1104
+ default: return 0;
1105
+ }
1106
+ }
1107
+ //#endregion
1108
+ //#region src/storage/json-codec.ts
1109
+ /** Default JSON codec. */
1110
+ function createJsonCodec() {
438
1111
  return {
439
- success: false,
440
- issues: result.issues
1112
+ encode(value) {
1113
+ return JSON.stringify(value);
1114
+ },
1115
+ decode(raw) {
1116
+ return JSON.parse(raw);
1117
+ }
441
1118
  };
442
1119
  }
1120
+ //#endregion
1121
+ //#region src/storage/ephemeral-flow-store.ts
1122
+ function resolveStorageKey(keyPrefix, key) {
1123
+ return keyPrefix.length > 0 ? `${keyPrefix}:${key}` : key;
1124
+ }
1125
+ function requireRecordStoreTake(store) {
1126
+ if (!store.take) throw new ClientError({
1127
+ kind: ClientErrorKind.Configuration,
1128
+ code: "storage.ephemeral.take_unavailable",
1129
+ message: "Ephemeral flow stores require StorageTrait.take() for atomic single-consume semantics",
1130
+ source: "storage.ephemeral"
1131
+ });
1132
+ return store.take.bind(store);
1133
+ }
1134
+ async function executeStorageOperation(operation) {
1135
+ try {
1136
+ return await operation();
1137
+ } catch (error) {
1138
+ if (error instanceof ClientError) throw error;
1139
+ throw new ClientError({
1140
+ kind: ClientErrorKind.Storage,
1141
+ code: "storage.ephemeral.io_failed",
1142
+ message: "The ephemeral flow store operation failed",
1143
+ source: "storage.ephemeral",
1144
+ cause: error
1145
+ });
1146
+ }
1147
+ }
1148
+ function decodeStoredValue(codec, value) {
1149
+ try {
1150
+ return codec.decode(value);
1151
+ } catch (error) {
1152
+ throw new ClientError({
1153
+ kind: ClientErrorKind.Protocol,
1154
+ code: "storage.ephemeral.invalid_payload",
1155
+ message: "The ephemeral flow store payload is invalid",
1156
+ source: "storage.ephemeral",
1157
+ cause: error
1158
+ });
1159
+ }
1160
+ }
443
1161
  /**
444
- * Validate an unknown input synchronously against a `@standard-schema`-compatible schema.
1162
+ * Create a typed ephemeral flow-state store backed by a low-level `StorageTrait`.
445
1163
  *
446
- * Only use this when you know the schema's validate function returns synchronously.
447
- * Throws if the schema returns a Promise.
1164
+ * Intended for short-lived browser/session coordination state such as pending
1165
+ * redirects or callback fragments.
448
1166
  */
449
- function validateWithSchemaSync(schema, input) {
450
- const result = schema["~standard"].validate(input);
451
- if (result instanceof Promise) throw new Error("validateWithSchemaSync: schema returned a Promise. Use validateWithSchema instead.");
452
- if ("value" in result) return {
453
- success: true,
454
- value: result.value
455
- };
1167
+ function createEphemeralFlowStore(options) {
1168
+ const codec = options.codec ?? createJsonCodec();
1169
+ const storeTake = requireRecordStoreTake(options.store);
1170
+ async function load() {
1171
+ const raw = await executeStorageOperation(() => options.store.get(options.key));
1172
+ return raw === null ? null : decodeStoredValue(codec, raw);
1173
+ }
1174
+ async function save(value) {
1175
+ await executeStorageOperation(() => options.store.set(options.key, codec.encode(value)));
1176
+ }
1177
+ async function consume() {
1178
+ const raw = await executeStorageOperation(() => storeTake(options.key));
1179
+ return raw === null ? null : decodeStoredValue(codec, raw);
1180
+ }
1181
+ async function clear() {
1182
+ await executeStorageOperation(() => options.store.remove(options.key));
1183
+ }
456
1184
  return {
457
- success: false,
458
- issues: result.issues
1185
+ load,
1186
+ save,
1187
+ consume,
1188
+ clear
459
1189
  };
460
1190
  }
461
1191
  /**
462
- * Create a minimal `@standard-schema`-compatible schema without requiring
463
- * an external validation library.
464
- *
465
- * This is useful for SDK-internal schemas where pulling in zod/valibot
466
- * would be excessive. For adopter-facing schemas, prefer a real validation
467
- * library.
468
- *
469
- * @example
470
- * ```ts
471
- * import { createSchema, validateWithSchema } from "@securitydept/client";
472
- *
473
- * const NameSchema = createSchema<unknown, { name: string }>({
474
- * validate(input) {
475
- * if (typeof input === "object" && input !== null && "name" in input &&
476
- * typeof (input as any).name === "string") {
477
- * return { value: { name: (input as any).name } };
478
- * }
479
- * return { issues: [{ message: "Expected object with string 'name'" }] };
480
- * },
481
- * });
1192
+ * Create a typed keyed ephemeral flow-state store backed by a low-level
1193
+ * `StorageTrait`.
482
1194
  *
483
- * const result = await validateWithSchema(NameSchema, { name: "Alice" });
484
- * ```
1195
+ * Intended for short-lived browser/session coordination state where multiple
1196
+ * pending records must coexist and be consumed independently.
1197
+ */
1198
+ function createKeyedEphemeralFlowStore(options) {
1199
+ const codec = options.codec ?? createJsonCodec();
1200
+ const storeTake = requireRecordStoreTake(options.store);
1201
+ async function load(key) {
1202
+ const raw = await executeStorageOperation(() => options.store.get(resolveStorageKey(options.keyPrefix, key)));
1203
+ return raw === null ? null : decodeStoredValue(codec, raw);
1204
+ }
1205
+ async function save(key, value) {
1206
+ await executeStorageOperation(() => options.store.set(resolveStorageKey(options.keyPrefix, key), codec.encode(value)));
1207
+ }
1208
+ async function take(key) {
1209
+ const raw = await executeStorageOperation(() => storeTake(resolveStorageKey(options.keyPrefix, key)));
1210
+ return raw === null ? null : decodeStoredValue(codec, raw);
1211
+ }
1212
+ async function clear(key) {
1213
+ await executeStorageOperation(() => options.store.remove(resolveStorageKey(options.keyPrefix, key)));
1214
+ }
1215
+ return {
1216
+ load,
1217
+ save,
1218
+ take,
1219
+ clear
1220
+ };
1221
+ }
1222
+ //#endregion
1223
+ //#region src/struct/async-lock.ts
1224
+ const OnceAsyncLockState = {
1225
+ Init: "init",
1226
+ Running: "running",
1227
+ Success: "success",
1228
+ Error: "error"
1229
+ };
1230
+ function createOnceAsyncLockCallable(fn) {
1231
+ const internalStateSignal = createSignal({ state: OnceAsyncLockState.Init });
1232
+ const coreFn = async function(...args) {
1233
+ const internalState = internalStateSignal.get();
1234
+ if (internalState.state === OnceAsyncLockState.Running) return internalState.promise;
1235
+ if (internalState.state === OnceAsyncLockState.Success) return internalState.data;
1236
+ let resolvePromise;
1237
+ let rejectPromise;
1238
+ const promise = new Promise((res, rej) => {
1239
+ resolvePromise = res;
1240
+ rejectPromise = rej;
1241
+ });
1242
+ promise.catch(() => void 0);
1243
+ internalStateSignal.set({
1244
+ state: OnceAsyncLockState.Running,
1245
+ promise
1246
+ });
1247
+ try {
1248
+ const result = await fn.apply(this, args);
1249
+ internalStateSignal.set({
1250
+ state: OnceAsyncLockState.Success,
1251
+ data: result
1252
+ });
1253
+ resolvePromise(result);
1254
+ return result;
1255
+ } catch (err) {
1256
+ internalStateSignal.set({
1257
+ state: OnceAsyncLockState.Error,
1258
+ error: err
1259
+ });
1260
+ rejectPromise(err);
1261
+ throw err;
1262
+ }
1263
+ };
1264
+ return new Proxy(coreFn, {
1265
+ get(target, prop, receiver) {
1266
+ const internalState = internalStateSignal.get();
1267
+ if (prop in internalState) return Reflect.get(internalState, prop);
1268
+ if (prop === "get" || prop === "watchStream" || prop === SYMBOL_OBSERVABLE) {
1269
+ const value = Reflect.get(internalStateSignal, prop, internalStateSignal);
1270
+ return typeof value === "function" ? value.bind(internalStateSignal) : value;
1271
+ }
1272
+ return Reflect.get(target, prop, receiver);
1273
+ },
1274
+ set() {
1275
+ return false;
1276
+ }
1277
+ });
1278
+ }
1279
+ //#endregion
1280
+ //#region src/transport/url.ts
1281
+ /**
1282
+ * Returns `true` for loopback HTTP URLs that browsers and local tooling often
1283
+ * permit during development flows.
485
1284
  */
486
- function createSchema(options) {
487
- return { "~standard": {
488
- version: 1,
489
- vendor: "securitydept",
490
- validate: options.validate
491
- } };
1285
+ function isLoopbackHttpUrl(url) {
1286
+ return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]");
492
1287
  }
493
1288
  //#endregion
494
- export { ClientError, ClientErrorKind, ClientErrorSource, ErrorPresentationTone, EventSourceKind, FetchTransportRedirectKind, LogLevel, OperationTraceEventType, PromiseSettlementKind, UserRecovery, concatMap, createCancellationTokenSource, createComputed, createConsoleLogger, createDefaultClock, createDefaultScheduler, createEphemeralFlowStore, createEventStream, createInMemoryRecordStore, createJsonCodec, createKeyedEphemeralFlowStore, createLinkedCancellationToken, createNoopLogger, createOperationTracer, createReplaySubject, createRuntime, createSchema, createSignal, createSubject, createTraceTimelineStore, debounceTime, exhaustMap, filter, finalize, fromEventPattern, fromPromise, fromRxObservable, fromSignal, interval, map, merge, normalizeAuthenticatedPrincipal, normalizeAuthenticatedPrincipalWire, pipe, projectAuthenticatedPrincipal, readErrorPresentationDescriptor, readonlySignal, scheduleAt, share, shareReplay, switchMap, takeUntil, tap, throttleTime, timer, toRxObservable, validateWithSchema, validateWithSchemaSync, withLatestFromSignal };
1289
+ export { Base64CompatErrorCode, BaseRequirementPlanner, BaseURIStringSchema, CLIENT_ERROR_SPAN_ATTRIBUTE_PROVIDER_ID, ClientError, ClientErrorKind, ClientErrorSource, ConsoleTracingSubscriber, ENVIRONMENT_TOKEN, ErrorPresentationTone, EventSourceKind, FetchTransportRedirectKind, IDLE_CALLBACK_TRAIT_TOKEN, INJECTOR_TOKEN, IdentityPrincipalSchema, IdentityPrincipalWireSchema, JwtDecodeError, JwtDecodeErrorCode, MergeRequirementPlanner, OnceAsyncLockState, OperationSpan, OperationTraceEventType, PAGE_LIFECYCLE_TRAIT_TOKEN, PERSISTENT_STORAGE_TRAIT_TOKEN, POPUP_TRAIT_TOKEN, PipelineOutcome, PlanStatus, PopupAttachFailureReason, PopupClientWindowHandle, PopupErrorCode, PopupServerWindowHandle, REALM_STORAGE_TRAIT_TOKEN, REQUIREMENT_PLANNER_HOST, ROUTER_TRAIT_TOKEN, RequirementPlannerError, RequirementPlannerHost, RequirementsComposition, ResolutionStatus, ResourceError, ResourceErrorCode, ResourceSnapshotUpdateKind, ResourceStatus, RouteCompositionRequirementPlanner, RouterNavigationIntent, RouterNavigationMode, SECURITYDEPT_COMPAT_FRAGMENT_VERSION, SECURITYDEPT_ROUTE_METADATA_KEY, SESSION_STORAGE_TRAIT_TOKEN, SPAN_TRAIT_TOKEN, SYMBOL_ASYNC_DISPOSE, SYMBOL_DISPOSABLE_STACK, SYMBOL_DISPOSE, SYMBOL_OBSERVABLE, SecurityDeptOptional, SecuritydeptDestroyRef, SecuritydeptInjectionToken, SecuritydeptInjector, SimpleDisposableStack, SpanSharedAttributeName, StaticAttrsAuthRequirement, StaticRequirementPlanner, StdDisposableStack, StorageChangeEventOrigin, TIME_TRAIT_TOKEN, TRACING_SPAN_ATTRIBUTE_PROVIDER_ID, TRACING_TRAIT_TOKEN, TRANSPORT_TRAIT_TOKEN, TraceTimelineStore, TracingLevel, TransportErrorCode, UriParseError, UriReferenceString, UriReferenceStringSchema, UriRelativeString, UriRelativeStringSchema, UriString, UriStringSchema, UserRecovery, abortSignalToCancellationToken, abortSignalToEventStream, appendOrReplaceCompatFragment, cancellationTokenToAbortSignal, createAuthorizationInterceptedFetch, createBaseTransportForStdFetch, createCancellationTokenSource, createComputed, createConsoleTracingSubscriber, createDisposableStack, createEmptyEventStream, createEphemeralFlowStore, createEventReplaySubject, createEventStream, createEventSubject, createFoundationEnvironment, createInMemoryRecordStore, createJsonCodec, createJsonRpcClient, createJsonRpcServer, createJsonRpcServerAndClient, createKeyedEphemeralFlowStore, createLinkedCancellationToken, createNeverEventStream, createOnceAsyncLockCallable, createProviderIfTokenMissing, createResource, createRootSpan, createSecuritydeptDestroyRef, createSignal, createTimeForStd, createTraceTimelineStore, createTracing, decodeBase64ToUint8Array, decodeJwtPayload, defaultSelectCandidate, defineInstrumentMethodDecorator, describeError, encodeUint8ArrayToBase64, flattenResourceSnapshot, formatClientErrorContext, formatValidationFailure, getSecuritydeptProviderToken, inject, injectDisposableStackFrom, injectRequirementPlannerHost, isCancellationTokenTrait, isClientErrorEvent, isCompatFragmentBlock, isInteropObservableTrait, isLoopbackHttpUrl, isRouteBehaviourContextExtra, isRouteStateSnapshotTrait, mapResource, normalizeAbortError, notMissingProvider, parseCompatFragment, parseDurationToMs, parseIdentityPrincipal, parseIdentityPrincipalWire, projectIdentityPrincipal, promisesToRacedAsyncGenerator, provideRequirementPlannerHost, readErrorPresentationDescriptor, readPopupErrorPresentationDescriptor, readSecuritydeptRouteMetadata, readonlySignal, reduceResourceSnapshot, resolveEffectiveRequirements, resourceFromSnapshots, runInInjectionContext, runOperation, takeCompatFragment, takeCompatFragmentFromRouter, throwValidationClientError, toEventStream, tryInjectInInjectionContext, validateTraitInput, validateWithSchema, validateWithSchemaSync, whenResourceSnapshotValue, withDisposableStack, writeSecuritydeptRouteMetadata };
495
1290
 
496
1291
  //# sourceMappingURL=index.mjs.map