@procedures/better-auth 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.d.ts +91 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +114 -0
- package/dist/browser.js.map +1 -0
- package/package.json +7 -2
- package/src/browser.test.ts +90 -0
- package/src/browser.ts +172 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the sign-in (Graph's split, wave 2, W8): the calls of a page to the auth routes of a
|
|
3
|
+
* procedures server with Better Auth. This entry has no framework and no global state: each function gives data,
|
|
4
|
+
* and the app decides what the page does (a redirect, a reload).
|
|
5
|
+
*
|
|
6
|
+
* - The session cookie is `httpOnly`, so each call sends the cookies (`credentials: "include"`).
|
|
7
|
+
* - The passkeys use WebAuthn through `@simplewebauthn/browser`. It loads only when a passkey function runs.
|
|
8
|
+
* - The functions give `{ error }` for a refused or failed call. They do not throw for an answer of the server.
|
|
9
|
+
*
|
|
10
|
+
* Import it as `@procedures/better-auth/browser`. The package root is for the server.
|
|
11
|
+
*/
|
|
12
|
+
/** A signed-in user, as the session route gives it. */
|
|
13
|
+
export interface BrowserUser {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name?: string | undefined;
|
|
16
|
+
readonly email?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** A refused or failed call. */
|
|
19
|
+
export interface BrowserAuthError {
|
|
20
|
+
readonly error: string;
|
|
21
|
+
}
|
|
22
|
+
/** The WebAuthn ceremonies. The default is `@simplewebauthn/browser`. A test gives its own. */
|
|
23
|
+
export interface WebAuthnCeremonies {
|
|
24
|
+
startRegistration(options: {
|
|
25
|
+
optionsJSON: unknown;
|
|
26
|
+
}): Promise<unknown>;
|
|
27
|
+
startAuthentication(options: {
|
|
28
|
+
optionsJSON: unknown;
|
|
29
|
+
}): Promise<unknown>;
|
|
30
|
+
}
|
|
31
|
+
/** The options of `createBrowserAuth`. */
|
|
32
|
+
export interface BrowserAuthOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The origin of the server. An empty text is the origin of the page, the simplest form. A server on another
|
|
35
|
+
* origin needs CORS with credentials for the origin of the page (refer to the README).
|
|
36
|
+
*/
|
|
37
|
+
readonly baseURL: string;
|
|
38
|
+
/** The path of the auth routes. The default is `/auth`, as `betterAuthConfig` gives it. */
|
|
39
|
+
readonly basePath?: string | undefined;
|
|
40
|
+
/** The fetch of the calls. The default is the fetch of the page. */
|
|
41
|
+
readonly fetch?: typeof fetch | undefined;
|
|
42
|
+
/** The WebAuthn ceremonies of the passkeys. */
|
|
43
|
+
readonly webauthn?: WebAuthnCeremonies | undefined;
|
|
44
|
+
/** The longest wait of `session()`, in milliseconds. The default is 4000. */
|
|
45
|
+
readonly sessionTimeoutMs?: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The sign-in calls of a page. */
|
|
48
|
+
export interface BrowserAuth {
|
|
49
|
+
/** The signed-in user of this browser, or undefined (signed out, or the server did not answer in time). */
|
|
50
|
+
session(): Promise<BrowserUser | undefined>;
|
|
51
|
+
/** The page of the provider: the app goes to `url`. After the sign-in, the provider comes back to `callbackURL`. */
|
|
52
|
+
signInSocial(provider: string, callbackURL: string, errorCallbackURL?: string): Promise<{
|
|
53
|
+
readonly url: string;
|
|
54
|
+
} | BrowserAuthError>;
|
|
55
|
+
/** This method ends the session. The app then loads the page again. */
|
|
56
|
+
signOut(): Promise<{
|
|
57
|
+
readonly signedOut: true;
|
|
58
|
+
} | BrowserAuthError>;
|
|
59
|
+
/** This method makes a passkey of this device for the signed-in user. */
|
|
60
|
+
addPasskey(name: string): Promise<{
|
|
61
|
+
readonly added: true;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
} | BrowserAuthError>;
|
|
64
|
+
/** This method signs in with a passkey of this device. The app then loads the page again. */
|
|
65
|
+
signInWithPasskey(): Promise<{
|
|
66
|
+
readonly signedIn: true;
|
|
67
|
+
} | BrowserAuthError>;
|
|
68
|
+
/** A test server only (`AUTH_TEST_ACCOUNTS`): a sign-in, or a sign-up, with an email and a password. */
|
|
69
|
+
signInWithPassword(input: {
|
|
70
|
+
email: string;
|
|
71
|
+
password: string;
|
|
72
|
+
name?: string | undefined;
|
|
73
|
+
signUp?: boolean | undefined;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
readonly signedIn: true;
|
|
76
|
+
readonly user?: string | undefined;
|
|
77
|
+
} | BrowserAuthError>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* This function gives the sign-in calls of a page to a server.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const auth = createBrowserAuth({ baseURL: "https://api.example.com" });
|
|
85
|
+
* const user = await auth.session();
|
|
86
|
+
* const out = await auth.signInSocial("github", location.href);
|
|
87
|
+
* if ("url" in out) location.assign(out.url);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function createBrowserAuth(options: BrowserAuthOptions): BrowserAuth;
|
|
91
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,gCAAgC;AAChC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,CAAC,OAAO,EAAE;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE,mBAAmB,CAAC,OAAO,EAAE;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAC;IAC1C,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACnD,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChD;AAED,mCAAmC;AACnC,MAAM,WAAW,WAAW;IAC1B,2GAA2G;IAC3G,OAAO,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IAC5C,oHAAoH;IACpH,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,gBAAgB,CAAC,CAAC;IACrI,uEAAuE;IACvE,OAAO,IAAI,OAAO,CAAC;QAAE,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAA;KAAE,GAAG,gBAAgB,CAAC,CAAC;IACpE,yEAAyE;IACzE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,gBAAgB,CAAC,CAAC;IACtG,6FAA6F;IAC7F,iBAAiB,IAAI,OAAO,CAAC;QAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,gBAAgB,CAAC,CAAC;IAC7E,wGAAwG;IACxG,kBAAkB,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,gBAAgB,CAAC,CAAC;CACtN;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAuF1E"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the sign-in (Graph's split, wave 2, W8): the calls of a page to the auth routes of a
|
|
3
|
+
* procedures server with Better Auth. This entry has no framework and no global state: each function gives data,
|
|
4
|
+
* and the app decides what the page does (a redirect, a reload).
|
|
5
|
+
*
|
|
6
|
+
* - The session cookie is `httpOnly`, so each call sends the cookies (`credentials: "include"`).
|
|
7
|
+
* - The passkeys use WebAuthn through `@simplewebauthn/browser`. It loads only when a passkey function runs.
|
|
8
|
+
* - The functions give `{ error }` for a refused or failed call. They do not throw for an answer of the server.
|
|
9
|
+
*
|
|
10
|
+
* Import it as `@procedures/better-auth/browser`. The package root is for the server.
|
|
11
|
+
*/
|
|
12
|
+
const errorOf = (answer) => ({
|
|
13
|
+
error: typeof answer.json?.["message"] === "string" ? answer.json["message"] : `HTTP ${answer.status}`,
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* This function gives the sign-in calls of a page to a server.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const auth = createBrowserAuth({ baseURL: "https://api.example.com" });
|
|
21
|
+
* const user = await auth.session();
|
|
22
|
+
* const out = await auth.signInSocial("github", location.href);
|
|
23
|
+
* if ("url" in out) location.assign(out.url);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function createBrowserAuth(options) {
|
|
27
|
+
const base = `${options.baseURL.replace(/\/+$/, "")}${options.basePath ?? "/auth"}`;
|
|
28
|
+
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
29
|
+
const timeoutMs = options.sessionTimeoutMs ?? 4000;
|
|
30
|
+
let ceremonies;
|
|
31
|
+
const webauthn = () => (ceremonies ??= options.webauthn ? Promise.resolve(options.webauthn) : import("@simplewebauthn/browser"));
|
|
32
|
+
const call = async (path, init = {}) => {
|
|
33
|
+
const response = await doFetch(`${base}${path}`, { credentials: "include", ...init });
|
|
34
|
+
let json;
|
|
35
|
+
try {
|
|
36
|
+
const body = await response.json();
|
|
37
|
+
json = body !== null && typeof body === "object" ? body : undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
json = undefined;
|
|
41
|
+
}
|
|
42
|
+
return { ok: response.ok, status: response.status, json };
|
|
43
|
+
};
|
|
44
|
+
const post = (path, body) => call(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body ?? {}) });
|
|
45
|
+
return {
|
|
46
|
+
async session() {
|
|
47
|
+
const abort = new AbortController();
|
|
48
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
49
|
+
try {
|
|
50
|
+
const answer = await call("/get-session", { signal: abort.signal });
|
|
51
|
+
const user = answer.ok ? answer.json?.["user"] : undefined;
|
|
52
|
+
if (!user || (typeof user["id"] !== "string" && typeof user["id"] !== "number"))
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
id: String(user["id"]),
|
|
56
|
+
...(typeof user["name"] === "string" && user["name"] ? { name: user["name"] } : {}),
|
|
57
|
+
...(typeof user["email"] === "string" && user["email"] ? { email: user["email"] } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async signInSocial(provider, callbackURL, errorCallbackURL = callbackURL) {
|
|
68
|
+
const answer = await post("/sign-in/social", { provider, callbackURL, errorCallbackURL });
|
|
69
|
+
const url = answer.json?.["url"];
|
|
70
|
+
return answer.ok && typeof url === "string" ? { url } : errorOf(answer);
|
|
71
|
+
},
|
|
72
|
+
async signOut() {
|
|
73
|
+
const answer = await post("/sign-out", {});
|
|
74
|
+
return answer.ok ? { signedOut: true } : errorOf(answer);
|
|
75
|
+
},
|
|
76
|
+
async addPasskey(name) {
|
|
77
|
+
const options = await call("/passkey/generate-register-options");
|
|
78
|
+
if (!options.ok)
|
|
79
|
+
return errorOf(options);
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await (await webauthn()).startRegistration({ optionsJSON: options.json });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return { error: `the passkey was not made: ${error.message}` };
|
|
86
|
+
}
|
|
87
|
+
const answer = await post("/passkey/verify-registration", { response, name });
|
|
88
|
+
return answer.ok ? { added: true, name } : errorOf(answer);
|
|
89
|
+
},
|
|
90
|
+
async signInWithPasskey() {
|
|
91
|
+
const options = await call("/passkey/generate-authenticate-options");
|
|
92
|
+
if (!options.ok)
|
|
93
|
+
return errorOf(options);
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await (await webauthn()).startAuthentication({ optionsJSON: options.json });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { error: `no passkey was used: ${error.message}` };
|
|
100
|
+
}
|
|
101
|
+
const answer = await post("/passkey/verify-authentication", { response });
|
|
102
|
+
return answer.ok ? { signedIn: true } : errorOf(answer);
|
|
103
|
+
},
|
|
104
|
+
async signInWithPassword({ email, password, name, signUp }) {
|
|
105
|
+
const path = signUp ? "/sign-up/email" : "/sign-in/email";
|
|
106
|
+
const answer = await post(path, { email, password, ...(signUp ? { name: name || email.split("@")[0] } : {}) });
|
|
107
|
+
if (!answer.ok)
|
|
108
|
+
return errorOf(answer);
|
|
109
|
+
const user = answer.json?.["user"];
|
|
110
|
+
return { signedIn: true, ...(user && typeof user["id"] === "string" ? { user: user["id"] } : {}) };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/better-auth",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.1",
|
|
5
5
|
"description": "Better Auth for a procedures server: the sign-in with GitHub, Google and passkeys over MongoDB, and a request hook that gives the principal of a session or of an agent token.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"procedures",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
".": {
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./browser": {
|
|
35
|
+
"types": "./dist/browser.d.ts",
|
|
36
|
+
"import": "./dist/browser.js"
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"files": [
|
|
@@ -49,7 +53,8 @@
|
|
|
49
53
|
"client": null,
|
|
50
54
|
"dependencies": {
|
|
51
55
|
"@better-auth/passkey": "^1.7.10",
|
|
52
|
-
"@procedures/client": "0.13.
|
|
56
|
+
"@procedures/client": "0.13.1",
|
|
57
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
53
58
|
"better-auth": "^1.7.10",
|
|
54
59
|
"zod": "^4.6.5"
|
|
55
60
|
},
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createBrowserAuth, type WebAuthnCeremonies } from "./browser.js";
|
|
3
|
+
|
|
4
|
+
interface Sent {
|
|
5
|
+
url: string;
|
|
6
|
+
method: string;
|
|
7
|
+
credentials: string | undefined;
|
|
8
|
+
body: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A fetch that answers from a table of path → [status, body], and records each call. */
|
|
12
|
+
function fakeServer(routes: Record<string, [number, unknown]>) {
|
|
13
|
+
const sent: Sent[] = [];
|
|
14
|
+
const fetch = (async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
15
|
+
const url = String(input);
|
|
16
|
+
sent.push({ url, method: init?.method ?? "GET", credentials: init?.credentials, body: init?.body ? JSON.parse(String(init.body)) : undefined });
|
|
17
|
+
const path = url.replace(/^https:\/\/api\.example\.com\/auth/, "");
|
|
18
|
+
const [status, body] = routes[path] ?? [404, { message: "no such route" }];
|
|
19
|
+
return new Response(body === undefined ? null : JSON.stringify(body), { status });
|
|
20
|
+
}) as typeof globalThis.fetch;
|
|
21
|
+
return { fetch, sent };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ceremonies = (fail = false): WebAuthnCeremonies => ({
|
|
25
|
+
startRegistration: async () => {
|
|
26
|
+
if (fail) throw new Error("the user cancelled");
|
|
27
|
+
return { id: "credential" };
|
|
28
|
+
},
|
|
29
|
+
startAuthentication: async () => {
|
|
30
|
+
if (fail) throw new Error("the user cancelled");
|
|
31
|
+
return { id: "assertion" };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("createBrowserAuth", () => {
|
|
36
|
+
it("reads the user of the session, with the cookies, and undefined when signed out or on an error", async () => {
|
|
37
|
+
const server = fakeServer({ "/get-session": [200, { user: { id: "u1", name: "Ana", email: "ana@example.com" }, session: {} }] });
|
|
38
|
+
const auth = createBrowserAuth({ baseURL: "https://api.example.com/", fetch: server.fetch });
|
|
39
|
+
expect(await auth.session()).toEqual({ id: "u1", name: "Ana", email: "ana@example.com" });
|
|
40
|
+
expect(server.sent[0]).toMatchObject({ url: "https://api.example.com/auth/get-session", credentials: "include" });
|
|
41
|
+
expect(await createBrowserAuth({ baseURL: "https://api.example.com", fetch: fakeServer({ "/get-session": [200, null] }).fetch }).session()).toBeUndefined();
|
|
42
|
+
const broken = (async () => { throw new Error("offline"); }) as typeof fetch;
|
|
43
|
+
expect(await createBrowserAuth({ baseURL: "https://api.example.com", fetch: broken }).session()).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("gives the page of a provider, or the error of the server", async () => {
|
|
47
|
+
const server = fakeServer({ "/sign-in/social": [200, { url: "https://github.com/login/oauth" }] });
|
|
48
|
+
const auth = createBrowserAuth({ baseURL: "https://api.example.com", fetch: server.fetch });
|
|
49
|
+
expect(await auth.signInSocial("github", "https://app.example.com/board")).toEqual({ url: "https://github.com/login/oauth" });
|
|
50
|
+
expect(server.sent[0]).toMatchObject({ method: "POST", body: { provider: "github", callbackURL: "https://app.example.com/board", errorCallbackURL: "https://app.example.com/board" } });
|
|
51
|
+
const refused = createBrowserAuth({ baseURL: "https://api.example.com", fetch: fakeServer({ "/sign-in/social": [400, { message: "unknown provider" }] }).fetch });
|
|
52
|
+
expect(await refused.signInSocial("x", "https://app.example.com")).toEqual({ error: "unknown provider" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("signs out, and gives the HTTP status when the server gives no message", async () => {
|
|
56
|
+
expect(await createBrowserAuth({ baseURL: "https://api.example.com", fetch: fakeServer({ "/sign-out": [200, { success: true }] }).fetch }).signOut()).toEqual({ signedOut: true });
|
|
57
|
+
expect(await createBrowserAuth({ baseURL: "https://api.example.com", fetch: fakeServer({ "/sign-out": [500, undefined] }).fetch }).signOut()).toEqual({ error: "HTTP 500" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("makes a passkey: the options, the ceremony, then the verification with the name", async () => {
|
|
61
|
+
const server = fakeServer({ "/passkey/generate-register-options": [200, { challenge: "c" }], "/passkey/verify-registration": [200, { ok: true }] });
|
|
62
|
+
const auth = createBrowserAuth({ baseURL: "https://api.example.com", fetch: server.fetch, webauthn: ceremonies() });
|
|
63
|
+
expect(await auth.addPasskey("My laptop")).toEqual({ added: true, name: "My laptop" });
|
|
64
|
+
expect(server.sent.map((s) => s.url.split("/auth")[1])).toEqual(["/passkey/generate-register-options", "/passkey/verify-registration"]);
|
|
65
|
+
expect(server.sent[1]!.body).toEqual({ response: { id: "credential" }, name: "My laptop" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("signs in with a passkey, and reports a cancelled ceremony without a verification", async () => {
|
|
69
|
+
const routes: Record<string, [number, unknown]> = { "/passkey/generate-authenticate-options": [200, { challenge: "c" }], "/passkey/verify-authentication": [200, { ok: true }] };
|
|
70
|
+
expect(await createBrowserAuth({ baseURL: "https://api.example.com", fetch: fakeServer(routes).fetch, webauthn: ceremonies() }).signInWithPasskey()).toEqual({ signedIn: true });
|
|
71
|
+
const server = fakeServer(routes);
|
|
72
|
+
const out = await createBrowserAuth({ baseURL: "https://api.example.com", fetch: server.fetch, webauthn: ceremonies(true) }).signInWithPasskey();
|
|
73
|
+
expect(out).toEqual({ error: "no passkey was used: the user cancelled" });
|
|
74
|
+
expect(server.sent).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("signs up and signs in with a password on a test server, with a name from the email", async () => {
|
|
78
|
+
const server = fakeServer({ "/sign-up/email": [200, { user: { id: "u2" } }], "/sign-in/email": [401, { message: "invalid password" }] });
|
|
79
|
+
const auth = createBrowserAuth({ baseURL: "https://api.example.com", fetch: server.fetch });
|
|
80
|
+
expect(await auth.signInWithPassword({ email: "bo@example.com", password: "pw", signUp: true })).toEqual({ signedIn: true, user: "u2" });
|
|
81
|
+
expect(server.sent[0]!.body).toEqual({ email: "bo@example.com", password: "pw", name: "bo" });
|
|
82
|
+
expect(await auth.signInWithPassword({ email: "bo@example.com", password: "bad" })).toEqual({ error: "invalid password" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("takes another base path", async () => {
|
|
86
|
+
const server = fakeServer({});
|
|
87
|
+
await createBrowserAuth({ baseURL: "", basePath: "/api/auth", fetch: server.fetch }).signOut();
|
|
88
|
+
expect(server.sent[0]!.url).toBe("/api/auth/sign-out");
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the sign-in (Graph's split, wave 2, W8): the calls of a page to the auth routes of a
|
|
3
|
+
* procedures server with Better Auth. This entry has no framework and no global state: each function gives data,
|
|
4
|
+
* and the app decides what the page does (a redirect, a reload).
|
|
5
|
+
*
|
|
6
|
+
* - The session cookie is `httpOnly`, so each call sends the cookies (`credentials: "include"`).
|
|
7
|
+
* - The passkeys use WebAuthn through `@simplewebauthn/browser`. It loads only when a passkey function runs.
|
|
8
|
+
* - The functions give `{ error }` for a refused or failed call. They do not throw for an answer of the server.
|
|
9
|
+
*
|
|
10
|
+
* Import it as `@procedures/better-auth/browser`. The package root is for the server.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** A signed-in user, as the session route gives it. */
|
|
14
|
+
export interface BrowserUser {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name?: string | undefined;
|
|
17
|
+
readonly email?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A refused or failed call. */
|
|
21
|
+
export interface BrowserAuthError {
|
|
22
|
+
readonly error: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The WebAuthn ceremonies. The default is `@simplewebauthn/browser`. A test gives its own. */
|
|
26
|
+
export interface WebAuthnCeremonies {
|
|
27
|
+
startRegistration(options: { optionsJSON: unknown }): Promise<unknown>;
|
|
28
|
+
startAuthentication(options: { optionsJSON: unknown }): Promise<unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The options of `createBrowserAuth`. */
|
|
32
|
+
export interface BrowserAuthOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The origin of the server. An empty text is the origin of the page, the simplest form. A server on another
|
|
35
|
+
* origin needs CORS with credentials for the origin of the page (refer to the README).
|
|
36
|
+
*/
|
|
37
|
+
readonly baseURL: string;
|
|
38
|
+
/** The path of the auth routes. The default is `/auth`, as `betterAuthConfig` gives it. */
|
|
39
|
+
readonly basePath?: string | undefined;
|
|
40
|
+
/** The fetch of the calls. The default is the fetch of the page. */
|
|
41
|
+
readonly fetch?: typeof fetch | undefined;
|
|
42
|
+
/** The WebAuthn ceremonies of the passkeys. */
|
|
43
|
+
readonly webauthn?: WebAuthnCeremonies | undefined;
|
|
44
|
+
/** The longest wait of `session()`, in milliseconds. The default is 4000. */
|
|
45
|
+
readonly sessionTimeoutMs?: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The sign-in calls of a page. */
|
|
49
|
+
export interface BrowserAuth {
|
|
50
|
+
/** The signed-in user of this browser, or undefined (signed out, or the server did not answer in time). */
|
|
51
|
+
session(): Promise<BrowserUser | undefined>;
|
|
52
|
+
/** The page of the provider: the app goes to `url`. After the sign-in, the provider comes back to `callbackURL`. */
|
|
53
|
+
signInSocial(provider: string, callbackURL: string, errorCallbackURL?: string): Promise<{ readonly url: string } | BrowserAuthError>;
|
|
54
|
+
/** This method ends the session. The app then loads the page again. */
|
|
55
|
+
signOut(): Promise<{ readonly signedOut: true } | BrowserAuthError>;
|
|
56
|
+
/** This method makes a passkey of this device for the signed-in user. */
|
|
57
|
+
addPasskey(name: string): Promise<{ readonly added: true; readonly name: string } | BrowserAuthError>;
|
|
58
|
+
/** This method signs in with a passkey of this device. The app then loads the page again. */
|
|
59
|
+
signInWithPasskey(): Promise<{ readonly signedIn: true } | BrowserAuthError>;
|
|
60
|
+
/** A test server only (`AUTH_TEST_ACCOUNTS`): a sign-in, or a sign-up, with an email and a password. */
|
|
61
|
+
signInWithPassword(input: { email: string; password: string; name?: string | undefined; signUp?: boolean | undefined }): Promise<{ readonly signedIn: true; readonly user?: string | undefined } | BrowserAuthError>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Answer {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
status: number;
|
|
67
|
+
json: Record<string, unknown> | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const errorOf = (answer: Answer): BrowserAuthError => ({
|
|
71
|
+
error: typeof answer.json?.["message"] === "string" ? answer.json["message"] : `HTTP ${answer.status}`,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* This function gives the sign-in calls of a page to a server.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const auth = createBrowserAuth({ baseURL: "https://api.example.com" });
|
|
80
|
+
* const user = await auth.session();
|
|
81
|
+
* const out = await auth.signInSocial("github", location.href);
|
|
82
|
+
* if ("url" in out) location.assign(out.url);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createBrowserAuth(options: BrowserAuthOptions): BrowserAuth {
|
|
86
|
+
const base = `${options.baseURL.replace(/\/+$/, "")}${options.basePath ?? "/auth"}`;
|
|
87
|
+
const doFetch = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args));
|
|
88
|
+
const timeoutMs = options.sessionTimeoutMs ?? 4000;
|
|
89
|
+
let ceremonies: Promise<WebAuthnCeremonies> | undefined;
|
|
90
|
+
const webauthn = (): Promise<WebAuthnCeremonies> =>
|
|
91
|
+
(ceremonies ??= options.webauthn ? Promise.resolve(options.webauthn) : import("@simplewebauthn/browser") as Promise<WebAuthnCeremonies>);
|
|
92
|
+
|
|
93
|
+
const call = async (path: string, init: RequestInit = {}): Promise<Answer> => {
|
|
94
|
+
const response = await doFetch(`${base}${path}`, { credentials: "include", ...init });
|
|
95
|
+
let json: Record<string, unknown> | undefined;
|
|
96
|
+
try {
|
|
97
|
+
const body: unknown = await response.json();
|
|
98
|
+
json = body !== null && typeof body === "object" ? (body as Record<string, unknown>) : undefined;
|
|
99
|
+
} catch {
|
|
100
|
+
json = undefined;
|
|
101
|
+
}
|
|
102
|
+
return { ok: response.ok, status: response.status, json };
|
|
103
|
+
};
|
|
104
|
+
const post = (path: string, body: unknown): Promise<Answer> =>
|
|
105
|
+
call(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body ?? {}) });
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
async session() {
|
|
109
|
+
const abort = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
111
|
+
try {
|
|
112
|
+
const answer = await call("/get-session", { signal: abort.signal });
|
|
113
|
+
const user = answer.ok ? (answer.json?.["user"] as Record<string, unknown> | undefined) : undefined;
|
|
114
|
+
if (!user || (typeof user["id"] !== "string" && typeof user["id"] !== "number")) return undefined;
|
|
115
|
+
return {
|
|
116
|
+
id: String(user["id"]),
|
|
117
|
+
...(typeof user["name"] === "string" && user["name"] ? { name: user["name"] } : {}),
|
|
118
|
+
...(typeof user["email"] === "string" && user["email"] ? { email: user["email"] } : {}),
|
|
119
|
+
};
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
} finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async signInSocial(provider, callbackURL, errorCallbackURL = callbackURL) {
|
|
128
|
+
const answer = await post("/sign-in/social", { provider, callbackURL, errorCallbackURL });
|
|
129
|
+
const url = answer.json?.["url"];
|
|
130
|
+
return answer.ok && typeof url === "string" ? { url } : errorOf(answer);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async signOut() {
|
|
134
|
+
const answer = await post("/sign-out", {});
|
|
135
|
+
return answer.ok ? { signedOut: true as const } : errorOf(answer);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async addPasskey(name) {
|
|
139
|
+
const options = await call("/passkey/generate-register-options");
|
|
140
|
+
if (!options.ok) return errorOf(options);
|
|
141
|
+
let response: unknown;
|
|
142
|
+
try {
|
|
143
|
+
response = await (await webauthn()).startRegistration({ optionsJSON: options.json });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { error: `the passkey was not made: ${(error as Error).message}` };
|
|
146
|
+
}
|
|
147
|
+
const answer = await post("/passkey/verify-registration", { response, name });
|
|
148
|
+
return answer.ok ? { added: true as const, name } : errorOf(answer);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async signInWithPasskey() {
|
|
152
|
+
const options = await call("/passkey/generate-authenticate-options");
|
|
153
|
+
if (!options.ok) return errorOf(options);
|
|
154
|
+
let response: unknown;
|
|
155
|
+
try {
|
|
156
|
+
response = await (await webauthn()).startAuthentication({ optionsJSON: options.json });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return { error: `no passkey was used: ${(error as Error).message}` };
|
|
159
|
+
}
|
|
160
|
+
const answer = await post("/passkey/verify-authentication", { response });
|
|
161
|
+
return answer.ok ? { signedIn: true as const } : errorOf(answer);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async signInWithPassword({ email, password, name, signUp }) {
|
|
165
|
+
const path = signUp ? "/sign-up/email" : "/sign-in/email";
|
|
166
|
+
const answer = await post(path, { email, password, ...(signUp ? { name: name || email.split("@")[0] } : {}) });
|
|
167
|
+
if (!answer.ok) return errorOf(answer);
|
|
168
|
+
const user = answer.json?.["user"] as Record<string, unknown> | undefined;
|
|
169
|
+
return { signedIn: true as const, ...(user && typeof user["id"] === "string" ? { user: user["id"] } : {}) };
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|