@parlor/auth 0.0.0-stage → 0.2.0

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -2
  3. package/dist/account.d.ts +59 -0
  4. package/dist/account.d.ts.map +1 -0
  5. package/dist/account.js +127 -0
  6. package/dist/account.js.map +1 -0
  7. package/dist/audit.d.ts +10 -0
  8. package/dist/audit.d.ts.map +1 -0
  9. package/dist/audit.js +130 -0
  10. package/dist/audit.js.map +1 -0
  11. package/dist/better-auth.d.ts +46 -0
  12. package/dist/better-auth.d.ts.map +1 -0
  13. package/dist/better-auth.js +120 -0
  14. package/dist/better-auth.js.map +1 -0
  15. package/dist/client/index.d.ts +79 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +554 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/database.d.ts +7 -0
  20. package/dist/database.d.ts.map +1 -0
  21. package/dist/database.js +11 -0
  22. package/dist/database.js.map +1 -0
  23. package/dist/devices.d.ts +157 -0
  24. package/dist/devices.d.ts.map +1 -0
  25. package/dist/devices.js +403 -0
  26. package/dist/devices.js.map +1 -0
  27. package/dist/es256.d.ts +4 -0
  28. package/dist/es256.d.ts.map +1 -0
  29. package/dist/es256.js +44 -0
  30. package/dist/es256.js.map +1 -0
  31. package/dist/grants.d.ts +4 -0
  32. package/dist/grants.d.ts.map +1 -0
  33. package/dist/grants.js +6 -0
  34. package/dist/grants.js.map +1 -0
  35. package/dist/hosts.d.ts +116 -0
  36. package/dist/hosts.d.ts.map +1 -0
  37. package/dist/hosts.js +538 -0
  38. package/dist/hosts.js.map +1 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +9 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/migrations.d.ts +3 -0
  44. package/dist/migrations.d.ts.map +1 -0
  45. package/dist/migrations.js +17 -0
  46. package/dist/migrations.js.map +1 -0
  47. package/dist/native-sign-in.d.ts +45 -0
  48. package/dist/native-sign-in.d.ts.map +1 -0
  49. package/dist/native-sign-in.js +183 -0
  50. package/dist/native-sign-in.js.map +1 -0
  51. package/dist/pages.d.ts +4 -0
  52. package/dist/pages.d.ts.map +1 -0
  53. package/dist/pages.js +53 -0
  54. package/dist/pages.js.map +1 -0
  55. package/dist/provider.d.ts +81 -0
  56. package/dist/provider.d.ts.map +1 -0
  57. package/dist/provider.js +301 -0
  58. package/dist/provider.js.map +1 -0
  59. package/dist/revocation.d.ts +51 -0
  60. package/dist/revocation.d.ts.map +1 -0
  61. package/dist/revocation.js +233 -0
  62. package/dist/revocation.js.map +1 -0
  63. package/dist/routes.d.ts +15 -0
  64. package/dist/routes.d.ts.map +1 -0
  65. package/dist/routes.js +116 -0
  66. package/dist/routes.js.map +1 -0
  67. package/dist/schema.d.ts +2970 -0
  68. package/dist/schema.d.ts.map +1 -0
  69. package/dist/schema.js +308 -0
  70. package/dist/schema.js.map +1 -0
  71. package/dist/statements.d.ts +92 -0
  72. package/dist/statements.d.ts.map +1 -0
  73. package/dist/statements.js +199 -0
  74. package/dist/statements.js.map +1 -0
  75. package/dist/tokens.d.ts +69 -0
  76. package/dist/tokens.d.ts.map +1 -0
  77. package/dist/tokens.js +134 -0
  78. package/dist/tokens.js.map +1 -0
  79. package/dist/turn-keys.d.ts +16 -0
  80. package/dist/turn-keys.d.ts.map +1 -0
  81. package/dist/turn-keys.js +108 -0
  82. package/dist/turn-keys.js.map +1 -0
  83. package/package.json +57 -3
