@cplieger/access 0.0.0-stage → 1.0.0-dev.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.
package/src/client.ts ADDED
@@ -0,0 +1,351 @@
1
+ import type {
2
+ ApiResult,
3
+ FetchInstance,
4
+ HttpMethod,
5
+ RequestOptions,
6
+ } from "@cplieger/reactive/fetch";
7
+ import {
8
+ list,
9
+ parseCeremonyResponse,
10
+ parseKeyCreatedResponse,
11
+ parseKeyView,
12
+ parseMeResponse,
13
+ parseOIDCStartResponse,
14
+ parsePasskeyView,
15
+ parsePendingLinkResponse,
16
+ parseRecoveryResponse,
17
+ parseSecurityLogView,
18
+ parseSessionView,
19
+ parseSignedInResponse,
20
+ parseStateResponse,
21
+ parseTokenResponse,
22
+ parseTokenView,
23
+ parseUserView,
24
+ parseWebAuthnSignals,
25
+ } from "./wire.js";
26
+ import type {
27
+ CeremonyResponse,
28
+ InviteAcceptRequest,
29
+ InviteRequest,
30
+ KeyCreatedResponse,
31
+ KeyView,
32
+ LoginRequest,
33
+ MeResponse,
34
+ NameRequest,
35
+ NewKeyRequest,
36
+ NewUserRequest,
37
+ OIDCStartResponse,
38
+ PasskeyView,
39
+ PasswordChangeRequest,
40
+ PasswordRemoveRequest,
41
+ PasswordRequest,
42
+ PendingLinkResponse,
43
+ ProfileRequest,
44
+ RecoverPasswordRequest,
45
+ RecoveryResponse,
46
+ RedirectRequest,
47
+ SecurityLogView,
48
+ SessionView,
49
+ SetupPasskeyBeginRequest,
50
+ SetupPasswordRequest,
51
+ SignedInResponse,
52
+ StateResponse,
53
+ TTLRequest,
54
+ TokenRequest,
55
+ TokenResponse,
56
+ TokenView,
57
+ UserUpdateRequest,
58
+ UserView,
59
+ Validator,
60
+ WebAuthnSignals,
61
+ } from "./wire.js";
62
+
63
+ export interface AuthClientOptions {
64
+ /** The app's instance: its credentials mode, base URL and error hook apply to every call. */
65
+ readonly fetch: FetchInstance;
66
+ /** accesshttp.Config.Prefix. */
67
+ readonly prefix: string;
68
+ /** accesshttp.Config.OIDCPrefix. */
69
+ readonly oidcPrefix: string;
70
+ }
71
+
72
+ /**
73
+ * A finish route's body as the ceremonies build it: clientExtensionResults carries only an output
74
+ * the begin options requested (credProps on the create leg), because go-webauthn refuses any other.
75
+ */
76
+ export type PublicKeyCredentialJSON =
77
+ | {
78
+ readonly id: string;
79
+ readonly rawId: string;
80
+ readonly type: string;
81
+ readonly authenticatorAttachment?: string;
82
+ readonly response: {
83
+ readonly attestationObject: string;
84
+ readonly clientDataJSON: string;
85
+ readonly transports?: readonly string[];
86
+ };
87
+ readonly clientExtensionResults: { readonly credProps?: { readonly rk?: boolean } };
88
+ }
89
+ | {
90
+ readonly id: string;
91
+ readonly rawId: string;
92
+ readonly type: string;
93
+ readonly authenticatorAttachment?: string;
94
+ readonly response: {
95
+ readonly authenticatorData: string;
96
+ readonly clientDataJSON: string;
97
+ readonly signature: string;
98
+ readonly userHandle?: string;
99
+ };
100
+ readonly clientExtensionResults: { readonly credProps?: { readonly rk?: boolean } };
101
+ };
102
+
103
+ export interface RecoverFlags {
104
+ readonly keepSessions: boolean;
105
+ readonly keepKeys: boolean;
106
+ }
107
+
108
+ /**
109
+ * One method per JSON route of accesshttp, each decoded through wire.ts: a 2xx body that is not
110
+ * the route's type is the fetch instance's "decode" failure. The two redirect routes are
111
+ * navigations, built by oidcSignInURL.
112
+ */
113
+ export interface AuthClient {
114
+ readonly prefix: string;
115
+ readonly oidcPrefix: string;
116
+ state(): Promise<ApiResult<StateResponse>>;
117
+ setupPassword(r: SetupPasswordRequest): Promise<ApiResult<SignedInResponse>>;
118
+ setupPasskeyBegin(r: SetupPasskeyBeginRequest): Promise<ApiResult<CeremonyResponse>>;
119
+ setupPasskeyFinish(
120
+ ceremony: string,
121
+ credential: PublicKeyCredentialJSON,
122
+ ): Promise<ApiResult<SignedInResponse>>;
123
+ loginPassword(r: LoginRequest): Promise<ApiResult<SignedInResponse>>;
124
+ loginPasskeyBegin(o?: {
125
+ readonly mediation?: "conditional";
126
+ }): Promise<ApiResult<CeremonyResponse>>;
127
+ loginPasskeyFinish(
128
+ ceremony: string,
129
+ credential: PublicKeyCredentialJSON,
130
+ ): Promise<ApiResult<SignedInResponse>>;
131
+ logout(): Promise<ApiResult<void>>;
132
+ oidcStartLink(r: RedirectRequest): Promise<ApiResult<OIDCStartResponse>>;
133
+ oidcStartStepUp(r: RedirectRequest): Promise<ApiResult<OIDCStartResponse>>;
134
+ oidcPending(): Promise<ApiResult<PendingLinkResponse>>;
135
+ oidcLinkPending(): Promise<ApiResult<MeResponse>>;
136
+ oidcUnlink(): Promise<ApiResult<void>>;
137
+ recoveryStart(r: TokenRequest): Promise<ApiResult<RecoveryResponse>>;
138
+ recoveryPassword(r: RecoverPasswordRequest): Promise<ApiResult<void>>;
139
+ recoveryPasskeyBegin(): Promise<ApiResult<CeremonyResponse>>;
140
+ recoveryPasskeyFinish(
141
+ ceremony: string,
142
+ credential: PublicKeyCredentialJSON,
143
+ o: RecoverFlags,
144
+ ): Promise<ApiResult<void>>;
145
+ inviteAcceptPassword(r: InviteAcceptRequest): Promise<ApiResult<SignedInResponse>>;
146
+ me(): Promise<ApiResult<MeResponse>>;
147
+ updateProfile(r: ProfileRequest): Promise<ApiResult<MeResponse>>;
148
+ reenableMethod(method: string): Promise<ApiResult<void>>;
149
+ stepUpPassword(r: PasswordRequest): Promise<ApiResult<void>>;
150
+ stepUpPasskeyBegin(): Promise<ApiResult<CeremonyResponse>>;
151
+ stepUpPasskeyFinish(
152
+ ceremony: string,
153
+ credential: PublicKeyCredentialJSON,
154
+ ): Promise<ApiResult<void>>;
155
+ setPassword(r: PasswordChangeRequest): Promise<ApiResult<void>>;
156
+ removePassword(r: PasswordRemoveRequest): Promise<ApiResult<void>>;
157
+ passkeys(): Promise<ApiResult<readonly PasskeyView[]>>;
158
+ passkeyRegisterBegin(): Promise<ApiResult<CeremonyResponse>>;
159
+ passkeyRegisterFinish(
160
+ ceremony: string,
161
+ credential: PublicKeyCredentialJSON,
162
+ ): Promise<ApiResult<PasskeyView>>;
163
+ renamePasskey(id: number, r: NameRequest): Promise<ApiResult<void>>;
164
+ removePasskey(id: number): Promise<ApiResult<void>>;
165
+ webauthnSignals(): Promise<ApiResult<WebAuthnSignals>>;
166
+ sessions(): Promise<ApiResult<readonly SessionView[]>>;
167
+ renameSession(id: string, r: NameRequest): Promise<ApiResult<void>>;
168
+ revokeSession(id: string): Promise<ApiResult<void>>;
169
+ revokeOtherSessions(): Promise<ApiResult<void>>;
170
+ keys(): Promise<ApiResult<readonly KeyView[]>>;
171
+ createKey(r: NewKeyRequest): Promise<ApiResult<KeyCreatedResponse>>;
172
+ revokeKey(id: number): Promise<ApiResult<void>>;
173
+ /** key lists the users any key in that key's lineage acted on (GET /users?key=). */
174
+ adminUsers(o?: { readonly key?: number }): Promise<ApiResult<readonly UserView[]>>;
175
+ adminCreateUser(r: NewUserRequest): Promise<ApiResult<UserView>>;
176
+ adminUpdateUser(id: number, r: UserUpdateRequest): Promise<ApiResult<UserView>>;
177
+ adminDeleteUser(id: number): Promise<ApiResult<void>>;
178
+ adminIssueRecovery(id: number, r: TTLRequest): Promise<ApiResult<TokenResponse>>;
179
+ adminInvite(r: InviteRequest): Promise<ApiResult<TokenResponse>>;
180
+ adminTokens(): Promise<ApiResult<readonly TokenView[]>>;
181
+ adminKeys(): Promise<ApiResult<readonly KeyView[]>>;
182
+ adminRevokeKey(id: number): Promise<ApiResult<void>>;
183
+ /**
184
+ * Newest first, or oldest first when oldestFirst is true; after and before are bounds only.
185
+ * kinds empty or absent is every kind.
186
+ */
187
+ adminSecurityLog(q?: {
188
+ readonly user?: number;
189
+ readonly actor?: number;
190
+ readonly actorKey?: number;
191
+ readonly after?: number;
192
+ readonly before?: number;
193
+ readonly kinds?: readonly string[];
194
+ readonly limit?: number;
195
+ readonly oldestFirst?: boolean;
196
+ }): Promise<ApiResult<SecurityLogView>>;
197
+ }
198
+
199
+ // The fetch instance's decoder contract is to throw on a mismatch, which it turns into a
200
+ // "decode" failure; a wire.ts validator answers undefined instead.
201
+ function decoder<T>(validate: Validator<T>): (v: unknown) => T {
202
+ return (v) => {
203
+ const out = validate(v);
204
+ if (out === undefined) {
205
+ throw new TypeError("response body is not the route's wire type");
206
+ }
207
+ return out;
208
+ };
209
+ }
210
+
211
+ function query(params: Readonly<Record<string, string | undefined>>): string {
212
+ const q = new URLSearchParams();
213
+ for (const [k, v] of Object.entries(params)) {
214
+ if (v !== undefined) {
215
+ q.set(k, v);
216
+ }
217
+ }
218
+ const s = q.toString();
219
+ return s === "" ? "" : `?${s}`;
220
+ }
221
+
222
+ const seg = encodeURIComponent;
223
+ const num = (n: number | undefined): string | undefined =>
224
+ n === undefined ? undefined : String(n);
225
+
226
+ export function createAuthClient(opts: AuthClientOptions): AuthClient {
227
+ const { fetch: f, prefix, oidcPrefix } = opts;
228
+
229
+ function json<T>(
230
+ method: HttpMethod,
231
+ path: string,
232
+ validate: Validator<T>,
233
+ o: RequestOptions<T> = {},
234
+ ): Promise<ApiResult<T>> {
235
+ return f.requestRaw<T>(method, path, { ...o, decoder: decoder(validate) });
236
+ }
237
+ function empty(method: HttpMethod, path: string, body?: unknown): Promise<ApiResult<void>> {
238
+ return f.requestRaw<undefined>(method, path, {
239
+ ignoreBody: true,
240
+ ...(body === undefined ? {} : { body }),
241
+ });
242
+ }
243
+ function finish(
244
+ ceremony: string,
245
+ credential: PublicKeyCredentialJSON,
246
+ ): { readonly body: unknown; readonly headers: Readonly<Record<string, string>> } {
247
+ return { body: credential, headers: { "X-Auth-Ceremony": ceremony } };
248
+ }
249
+ const p = (path: string): string => prefix + path;
250
+ const o = (path: string): string => oidcPrefix + path;
251
+ const passkeyList = list(parsePasskeyView);
252
+ const sessionList = list(parseSessionView);
253
+ const keyList = list(parseKeyView);
254
+ const userList = list(parseUserView);
255
+ const tokenList = list(parseTokenView);
256
+
257
+ return {
258
+ prefix,
259
+ oidcPrefix,
260
+ state: () => json("GET", p("/state"), parseStateResponse),
261
+ setupPassword: (r) => json("POST", p("/setup/password"), parseSignedInResponse, { body: r }),
262
+ setupPasskeyBegin: (r) =>
263
+ json("POST", p("/setup/passkey/begin"), parseCeremonyResponse, { body: r }),
264
+ setupPasskeyFinish: (c, cred) =>
265
+ json("POST", p("/setup/passkey/finish"), parseSignedInResponse, finish(c, cred)),
266
+ loginPassword: (r) => json("POST", p("/login/password"), parseSignedInResponse, { body: r }),
267
+ loginPasskeyBegin: (q) =>
268
+ json(
269
+ "POST",
270
+ p(`/login/passkey/begin${query({ mediation: q?.mediation })}`),
271
+ parseCeremonyResponse,
272
+ ),
273
+ loginPasskeyFinish: (c, cred) =>
274
+ json("POST", p("/login/passkey/finish"), parseSignedInResponse, finish(c, cred)),
275
+ logout: () => empty("POST", p("/logout")),
276
+ oidcStartLink: (r) => json("POST", o("/start/link"), parseOIDCStartResponse, { body: r }),
277
+ oidcStartStepUp: (r) => json("POST", o("/start/stepup"), parseOIDCStartResponse, { body: r }),
278
+ oidcPending: () => json("GET", p("/oidc/pending"), parsePendingLinkResponse),
279
+ oidcLinkPending: () => json("POST", p("/oidc/link"), parseMeResponse),
280
+ oidcUnlink: () => empty("DELETE", p("/oidc/link")),
281
+ recoveryStart: (r) => json("POST", p("/recovery"), parseRecoveryResponse, { body: r }),
282
+ recoveryPassword: (r) => empty("POST", p("/recovery/password"), r),
283
+ recoveryPasskeyBegin: () => json("POST", p("/recovery/passkey/begin"), parseCeremonyResponse),
284
+ recoveryPasskeyFinish: (c, cred, flags) =>
285
+ f.requestRaw<undefined>(
286
+ "POST",
287
+ p(
288
+ `/recovery/passkey/finish${query({
289
+ keep_sessions: flags.keepSessions ? "true" : undefined,
290
+ keep_keys: flags.keepKeys ? "true" : undefined,
291
+ })}`,
292
+ ),
293
+ { ...finish(c, cred), ignoreBody: true },
294
+ ),
295
+ inviteAcceptPassword: (r) =>
296
+ json("POST", p("/invite/password"), parseSignedInResponse, { body: r }),
297
+ me: () => json("GET", p("/me"), parseMeResponse),
298
+ updateProfile: (r) => json("PUT", p("/profile"), parseMeResponse, { body: r }),
299
+ reenableMethod: (m) => empty("POST", p(`/methods/${seg(m)}/reenable`)),
300
+ stepUpPassword: (r) => empty("POST", p("/stepup/password"), r),
301
+ stepUpPasskeyBegin: () => json("POST", p("/stepup/passkey/begin"), parseCeremonyResponse),
302
+ stepUpPasskeyFinish: (c, cred) =>
303
+ f.requestRaw<undefined>("POST", p("/stepup/passkey/finish"), {
304
+ ...finish(c, cred),
305
+ ignoreBody: true,
306
+ }),
307
+ setPassword: (r) => empty("PUT", p("/password"), r),
308
+ removePassword: (r) => empty("DELETE", p("/password"), r),
309
+ passkeys: () => json("GET", p("/passkeys"), passkeyList),
310
+ passkeyRegisterBegin: () => json("POST", p("/passkeys/begin"), parseCeremonyResponse),
311
+ passkeyRegisterFinish: (c, cred) =>
312
+ json("POST", p("/passkeys/finish"), parsePasskeyView, finish(c, cred)),
313
+ renamePasskey: (id, r) => empty("PUT", p(`/passkeys/${String(id)}`), r),
314
+ removePasskey: (id) => empty("DELETE", p(`/passkeys/${String(id)}`)),
315
+ webauthnSignals: () => json("GET", p("/webauthn/signals"), parseWebAuthnSignals),
316
+ sessions: () => json("GET", p("/sessions"), sessionList),
317
+ renameSession: (id, r) => empty("PUT", p(`/sessions/${seg(id)}`), r),
318
+ revokeSession: (id) => empty("DELETE", p(`/sessions/${seg(id)}`)),
319
+ revokeOtherSessions: () => empty("POST", p("/sessions/revoke-others")),
320
+ keys: () => json("GET", p("/apikeys"), keyList),
321
+ createKey: (r) => json("POST", p("/apikeys"), parseKeyCreatedResponse, { body: r }),
322
+ revokeKey: (id) => empty("DELETE", p(`/apikeys/${String(id)}`)),
323
+ adminUsers: (q) => json("GET", p(`/users${query({ key: num(q?.key) })}`), userList),
324
+ adminCreateUser: (r) => json("POST", p("/users"), parseUserView, { body: r }),
325
+ adminUpdateUser: (id, r) => json("PUT", p(`/users/${String(id)}`), parseUserView, { body: r }),
326
+ adminDeleteUser: (id) => empty("DELETE", p(`/users/${String(id)}`)),
327
+ adminIssueRecovery: (id, r) =>
328
+ json("POST", p(`/users/${String(id)}/recovery`), parseTokenResponse, { body: r }),
329
+ adminInvite: (r) => json("POST", p("/invites"), parseTokenResponse, { body: r }),
330
+ adminTokens: () => json("GET", p("/tokens"), tokenList),
331
+ adminKeys: () => json("GET", p("/admin/apikeys"), keyList),
332
+ adminRevokeKey: (id) => empty("DELETE", p(`/admin/apikeys/${String(id)}`)),
333
+ adminSecurityLog: (q) =>
334
+ json(
335
+ "GET",
336
+ p(
337
+ `/security-log${query({
338
+ user: num(q?.user),
339
+ actor: num(q?.actor),
340
+ actor_key: num(q?.actorKey),
341
+ after: num(q?.after),
342
+ before: num(q?.before),
343
+ kinds: q?.kinds === undefined || q.kinds.length === 0 ? undefined : q.kinds.join(","),
344
+ limit: num(q?.limit),
345
+ oldest_first: q?.oldestFirst === true ? "true" : undefined,
346
+ })}`,
347
+ ),
348
+ parseSecurityLogView,
349
+ ),
350
+ };
351
+ }
@@ -0,0 +1,174 @@
1
+ import { readOIDCReturn } from "./oidc.js";
2
+
3
+ const PREFIX = "@cplieger/access:continue:";
4
+ const NONCE = /^[A-Za-z0-9_-]{22}$/;
5
+
6
+ function storage(): Storage | undefined {
7
+ try {
8
+ return (globalThis as { readonly sessionStorage?: Storage }).sessionStorage;
9
+ } catch {
10
+ // The getter itself throws SecurityError where storage is blocked
11
+ // (https://html.spec.whatwg.org/multipage/webstorage.html#dom-sessionstorage).
12
+ return undefined;
13
+ }
14
+ }
15
+
16
+ function nonce(): string {
17
+ const b = crypto.getRandomValues(new Uint8Array(16));
18
+ return btoa(String.fromCharCode(...b))
19
+ .replaceAll("+", "-")
20
+ .replaceAll("/", "_")
21
+ .replace(/=+$/, "");
22
+ }
23
+
24
+ function isRecord(v: unknown): v is Record<string, unknown> {
25
+ return typeof v === "object" && v !== null && !Array.isArray(v);
26
+ }
27
+
28
+ // Undefined for an entry this module did not write.
29
+ function entry(raw: string | null): { readonly exp: number; readonly v: unknown } | undefined {
30
+ if (raw === null) {
31
+ return undefined;
32
+ }
33
+ let e: unknown;
34
+ try {
35
+ e = JSON.parse(raw);
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ if (!isRecord(e) || typeof e["exp"] !== "number" || !Object.hasOwn(e, "v")) {
40
+ return undefined;
41
+ }
42
+ return { exp: e["exp"], v: e["v"] };
43
+ }
44
+
45
+ function dropExpired(s: Storage, now: number): void {
46
+ const keys: string[] = [];
47
+ for (let i = 0; i < s.length; i++) {
48
+ const k = s.key(i);
49
+ if (k?.startsWith(PREFIX) === true) {
50
+ keys.push(k);
51
+ }
52
+ }
53
+ for (const k of keys) {
54
+ const e = entry(s.getItem(k));
55
+ if (e === undefined || now > e.exp) {
56
+ s.removeItem(k);
57
+ }
58
+ }
59
+ }
60
+
61
+ /** The same-origin half of an OIDC step-up's continuation, kept per tab in sessionStorage. */
62
+ export const continuation: {
63
+ /**
64
+ * Stores value (JSON-serialisable) under a fresh 128-bit base64url nonce for at most
65
+ * maxAgeMs, and returns the nonce, which the app puts in its startOIDC redirect as
66
+ * continue=<nonce>. Throws, storing nothing, unless maxAgeMs is a finite number greater than
67
+ * 0 and JSON.stringify can serialise value.
68
+ */
69
+ put(value: unknown, maxAgeMs: number): string;
70
+ /**
71
+ * Reads search (location.search). Returns the stored value once, only when the oidc marker
72
+ * is stepped_up and continue names a known, unspent, unexpired nonce; otherwise undefined.
73
+ * Every call that names a nonce spends it, whatever it returns, and drops every expired
74
+ * entry.
75
+ */
76
+ take(search: string): unknown;
77
+ } = {
78
+ put(value, maxAgeMs) {
79
+ if (!(Number.isFinite(maxAgeMs) && maxAgeMs > 0)) {
80
+ throw new RangeError("continuation.put: maxAgeMs must be a finite number above 0");
81
+ }
82
+ if ((JSON.stringify(value) as string | undefined) === undefined) {
83
+ throw new TypeError("continuation.put: value has no JSON form");
84
+ }
85
+ const record = JSON.stringify({ v: value, exp: Date.now() + maxAgeMs });
86
+ const s = storage();
87
+ if (s === undefined) {
88
+ throw new Error("continuation.put: sessionStorage is unavailable");
89
+ }
90
+ const n = nonce();
91
+ s.setItem(PREFIX + n, record);
92
+ return n;
93
+ },
94
+
95
+ take(search) {
96
+ const s = storage();
97
+ if (s === undefined) {
98
+ return undefined;
99
+ }
100
+ const now = Date.now();
101
+ const named = new URLSearchParams(search).getAll("continue");
102
+ const found = named.map((n) => {
103
+ const k = PREFIX + n;
104
+ const e = entry(s.getItem(k));
105
+ s.removeItem(k);
106
+ return e;
107
+ });
108
+ dropExpired(s, now);
109
+ const [only] = named;
110
+ const [e] = found;
111
+ if (
112
+ named.length !== 1 ||
113
+ only === undefined ||
114
+ !NONCE.test(only) ||
115
+ e === undefined ||
116
+ now > e.exp ||
117
+ readOIDCReturn(search).kind !== "stepped_up"
118
+ ) {
119
+ return undefined;
120
+ }
121
+ return e.v;
122
+ },
123
+ };
124
+
125
+ function utf8Length(s: string): number {
126
+ return new TextEncoder().encode(s).length;
127
+ }
128
+
129
+ function decode(s: string): string | undefined {
130
+ try {
131
+ return decodeURIComponent(s);
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Reads name from location.hash's key=value pairs, removes every pair named name with
139
+ * history.replaceState (and the "#" when none is left), and returns the value when name
140
+ * appears exactly once and its value is non-empty and at most maxBytes UTF-8 bytes; otherwise
141
+ * undefined. The pairs are removed in every case, so no token stays in the URL or in this
142
+ * history entry; an entry the browser recorded elsewhere before the call is out of its reach.
143
+ */
144
+ export function takeFragmentToken(name: string, maxBytes = 512): string | undefined {
145
+ const hash = globalThis.location.hash;
146
+ if (hash.length <= 1) {
147
+ return undefined;
148
+ }
149
+ const pairs = hash.slice(1).split("&");
150
+ const values: (string | undefined)[] = [];
151
+ const kept: string[] = [];
152
+ for (const p of pairs) {
153
+ const eq = p.indexOf("=");
154
+ const key = eq === -1 ? p : p.slice(0, eq);
155
+ if (decode(key) === name) {
156
+ values.push(eq === -1 ? "" : decode(p.slice(eq + 1)));
157
+ } else {
158
+ kept.push(p);
159
+ }
160
+ }
161
+ if (values.length === 0) {
162
+ return undefined;
163
+ }
164
+ const { pathname, search } = globalThis.location;
165
+ history.replaceState(
166
+ history.state,
167
+ "",
168
+ pathname + search + (kept.length === 0 ? "" : `#${kept.join("&")}`),
169
+ );
170
+ const [v] = values;
171
+ return values.length === 1 && v !== undefined && v !== "" && utf8Length(v) <= maxBytes
172
+ ? v
173
+ : undefined;
174
+ }
package/src/index.ts ADDED
@@ -0,0 +1,125 @@
1
+ export { createAuthClient } from "./client.js";
2
+ export type {
3
+ AuthClient,
4
+ AuthClientOptions,
5
+ PublicKeyCredentialJSON,
6
+ RecoverFlags,
7
+ } from "./client.js";
8
+ export { continuation, takeFragmentToken } from "./continuation.js";
9
+ export {
10
+ completePendingLink,
11
+ oidcSignInURL,
12
+ pendingLink,
13
+ readOIDCReturn,
14
+ startOIDC,
15
+ } from "./oidc.js";
16
+ export type { OIDCReturn } from "./oidc.js";
17
+ export { storePasswordCredential } from "./password-credential.js";
18
+ export { shownOnce } from "./secret.js";
19
+ export type { ShownOnce } from "./secret.js";
20
+ export { createSessionsModel } from "./sessions.js";
21
+ export type { SessionsModel } from "./sessions.js";
22
+ export { withStepUp } from "./stepup.js";
23
+ export type { StepUpOptions, StepUpPrompt, StepUpResult } from "./stepup.js";
24
+ export {
25
+ passkeySupport,
26
+ recoverWithPasskey,
27
+ registerPasskey,
28
+ sendSignals,
29
+ setupWithPasskey,
30
+ signInWithPasskey,
31
+ stepUpWithPasskey,
32
+ } from "./webauthn.js";
33
+ export type {
34
+ DOMExceptionName,
35
+ RecoverOutcome,
36
+ RegisterOutcome,
37
+ SignInOutcome,
38
+ StepUpOutcome,
39
+ } from "./webauthn.js";
40
+ export {
41
+ authCode,
42
+ authReason,
43
+ parseCeremonyResponse,
44
+ parseCurrentSessionView,
45
+ parseInviteAcceptRequest,
46
+ parseInviteRequest,
47
+ parseKeyCreatedResponse,
48
+ parseKeyView,
49
+ parseLoginRequest,
50
+ parseMeResponse,
51
+ parseMethodLockView,
52
+ parseMethodsView,
53
+ parseNameRequest,
54
+ parseNewKeyRequest,
55
+ parseNewUserRequest,
56
+ parseOIDCStartResponse,
57
+ parsePasskeyView,
58
+ parsePasswordChangeRequest,
59
+ parsePasswordRemoveRequest,
60
+ parsePasswordRequest,
61
+ parsePendingLinkResponse,
62
+ parseProfileRequest,
63
+ parseRecoverPasswordRequest,
64
+ parseRecoveryResponse,
65
+ parseRedirectRequest,
66
+ parseSessionView,
67
+ parseSetupPasskeyBeginRequest,
68
+ parseSetupPasswordRequest,
69
+ parseSignedInResponse,
70
+ parseStateResponse,
71
+ parseTokenRequest,
72
+ parseTokenResponse,
73
+ parseTokenView,
74
+ parseTTLRequest,
75
+ parseUserMethodsView,
76
+ parseUserUpdateRequest,
77
+ parseUserView,
78
+ parseWebAuthnSignals,
79
+ } from "./wire.js";
80
+ export type {
81
+ AuthCode,
82
+ AuthReason,
83
+ CeremonyOptions,
84
+ CeremonyResponse,
85
+ CurrentSessionView,
86
+ InviteAcceptRequest,
87
+ InviteRequest,
88
+ KeyCreatedResponse,
89
+ KeyView,
90
+ LogActorView,
91
+ LogClassView,
92
+ LogEntryView,
93
+ LoginRequest,
94
+ MeResponse,
95
+ MethodLockView,
96
+ MethodsView,
97
+ NameRequest,
98
+ NewKeyRequest,
99
+ NewUserRequest,
100
+ OIDCStartResponse,
101
+ PasskeyView,
102
+ PasswordChangeRequest,
103
+ PasswordRemoveRequest,
104
+ PasswordRequest,
105
+ PendingLinkResponse,
106
+ ProfileRequest,
107
+ RecoverPasswordRequest,
108
+ RecoveryResponse,
109
+ RedirectRequest,
110
+ SecurityLogView,
111
+ SessionView,
112
+ SetupPasskeyBeginRequest,
113
+ SetupPasswordRequest,
114
+ SignedInResponse,
115
+ StateResponse,
116
+ TokenRequest,
117
+ TokenResponse,
118
+ TokenView,
119
+ TTLRequest,
120
+ UserMethodsView,
121
+ UserUpdateRequest,
122
+ UserView,
123
+ Validator,
124
+ WebAuthnSignals,
125
+ } from "./wire.js";
@@ -0,0 +1,5 @@
1
+ // Its own module because location is non-configurable: a test replaces this import instead.
2
+ /** @internal */
3
+ export function assign(url: string): void {
4
+ globalThis.location.assign(url);
5
+ }