@dbx-tools/auth-gate 0.6.168
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 +50 -0
- package/index.ts +9 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +7 -0
- package/lib/src/auth.d.ts +47 -0
- package/lib/src/auth.js +217 -0
- package/lib/src/storage.d.ts +31 -0
- package/lib/src/storage.js +120 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +69 -0
- package/src/auth.ts +265 -0
- package/src/storage.ts +161 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `@dbx-tools/auth-gate`
|
|
2
|
+
|
|
3
|
+
Passwordless authentication runtime built on Better Auth, email OTP, passkeys,
|
|
4
|
+
and caller-provided identity policy and delivery.
|
|
5
|
+
|
|
6
|
+
Key features:
|
|
7
|
+
|
|
8
|
+
- Better Auth ownership of users, sessions, OTP lifecycle, rate limits, and
|
|
9
|
+
passkey credentials;
|
|
10
|
+
- session-required passkey enrollment and discoverable passkey login;
|
|
11
|
+
- POST and browser-redirect logout routes with a same-origin destination;
|
|
12
|
+
- caller-provided `authorizeIdentity`, email sender, secret, origin, and
|
|
13
|
+
database;
|
|
14
|
+
- native AppKit Lakebase pool or local SQLite storage;
|
|
15
|
+
- programmatic Better Auth migrations under advisory or file locks.
|
|
16
|
+
|
|
17
|
+
## Relationship To Native AppKit
|
|
18
|
+
|
|
19
|
+
Use the Databricks Apps front door and AppKit execution context when traffic
|
|
20
|
+
arrives through the platform. Use this package for a public tunnel or another
|
|
21
|
+
route that bypasses that identity-aware proxy and therefore needs its own
|
|
22
|
+
passwordless session. A passkey session proves the configured identity only; it
|
|
23
|
+
does not mint a Databricks OBO access token.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { auth, storage } from "@dbx-tools/auth-gate";
|
|
27
|
+
|
|
28
|
+
const database = await storage.createAuthStorage({ storage: "sqlite" });
|
|
29
|
+
const runtime = await auth.createPasswordlessAuth({
|
|
30
|
+
storage: database,
|
|
31
|
+
baseURL: "http://localhost:8000",
|
|
32
|
+
appName: "My app",
|
|
33
|
+
secret: process.env.AUTH_SECRET!,
|
|
34
|
+
logoutRedirectPath: "/",
|
|
35
|
+
sessionTtlSeconds: 2_592_000,
|
|
36
|
+
codeTtlSeconds: 600,
|
|
37
|
+
maxAttempts: 5,
|
|
38
|
+
authorizeIdentity: (email) => email.endsWith("@example.com"),
|
|
39
|
+
sendCode: async (email, code) => sendEmail(email, code),
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`POST <basePath>/logout` returns `{ ok, redirectTo }`; `GET` clears the same
|
|
44
|
+
session and redirects with status `303`. The redirect defaults to `/` and is
|
|
45
|
+
restricted to a same-origin path.
|
|
46
|
+
|
|
47
|
+
## Modules
|
|
48
|
+
|
|
49
|
+
- `auth` - Better Auth runtime and compatibility routes;
|
|
50
|
+
- `storage` - Lakebase/SQLite selection and migration locking.
|
package/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
|
|
5
|
+
export const PACKAGE_IDENTIFIER = "@dbx-tools/auth-gate";
|
|
6
|
+
export * as auth from "./src/auth.ts";
|
|
7
|
+
export * as storage from "./src/storage.ts";
|
|
8
|
+
export type { AuthorizeIdentity, AuthEmailOptions, PasswordlessAuthOptions, PasswordlessAuthRuntime } from "./src/auth.ts";
|
|
9
|
+
export type { AuthStorageMode, AuthStorageConfig, ResolvedAuthStorageConfig, AuthDatabase, AuthStorage } from "./src/storage.ts";
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const PACKAGE_IDENTIFIER = "@dbx-tools/auth-gate";
|
|
2
|
+
export * as auth from "./src/auth.ts";
|
|
3
|
+
export * as storage from "./src/storage.ts";
|
|
4
|
+
export type { AuthorizeIdentity, AuthEmailOptions, PasswordlessAuthOptions, PasswordlessAuthRuntime } from "./src/auth.ts";
|
|
5
|
+
export type { AuthStorageMode, AuthStorageConfig, ResolvedAuthStorageConfig, AuthDatabase, AuthStorage } from "./src/storage.ts";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
export const PACKAGE_IDENTIFIER = "@dbx-tools/auth-gate";
|
|
5
|
+
export * as auth from "./src/auth.js";
|
|
6
|
+
export * as storage from "./src/storage.js";
|
|
7
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxNQUFNLENBQUMsTUFBTSxrQkFBa0IsR0FBRyxzQkFBc0IsQ0FBQztBQUN6RCxPQUFPLEtBQUssSUFBSSxNQUFNLGVBQWUsQ0FBQztBQUN0QyxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gR0VORVJBVEVEIGJ5IHByb2plbiB3YXRjaCAtIERPIE5PVCBFRElULlxuLy8gUmVnZW5lcmF0ZWQgZnJvbSB0aGUgZXhwb3J0aW5nIG1vZHVsZXMgaW4gLi9zcmMuXG4vLyBIYW5kIGVkaXRzIGFyZSBvdmVyd3JpdHRlbiBvbiB0aGUgbmV4dCB3YXRjaDsgdGhpcyBmaWxlIGlzIHJlYWQtb25seS5cblxuZXhwb3J0IGNvbnN0IFBBQ0tBR0VfSURFTlRJRklFUiA9IFwiQGRieC10b29scy9hdXRoLWdhdGVcIjtcbmV4cG9ydCAqIGFzIGF1dGggZnJvbSBcIi4vc3JjL2F1dGgudHNcIjtcbmV4cG9ydCAqIGFzIHN0b3JhZ2UgZnJvbSBcIi4vc3JjL3N0b3JhZ2UudHNcIjtcbmV4cG9ydCB0eXBlIHsgQXV0aG9yaXplSWRlbnRpdHksIEF1dGhFbWFpbE9wdGlvbnMsIFBhc3N3b3JkbGVzc0F1dGhPcHRpb25zLCBQYXNzd29yZGxlc3NBdXRoUnVudGltZSB9IGZyb20gXCIuL3NyYy9hdXRoLnRzXCI7XG5leHBvcnQgdHlwZSB7IEF1dGhTdG9yYWdlTW9kZSwgQXV0aFN0b3JhZ2VDb25maWcsIFJlc29sdmVkQXV0aFN0b3JhZ2VDb25maWcsIEF1dGhEYXRhYmFzZSwgQXV0aFN0b3JhZ2UgfSBmcm9tIFwiLi9zcmMvc3RvcmFnZS50c1wiO1xuIl19
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth passwordless runtime with email OTP and passkeys.
|
|
3
|
+
*
|
|
4
|
+
* Callers provide authorization, delivery, storage, origin, and secret. Better
|
|
5
|
+
* Auth owns users, OTP verification records, sessions, rate limits, passkey
|
|
6
|
+
* credentials, and their native HTTP routes.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { type AuthStatus } from "@dbx-tools/shared-auth";
|
|
11
|
+
import type { AuthStorage } from "./storage.ts";
|
|
12
|
+
export type AuthorizeIdentity = (email: string) => boolean | Promise<boolean>;
|
|
13
|
+
export interface AuthEmailOptions {
|
|
14
|
+
subject: string;
|
|
15
|
+
brandName: string;
|
|
16
|
+
message: string;
|
|
17
|
+
codeTtlSeconds: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PasswordlessAuthOptions {
|
|
20
|
+
storage: AuthStorage;
|
|
21
|
+
baseURL: string;
|
|
22
|
+
basePath?: string;
|
|
23
|
+
appName: string;
|
|
24
|
+
secret: string;
|
|
25
|
+
sessionCookieName?: string;
|
|
26
|
+
/** Same-origin path returned after logout. Defaults to `/`. */
|
|
27
|
+
logoutRedirectPath?: string;
|
|
28
|
+
sessionTtlSeconds: number;
|
|
29
|
+
sessionCutoffMs?: number;
|
|
30
|
+
codeTtlSeconds: number;
|
|
31
|
+
maxAttempts: number;
|
|
32
|
+
authorizeIdentity: AuthorizeIdentity;
|
|
33
|
+
sendCode(email: string, code: string, options: AuthEmailOptions): Promise<void>;
|
|
34
|
+
subject?: string;
|
|
35
|
+
message?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PasswordlessAuthRuntime {
|
|
38
|
+
readonly basePath: string;
|
|
39
|
+
readonly passkeysEnabled: boolean;
|
|
40
|
+
handler(request: Request): Promise<Response>;
|
|
41
|
+
session(headers: Headers): Promise<string | undefined>;
|
|
42
|
+
status(headers: Headers): Promise<AuthStatus>;
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export declare function createPasswordlessAuth(config: PasswordlessAuthOptions): Promise<PasswordlessAuthRuntime>;
|
|
46
|
+
/** Restrict logout redirects to one same-origin application path. */
|
|
47
|
+
export declare function normalizeLogoutRedirectPath(value: string | undefined): string;
|
package/lib/src/auth.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth passwordless runtime with email OTP and passkeys.
|
|
3
|
+
*
|
|
4
|
+
* Callers provide authorization, delivery, storage, origin, and secret. Better
|
|
5
|
+
* Auth owns users, OTP verification records, sessions, rate limits, passkey
|
|
6
|
+
* credentials, and their native HTTP routes.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { passkey } from "@better-auth/passkey";
|
|
11
|
+
import { SESSION_COOKIE_NAME } from "@dbx-tools/shared-auth";
|
|
12
|
+
import { log } from "@dbx-tools/shared-core";
|
|
13
|
+
import { APIError, betterAuth } from "better-auth";
|
|
14
|
+
import { emailOTP } from "better-auth/plugins";
|
|
15
|
+
import { migrateAuth } from "./storage.js";
|
|
16
|
+
const logger = log.logger("auth");
|
|
17
|
+
export async function createPasswordlessAuth(config) {
|
|
18
|
+
const origin = new URL(config.baseURL).origin;
|
|
19
|
+
const rpID = new URL(origin).hostname;
|
|
20
|
+
const basePath = config.basePath ?? "/api/auth";
|
|
21
|
+
const logoutRedirectPath = normalizeLogoutRedirectPath(config.logoutRedirectPath);
|
|
22
|
+
const emailOptions = {
|
|
23
|
+
subject: config.subject ?? "Your verification code",
|
|
24
|
+
brandName: config.appName,
|
|
25
|
+
message: config.message ?? "Your verification code is:",
|
|
26
|
+
codeTtlSeconds: config.codeTtlSeconds,
|
|
27
|
+
};
|
|
28
|
+
const options = {
|
|
29
|
+
appName: config.appName,
|
|
30
|
+
baseURL: origin,
|
|
31
|
+
basePath,
|
|
32
|
+
database: config.storage.database,
|
|
33
|
+
secret: config.secret,
|
|
34
|
+
// The gate fronts the app on whatever interface address the tunnel binds
|
|
35
|
+
// (an overlay/LAN IP, localhost, or a public domain) — not just `baseURL`.
|
|
36
|
+
// Better Auth's origin check would reject every other host with
|
|
37
|
+
// INVALID_ORIGIN and block sign-in. Trust the request's own origin here:
|
|
38
|
+
// this endpoint sits behind the OTP gate and the tunnel preserves the
|
|
39
|
+
// browser's Host, so the fixed-origin CSRF check adds nothing while breaking
|
|
40
|
+
// legitimate access. Same-origin requests (no Origin header) are allowed too.
|
|
41
|
+
trustedOrigins: (request) => {
|
|
42
|
+
const requestOrigin = request?.headers.get("origin");
|
|
43
|
+
return requestOrigin ? [requestOrigin, origin] : [origin];
|
|
44
|
+
},
|
|
45
|
+
session: {
|
|
46
|
+
expiresIn: config.sessionTtlSeconds,
|
|
47
|
+
updateAge: Math.min(24 * 60 * 60, config.sessionTtlSeconds),
|
|
48
|
+
},
|
|
49
|
+
verification: {
|
|
50
|
+
storeIdentifier: "hashed",
|
|
51
|
+
storeInDatabase: true,
|
|
52
|
+
},
|
|
53
|
+
rateLimit: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
window: 15 * 60,
|
|
56
|
+
max: 10,
|
|
57
|
+
customRules: {
|
|
58
|
+
"/email-otp/send-verification-otp": { window: 15 * 60, max: 5 },
|
|
59
|
+
"/sign-in/email-otp": { window: 15 * 60, max: 10 },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
advanced: {
|
|
63
|
+
useSecureCookies: origin.startsWith("https://"),
|
|
64
|
+
ipAddress: {
|
|
65
|
+
ipAddressHeaders: ["x-real-ip"],
|
|
66
|
+
disableIpTracking: false,
|
|
67
|
+
},
|
|
68
|
+
cookies: {
|
|
69
|
+
session_token: {
|
|
70
|
+
name: config.sessionCookieName ?? SESSION_COOKIE_NAME,
|
|
71
|
+
attributes: {
|
|
72
|
+
httpOnly: true,
|
|
73
|
+
sameSite: "lax",
|
|
74
|
+
secure: origin.startsWith("https://"),
|
|
75
|
+
path: "/",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
databaseHooks: {
|
|
81
|
+
user: {
|
|
82
|
+
create: {
|
|
83
|
+
before: async (user) => {
|
|
84
|
+
if (!(await config.authorizeIdentity(normalizeEmail(user.email)))) {
|
|
85
|
+
throw new APIError("FORBIDDEN", { message: "Identity is not authorized" });
|
|
86
|
+
}
|
|
87
|
+
return { data: user };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
plugins: [
|
|
93
|
+
emailOTP({
|
|
94
|
+
otpLength: 6,
|
|
95
|
+
expiresIn: config.codeTtlSeconds,
|
|
96
|
+
allowedAttempts: config.maxAttempts,
|
|
97
|
+
storeOTP: "hashed",
|
|
98
|
+
async sendVerificationOTP({ email, otp, type }) {
|
|
99
|
+
if (type !== "sign-in")
|
|
100
|
+
return;
|
|
101
|
+
const address = normalizeEmail(email);
|
|
102
|
+
if (!(await config.authorizeIdentity(address))) {
|
|
103
|
+
// Accept any address and fail SILENTLY for one not on the allow-list:
|
|
104
|
+
// the request still returns 200 (so the gate never reveals who is
|
|
105
|
+
// allowed), and no code is sent. Logged at debug for diagnostics, not
|
|
106
|
+
// as a warning — an unknown address hitting the login page is normal.
|
|
107
|
+
logger.debug("verification code suppressed: identity not authorized", {
|
|
108
|
+
email: address,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
logger.info("sending verification code", { email: address });
|
|
113
|
+
void config.sendCode(address, otp, emailOptions).catch((error) => {
|
|
114
|
+
logger.error("verification email failed", { email: address, error });
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
passkey({
|
|
119
|
+
rpID,
|
|
120
|
+
rpName: config.appName,
|
|
121
|
+
origin,
|
|
122
|
+
authenticatorSelection: {
|
|
123
|
+
residentKey: "preferred",
|
|
124
|
+
userVerification: "preferred",
|
|
125
|
+
},
|
|
126
|
+
registration: { requireSession: true },
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
await migrateAuth(options, config.storage);
|
|
131
|
+
const auth = betterAuth(options);
|
|
132
|
+
const session = async (headers) => {
|
|
133
|
+
const current = await auth.api.getSession({ headers });
|
|
134
|
+
const email = normalizeEmail(current?.user.email);
|
|
135
|
+
const createdAt = current?.session.createdAt;
|
|
136
|
+
const createdAtMs = createdAt ? new Date(createdAt).getTime() : Number.NaN;
|
|
137
|
+
if (config.sessionCutoffMs && !(createdAtMs >= config.sessionCutoffMs))
|
|
138
|
+
return undefined;
|
|
139
|
+
if (!email || !(await config.authorizeIdentity(email)))
|
|
140
|
+
return undefined;
|
|
141
|
+
return email;
|
|
142
|
+
};
|
|
143
|
+
const handleCompatibilityRoute = async (request) => {
|
|
144
|
+
const path = new URL(request.url).pathname;
|
|
145
|
+
if (path === `${basePath}/status`) {
|
|
146
|
+
const email = await session(request.headers);
|
|
147
|
+
return jsonResponse({
|
|
148
|
+
authenticated: Boolean(email),
|
|
149
|
+
...(email ? { email } : {}),
|
|
150
|
+
enabled: true,
|
|
151
|
+
passkeysEnabled: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// OTP send + verify go straight to better-auth's native emailOTP endpoints
|
|
155
|
+
// (`/email-otp/send-verification-otp`, `/sign-in/email-otp`); there is no
|
|
156
|
+
// compatibility wrapper for them. Failures there are logged by the plugin's
|
|
157
|
+
// sendVerificationOTP hook, not swallowed behind an always-ok response.
|
|
158
|
+
if (path === `${basePath}/logout` && request.method === "POST") {
|
|
159
|
+
const response = await auth.api.signOut({
|
|
160
|
+
headers: request.headers,
|
|
161
|
+
asResponse: true,
|
|
162
|
+
});
|
|
163
|
+
return compatibilityResponse(response, { ok: response.ok, redirectTo: logoutRedirectPath });
|
|
164
|
+
}
|
|
165
|
+
if (path === `${basePath}/logout` && request.method === "GET") {
|
|
166
|
+
const response = await auth.api.signOut({
|
|
167
|
+
headers: request.headers,
|
|
168
|
+
asResponse: true,
|
|
169
|
+
});
|
|
170
|
+
return redirectResponse(response, logoutRedirectPath);
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
basePath,
|
|
176
|
+
passkeysEnabled: true,
|
|
177
|
+
handler: async (request) => (await handleCompatibilityRoute(request)) ?? auth.handler(request),
|
|
178
|
+
session,
|
|
179
|
+
status: async (headers) => {
|
|
180
|
+
const email = await session(headers);
|
|
181
|
+
return {
|
|
182
|
+
authenticated: Boolean(email),
|
|
183
|
+
...(email ? { email } : {}),
|
|
184
|
+
enabled: true,
|
|
185
|
+
passkeysEnabled: true,
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
close: () => config.storage.close(),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Restrict logout redirects to one same-origin application path. */
|
|
192
|
+
export function normalizeLogoutRedirectPath(value) {
|
|
193
|
+
const path = value?.trim();
|
|
194
|
+
return path?.startsWith("/") && !path.startsWith("//") && !path.includes("\\") ? path : "/";
|
|
195
|
+
}
|
|
196
|
+
function normalizeEmail(email) {
|
|
197
|
+
return email?.trim().toLowerCase() ?? "";
|
|
198
|
+
}
|
|
199
|
+
function jsonResponse(body, status = 200) {
|
|
200
|
+
return new Response(JSON.stringify(body), {
|
|
201
|
+
status,
|
|
202
|
+
headers: { "content-type": "application/json" },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function compatibilityResponse(response, body) {
|
|
206
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
207
|
+
for (const cookie of response.headers.getSetCookie())
|
|
208
|
+
headers.append("set-cookie", cookie);
|
|
209
|
+
return new Response(JSON.stringify(body), { status: 200, headers });
|
|
210
|
+
}
|
|
211
|
+
function redirectResponse(response, location) {
|
|
212
|
+
const headers = new Headers({ location });
|
|
213
|
+
for (const cookie of response.headers.getSetCookie())
|
|
214
|
+
headers.append("set-cookie", cookie);
|
|
215
|
+
return new Response(null, { status: 303, headers });
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hdXRoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7OztHQVFHO0FBRUgsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQy9DLE9BQU8sRUFBbUIsbUJBQW1CLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUM5RSxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDN0MsT0FBTyxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQTBCLE1BQU0sYUFBYSxDQUFDO0FBQzNFLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUcvQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBRTNDLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUM7QUF1Q2xDLE1BQU0sQ0FBQyxLQUFLLFVBQVUsc0JBQXNCLENBQzFDLE1BQStCO0lBRS9CLE1BQU0sTUFBTSxHQUFHLElBQUksR0FBRyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDOUMsTUFBTSxJQUFJLEdBQUcsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUMsUUFBUSxDQUFDO0lBQ3RDLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxRQUFRLElBQUksV0FBVyxDQUFDO0lBQ2hELE1BQU0sa0JBQWtCLEdBQUcsMkJBQTJCLENBQUMsTUFBTSxDQUFDLGtCQUFrQixDQUFDLENBQUM7SUFDbEYsTUFBTSxZQUFZLEdBQXFCO1FBQ3JDLE9BQU8sRUFBRSxNQUFNLENBQUMsT0FBTyxJQUFJLHdCQUF3QjtRQUNuRCxTQUFTLEVBQUUsTUFBTSxDQUFDLE9BQU87UUFDekIsT0FBTyxFQUFFLE1BQU0sQ0FBQyxPQUFPLElBQUksNEJBQTRCO1FBQ3ZELGNBQWMsRUFBRSxNQUFNLENBQUMsY0FB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LENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFO1FBQ3hDLE1BQU07UUFDTixPQUFPLEVBQUUsRUFBRSxjQUFjLEVBQUUsa0JBQWtCLEVBQUU7S0FDaEQsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQUVELFNBQVMscUJBQXFCLENBQUMsUUFBa0IsRUFBRSxJQUFhO0lBQzlELE1BQU0sT0FBTyxHQUFHLElBQUksT0FBTyxDQUFDLEVBQUUsY0FBYyxFQUFFLGtCQUFrQixFQUFFLENBQUMsQ0FBQztJQUNwRSxLQUFLLE1BQU0sTUFBTSxJQUFJLFFBQVEsQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFO1FBQUUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxZQUFZLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDM0YsT0FBTyxJQUFJLFFBQVEsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsTUFBTSxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO0FBQ3RFLENBQUM7QUFFRCxTQUFTLGdCQUFnQixDQUFDLFFBQWtCLEVBQUUsUUFBZ0I7SUFDNUQsTUFBTSxPQUFPLEdBQUcsSUFBSSxPQUFPLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQzFDLEtBQUssTUFBTSxNQUFNLElBQUksUUFBUSxDQUFDLE9BQU8sQ0FBQyxZQUFZLEVBQUU7UUFBRSxPQUFPLENBQUMsTUFBTSxDQUFDLFlBQVksRUFBRSxNQUFNLENBQUMsQ0FBQztJQUMzRixPQUFPLElBQUksUUFBUSxDQUFDLElBQUksRUFBRSxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztBQUN0RCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBCZXR0ZXIgQXV0aCBwYXNzd29yZGxlc3MgcnVudGltZSB3aXRoIGVtYWlsIE9UUCBhbmQgcGFzc2tleXMuXG4gKlxuICogQ2FsbGVycyBwcm92aWRlIGF1dGhvcml6YXRpb24sIGRlbGl2ZXJ5LCBzdG9yYWdlLCBvcmlnaW4sIGFuZCBzZWNyZXQuIEJldHRlclxuICogQXV0aCBvd25zIHVzZXJzLCBPVFAgdmVyaWZpY2F0aW9uIHJlY29yZHMsIHNlc3Npb25zLCByYXRlIGxpbWl0cywgcGFzc2tleVxuICogY3JlZGVudGlhbHMsIGFuZCB0aGVpciBuYXRpdmUgSFRUUCByb3V0ZXMuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IHBhc3NrZXkgfSBmcm9tIFwiQGJldHRlci1hdXRoL3Bhc3NrZXlcIjtcbmltcG9ydCB7IHR5cGUgQXV0aFN0YXR1cywgU0VTU0lPTl9DT09LSUVfTkFNRSB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1hdXRoXCI7XG5pbXBvcnQgeyBsb2cgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHsgQVBJRXJyb3IsIGJldHRlckF1dGgsIHR5cGUgQmV0dGVyQXV0aE9wdGlvbnMgfSBmcm9tIFwiYmV0dGVyLWF1dGhcIjtcbmltcG9ydCB7IGVtYWlsT1RQIH0gZnJvbSBcImJldHRlci1hdXRoL3BsdWdpbnNcIjtcblxuaW1wb3J0IHR5cGUgeyBBdXRoU3RvcmFnZSB9IGZyb20gXCIuL3N0b3JhZ2UudHNcIjtcbmltcG9ydCB7IG1pZ3JhdGVBdXRoIH0gZnJvbSBcIi4vc3RvcmFnZS50c1wiO1xuXG5jb25zdCBsb2dnZXIgPSBsb2cubG9nZ2VyKFwiYXV0aFwiKTtcblxuZXhwb3J0IHR5cGUgQXV0aG9yaXplSWRlbnRpdHkgPSAoZW1haWw6IHN0cmluZykgPT4gYm9vbGVhbiB8IFByb21pc2U8Ym9vbGVhbj47XG5cbmV4cG9ydCBpbnRlcmZhY2UgQXV0aEVtYWlsT3B0aW9ucyB7XG4gIHN1YmplY3Q6IHN0cmluZztcbiAgYnJhbmROYW1lOiBzdHJpbmc7XG4gIG1lc3NhZ2U6IHN0cmluZztcbiAgY29kZVR0bFNlY29uZHM6IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBQYXNzd29yZGxlc3NBdXRoT3B0aW9ucyB7XG4gIHN0b3JhZ2U6IEF1dGhTdG9yYWdlO1xuICBiYXNlVVJMOiBzdHJpbmc7XG4gIGJhc2VQYXRoPzogc3RyaW5nO1xuICBhcHBOYW1lOiBzdHJpbmc7XG4gIHNlY3JldDogc3RyaW5nO1xuICBzZXNzaW9uQ29va2llTmFtZT86IHN0cmluZztcbiAgLyoqIFNhbWUtb3JpZ2luIHBhdGggcmV0dXJuZWQgYWZ0ZXIgbG9nb3V0LiBEZWZhdWx0cyB0byBgL2AuICovXG4gIGxvZ291dFJlZGlyZWN0UGF0aD86IHN0cmluZztcbiAgc2Vzc2lvblR0bFNlY29uZHM6IG51bWJlcjtcbiAgc2Vzc2lvbkN1dG9mZk1zPzogbnVtYmVyO1xuICBjb2RlVHRsU2Vjb25kczogbnVtYmVyO1xuICBtYXhBdHRlbXB0czogbnVtYmVyO1xuICBhdXRob3JpemVJZGVudGl0eTogQXV0aG9yaXplSWRlbnRpdHk7XG4gIHNlbmRDb2RlKGVtYWlsOiBzdHJpbmcsIGNvZGU6IHN0cmluZywgb3B0aW9uczogQXV0aEVtYWlsT3B0aW9ucyk6IFByb21pc2U8dm9pZD47XG4gIHN1YmplY3Q/OiBzdHJpbmc7XG4gIG1lc3NhZ2U/OiBzdHJpbmc7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUGFzc3dvcmRsZXNzQXV0aFJ1bnRpbWUge1xuICByZWFkb25seSBiYXNlUGF0aDogc3RyaW5nO1xuICByZWFkb25seSBwYXNza2V5c0VuYWJsZWQ6IGJvb2xlYW47XG4gIGhhbmRsZXIocmVxdWVzdDogUmVxdWVzdCk6IFByb21pc2U8UmVzcG9uc2U+O1xuICBzZXNzaW9uKGhlYWRlcnM6IEhlYWRlcnMpOiBQcm9taXNlPHN0cmluZyB8IHVuZGVmaW5lZD47XG4gIHN0YXR1cyhoZWFkZXJzOiBIZWFkZXJzKTogUHJvbWlzZTxBdXRoU3RhdHVzPjtcbiAgY2xvc2UoKTogUHJvbWlzZTx2b2lkPjtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNyZWF0ZVBhc3N3b3JkbGVzc0F1dGgoXG4gIGNvbmZpZzogUGFzc3dvcmRsZXNzQXV0aE9wdGlvbnMsXG4pOiBQcm9taXNlPFBhc3N3b3JkbGVzc0F1dGhSdW50aW1lPiB7XG4gIGNvbnN0IG9yaWdpbiA9IG5ldyBVUkwoY29uZmlnLmJhc2VVUkwpLm9yaWdpbjtcbiAgY29uc3QgcnBJRCA9IG5ldyBVUkwob3JpZ2luKS5ob3N0bmFtZTtcbiAgY29uc3QgYmFzZVBhdGggPSBjb25maWcuYmFzZVBhdGggPz8gXCIvYXBpL2F1dGhcIjtcbiAgY29uc3QgbG9nb3V0UmVkaXJlY3RQYXRoID0gbm9ybWFsaXplTG9nb3V0UmVkaXJlY3RQYXRoKGNvbmZpZy5sb2dvdXRSZWRpcmVjdFBhdGgpO1xuICBjb25zdCBlbWFpbE9wdGlvbnM6IEF1dGhFbWFpbE9wdGlvbnMgPSB7XG4gICAgc3ViamVjdDogY29uZmlnLnN1YmplY3QgPz8gXCJZb3VyIHZlcmlmaWNhdGlvbiBjb2RlXCIsXG4gICAgYnJhbmROYW1lOiBjb25maWcuYXBwTmFtZSxcbiAgICBtZXNzYWdlOiBjb25maWcubWVzc2FnZSA/PyBcIllvdXIgdmVyaWZpY2F0aW9uIGNvZGUgaXM6XCIsXG4gICAgY29kZVR0bFNlY29uZHM6IGNvbmZpZy5jb2RlVHRsU2Vjb25kcyxcbiAgfTtcblxuICBjb25zdCBvcHRpb25zID0ge1xuICAgIGFwcE5hbWU6IGNvbmZpZy5hcHBOYW1lLFxuICAgIGJhc2VVUkw6IG9yaWdpbixcbiAgICBiYXNlUGF0aCxcbiAgICBkYXRhYmFzZTogY29uZmlnLnN0b3JhZ2UuZGF0YWJhc2UsXG4gICAgc2VjcmV0OiBjb25maWcuc2VjcmV0LFxuICAgIC8vIFRoZSBnYXRlIGZyb250cyB0aGUgYXBwIG9uIHdoYXRldmVyIGludGVyZmFjZSBhZGRyZXNzIHRoZSB0dW5uZWwgYmluZHNcbiAgICAvLyAoYW4gb3ZlcmxheS9MQU4gSVAsIGxvY2FsaG9zdCwgb3IgYSBwdWJsaWMgZG9tYWluKSDigJQgbm90IGp1c3QgYGJhc2VVUkxgLlxuICAgIC8vIEJldHRlciBBdXRoJ3Mgb3JpZ2luIGNoZWNrIHdvdWxkIHJlamVjdCBldmVyeSBvdGhlciBob3N0IHdpdGhcbiAgICAvLyBJTlZBTElEX09SSUdJTiBhbmQgYmxvY2sgc2lnbi1pbi4gVHJ1c3QgdGhlIHJlcXVlc3QncyBvd24gb3JpZ2luIGhlcmU6XG4gICAgLy8gdGhpcyBlbmRwb2ludCBzaXRzIGJlaGluZCB0aGUgT1RQIGdhdGUgYW5kIHRoZSB0dW5uZWwgcHJlc2VydmVzIHRoZVxuICAgIC8vIGJyb3dzZXIncyBIb3N0LCBzbyB0aGUgZml4ZWQtb3JpZ2luIENTUkYgY2hlY2sgYWRkcyBub3RoaW5nIHdoaWxlIGJyZWFraW5nXG4gICAgLy8gbGVnaXRpbWF0ZSBhY2Nlc3MuIFNhbWUtb3JpZ2luIHJlcXVlc3RzIChubyBPcmlnaW4gaGVhZGVyKSBhcmUgYWxsb3dlZCB0b28uXG4gICAgdHJ1c3RlZE9yaWdpbnM6IChyZXF1ZXN0PzogUmVxdWVzdCkgPT4ge1xuICAgICAgY29uc3QgcmVxdWVzdE9yaWdpbiA9IHJlcXVlc3Q/LmhlYWRlcnMuZ2V0KFwib3JpZ2luXCIpO1xuICAgICAgcmV0dXJuIHJlcXVlc3RPcmlnaW4gPyBbcmVxdWVzdE9yaWdpbiwgb3JpZ2luXSA6IFtvcmlnaW5dO1xuICAgIH0sXG4gICAgc2Vzc2lvbjoge1xuICAgICAgZXhwaXJlc0luOiBjb25maWcuc2Vzc2lvblR0bFNlY29uZHMsXG4gICAgICB1cGRhdGVBZ2U6IE1hdGgubWluKDI0ICogNjAgKiA2MCwgY29uZmlnLnNlc3Npb25UdGxTZWNvbmRzKSxcbiAgICB9LFxuICAgIHZlcmlmaWNhdGlvbjoge1xuICAgICAgc3RvcmVJZGVudGlmaWVyOiBcImhhc2hlZFwiLFxuICAgICAgc3RvcmVJbkRhdGFiYXNlOiB0cnVlLFxuICAgIH0sXG4gICAgcmF0ZUxpbWl0OiB7XG4gICAgICBlbmFibGVkOiB0cnVlLFxuICAgICAgd2luZG93OiAxNSAqIDYwLFxuICAgICAgbWF4OiAxMCxcbiAgICAgIGN1c3RvbVJ1bGVzOiB7XG4gICAgICAgIFwiL2VtYWlsLW90cC9zZW5kLXZlcmlmaWNhdGlvbi1vdHBcIjogeyB3aW5kb3c6IDE1ICogNjAsIG1heDogNSB9LFxuICAgICAgICBcIi9zaWduLWluL2VtYWlsLW90cFwiOiB7IHdpbmRvdzogMTUgKiA2MCwgbWF4OiAxMCB9LFxuICAgICAgfSxcbiAgICB9LFxuICAgIGFkdmFuY2VkOiB7XG4gICAgICB1c2VTZWN1cmVDb29raWVzOiBvcmlnaW4uc3RhcnRzV2l0aChcImh0dHBzOi8vXCIpLFxuICAgICAgaXBBZGRyZXNzOiB7XG4gICAgICAgIGlwQWRkcmVzc0hlYWRlcnM6IFtcIngtcmVhbC1pcFwiXSxcbiAgICAgICAgZGlzYWJsZUlwVHJhY2tpbmc6IGZhbHNlLFxuICAgICAgfSxcbiAgICAgIGNvb2tpZXM6IHtcbiAgICAgICAgc2Vzc2lvbl90b2tlbjoge1xuICAgICAgICAgIG5hbWU6IGNvbmZpZy5zZXNzaW9uQ29va2llTmFtZSA/PyBTRVNTSU9OX0NPT0tJRV9OQU1FLFxuICAgICAgICAgIGF0dHJpYnV0ZXM6IHtcbiAgICAgICAgICAgIGh0dHBPbmx5OiB0cnVlLFxuICAgICAgICAgICAgc2FtZVNpdGU6IFwibGF4XCIsXG4gICAgICAgICAgICBzZWN1cmU6IG9yaWdpbi5zdGFydHNXaXRoKFwiaHR0cHM6Ly9cIiksXG4gICAgICAgICAgICBwYXRoOiBcIi9cIixcbiAgICAgICAgICB9LFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICB9LFxuICAgIGRhdGFiYXNlSG9va3M6IHtcbiAgICAgIHVzZXI6IHtcbiAgICAgICAgY3JlYXRlOiB7XG4gICAgICAgICAgYmVmb3JlOiBhc3luYyAodXNlcjogeyBlbWFpbDogc3RyaW5nIH0pID0+IHtcbiAgICAgICAgICAgIGlmICghKGF3YWl0IGNvbmZpZy5hdXRob3JpemVJZGVudGl0eShub3JtYWxpemVFbWFpbCh1c2VyLmVtYWlsKSkpKSB7XG4gICAgICAgICAgICAgIHRocm93IG5ldyBBUElFcnJvcihcIkZPUkJJRERFTlwiLCB7IG1lc3NhZ2U6IFwiSWRlbnRpdHkgaXMgbm90IGF1dGhvcml6ZWRcIiB9KTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiB7IGRhdGE6IHVzZXIgfTtcbiAgICAgICAgICB9LFxuICAgICAgICB9LFxuICAgICAgfSxcbiAgICB9LFxuICAgIHBsdWdpbnM6IFtcbiAgICAgIGVtYWlsT1RQKHtcbiAgICAgICAgb3RwTGVuZ3RoOiA2LFxuICAgICAgICBleHBpcmVzSW46IGNvbmZpZy5jb2RlVHRsU2Vjb25kcyxcbiAgICAgICAgYWxsb3dlZEF0dGVtcHRzOiBjb25maWcubWF4QXR0ZW1wdHMsXG4gICAgICAgIHN0b3JlT1RQOiBcImhhc2hlZFwiLFxuICAgICAgICBhc3luYyBzZW5kVmVyaWZpY2F0aW9uT1RQKHsgZW1haWwsIG90cCwgdHlwZSB9KSB7XG4gICAgICAgICAgaWYgKHR5cGUgIT09IFwic2lnbi1pblwiKSByZXR1cm47XG4gICAgICAgICAgY29uc3QgYWRkcmVzcyA9IG5vcm1hbGl6ZUVtYWlsKGVtYWlsKTtcbiAgICAgICAgICBpZiAoIShhd2FpdCBjb25maWcuYXV0aG9yaXplSWRlbnRpdHkoYWRkcmVzcykpKSB7XG4gICAgICAgICAgICAvLyBBY2NlcHQgYW55IGFkZHJlc3MgYW5kIGZhaWwgU0lMRU5UTFkgZm9yIG9uZSBub3Qgb24gdGhlIGFsbG93LWxpc3Q6XG4gICAgICAgICAgICAvLyB0aGUgcmVxdWVzdCBzdGlsbCByZXR1cm5zIDIwMCAoc28gdGhlIGdhdGUgbmV2ZXIgcmV2ZWFscyB3aG8gaXNcbiAgICAgICAgICAgIC8vIGFsbG93ZWQpLCBhbmQgbm8gY29kZSBpcyBzZW50LiBMb2dnZWQgYXQgZGVidWcgZm9yIGRpYWdub3N0aWNzLCBub3RcbiAgICAgICAgICAgIC8vIGFzIGEgd2FybmluZyDigJQgYW4gdW5rbm93biBhZGRyZXNzIGhpdHRpbmcgdGhlIGxvZ2luIHBhZ2UgaXMgbm9ybWFsLlxuICAgICAgICAgICAgbG9nZ2VyLmRlYnVnKFwidmVyaWZpY2F0aW9uIGNvZGUgc3VwcHJlc3NlZDogaWRlbnRpdHkgbm90IGF1dGhvcml6ZWRcIiwge1xuICAgICAgICAgICAgICBlbWFpbDogYWRkcmVzcyxcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICAgIH1cbiAgICAgICAgICBsb2dnZXIuaW5mbyhcInNlbmRpbmcgdmVyaWZpY2F0aW9uIGNvZGVcIiwgeyBlbWFpbDogYWRkcmVzcyB9KTtcbiAgICAgICAgICB2b2lkIGNvbmZpZy5zZW5kQ29kZShhZGRyZXNzLCBvdHAsIGVtYWlsT3B0aW9ucykuY2F0Y2goKGVycm9yOiB1bmtub3duKSA9PiB7XG4gICAgICAgICAgICBsb2dnZXIuZXJyb3IoXCJ2ZXJpZmljYXRpb24gZW1haWwgZmFpbGVkXCIsIHsgZW1haWw6IGFkZHJlc3MsIGVycm9yIH0pO1xuICAgICAgICAgIH0pO1xuICAgICAgICB9LFxuICAgICAgfSksXG4gICAgICBwYXNza2V5KHtcbiAgICAgICAgcnBJRCxcbiAgICAgICAgcnBOYW1lOiBjb25maWcuYXBwTmFtZSxcbiAgICAgICAgb3JpZ2luLFxuICAgICAgICBhdXRoZW50aWNhdG9yU2VsZWN0aW9uOiB7XG4gICAgICAgICAgcmVzaWRlbnRLZXk6IFwicHJlZmVycmVkXCIsXG4gICAgICAgICAgdXNlclZlcmlmaWNhdGlvbjogXCJwcmVmZXJyZWRcIixcbiAgICAgICAgfSxcbiAgICAgICAgcmVnaXN0cmF0aW9uOiB7IHJlcXVpcmVTZXNzaW9uOiB0cnVlIH0sXG4gICAgICB9KSxcbiAgICBdIGFzIGNvbnN0LFxuICB9IHNhdGlzZmllcyBCZXR0ZXJBdXRoT3B0aW9ucztcblxuICBhd2FpdCBtaWdyYXRlQXV0aChvcHRpb25zLCBjb25maWcuc3RvcmFnZSk7XG4gIGNvbnN0IGF1dGggPSBiZXR0ZXJBdXRoKG9wdGlvbnMpO1xuXG4gIGNvbnN0IHNlc3Npb24gPSBhc3luYyAoaGVhZGVyczogSGVhZGVycyk6IFByb21pc2U8c3RyaW5nIHwgdW5kZWZpbmVkPiA9PiB7XG4gICAgY29uc3QgY3VycmVudCA9IGF3YWl0IGF1dGguYXBpLmdldFNlc3Npb24oeyBoZWFkZXJzIH0pO1xuICAgIGNvbnN0IGVtYWlsID0gbm9ybWFsaXplRW1haWwoY3VycmVudD8udXNlci5lbWFpbCk7XG4gICAgY29uc3QgY3JlYXRlZEF0ID0gY3VycmVudD8uc2Vzc2lvbi5jcmVhdGVkQXQ7XG4gICAgY29uc3QgY3JlYXRlZEF0TXMgPSBjcmVhdGVkQXQgPyBuZXcgRGF0ZShjcmVhdGVkQXQpLmdldFRpbWUoKSA6IE51bWJlci5OYU47XG4gICAgaWYgKGNvbmZpZy5zZXNzaW9uQ3V0b2ZmTXMgJiYgIShjcmVhdGVkQXRNcyA+PSBjb25maWcuc2Vzc2lvbkN1dG9mZk1zKSkgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICBpZiAoIWVtYWlsIHx8ICEoYXdhaXQgY29uZmlnLmF1dGhvcml6ZUlkZW50aXR5KGVtYWlsKSkpIHJldHVybiB1bmRlZmluZWQ7XG4gICAgcmV0dXJuIGVtYWlsO1xuICB9O1xuXG4gIGNvbnN0IGhhbmRsZUNvbXBhdGliaWxpdHlSb3V0ZSA9IGFzeW5jIChyZXF1ZXN0OiBSZXF1ZXN0KTogUHJvbWlzZTxSZXNwb25zZSB8IHVuZGVmaW5lZD4gPT4ge1xuICAgIGNvbnN0IHBhdGggPSBuZXcgVVJMKHJlcXVlc3QudXJsKS5wYXRobmFtZTtcbiAgICBpZiAocGF0aCA9PT0gYCR7YmFzZVBhdGh9L3N0YXR1c2ApIHtcbiAgICAgIGNvbnN0IGVtYWlsID0gYXdhaXQgc2Vzc2lvbihyZXF1ZXN0LmhlYWRlcnMpO1xuICAgICAgcmV0dXJuIGpzb25SZXNwb25zZSh7XG4gICAgICAgIGF1dGhlbnRpY2F0ZWQ6IEJvb2xlYW4oZW1haWwpLFxuICAgICAgICAuLi4oZW1haWwgPyB7IGVtYWlsIH0gOiB7fSksXG4gICAgICAgIGVuYWJsZWQ6IHRydWUsXG4gICAgICAgIHBhc3NrZXlzRW5hYmxlZDogdHJ1ZSxcbiAgICAgIH0pO1xuICAgIH1cbiAgICAvLyBPVFAgc2VuZCArIHZlcmlmeSBnbyBzdHJhaWdodCB0byBiZXR0ZXItYXV0aCdzIG5hdGl2ZSBlbWFpbE9UUCBlbmRwb2ludHNcbiAgICAvLyAoYC9lbWFpbC1vdHAvc2VuZC12ZXJpZmljYXRpb24tb3RwYCwgYC9zaWduLWluL2VtYWlsLW90cGApOyB0aGVyZSBpcyBub1xuICAgIC8vIGNvbXBhdGliaWxpdHkgd3JhcHBlciBmb3IgdGhlbS4gRmFpbHVyZXMgdGhlcmUgYXJlIGxvZ2dlZCBieSB0aGUgcGx1Z2luJ3NcbiAgICAvLyBzZW5kVmVyaWZpY2F0aW9uT1RQIGhvb2ssIG5vdCBzd2FsbG93ZWQgYmVoaW5kIGFuIGFsd2F5cy1vayByZXNwb25zZS5cbiAgICBpZiAocGF0aCA9PT0gYCR7YmFzZVBhdGh9L2xvZ291dGAgJiYgcmVxdWVzdC5tZXRob2QgPT09IFwiUE9TVFwiKSB7XG4gICAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGF1dGguYXBpLnNpZ25PdXQoe1xuICAgICAgICBoZWFkZXJzOiByZXF1ZXN0LmhlYWRlcnMsXG4gICAgICAgIGFzUmVzcG9uc2U6IHRydWUsXG4gICAgICB9KTtcbiAgICAgIHJldHVybiBjb21wYXRpYmlsaXR5UmVzcG9uc2UocmVzcG9uc2UsIHsgb2s6IHJlc3BvbnNlLm9rLCByZWRpcmVjdFRvOiBsb2dvdXRSZWRpcmVjdFBhdGggfSk7XG4gICAgfVxuICAgIGlmIChwYXRoID09PSBgJHtiYXNlUGF0aH0vbG9nb3V0YCAmJiByZXF1ZXN0Lm1ldGhvZCA9PT0gXCJHRVRcIikge1xuICAgICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBhdXRoLmFwaS5zaWduT3V0KHtcbiAgICAgICAgaGVhZGVyczogcmVxdWVzdC5oZWFkZXJzLFxuICAgICAgICBhc1Jlc3BvbnNlOiB0cnVlLFxuICAgICAgfSk7XG4gICAgICByZXR1cm4gcmVkaXJlY3RSZXNwb25zZShyZXNwb25zZSwgbG9nb3V0UmVkaXJlY3RQYXRoKTtcbiAgICB9XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfTtcblxuICByZXR1cm4ge1xuICAgIGJhc2VQYXRoLFxuICAgIHBhc3NrZXlzRW5hYmxlZDogdHJ1ZSxcbiAgICBoYW5kbGVyOiBhc3luYyAocmVxdWVzdCkgPT4gKGF3YWl0IGhhbmRsZUNvbXBhdGliaWxpdHlSb3V0ZShyZXF1ZXN0KSkgPz8gYXV0aC5oYW5kbGVyKHJlcXVlc3QpLFxuICAgIHNlc3Npb24sXG4gICAgc3RhdHVzOiBhc3luYyAoaGVhZGVycykgPT4ge1xuICAgICAgY29uc3QgZW1haWwgPSBhd2FpdCBzZXNzaW9uKGhlYWRlcnMpO1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgYXV0aGVudGljYXRlZDogQm9vbGVhbihlbWFpbCksXG4gICAgICAgIC4uLihlbWFpbCA/IHsgZW1haWwgfSA6IHt9KSxcbiAgICAgICAgZW5hYmxlZDogdHJ1ZSxcbiAgICAgICAgcGFzc2tleXNFbmFibGVkOiB0cnVlLFxuICAgICAgfTtcbiAgICB9LFxuICAgIGNsb3NlOiAoKSA9PiBjb25maWcuc3RvcmFnZS5jbG9zZSgpLFxuICB9O1xufVxuXG4vKiogUmVzdHJpY3QgbG9nb3V0IHJlZGlyZWN0cyB0byBvbmUgc2FtZS1vcmlnaW4gYXBwbGljYXRpb24gcGF0aC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBub3JtYWxpemVMb2dvdXRSZWRpcmVjdFBhdGgodmFsdWU6IHN0cmluZyB8IHVuZGVmaW5lZCk6IHN0cmluZyB7XG4gIGNvbnN0IHBhdGggPSB2YWx1ZT8udHJpbSgpO1xuICByZXR1cm4gcGF0aD8uc3RhcnRzV2l0aChcIi9cIikgJiYgIXBhdGguc3RhcnRzV2l0aChcIi8vXCIpICYmICFwYXRoLmluY2x1ZGVzKFwiXFxcXFwiKSA/IHBhdGggOiBcIi9cIjtcbn1cblxuZnVuY3Rpb24gbm9ybWFsaXplRW1haWwoZW1haWw6IHN0cmluZyB8IHVuZGVmaW5lZCk6IHN0cmluZyB7XG4gIHJldHVybiBlbWFpbD8udHJpbSgpLnRvTG93ZXJDYXNlKCkgPz8gXCJcIjtcbn1cblxuZnVuY3Rpb24ganNvblJlc3BvbnNlKGJvZHk6IHVua25vd24sIHN0YXR1cyA9IDIwMCk6IFJlc3BvbnNlIHtcbiAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeShib2R5KSwge1xuICAgIHN0YXR1cyxcbiAgICBoZWFkZXJzOiB7IFwiY29udGVudC10eXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwiIH0sXG4gIH0pO1xufVxuXG5mdW5jdGlvbiBjb21wYXRpYmlsaXR5UmVzcG9uc2UocmVzcG9uc2U6IFJlc3BvbnNlLCBib2R5OiB1bmtub3duKTogUmVzcG9uc2Uge1xuICBjb25zdCBoZWFkZXJzID0gbmV3IEhlYWRlcnMoeyBcImNvbnRlbnQtdHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9KTtcbiAgZm9yIChjb25zdCBjb29raWUgb2YgcmVzcG9uc2UuaGVhZGVycy5nZXRTZXRDb29raWUoKSkgaGVhZGVycy5hcHBlbmQoXCJzZXQtY29va2llXCIsIGNvb2tpZSk7XG4gIHJldHVybiBuZXcgUmVzcG9uc2UoSlNPTi5zdHJpbmdpZnkoYm9keSksIHsgc3RhdHVzOiAyMDAsIGhlYWRlcnMgfSk7XG59XG5cbmZ1bmN0aW9uIHJlZGlyZWN0UmVzcG9uc2UocmVzcG9uc2U6IFJlc3BvbnNlLCBsb2NhdGlvbjogc3RyaW5nKTogUmVzcG9uc2Uge1xuICBjb25zdCBoZWFkZXJzID0gbmV3IEhlYWRlcnMoeyBsb2NhdGlvbiB9KTtcbiAgZm9yIChjb25zdCBjb29raWUgb2YgcmVzcG9uc2UuaGVhZGVycy5nZXRTZXRDb29raWUoKSkgaGVhZGVycy5hcHBlbmQoXCJzZXQtY29va2llXCIsIGNvb2tpZSk7XG4gIHJldHVybiBuZXcgUmVzcG9uc2UobnVsbCwgeyBzdGF0dXM6IDMwMywgaGVhZGVycyB9KTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth database selection and migration locking.
|
|
3
|
+
*
|
|
4
|
+
* Callers pass a native AppKit Lakebase pool when available. Otherwise auth
|
|
5
|
+
* uses SQLite in the operating system's application-data directory.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { type PgPoolLike } from "@dbx-tools/postgres";
|
|
10
|
+
import type { BetterAuthOptions } from "better-auth";
|
|
11
|
+
export type AuthStorageMode = "auto" | "lakebase" | "sqlite";
|
|
12
|
+
export interface AuthStorageConfig {
|
|
13
|
+
storage?: AuthStorageMode;
|
|
14
|
+
sqlitePath?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolvedAuthStorageConfig {
|
|
17
|
+
mode: AuthStorageMode;
|
|
18
|
+
sqlitePath: string;
|
|
19
|
+
}
|
|
20
|
+
export type AuthDatabase = NonNullable<BetterAuthOptions["database"]>;
|
|
21
|
+
export interface AuthStorage {
|
|
22
|
+
kind: "lakebase" | "sqlite" | "memory";
|
|
23
|
+
database: AuthDatabase;
|
|
24
|
+
pool?: PgPoolLike;
|
|
25
|
+
path?: string;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveAuthStorageConfig(config?: AuthStorageConfig): ResolvedAuthStorageConfig;
|
|
29
|
+
export declare function shouldUseLakebase(config?: AuthStorageConfig): boolean;
|
|
30
|
+
export declare function createAuthStorage(config: AuthStorageConfig, pool?: PgPoolLike): Promise<AuthStorage>;
|
|
31
|
+
export declare function migrateAuth(options: BetterAuthOptions, storage: AuthStorage): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth database selection and migration locking.
|
|
3
|
+
*
|
|
4
|
+
* Callers pass a native AppKit Lakebase pool when available. Otherwise auth
|
|
5
|
+
* uses SQLite in the operating system's application-data directory.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
10
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
11
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
12
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
};
|
|
17
|
+
import { mkdirSync } from "node:fs";
|
|
18
|
+
import { dirname, resolve } from "node:path";
|
|
19
|
+
import { fileLock } from "@dbx-tools/core";
|
|
20
|
+
import { advisoryLock } from "@dbx-tools/postgres";
|
|
21
|
+
import { log } from "@dbx-tools/shared-core";
|
|
22
|
+
import envPaths from "env-paths";
|
|
23
|
+
const logger = log.logger("auth:storage");
|
|
24
|
+
const MIGRATION_LOCK = ["auth", "better-auth", "migrations"];
|
|
25
|
+
export function resolveAuthStorageConfig(config = {}) {
|
|
26
|
+
const mode = config.storage ?? "auto";
|
|
27
|
+
if (mode !== "auto" && mode !== "lakebase" && mode !== "sqlite") {
|
|
28
|
+
throw new TypeError('auth storage must be "auto", "lakebase", or "sqlite"');
|
|
29
|
+
}
|
|
30
|
+
const dataDirectory = envPaths("dbx-tools", { suffix: "" }).data;
|
|
31
|
+
return {
|
|
32
|
+
mode,
|
|
33
|
+
sqlitePath: resolve(config.sqlitePath ?? resolve(dataDirectory, "auth", "auth.sqlite")),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function shouldUseLakebase(config = {}) {
|
|
37
|
+
const resolved = resolveAuthStorageConfig(config);
|
|
38
|
+
if (resolved.mode === "lakebase")
|
|
39
|
+
return true;
|
|
40
|
+
if (resolved.mode === "sqlite")
|
|
41
|
+
return false;
|
|
42
|
+
return Boolean(process.env.LAKEBASE_ENDPOINT ?? process.env.PGHOST);
|
|
43
|
+
}
|
|
44
|
+
export async function createAuthStorage(config, pool) {
|
|
45
|
+
const resolved = resolveAuthStorageConfig(config);
|
|
46
|
+
if (pool && resolved.mode !== "sqlite") {
|
|
47
|
+
return {
|
|
48
|
+
kind: "lakebase",
|
|
49
|
+
database: pool,
|
|
50
|
+
pool,
|
|
51
|
+
close: async () => undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (resolved.mode === "lakebase") {
|
|
55
|
+
throw new Error("auth storage is lakebase but no Lakebase pool was supplied");
|
|
56
|
+
}
|
|
57
|
+
// Prefer SQLite (durable, survives restarts) when a SQLite binding is present
|
|
58
|
+
// — bun:sqlite in Bun, node:sqlite in Node. Both are optional runtime
|
|
59
|
+
// features, so fall back to an in-memory adapter when neither can be opened
|
|
60
|
+
// rather than failing the whole gate. Memory loses sessions/OTPs on restart
|
|
61
|
+
// but keeps sign-in working; an explicit `--auth-storage sqlite` still errors
|
|
62
|
+
// if SQLite is genuinely unavailable, so the fallback is auto-mode only.
|
|
63
|
+
try {
|
|
64
|
+
mkdirSync(dirname(resolved.sqlitePath), { recursive: true });
|
|
65
|
+
const database = await openSqlite(resolved.sqlitePath);
|
|
66
|
+
return {
|
|
67
|
+
kind: "sqlite",
|
|
68
|
+
database,
|
|
69
|
+
path: resolved.sqlitePath,
|
|
70
|
+
close: async () => {
|
|
71
|
+
database.close();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (resolved.mode === "sqlite")
|
|
77
|
+
throw error;
|
|
78
|
+
logger.warn("sqlite unavailable for auth storage; using in-memory adapter", { error });
|
|
79
|
+
const { memoryAdapter } = await import("better-auth/adapters/memory");
|
|
80
|
+
return {
|
|
81
|
+
kind: "memory",
|
|
82
|
+
database: memoryAdapter({}),
|
|
83
|
+
close: async () => undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function migrateAuth(options, storage) {
|
|
88
|
+
// The in-memory adapter builds its schema in memory on init — there is no
|
|
89
|
+
// database to migrate.
|
|
90
|
+
if (storage.kind === "memory")
|
|
91
|
+
return;
|
|
92
|
+
const run = async () => {
|
|
93
|
+
const module = (await import(__rewriteRelativeImportExtension(migrationModuleUrl())));
|
|
94
|
+
const migrations = await module.getMigrations(options);
|
|
95
|
+
await migrations.runMigrations();
|
|
96
|
+
};
|
|
97
|
+
if (storage.kind === "lakebase" && storage.pool) {
|
|
98
|
+
await advisoryLock.withAdvisoryLock(storage.pool, MIGRATION_LOCK, run);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await fileLock.withFileLock(MIGRATION_LOCK, run);
|
|
102
|
+
}
|
|
103
|
+
async function openSqlite(path) {
|
|
104
|
+
if (process.versions.bun) {
|
|
105
|
+
const specifier = "bun:sqlite";
|
|
106
|
+
const { Database } = (await import(__rewriteRelativeImportExtension(specifier)));
|
|
107
|
+
const database = new Database(path, { create: true, strict: true });
|
|
108
|
+
database.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON");
|
|
109
|
+
return database;
|
|
110
|
+
}
|
|
111
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
112
|
+
const database = new DatabaseSync(path);
|
|
113
|
+
database.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON");
|
|
114
|
+
return database;
|
|
115
|
+
}
|
|
116
|
+
function migrationModuleUrl() {
|
|
117
|
+
const entry = import.meta.resolve("better-auth");
|
|
118
|
+
return new URL("./db/get-migration.mjs", entry).href;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zdG9yYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7O0dBT0c7Ozs7Ozs7OztBQUVILE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDcEMsT0FBTyxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDN0MsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzNDLE9BQU8sRUFBRSxZQUFZLEVBQW1CLE1BQU0scUJBQXFCLENBQUM7QUFDcEUsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRTdDLE9BQU8sUUFBUSxNQUFNLFdBQVcsQ0FBQztBQUVqQyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFDLGNBQWMsQ0FBQyxDQUFDO0FBdUMxQyxNQUFNLGNBQWMsR0FBRyxDQUFDLE1BQU0sRUFBRSxhQUFhLEVBQUUsWUFBWSxDQUFVLENBQUM7QUFFdEUsTUFBTSxVQUFVLHdCQUF3QixDQUN0QyxTQUE0QixFQUFFO0lBRTlCLE1BQU0sSUFBSSxHQUFHLE1BQU0sQ0FBQyxPQUFPLElBQUksTUFBTSxDQUFDO0lBQ3RDLElBQUksSUFBSSxLQUFLLE1BQU0sSUFBSSxJQUFJLEtBQUssVUFBVSxJQUFJLElBQUksS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUNoRSxNQUFNLElBQUksU0FBUyxDQUFDLHNEQUFzRCxDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUNELE1BQU0sYUFBYSxHQUFHLFFBQVEsQ0FBQyxXQUFXLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUM7SUFDakUsT0FBTztRQUNMLElBQUk7UUFDSixVQUFVLEVBQUUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxVQUFVLElBQUksT0FBTyxDQUFDLGFBQWEsRUFBRSxNQUFNLEVB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QUFFLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQztBQUN2RCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBCZXR0ZXIgQXV0aCBkYXRhYmFzZSBzZWxlY3Rpb24gYW5kIG1pZ3JhdGlvbiBsb2NraW5nLlxuICpcbiAqIENhbGxlcnMgcGFzcyBhIG5hdGl2ZSBBcHBLaXQgTGFrZWJhc2UgcG9vbCB3aGVuIGF2YWlsYWJsZS4gT3RoZXJ3aXNlIGF1dGhcbiAqIHVzZXMgU1FMaXRlIGluIHRoZSBvcGVyYXRpbmcgc3lzdGVtJ3MgYXBwbGljYXRpb24tZGF0YSBkaXJlY3RvcnkuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IG1rZGlyU3luYyB9IGZyb20gXCJub2RlOmZzXCI7XG5pbXBvcnQgeyBkaXJuYW1lLCByZXNvbHZlIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgZmlsZUxvY2sgfSBmcm9tIFwiQGRieC10b29scy9jb3JlXCI7XG5pbXBvcnQgeyBhZHZpc29yeUxvY2ssIHR5cGUgUGdQb29sTGlrZSB9IGZyb20gXCJAZGJ4LXRvb2xzL3Bvc3RncmVzXCI7XG5pbXBvcnQgeyBsb2cgfSBmcm9tIFwiQGRieC10b29scy9zaGFyZWQtY29yZVwiO1xuaW1wb3J0IHR5cGUgeyBCZXR0ZXJBdXRoT3B0aW9ucyB9IGZyb20gXCJiZXR0ZXItYXV0aFwiO1xuaW1wb3J0IGVudlBhdGhzIGZyb20gXCJlbnYtcGF0aHNcIjtcblxuY29uc3QgbG9nZ2VyID0gbG9nLmxvZ2dlcihcImF1dGg6c3RvcmFnZVwiKTtcblxuZXhwb3J0IHR5cGUgQXV0aFN0b3JhZ2VNb2RlID0gXCJhdXRvXCIgfCBcImxha2ViYXNlXCIgfCBcInNxbGl0ZVwiO1xuXG5leHBvcnQgaW50ZXJmYWNlIEF1dGhTdG9yYWdlQ29uZmlnIHtcbiAgc3RvcmFnZT86IEF1dGhTdG9yYWdlTW9kZTtcbiAgc3FsaXRlUGF0aD86IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBSZXNvbHZlZEF1dGhTdG9yYWdlQ29uZmlnIHtcbiAgbW9kZTogQXV0aFN0b3JhZ2VNb2RlO1xuICBzcWxpdGVQYXRoOiBzdHJpbmc7XG59XG5cbmV4cG9ydCB0eXBlIEF1dGhEYXRhYmFzZSA9IE5vbk51bGxhYmxlPEJldHRlckF1dGhPcHRpb25zW1wiZGF0YWJhc2VcIl0+O1xuXG5leHBvcnQgaW50ZXJmYWNlIEF1dGhTdG9yYWdlIHtcbiAga2luZDogXCJsYWtlYmFzZVwiIHwgXCJzcWxpdGVcIiB8IFwibWVtb3J5XCI7XG4gIGRhdGFiYXNlOiBBdXRoRGF0YWJhc2U7XG4gIHBvb2w/OiBQZ1Bvb2xMaWtlO1xuICBwYXRoPzogc3RyaW5nO1xuICBjbG9zZSgpOiBQcm9taXNlPHZvaWQ+O1xufVxuXG50eXBlIE1pZ3JhdGlvbk1vZHVsZSA9IHtcbiAgZ2V0TWlncmF0aW9ucyhvcHRpb25zOiBCZXR0ZXJBdXRoT3B0aW9ucyk6IFByb21pc2U8e1xuICAgIHJ1bk1pZ3JhdGlvbnMoKTogUHJvbWlzZTx2b2lkPjtcbiAgfT47XG59O1xuXG5pbnRlcmZhY2UgU3FsaXRlRGF0YWJhc2Uge1xuICBleGVjKHNxbDogc3RyaW5nKTogdW5rbm93bjtcbiAgY2xvc2UoKTogdm9pZDtcbn1cblxuaW50ZXJmYWNlIEJ1blNxbGl0ZU1vZHVsZSB7XG4gIERhdGFiYXNlOiBuZXcgKHBhdGg6IHN0cmluZywgb3B0aW9ucz86IHsgY3JlYXRlPzogYm9vbGVhbjsgc3RyaWN0PzogYm9vbGVhbiB9KSA9PiBTcWxpdGVEYXRhYmFzZTtcbn1cblxuY29uc3QgTUlHUkFUSU9OX0xPQ0sgPSBbXCJhdXRoXCIsIFwiYmV0dGVyLWF1dGhcIiwgXCJtaWdyYXRpb25zXCJdIGFzIGNvbnN0O1xuXG5leHBvcnQgZnVuY3Rpb24gcmVzb2x2ZUF1dGhTdG9yYWdlQ29uZmlnKFxuICBjb25maWc6IEF1dGhTdG9yYWdlQ29uZmlnID0ge30sXG4pOiBSZXNvbHZlZEF1dGhTdG9yYWdlQ29uZmlnIHtcbiAgY29uc3QgbW9kZSA9IGNvbmZpZy5zdG9yYWdlID8/IFwiYXV0b1wiO1xuICBpZiAobW9kZSAhPT0gXCJhdXRvXCIgJiYgbW9kZSAhPT0gXCJsYWtlYmFzZVwiICYmIG1vZGUgIT09IFwic3FsaXRlXCIpIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdhdXRoIHN0b3JhZ2UgbXVzdCBiZSBcImF1dG9cIiwgXCJsYWtlYmFzZVwiLCBvciBcInNxbGl0ZVwiJyk7XG4gIH1cbiAgY29uc3QgZGF0YURpcmVjdG9yeSA9IGVudlBhdGhzKFwiZGJ4LXRvb2xzXCIsIHsgc3VmZml4OiBcIlwiIH0pLmRhdGE7XG4gIHJldHVybiB7XG4gICAgbW9kZSxcbiAgICBzcWxpdGVQYXRoOiByZXNvbHZlKGNvbmZpZy5zcWxpdGVQYXRoID8/IHJlc29sdmUoZGF0YURpcmVjdG9yeSwgXCJhdXRoXCIsIFwiYXV0aC5zcWxpdGVcIikpLFxuICB9O1xufVxuXG5leHBvcnQgZnVuY3Rpb24gc2hvdWxkVXNlTGFrZWJhc2UoY29uZmlnOiBBdXRoU3RvcmFnZUNvbmZpZyA9IHt9KTogYm9vbGVhbiB7XG4gIGNvbnN0IHJlc29sdmVkID0gcmVzb2x2ZUF1dGhTdG9yYWdlQ29uZmlnKGNvbmZpZyk7XG4gIGlmIChyZXNvbHZlZC5tb2RlID09PSBcImxha2ViYXNlXCIpIHJldHVybiB0cnVlO1xuICBpZiAocmVzb2x2ZWQubW9kZSA9PT0gXCJzcWxpdGVcIikgcmV0dXJuIGZhbHNlO1xuICByZXR1cm4gQm9vbGVhbihwcm9jZXNzLmVudi5MQUtFQkFTRV9FTkRQT0lOVCA/PyBwcm9jZXNzLmVudi5QR0hPU1QpO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY3JlYXRlQXV0aFN0b3JhZ2UoXG4gIGNvbmZpZzogQXV0aFN0b3JhZ2VDb25maWcsXG4gIHBvb2w/OiBQZ1Bvb2xMaWtlLFxuKTogUHJvbWlzZTxBdXRoU3RvcmFnZT4ge1xuICBjb25zdCByZXNvbHZlZCA9IHJlc29sdmVBdXRoU3RvcmFnZUNvbmZpZyhjb25maWcpO1xuICBpZiAocG9vbCAmJiByZXNvbHZlZC5tb2RlICE9PSBcInNxbGl0ZVwiKSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIGtpbmQ6IFwibGFrZWJhc2VcIixcbiAgICAgIGRhdGFiYXNlOiBwb29sLFxuICAgICAgcG9vbCxcbiAgICAgIGNsb3NlOiBhc3luYyAoKSA9PiB1bmRlZmluZWQsXG4gICAgfTtcbiAgfVxuICBpZiAocmVzb2x2ZWQubW9kZSA9PT0gXCJsYWtlYmFzZVwiKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKFwiYXV0aCBzdG9yYWdlIGlzIGxha2ViYXNlIGJ1dCBubyBMYWtlYmFzZSBwb29sIHdhcyBzdXBwbGllZFwiKTtcbiAgfVxuXG4gIC8vIFByZWZlciBTUUxpdGUgKGR1cmFibGUsIHN1cnZpdmVzIHJlc3RhcnRzKSB3aGVuIGEgU1FMaXRlIGJpbmRpbmcgaXMgcHJlc2VudFxuICAvLyDigJQgYnVuOnNxbGl0ZSBpbiBCdW4sIG5vZGU6c3FsaXRlIGluIE5vZGUuIEJvdGggYXJlIG9wdGlvbmFsIHJ1bnRpbWVcbiAgLy8gZmVhdHVyZXMsIHNvIGZhbGwgYmFjayB0byBhbiBpbi1tZW1vcnkgYWRhcHRlciB3aGVuIG5laXRoZXIgY2FuIGJlIG9wZW5lZFxuICAvLyByYXRoZXIgdGhhbiBmYWlsaW5nIHRoZSB3aG9sZSBnYXRlLiBNZW1vcnkgbG9zZXMgc2Vzc2lvbnMvT1RQcyBvbiByZXN0YXJ0XG4gIC8vIGJ1dCBrZWVwcyBzaWduLWluIHdvcmtpbmc7IGFuIGV4cGxpY2l0IGAtLWF1dGgtc3RvcmFnZSBzcWxpdGVgIHN0aWxsIGVycm9yc1xuICAvLyBpZiBTUUxpdGUgaXMgZ2VudWluZWx5IHVuYXZhaWxhYmxlLCBzbyB0aGUgZmFsbGJhY2sgaXMgYXV0by1tb2RlIG9ubHkuXG4gIHRyeSB7XG4gICAgbWtkaXJTeW5jKGRpcm5hbWUocmVzb2x2ZWQuc3FsaXRlUGF0aCksIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICAgIGNvbnN0IGRhdGFiYXNlID0gYXdhaXQgb3BlblNxbGl0ZShyZXNvbHZlZC5zcWxpdGVQYXRoKTtcbiAgICByZXR1cm4ge1xuICAgICAga2luZDogXCJzcWxpdGVcIixcbiAgICAgIGRhdGFiYXNlLFxuICAgICAgcGF0aDogcmVzb2x2ZWQuc3FsaXRlUGF0aCxcbiAgICAgIGNsb3NlOiBhc3luYyAoKSA9PiB7XG4gICAgICAgIGRhdGFiYXNlLmNsb3NlKCk7XG4gICAgICB9LFxuICAgIH07XG4gIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgaWYgKHJlc29sdmVkLm1vZGUgPT09IFwic3FsaXRlXCIpIHRocm93IGVycm9yO1xuICAgIGxvZ2dlci53YXJuKFwic3FsaXRlIHVuYXZhaWxhYmxlIGZvciBhdXRoIHN0b3JhZ2U7IHVzaW5nIGluLW1lbW9yeSBhZGFwdGVyXCIsIHsgZXJyb3IgfSk7XG4gICAgY29uc3QgeyBtZW1vcnlBZGFwdGVyIH0gPSBhd2FpdCBpbXBvcnQoXCJiZXR0ZXItYXV0aC9hZGFwdGVycy9tZW1vcnlcIik7XG4gICAgcmV0dXJuIHtcbiAgICAgIGtpbmQ6IFwibWVtb3J5XCIsXG4gICAgICBkYXRhYmFzZTogbWVtb3J5QWRhcHRlcih7fSkgYXMgdW5rbm93biBhcyBBdXRoRGF0YWJhc2UsXG4gICAgICBjbG9zZTogYXN5bmMgKCkgPT4gdW5kZWZpbmVkLFxuICAgIH07XG4gIH1cbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIG1pZ3JhdGVBdXRoKG9wdGlvbnM6IEJldHRlckF1dGhPcHRpb25zLCBzdG9yYWdlOiBBdXRoU3RvcmFnZSk6IFByb21pc2U8dm9pZD4ge1xuICAvLyBUaGUgaW4tbWVtb3J5IGFkYXB0ZXIgYnVpbGRzIGl0cyBzY2hlbWEgaW4gbWVtb3J5IG9uIGluaXQg4oCUIHRoZXJlIGlzIG5vXG4gIC8vIGRhdGFiYXNlIHRvIG1pZ3JhdGUuXG4gIGlmIChzdG9yYWdlLmtpbmQgPT09IFwibWVtb3J5XCIpIHJldHVybjtcblxuICBjb25zdCBydW4gPSBhc3luYyAoKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgY29uc3QgbW9kdWxlID0gKGF3YWl0IGltcG9ydChtaWdyYXRpb25Nb2R1bGVVcmwoKSkpIGFzIE1pZ3JhdGlvbk1vZHVsZTtcbiAgICBjb25zdCBtaWdyYXRpb25zID0gYXdhaXQgbW9kdWxlLmdldE1pZ3JhdGlvbnMob3B0aW9ucyk7XG4gICAgYXdhaXQgbWlncmF0aW9ucy5ydW5NaWdyYXRpb25zKCk7XG4gIH07XG5cbiAgaWYgKHN0b3JhZ2Uua2luZCA9PT0gXCJsYWtlYmFzZVwiICYmIHN0b3JhZ2UucG9vbCkge1xuICAgIGF3YWl0IGFkdmlzb3J5TG9jay53aXRoQWR2aXNvcnlMb2NrKHN0b3JhZ2UucG9vbCwgTUlHUkFUSU9OX0xPQ0ssIHJ1bik7XG4gICAgcmV0dXJuO1xuICB9XG4gIGF3YWl0IGZpbGVMb2NrLndpdGhGaWxlTG9jayhNSUdSQVRJT05fTE9DSywgcnVuKTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gb3BlblNxbGl0ZShwYXRoOiBzdHJpbmcpOiBQcm9taXNlPEF1dGhEYXRhYmFzZSAmIHsgY2xvc2UoKTogdm9pZCB9PiB7XG4gIGlmIChwcm9jZXNzLnZlcnNpb25zLmJ1bikge1xuICAgIGNvbnN0IHNwZWNpZmllciA9IFwiYnVuOnNxbGl0ZVwiO1xuICAgIGNvbnN0IHsgRGF0YWJhc2UgfSA9IChhd2FpdCBpbXBvcnQoc3BlY2lmaWVyKSkgYXMgQnVuU3FsaXRlTW9kdWxlO1xuICAgIGNvbnN0IGRhdGFiYXNlID0gbmV3IERhdGFiYXNlKHBhdGgsIHsgY3JlYXRlOiB0cnVlLCBzdHJpY3Q6IHRydWUgfSk7XG4gICAgZGF0YWJhc2UuZXhlYyhcIlBSQUdNQSBqb3VybmFsX21vZGUgPSBXQUw7IFBSQUdNQSBmb3JlaWduX2tleXMgPSBPTlwiKTtcbiAgICByZXR1cm4gZGF0YWJhc2U7XG4gIH1cbiAgY29uc3QgeyBEYXRhYmFzZVN5bmMgfSA9IGF3YWl0IGltcG9ydChcIm5vZGU6c3FsaXRlXCIpO1xuICBjb25zdCBkYXRhYmFzZSA9IG5ldyBEYXRhYmFzZVN5bmMocGF0aCk7XG4gIGRhdGFiYXNlLmV4ZWMoXCJQUkFHTUEgam91cm5hbF9tb2RlID0gV0FMOyBQUkFHTUEgZm9yZWlnbl9rZXlzID0gT05cIik7XG4gIHJldHVybiBkYXRhYmFzZTtcbn1cblxuZnVuY3Rpb24gbWlncmF0aW9uTW9kdWxlVXJsKCk6IHN0cmluZyB7XG4gIGNvbnN0IGVudHJ5ID0gaW1wb3J0Lm1ldGEucmVzb2x2ZShcImJldHRlci1hdXRoXCIpO1xuICByZXR1cm4gbmV3IFVSTChcIi4vZGIvZ2V0LW1pZ3JhdGlvbi5tanNcIiwgZW50cnkpLmhyZWY7XG59XG4iXX0=
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/auth.ts","../src/storage.ts","../index.ts"],"version":"5.9.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dbx-tools/auth-gate",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/reggie-db/dbx-tools.git",
|
|
6
|
+
"directory": "packages/js/node/auth-gate"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "projen build",
|
|
10
|
+
"compile": "projen compile",
|
|
11
|
+
"default": "projen default",
|
|
12
|
+
"package": "projen package",
|
|
13
|
+
"post-compile": "projen post-compile",
|
|
14
|
+
"pre-compile": "projen pre-compile",
|
|
15
|
+
"prepack": "projen prepack",
|
|
16
|
+
"test": "projen test",
|
|
17
|
+
"watch": "projen watch",
|
|
18
|
+
"projen": "projen"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "1.3.14",
|
|
22
|
+
"@types/node": "^24.6.0",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@better-auth/passkey": "^1.6.25",
|
|
27
|
+
"@dbx-tools/core": "0.6.168",
|
|
28
|
+
"@dbx-tools/postgres": "0.6.168",
|
|
29
|
+
"@dbx-tools/shared-auth": "0.6.168",
|
|
30
|
+
"@dbx-tools/shared-core": "0.6.168",
|
|
31
|
+
"better-auth": "^1.6.25",
|
|
32
|
+
"env-paths": "^4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"main": "./lib/index.js",
|
|
35
|
+
"license": "UNLICENSED",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"main": "./lib/index.js",
|
|
39
|
+
"types": "./lib/index.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./lib/index.d.ts",
|
|
43
|
+
"default": "./lib/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"version": "0.6.168",
|
|
49
|
+
"types": "./lib/index.d.ts",
|
|
50
|
+
"type": "module",
|
|
51
|
+
"exports": {
|
|
52
|
+
".": {
|
|
53
|
+
"types": "./lib/index.d.ts",
|
|
54
|
+
"default": "./lib/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./package.json": "./package.json"
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"index.ts",
|
|
60
|
+
"src",
|
|
61
|
+
"lib"
|
|
62
|
+
],
|
|
63
|
+
"dbxToolsConfig": {
|
|
64
|
+
"tags": [
|
|
65
|
+
"node"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
|
|
69
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth passwordless runtime with email OTP and passkeys.
|
|
3
|
+
*
|
|
4
|
+
* Callers provide authorization, delivery, storage, origin, and secret. Better
|
|
5
|
+
* Auth owns users, OTP verification records, sessions, rate limits, passkey
|
|
6
|
+
* credentials, and their native HTTP routes.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { passkey } from "@better-auth/passkey";
|
|
12
|
+
import { type AuthStatus, SESSION_COOKIE_NAME } from "@dbx-tools/shared-auth";
|
|
13
|
+
import { log } from "@dbx-tools/shared-core";
|
|
14
|
+
import { APIError, betterAuth, type BetterAuthOptions } from "better-auth";
|
|
15
|
+
import { emailOTP } from "better-auth/plugins";
|
|
16
|
+
|
|
17
|
+
import type { AuthStorage } from "./storage.ts";
|
|
18
|
+
import { migrateAuth } from "./storage.ts";
|
|
19
|
+
|
|
20
|
+
const logger = log.logger("auth");
|
|
21
|
+
|
|
22
|
+
export type AuthorizeIdentity = (email: string) => boolean | Promise<boolean>;
|
|
23
|
+
|
|
24
|
+
export interface AuthEmailOptions {
|
|
25
|
+
subject: string;
|
|
26
|
+
brandName: string;
|
|
27
|
+
message: string;
|
|
28
|
+
codeTtlSeconds: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PasswordlessAuthOptions {
|
|
32
|
+
storage: AuthStorage;
|
|
33
|
+
baseURL: string;
|
|
34
|
+
basePath?: string;
|
|
35
|
+
appName: string;
|
|
36
|
+
secret: string;
|
|
37
|
+
sessionCookieName?: string;
|
|
38
|
+
/** Same-origin path returned after logout. Defaults to `/`. */
|
|
39
|
+
logoutRedirectPath?: string;
|
|
40
|
+
sessionTtlSeconds: number;
|
|
41
|
+
sessionCutoffMs?: number;
|
|
42
|
+
codeTtlSeconds: number;
|
|
43
|
+
maxAttempts: number;
|
|
44
|
+
authorizeIdentity: AuthorizeIdentity;
|
|
45
|
+
sendCode(email: string, code: string, options: AuthEmailOptions): Promise<void>;
|
|
46
|
+
subject?: string;
|
|
47
|
+
message?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PasswordlessAuthRuntime {
|
|
51
|
+
readonly basePath: string;
|
|
52
|
+
readonly passkeysEnabled: boolean;
|
|
53
|
+
handler(request: Request): Promise<Response>;
|
|
54
|
+
session(headers: Headers): Promise<string | undefined>;
|
|
55
|
+
status(headers: Headers): Promise<AuthStatus>;
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function createPasswordlessAuth(
|
|
60
|
+
config: PasswordlessAuthOptions,
|
|
61
|
+
): Promise<PasswordlessAuthRuntime> {
|
|
62
|
+
const origin = new URL(config.baseURL).origin;
|
|
63
|
+
const rpID = new URL(origin).hostname;
|
|
64
|
+
const basePath = config.basePath ?? "/api/auth";
|
|
65
|
+
const logoutRedirectPath = normalizeLogoutRedirectPath(config.logoutRedirectPath);
|
|
66
|
+
const emailOptions: AuthEmailOptions = {
|
|
67
|
+
subject: config.subject ?? "Your verification code",
|
|
68
|
+
brandName: config.appName,
|
|
69
|
+
message: config.message ?? "Your verification code is:",
|
|
70
|
+
codeTtlSeconds: config.codeTtlSeconds,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const options = {
|
|
74
|
+
appName: config.appName,
|
|
75
|
+
baseURL: origin,
|
|
76
|
+
basePath,
|
|
77
|
+
database: config.storage.database,
|
|
78
|
+
secret: config.secret,
|
|
79
|
+
// The gate fronts the app on whatever interface address the tunnel binds
|
|
80
|
+
// (an overlay/LAN IP, localhost, or a public domain) — not just `baseURL`.
|
|
81
|
+
// Better Auth's origin check would reject every other host with
|
|
82
|
+
// INVALID_ORIGIN and block sign-in. Trust the request's own origin here:
|
|
83
|
+
// this endpoint sits behind the OTP gate and the tunnel preserves the
|
|
84
|
+
// browser's Host, so the fixed-origin CSRF check adds nothing while breaking
|
|
85
|
+
// legitimate access. Same-origin requests (no Origin header) are allowed too.
|
|
86
|
+
trustedOrigins: (request?: Request) => {
|
|
87
|
+
const requestOrigin = request?.headers.get("origin");
|
|
88
|
+
return requestOrigin ? [requestOrigin, origin] : [origin];
|
|
89
|
+
},
|
|
90
|
+
session: {
|
|
91
|
+
expiresIn: config.sessionTtlSeconds,
|
|
92
|
+
updateAge: Math.min(24 * 60 * 60, config.sessionTtlSeconds),
|
|
93
|
+
},
|
|
94
|
+
verification: {
|
|
95
|
+
storeIdentifier: "hashed",
|
|
96
|
+
storeInDatabase: true,
|
|
97
|
+
},
|
|
98
|
+
rateLimit: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
window: 15 * 60,
|
|
101
|
+
max: 10,
|
|
102
|
+
customRules: {
|
|
103
|
+
"/email-otp/send-verification-otp": { window: 15 * 60, max: 5 },
|
|
104
|
+
"/sign-in/email-otp": { window: 15 * 60, max: 10 },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
advanced: {
|
|
108
|
+
useSecureCookies: origin.startsWith("https://"),
|
|
109
|
+
ipAddress: {
|
|
110
|
+
ipAddressHeaders: ["x-real-ip"],
|
|
111
|
+
disableIpTracking: false,
|
|
112
|
+
},
|
|
113
|
+
cookies: {
|
|
114
|
+
session_token: {
|
|
115
|
+
name: config.sessionCookieName ?? SESSION_COOKIE_NAME,
|
|
116
|
+
attributes: {
|
|
117
|
+
httpOnly: true,
|
|
118
|
+
sameSite: "lax",
|
|
119
|
+
secure: origin.startsWith("https://"),
|
|
120
|
+
path: "/",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
databaseHooks: {
|
|
126
|
+
user: {
|
|
127
|
+
create: {
|
|
128
|
+
before: async (user: { email: string }) => {
|
|
129
|
+
if (!(await config.authorizeIdentity(normalizeEmail(user.email)))) {
|
|
130
|
+
throw new APIError("FORBIDDEN", { message: "Identity is not authorized" });
|
|
131
|
+
}
|
|
132
|
+
return { data: user };
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
plugins: [
|
|
138
|
+
emailOTP({
|
|
139
|
+
otpLength: 6,
|
|
140
|
+
expiresIn: config.codeTtlSeconds,
|
|
141
|
+
allowedAttempts: config.maxAttempts,
|
|
142
|
+
storeOTP: "hashed",
|
|
143
|
+
async sendVerificationOTP({ email, otp, type }) {
|
|
144
|
+
if (type !== "sign-in") return;
|
|
145
|
+
const address = normalizeEmail(email);
|
|
146
|
+
if (!(await config.authorizeIdentity(address))) {
|
|
147
|
+
// Accept any address and fail SILENTLY for one not on the allow-list:
|
|
148
|
+
// the request still returns 200 (so the gate never reveals who is
|
|
149
|
+
// allowed), and no code is sent. Logged at debug for diagnostics, not
|
|
150
|
+
// as a warning — an unknown address hitting the login page is normal.
|
|
151
|
+
logger.debug("verification code suppressed: identity not authorized", {
|
|
152
|
+
email: address,
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
logger.info("sending verification code", { email: address });
|
|
157
|
+
void config.sendCode(address, otp, emailOptions).catch((error: unknown) => {
|
|
158
|
+
logger.error("verification email failed", { email: address, error });
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
passkey({
|
|
163
|
+
rpID,
|
|
164
|
+
rpName: config.appName,
|
|
165
|
+
origin,
|
|
166
|
+
authenticatorSelection: {
|
|
167
|
+
residentKey: "preferred",
|
|
168
|
+
userVerification: "preferred",
|
|
169
|
+
},
|
|
170
|
+
registration: { requireSession: true },
|
|
171
|
+
}),
|
|
172
|
+
] as const,
|
|
173
|
+
} satisfies BetterAuthOptions;
|
|
174
|
+
|
|
175
|
+
await migrateAuth(options, config.storage);
|
|
176
|
+
const auth = betterAuth(options);
|
|
177
|
+
|
|
178
|
+
const session = async (headers: Headers): Promise<string | undefined> => {
|
|
179
|
+
const current = await auth.api.getSession({ headers });
|
|
180
|
+
const email = normalizeEmail(current?.user.email);
|
|
181
|
+
const createdAt = current?.session.createdAt;
|
|
182
|
+
const createdAtMs = createdAt ? new Date(createdAt).getTime() : Number.NaN;
|
|
183
|
+
if (config.sessionCutoffMs && !(createdAtMs >= config.sessionCutoffMs)) return undefined;
|
|
184
|
+
if (!email || !(await config.authorizeIdentity(email))) return undefined;
|
|
185
|
+
return email;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleCompatibilityRoute = async (request: Request): Promise<Response | undefined> => {
|
|
189
|
+
const path = new URL(request.url).pathname;
|
|
190
|
+
if (path === `${basePath}/status`) {
|
|
191
|
+
const email = await session(request.headers);
|
|
192
|
+
return jsonResponse({
|
|
193
|
+
authenticated: Boolean(email),
|
|
194
|
+
...(email ? { email } : {}),
|
|
195
|
+
enabled: true,
|
|
196
|
+
passkeysEnabled: true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// OTP send + verify go straight to better-auth's native emailOTP endpoints
|
|
200
|
+
// (`/email-otp/send-verification-otp`, `/sign-in/email-otp`); there is no
|
|
201
|
+
// compatibility wrapper for them. Failures there are logged by the plugin's
|
|
202
|
+
// sendVerificationOTP hook, not swallowed behind an always-ok response.
|
|
203
|
+
if (path === `${basePath}/logout` && request.method === "POST") {
|
|
204
|
+
const response = await auth.api.signOut({
|
|
205
|
+
headers: request.headers,
|
|
206
|
+
asResponse: true,
|
|
207
|
+
});
|
|
208
|
+
return compatibilityResponse(response, { ok: response.ok, redirectTo: logoutRedirectPath });
|
|
209
|
+
}
|
|
210
|
+
if (path === `${basePath}/logout` && request.method === "GET") {
|
|
211
|
+
const response = await auth.api.signOut({
|
|
212
|
+
headers: request.headers,
|
|
213
|
+
asResponse: true,
|
|
214
|
+
});
|
|
215
|
+
return redirectResponse(response, logoutRedirectPath);
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
basePath,
|
|
222
|
+
passkeysEnabled: true,
|
|
223
|
+
handler: async (request) => (await handleCompatibilityRoute(request)) ?? auth.handler(request),
|
|
224
|
+
session,
|
|
225
|
+
status: async (headers) => {
|
|
226
|
+
const email = await session(headers);
|
|
227
|
+
return {
|
|
228
|
+
authenticated: Boolean(email),
|
|
229
|
+
...(email ? { email } : {}),
|
|
230
|
+
enabled: true,
|
|
231
|
+
passkeysEnabled: true,
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
close: () => config.storage.close(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Restrict logout redirects to one same-origin application path. */
|
|
239
|
+
export function normalizeLogoutRedirectPath(value: string | undefined): string {
|
|
240
|
+
const path = value?.trim();
|
|
241
|
+
return path?.startsWith("/") && !path.startsWith("//") && !path.includes("\\") ? path : "/";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeEmail(email: string | undefined): string {
|
|
245
|
+
return email?.trim().toLowerCase() ?? "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
249
|
+
return new Response(JSON.stringify(body), {
|
|
250
|
+
status,
|
|
251
|
+
headers: { "content-type": "application/json" },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function compatibilityResponse(response: Response, body: unknown): Response {
|
|
256
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
257
|
+
for (const cookie of response.headers.getSetCookie()) headers.append("set-cookie", cookie);
|
|
258
|
+
return new Response(JSON.stringify(body), { status: 200, headers });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function redirectResponse(response: Response, location: string): Response {
|
|
262
|
+
const headers = new Headers({ location });
|
|
263
|
+
for (const cookie of response.headers.getSetCookie()) headers.append("set-cookie", cookie);
|
|
264
|
+
return new Response(null, { status: 303, headers });
|
|
265
|
+
}
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth database selection and migration locking.
|
|
3
|
+
*
|
|
4
|
+
* Callers pass a native AppKit Lakebase pool when available. Otherwise auth
|
|
5
|
+
* uses SQLite in the operating system's application-data directory.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { fileLock } from "@dbx-tools/core";
|
|
13
|
+
import { advisoryLock, type PgPoolLike } from "@dbx-tools/postgres";
|
|
14
|
+
import { log } from "@dbx-tools/shared-core";
|
|
15
|
+
import type { BetterAuthOptions } from "better-auth";
|
|
16
|
+
import envPaths from "env-paths";
|
|
17
|
+
|
|
18
|
+
const logger = log.logger("auth:storage");
|
|
19
|
+
|
|
20
|
+
export type AuthStorageMode = "auto" | "lakebase" | "sqlite";
|
|
21
|
+
|
|
22
|
+
export interface AuthStorageConfig {
|
|
23
|
+
storage?: AuthStorageMode;
|
|
24
|
+
sqlitePath?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResolvedAuthStorageConfig {
|
|
28
|
+
mode: AuthStorageMode;
|
|
29
|
+
sqlitePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type AuthDatabase = NonNullable<BetterAuthOptions["database"]>;
|
|
33
|
+
|
|
34
|
+
export interface AuthStorage {
|
|
35
|
+
kind: "lakebase" | "sqlite" | "memory";
|
|
36
|
+
database: AuthDatabase;
|
|
37
|
+
pool?: PgPoolLike;
|
|
38
|
+
path?: string;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type MigrationModule = {
|
|
43
|
+
getMigrations(options: BetterAuthOptions): Promise<{
|
|
44
|
+
runMigrations(): Promise<void>;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
interface SqliteDatabase {
|
|
49
|
+
exec(sql: string): unknown;
|
|
50
|
+
close(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BunSqliteModule {
|
|
54
|
+
Database: new (path: string, options?: { create?: boolean; strict?: boolean }) => SqliteDatabase;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const MIGRATION_LOCK = ["auth", "better-auth", "migrations"] as const;
|
|
58
|
+
|
|
59
|
+
export function resolveAuthStorageConfig(
|
|
60
|
+
config: AuthStorageConfig = {},
|
|
61
|
+
): ResolvedAuthStorageConfig {
|
|
62
|
+
const mode = config.storage ?? "auto";
|
|
63
|
+
if (mode !== "auto" && mode !== "lakebase" && mode !== "sqlite") {
|
|
64
|
+
throw new TypeError('auth storage must be "auto", "lakebase", or "sqlite"');
|
|
65
|
+
}
|
|
66
|
+
const dataDirectory = envPaths("dbx-tools", { suffix: "" }).data;
|
|
67
|
+
return {
|
|
68
|
+
mode,
|
|
69
|
+
sqlitePath: resolve(config.sqlitePath ?? resolve(dataDirectory, "auth", "auth.sqlite")),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function shouldUseLakebase(config: AuthStorageConfig = {}): boolean {
|
|
74
|
+
const resolved = resolveAuthStorageConfig(config);
|
|
75
|
+
if (resolved.mode === "lakebase") return true;
|
|
76
|
+
if (resolved.mode === "sqlite") return false;
|
|
77
|
+
return Boolean(process.env.LAKEBASE_ENDPOINT ?? process.env.PGHOST);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function createAuthStorage(
|
|
81
|
+
config: AuthStorageConfig,
|
|
82
|
+
pool?: PgPoolLike,
|
|
83
|
+
): Promise<AuthStorage> {
|
|
84
|
+
const resolved = resolveAuthStorageConfig(config);
|
|
85
|
+
if (pool && resolved.mode !== "sqlite") {
|
|
86
|
+
return {
|
|
87
|
+
kind: "lakebase",
|
|
88
|
+
database: pool,
|
|
89
|
+
pool,
|
|
90
|
+
close: async () => undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (resolved.mode === "lakebase") {
|
|
94
|
+
throw new Error("auth storage is lakebase but no Lakebase pool was supplied");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Prefer SQLite (durable, survives restarts) when a SQLite binding is present
|
|
98
|
+
// — bun:sqlite in Bun, node:sqlite in Node. Both are optional runtime
|
|
99
|
+
// features, so fall back to an in-memory adapter when neither can be opened
|
|
100
|
+
// rather than failing the whole gate. Memory loses sessions/OTPs on restart
|
|
101
|
+
// but keeps sign-in working; an explicit `--auth-storage sqlite` still errors
|
|
102
|
+
// if SQLite is genuinely unavailable, so the fallback is auto-mode only.
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(dirname(resolved.sqlitePath), { recursive: true });
|
|
105
|
+
const database = await openSqlite(resolved.sqlitePath);
|
|
106
|
+
return {
|
|
107
|
+
kind: "sqlite",
|
|
108
|
+
database,
|
|
109
|
+
path: resolved.sqlitePath,
|
|
110
|
+
close: async () => {
|
|
111
|
+
database.close();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (resolved.mode === "sqlite") throw error;
|
|
116
|
+
logger.warn("sqlite unavailable for auth storage; using in-memory adapter", { error });
|
|
117
|
+
const { memoryAdapter } = await import("better-auth/adapters/memory");
|
|
118
|
+
return {
|
|
119
|
+
kind: "memory",
|
|
120
|
+
database: memoryAdapter({}) as unknown as AuthDatabase,
|
|
121
|
+
close: async () => undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function migrateAuth(options: BetterAuthOptions, storage: AuthStorage): Promise<void> {
|
|
127
|
+
// The in-memory adapter builds its schema in memory on init — there is no
|
|
128
|
+
// database to migrate.
|
|
129
|
+
if (storage.kind === "memory") return;
|
|
130
|
+
|
|
131
|
+
const run = async (): Promise<void> => {
|
|
132
|
+
const module = (await import(migrationModuleUrl())) as MigrationModule;
|
|
133
|
+
const migrations = await module.getMigrations(options);
|
|
134
|
+
await migrations.runMigrations();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (storage.kind === "lakebase" && storage.pool) {
|
|
138
|
+
await advisoryLock.withAdvisoryLock(storage.pool, MIGRATION_LOCK, run);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await fileLock.withFileLock(MIGRATION_LOCK, run);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function openSqlite(path: string): Promise<AuthDatabase & { close(): void }> {
|
|
145
|
+
if (process.versions.bun) {
|
|
146
|
+
const specifier = "bun:sqlite";
|
|
147
|
+
const { Database } = (await import(specifier)) as BunSqliteModule;
|
|
148
|
+
const database = new Database(path, { create: true, strict: true });
|
|
149
|
+
database.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON");
|
|
150
|
+
return database;
|
|
151
|
+
}
|
|
152
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
153
|
+
const database = new DatabaseSync(path);
|
|
154
|
+
database.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON");
|
|
155
|
+
return database;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function migrationModuleUrl(): string {
|
|
159
|
+
const entry = import.meta.resolve("better-auth");
|
|
160
|
+
return new URL("./db/get-migration.mjs", entry).href;
|
|
161
|
+
}
|