@@ -0,0 +1,79 @@
1
+ import { type AuthBrowser, type DeviceKey, type SessionStore } from "@parlor/capabilities";
2
+ import { type ActionClient, type DeviceLinker, type DeviceSource, type HostAuthorizer, type SessionDevice, type SessionSource } from "@parlor/client";
3
+ import type { AccountActions } from "../account.js";
4
+ export interface AuthClientOptions {
5
+ /** The backend origin; defaults to the shell's `server.url`, or this page's origin. */
6
+ readonly baseUrl?: string | undefined;
7
+ readonly basePath?: string | undefined;
8
+ /**
9
+ * Where a bearer session is kept. Defaults to the `sessionStore` capability: native
10
+ * shells keep a bearer token, the web keeps a cookie.
11
+ */
12
+ readonly session?: SessionStore | undefined;
13
+ /** Where this device's signing keys live. Defaults to the `deviceKey` capability. */
14
+ readonly deviceKey?: DeviceKey | undefined;
15
+ /** Where social sign-in runs. Defaults to the `authBrowser` capability. */
16
+ readonly browser?: AuthBrowser | undefined;
17
+ /** The app's URL scheme, where social sign-in returns in a native shell. Defaults to `app.scheme`. */
18
+ readonly scheme?: string | undefined;
19
+ readonly fetch?: typeof fetch | undefined;
20
+ }
21
+ export declare class AuthClientError extends Error {
22
+ readonly status: number;
23
+ readonly code: string | undefined;
24
+ readonly name = "AuthClientError";
25
+ constructor(message: string, status: number, code: string | undefined);
26
+ }
27
+ export type DeviceHandle = SessionDevice;
28
+ export interface SocialSignInInput {
29
+ /** A provider in Better Auth's `socialProviders`, or a `genericOAuth` provider id. */
30
+ readonly provider: string;
31
+ /** On the web, the page the provider returns to once signed in. Defaults to `/`. */
32
+ readonly callbackURL?: string | undefined;
33
+ /** On the web, the page a failed sign-in returns to with `?error=`. Defaults to this page. */
34
+ readonly errorCallbackURL?: string | undefined;
35
+ }
36
+ export interface AuthClient extends SessionSource, DeviceSource, HostAuthorizer, DeviceLinker {
37
+ deviceSetup(): Promise<"needed" | "ready">;
38
+ /** Plug into `createActionClient({ headers })` so every surface carries the session. */
39
+ readonly headers: () => Promise<HeadersInit>;
40
+ signIn(input: {
41
+ readonly email: string;
42
+ readonly password: string;
43
+ }): Promise<void>;
44
+ signUp(input: {
45
+ readonly name: string;
46
+ readonly email: string;
47
+ readonly password: string;
48
+ }): Promise<void>;
49
+ signOut(): Promise<void>;
50
+ /** Sign-in with a one-time code sent by email, through Better Auth's `emailOTP` plugin. */
51
+ readonly emailOtp: {
52
+ send(input: {
53
+ readonly email: string;
54
+ }): Promise<void>;
55
+ /** Signs in, creating the account on first use unless the plugin disables sign-up. */
56
+ signIn(input: {
57
+ readonly email: string;
58
+ readonly otp: string;
59
+ readonly name?: string | undefined;
60
+ }): Promise<void>;
61
+ };
62
+ /** Sign-in with a provider such as Google, through Better Auth's `socialProviders`. */
63
+ readonly social: {
64
+ /** Whether this platform can run it: false in a native shell without the `authBrowser` capability. */
65
+ available(): Promise<boolean>;
66
+ /**
67
+ * On the web the page leaves for the provider and comes back to `callbackURL`, so this
68
+ * never resolves. In a native shell the provider opens in the system's sign-in browser
69
+ * and this resolves once the app is signed in; it throws `AuthClientError` with code
70
+ * `CANCELLED` when the user closes it.
71
+ */
72
+ signIn(input: SocialSignInInput): Promise<void>;
73
+ };
74
+ /** The account actions: `sessions`, `revokeSession`, `agents`, `revokeAgent`. */
75
+ readonly account: ActionClient<AccountActions>;
76
+ }
77
+ export declare function createAuthClient(options?: AuthClientOptions): AuthClient;
78
+ export type { AuthBrowser, HostPairing, SessionStore } from "@parlor/capabilities";
79
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,554 @@
1
+ import { oauthProviderClient } from "@better-auth/oauth-provider/client";
2
+ import { authBrowser, detectPlatform, deviceKey, resolveCapability, sessionStore, } from "@parlor/capabilities";
3
+ import { decodeBase64url, encodeBase64url } from "@parlor/capabilities/encoding";
4
+ import { appOrigin, createActionClient, LinkOnComputer, } from "@parlor/client";
5
+ import { ActionError } from "@parlor/core";
6
+ import { createAuthClient as createBetterAuthClient } from "better-auth/client";
7
+ import { emailOTPClient } from "better-auth/client/plugins";
8
+ import { answerStatement, deviceCommitment, HOST_PAIRING_FRAGMENT, linkCode, normalizeLinkCode, pairingProof, promptStatement, removeStatement, statementBytes, vouchStatement, } from "../statements.js";
9
+ const DEFAULT_BASE_PATH = "/_parlor";
10
+ const RETRY_BASE_MS = 1000;
11
+ const RETRY_MAX_MS = 30_000;
12
+ const TOKEN_HEADER = "set-auth-token";
13
+ const HOST_IPC_REDIRECT = "urn:parlor:ipc";
14
+ const DEBUG_KEY = "parlor:debug";
15
+ const HOST_PAIRING_KEY = "parlor:host-pairing";
16
+ const NATIVE_CALLBACK = "//_parlor/auth/callback";
17
+ function injectedServerUrl() {
18
+ const env = import.meta.env;
19
+ return env?.PARLOR_SERVER_URL ?? "";
20
+ }
21
+ function injectedScheme() {
22
+ const env = import.meta.env;
23
+ return env?.PARLOR_APP_SCHEME ?? "";
24
+ }
25
+ const nativeSignInClient = {
26
+ id: "parlor-native-sign-in",
27
+ $InferServerPlugin: {},
28
+ atomListeners: [
29
+ { matcher: (path) => path === "/native/token", signal: "$sessionSignal" },
30
+ ],
31
+ };
32
+ const randomText = (bytes) => encodeBase64url(crypto.getRandomValues(new Uint8Array(bytes)));
33
+ async function pkceChallenge(verifier) {
34
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
35
+ return encodeBase64url(new Uint8Array(digest));
36
+ }
37
+ export class AuthClientError extends Error {
38
+ status;
39
+ code;
40
+ name = "AuthClientError";
41
+ constructor(message, status, code) {
42
+ super(message);
43
+ this.status = status;
44
+ this.code = code;
45
+ }
46
+ }
47
+ function settle(result) {
48
+ if (!result.error)
49
+ return;
50
+ const { message, status, code } = result.error;
51
+ throw new AuthClientError(message || "Authentication failed.", status, code);
52
+ }
53
+ const ANONYMOUS_SESSION = { status: "anonymous" };
54
+ const BROWSERS = [
55
+ [/Edg\//, "Edge"],
56
+ [/OPR\//, "Opera"],
57
+ [/Firefox\//, "Firefox"],
58
+ [/Chrome\//, "Chrome"],
59
+ [/Safari\//, "Safari"],
60
+ ];
61
+ const SYSTEMS = [
62
+ [/iPhone|iPad/, "iOS"],
63
+ [/Android/, "Android"],
64
+ [/Mac OS X/, "macOS"],
65
+ [/Windows/, "Windows"],
66
+ [/Linux/, "Linux"],
67
+ ];
68
+ function deviceName() {
69
+ const agent = globalThis.navigator?.userAgent ?? "";
70
+ const browser = BROWSERS.find(([pattern]) => pattern.test(agent))?.[1];
71
+ const system = SYSTEMS.find(([pattern]) => pattern.test(agent))?.[1];
72
+ if (browser && system)
73
+ return `${browser} on ${system}`;
74
+ return browser ?? system ?? "This device";
75
+ }
76
+ function debugStatements() {
77
+ try {
78
+ return (globalThis.localStorage?.getItem(DEBUG_KEY)?.split(",").includes("statements") ??
79
+ false);
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ async function logSigned(statement) {
86
+ if (!debugStatements())
87
+ return;
88
+ const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(statementBytes(statement)));
89
+ const hex = [...new Uint8Array(digest)]
90
+ .map((byte) => byte.toString(16).padStart(2, "0"))
91
+ .join("");
92
+ console.info(`[parlor] signed ${statement.kind} sha256=${hex}`);
93
+ }
94
+ /** Takes the host's pairing secret off this page's URL, where only that redirect puts it. */
95
+ function keepHostPairing() {
96
+ try {
97
+ const location = globalThis.location;
98
+ if (!location?.hash)
99
+ return;
100
+ const fragment = new URLSearchParams(location.hash.slice(1));
101
+ const secret = fragment.get(HOST_PAIRING_FRAGMENT);
102
+ if (!secret)
103
+ return;
104
+ globalThis.localStorage?.setItem(HOST_PAIRING_KEY, secret);
105
+ fragment.delete(HOST_PAIRING_FRAGMENT);
106
+ const rest = fragment.toString();
107
+ globalThis.history?.replaceState(globalThis.history.state, "", `${location.pathname}${location.search}${rest ? `#${rest}` : ""}`);
108
+ }
109
+ catch { }
110
+ }
111
+ function hostPairing() {
112
+ try {
113
+ return globalThis.localStorage?.getItem(HOST_PAIRING_KEY) ?? undefined;
114
+ }
115
+ catch {
116
+ return undefined;
117
+ }
118
+ }
119
+ const serverHasNoAuth = (error) => error.status === 404;
120
+ function enrollmentPresence(presence, rpId) {
121
+ if (!presence)
122
+ return undefined;
123
+ return presence.format === "webauthn"
124
+ ? {
125
+ format: "webauthn",
126
+ credentialId: presence.credentialId,
127
+ rpId,
128
+ publicKey: { ...presence.publicKey },
129
+ attestation: presence.attestation,
130
+ }
131
+ : {
132
+ format: "es256-raw",
133
+ keyId: presence.keyId,
134
+ publicKey: { ...presence.publicKey },
135
+ };
136
+ }
137
+ function registered(presence) {
138
+ if (presence?.format !== "webauthn")
139
+ return presence;
140
+ const { attestation: _attestation, ...key } = presence;
141
+ return key;
142
+ }
143
+ function hostnameOf(origin) {
144
+ try {
145
+ return new URL(origin).hostname;
146
+ }
147
+ catch {
148
+ return globalThis.location.hostname;
149
+ }
150
+ }
151
+ export function createAuthClient(options = {}) {
152
+ keepHostPairing();
153
+ const baseUrl = options.baseUrl ?? injectedServerUrl();
154
+ const basePath = options.basePath ?? DEFAULT_BASE_PATH;
155
+ const store = options.session
156
+ ? Promise.resolve(options.session)
157
+ : resolveCapability(sessionStore).then((handle) => handle.available ? handle.api : undefined);
158
+ const headers = async () => {
159
+ const token = await (await store)?.get();
160
+ return token ? { authorization: `Bearer ${token}` } : {};
161
+ };
162
+ // Built on first use: importing a module that creates the client must work outside a page.
163
+ let client;
164
+ const connect = () => createBetterAuthClient({
165
+ baseURL: `${baseUrl || globalThis.location.origin}${basePath}/auth`,
166
+ plugins: [oauthProviderClient(), emailOTPClient(), nativeSignInClient],
167
+ fetchOptions: {
168
+ ...(options.fetch ? { customFetchImpl: options.fetch } : {}),
169
+ onRequest: async (context) => {
170
+ for (const [name, value] of new Headers(await headers())) {
171
+ context.headers.set(name, value);
172
+ }
173
+ return context;
174
+ },
175
+ onSuccess: async (context) => {
176
+ const token = context.response.headers.get(TOKEN_HEADER);
177
+ if (token)
178
+ await (await store)?.set(token);
179
+ else if (context.request.url.toString().endsWith("/get-session") &&
180
+ !context.data) {
181
+ // A bearer token the server no longer knows would fail every later request.
182
+ await (await store)?.set(undefined);
183
+ }
184
+ },
185
+ },
186
+ });
187
+ const betterAuth = () => (client ??= connect());
188
+ let watchers = 0;
189
+ let retry;
190
+ let retryDelay = RETRY_BASE_MS;
191
+ const retryAfterError = () => {
192
+ const state = betterAuth().useSession.get();
193
+ if (!state.error || serverHasNoAuth(state.error)) {
194
+ retryDelay = RETRY_BASE_MS;
195
+ return;
196
+ }
197
+ if (retry !== undefined || state.isRefetching)
198
+ return;
199
+ retry = setTimeout(() => {
200
+ retry = undefined;
201
+ void state.refetch();
202
+ }, retryDelay);
203
+ retryDelay = Math.min(retryDelay * 2, RETRY_MAX_MS);
204
+ };
205
+ let last;
206
+ let snapshot = { status: "loading" };
207
+ const current = () => {
208
+ const state = betterAuth().useSession.get();
209
+ if (state === last)
210
+ return snapshot;
211
+ last = state;
212
+ if (state.error) {
213
+ if (serverHasNoAuth(state.error))
214
+ snapshot = ANONYMOUS_SESSION;
215
+ // Only the server saying there is no session signs the user out, never an unreachable one.
216
+ return snapshot;
217
+ }
218
+ const user = state.data?.user;
219
+ snapshot = user
220
+ ? { status: "signed-in", user: { id: user.id, name: user.name, email: user.email } }
221
+ : state.isPending
222
+ ? { status: "loading" }
223
+ : { status: "signed-out" };
224
+ return snapshot;
225
+ };
226
+ const account = createActionClient({
227
+ baseUrl,
228
+ fetch: options.fetch,
229
+ basePath: `${basePath}/auth`,
230
+ headers,
231
+ });
232
+ const signInBrowser = async () => {
233
+ if (options.browser)
234
+ return options.browser;
235
+ const handle = await resolveCapability(authBrowser);
236
+ return handle.available ? handle.api : undefined;
237
+ };
238
+ const signInOnThePage = async (browser, input) => {
239
+ const page = new URL(globalThis.location.href);
240
+ const callbackURL = new URL(input.callbackURL ?? "/", page).href;
241
+ const errorPage = new URL(input.errorCallbackURL ?? page, page);
242
+ errorPage.searchParams.delete("error");
243
+ errorPage.searchParams.delete("error_description");
244
+ const result = await betterAuth().signIn.social({
245
+ provider: input.provider,
246
+ callbackURL,
247
+ errorCallbackURL: errorPage.href,
248
+ disableRedirect: true,
249
+ });
250
+ settle(result);
251
+ const url = result.data?.url;
252
+ if (!url)
253
+ throw new AuthClientError("The provider gave no sign-in page.", 500, undefined);
254
+ await browser.open(url, callbackURL);
255
+ };
256
+ const signInInTheApp = async (browser, provider) => {
257
+ const scheme = options.scheme ?? injectedScheme();
258
+ if (!scheme) {
259
+ throw new AuthClientError("Set app.scheme in parlor.config.ts to sign in with a provider in the app.", 0, "NO_SCHEME");
260
+ }
261
+ const callback = `${scheme}:${NATIVE_CALLBACK}`;
262
+ const verifier = randomText(32);
263
+ const state = randomText(16);
264
+ const start = new URL(`${baseUrl || globalThis.location.origin}${basePath}/auth/native/sign-in/social`);
265
+ start.search = new URLSearchParams({
266
+ provider,
267
+ redirect_uri: callback,
268
+ code_challenge: await pkceChallenge(verifier),
269
+ code_challenge_method: "S256",
270
+ state,
271
+ }).toString();
272
+ const returned = await browser.open(start.href, callback);
273
+ if (!returned)
274
+ throw new AuthClientError("Sign-in was cancelled.", 0, "CANCELLED");
275
+ const answer = new URL(returned).searchParams;
276
+ if (answer.get("state") !== state) {
277
+ throw new AuthClientError("This sign-in was not started here.", 0, "STATE_MISMATCH");
278
+ }
279
+ const failed = answer.get("error");
280
+ const code = answer.get("code");
281
+ if (failed || !code) {
282
+ throw new AuthClientError(answer.get("error_description") || `Sign-in failed (${failed ?? "no code"}).`, 400, failed ?? undefined);
283
+ }
284
+ settle(await betterAuth().native.token({ code, code_verifier: verifier }));
285
+ };
286
+ const keysOfThisPlatform = async () => {
287
+ const handle = await resolveCapability(deviceKey);
288
+ return handle.available ? handle.api : undefined;
289
+ };
290
+ const devices = new Map();
291
+ const enroll = async (user, wantsPresence) => {
292
+ const key = options.deviceKey ?? (await keysOfThisPlatform());
293
+ if (!key)
294
+ return undefined;
295
+ const app = appOrigin(baseUrl || undefined);
296
+ const rpId = hostnameOf(app);
297
+ const { challenge } = await account.call("deviceChallenge", {});
298
+ const keys = await key.publicKeys({
299
+ rpId,
300
+ userId: user.id,
301
+ userName: user.email,
302
+ challenge: decodeBase64url(challenge),
303
+ }, { presence: wantsPresence });
304
+ const presence = enrollmentPresence(keys.presence, rpId);
305
+ const name = deviceName();
306
+ const platform = detectPlatform();
307
+ const signingKey = { ...keys.signing.publicKey };
308
+ const { id } = await account.call("enrollDevice", {
309
+ name,
310
+ platform,
311
+ challenge,
312
+ signing: { keyId: keys.signing.keyId, publicKey: signingKey },
313
+ ...(presence ? { presence } : {}),
314
+ proof: keys.proof,
315
+ });
316
+ return {
317
+ id,
318
+ keys: {
319
+ deviceId: id,
320
+ name,
321
+ platform,
322
+ signingKey,
323
+ presenceKey: registered(presence),
324
+ },
325
+ signPrompt: async (prompt, wanted) => {
326
+ const at = Date.now();
327
+ const statement = await promptStatement(app, prompt, at);
328
+ const bytes = statementBytes(statement);
329
+ const signature = wanted?.presence
330
+ ? await key.sign(bytes, { presence: true })
331
+ : await key.sign(bytes, { presence: false });
332
+ await logSigned(statement);
333
+ const secret = hostPairing();
334
+ const pairing = secret && (await pairingProof(secret, id, signingKey));
335
+ return { deviceId: id, at, signature, ...(pairing ? { pairing } : {}) };
336
+ },
337
+ signAnswer: async (answer, { presence: wanted }) => {
338
+ const at = Date.now();
339
+ const statement = await answerStatement(app, answer, at);
340
+ const bytes = statementBytes(statement);
341
+ const signature = wanted
342
+ ? await key.sign(bytes, { presence: true })
343
+ : await key.sign(bytes, { presence: false });
344
+ await logSigned(statement);
345
+ return { deviceId: id, at, signature };
346
+ },
347
+ };
348
+ };
349
+ const device = async (wanted) => {
350
+ const session = current();
351
+ if (session.status !== "signed-in")
352
+ return undefined;
353
+ const known = devices.get(session.user.id);
354
+ if (known)
355
+ return known;
356
+ // The browser that approved `connect` is on the computer, which answers its permission questions.
357
+ const presence = wanted?.presence ?? hostPairing() === undefined;
358
+ const enrolling = enroll(session.user, presence).catch((error) => {
359
+ if (error instanceof ActionError && error.code === "not_found")
360
+ return undefined;
361
+ throw error;
362
+ });
363
+ devices.set(session.user.id, enrolling);
364
+ enrolling.then((enrolled) => {
365
+ if (!enrolled)
366
+ devices.delete(session.user.id);
367
+ }, () => devices.delete(session.user.id));
368
+ return enrolling;
369
+ };
370
+ return {
371
+ headers,
372
+ current,
373
+ subscribe: (listener) => {
374
+ watchers += 1;
375
+ const stop = betterAuth().useSession.listen(() => {
376
+ retryAfterError();
377
+ listener();
378
+ });
379
+ return () => {
380
+ stop();
381
+ watchers -= 1;
382
+ if (watchers === 0 && retry !== undefined) {
383
+ clearTimeout(retry);
384
+ retry = undefined;
385
+ }
386
+ };
387
+ },
388
+ signIn: async (input) => settle(await betterAuth().signIn.email(input)),
389
+ signUp: async (input) => settle(await betterAuth().signUp.email(input)),
390
+ emailOtp: {
391
+ send: async ({ email }) => settle(await betterAuth().emailOtp.sendVerificationOtp({ email, type: "sign-in" })),
392
+ signIn: async ({ email, otp, name }) => settle(await betterAuth().signIn.emailOtp({ email, otp, ...(name ? { name } : {}) })),
393
+ },
394
+ social: {
395
+ available: async () => (await signInBrowser()) !== undefined,
396
+ signIn: async (input) => {
397
+ const browser = await signInBrowser();
398
+ if (!browser) {
399
+ throw new AuthClientError(`Signing in with ${input.provider} is not available here.`, 0, "UNSUPPORTED");
400
+ }
401
+ if (await store)
402
+ await signInInTheApp(browser, input.provider);
403
+ else
404
+ await signInOnThePage(browser, input);
405
+ },
406
+ },
407
+ signOut: async () => {
408
+ try {
409
+ settle(await betterAuth().signOut());
410
+ }
411
+ finally {
412
+ await (await store)?.set(undefined);
413
+ }
414
+ },
415
+ linkState: async () => {
416
+ const pairing = hostPairing() !== undefined;
417
+ const session = current();
418
+ if (session.status !== "signed-in")
419
+ return { vouched: false, pairing };
420
+ const known = devices.get(session.user.id);
421
+ const key = options.deviceKey ?? (await keysOfThisPlatform());
422
+ const silent = known !== undefined ||
423
+ (key !== undefined && (await key.presenceSetup?.()) !== "needed");
424
+ const own = silent ? await device() : undefined;
425
+ if (!own)
426
+ return { vouched: false, pairing };
427
+ const listed = await account.call("devices", {});
428
+ const listing = listed.find((entry) => entry.id === own.id);
429
+ if (!listing) {
430
+ devices.delete(session.user.id);
431
+ return { vouched: false, pairing };
432
+ }
433
+ return { deviceId: own.id, vouched: listing.vouched, pairing };
434
+ },
435
+ startLink: async (wanted) => {
436
+ const own = await device(wanted);
437
+ if (!own)
438
+ throw new Error("Sign in on this device first.");
439
+ const app = appOrigin(baseUrl || undefined);
440
+ const commitment = await deviceCommitment(app, own.id, own.keys.signingKey, own.keys.presenceKey?.publicKey);
441
+ const code = linkCode(commitment);
442
+ const { expiresAt } = await account.call("startDeviceLink", {
443
+ device: own.id,
444
+ code,
445
+ });
446
+ return { deviceId: own.id, code, commitment, expiresAt };
447
+ },
448
+ resolveLink: async (from) => {
449
+ const code = typeof from === "string" ? normalizeLinkCode(from) : undefined;
450
+ const found = await account.call("resolveDeviceLink", code ? { code } : { device: from.deviceId });
451
+ const commitment = await deviceCommitment(appOrigin(baseUrl || undefined), found.id, found.signingKey, found.presenceKey?.publicKey);
452
+ const matches = typeof from === "string"
453
+ ? linkCode(commitment) === code
454
+ : found.id === from.deviceId && commitment === from.commitment;
455
+ if (!matches) {
456
+ throw new Error("The app returned a device whose keys do not match this code, so it was not linked.");
457
+ }
458
+ return {
459
+ id: found.id,
460
+ name: found.name,
461
+ platform: found.platform,
462
+ commitment,
463
+ code: linkCode(commitment),
464
+ };
465
+ },
466
+ listDevices: async () => {
467
+ const session = current();
468
+ if (session.status !== "signed-in")
469
+ return [];
470
+ const own = devices.has(session.user.id) ? await device() : undefined;
471
+ const listed = await account.call("devices", {});
472
+ return listed.map((entry) => ({ ...entry, current: entry.id === own?.id }));
473
+ },
474
+ removeDevice: async (id) => {
475
+ const own = await device();
476
+ const key = options.deviceKey ?? (await keysOfThisPlatform());
477
+ if (!own?.keys.presenceKey || !key) {
478
+ throw new LinkOnComputer("This device cannot confirm it is you.");
479
+ }
480
+ const app = appOrigin(baseUrl || undefined);
481
+ const at = Date.now();
482
+ const signature = await key.sign(statementBytes(removeStatement(app, own.id, id, at)), {
483
+ presence: true,
484
+ });
485
+ await account.call("removeDevice", {
486
+ device: id,
487
+ actor: own.id,
488
+ app,
489
+ at,
490
+ signature,
491
+ });
492
+ const session = current();
493
+ if (id === own.id && session.status === "signed-in")
494
+ devices.delete(session.user.id);
495
+ },
496
+ canVouch: async () => {
497
+ const session = current();
498
+ if (session.status !== "signed-in")
499
+ return false;
500
+ const known = devices.get(session.user.id);
501
+ const key = options.deviceKey ?? (await keysOfThisPlatform());
502
+ if (!known && (await key?.presenceSetup?.()) === "needed")
503
+ return false;
504
+ return (await device())?.keys.presenceKey !== undefined;
505
+ },
506
+ vouch: async (link) => {
507
+ const own = await device();
508
+ const key = options.deviceKey ?? (await keysOfThisPlatform());
509
+ if (!own?.keys.presenceKey || !key) {
510
+ throw new LinkOnComputer("This device cannot confirm it is you.");
511
+ }
512
+ const app = appOrigin(baseUrl || undefined);
513
+ const at = Date.now();
514
+ const statement = vouchStatement(app, own.id, link.id, link.commitment, at);
515
+ const signature = await key.sign(statementBytes(statement), { presence: true });
516
+ await account.call("vouchDevice", {
517
+ device: link.id,
518
+ voucher: own.id,
519
+ app,
520
+ commitment: link.commitment,
521
+ at,
522
+ signature,
523
+ });
524
+ },
525
+ deviceSetup: async () => {
526
+ const session = current();
527
+ if (session.status !== "signed-in" || devices.has(session.user.id))
528
+ return "ready";
529
+ const key = options.deviceKey ?? (await keysOfThisPlatform());
530
+ return (await key?.presenceSetup?.()) === "needed" ? "needed" : "ready";
531
+ },
532
+ device,
533
+ authorizeHost: async (pairing) => {
534
+ const response = await (options.fetch ?? fetch)(`${baseUrl}${basePath}/auth/hosts/authorize`, {
535
+ method: "POST",
536
+ headers: { ...(await headers()), "content-type": "application/json" },
537
+ body: JSON.stringify({
538
+ ...pairing,
539
+ redirectUri: HOST_IPC_REDIRECT,
540
+ decision: "connect",
541
+ }),
542
+ });
543
+ const body = (await response.json().catch(() => undefined));
544
+ if (!response.ok ||
545
+ typeof body?.code !== "string" ||
546
+ body.state !== pairing.state) {
547
+ throw new AuthClientError(body?.error?.message ?? "The computer could not be connected.", response.status, body?.error?.code);
548
+ }
549
+ return body.code;
550
+ },
551
+ account,
552
+ };
553
+ }
554
+ //# sourceMappingURL=index.js.map