@lalternative/auth 0.13.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +21 -4
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +96 -5
- package/dist/server.js +364 -3
- package/dist/server.js.map +1 -1
- package/dist/{sso-profile-HO-u17jd.d.ts → sso-profile-aNyHaZHJ.d.ts} +1 -1
- package/dist/{types-3OHf736K.d.ts → types-COX3VaBw.d.ts} +39 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -16,6 +16,216 @@ import {
|
|
|
16
16
|
import { betterAuth, APIError } from "better-auth";
|
|
17
17
|
import { emailOTP, admin, magicLink, twoFactor, genericOAuth } from "better-auth/plugins";
|
|
18
18
|
|
|
19
|
+
// src/kratos-credentials.ts
|
|
20
|
+
var KRATOS_SENTINEL_HASH = "kratos:external-credential:no-local-hash";
|
|
21
|
+
function isKratosSentinel(hash) {
|
|
22
|
+
return hash === KRATOS_SENTINEL_HASH;
|
|
23
|
+
}
|
|
24
|
+
var FLOW_TIMEOUT_MS = 5e3;
|
|
25
|
+
function csrfTokenOf(flow) {
|
|
26
|
+
return flow.ui?.nodes?.find((n) => n.attributes?.name === "csrf_token")?.attributes?.value;
|
|
27
|
+
}
|
|
28
|
+
function outcomeFromMessages(body) {
|
|
29
|
+
const ids = (body.ui?.messages ?? []).map((m) => m.id);
|
|
30
|
+
if (ids.includes(4000010)) return { status: "account_disabled" };
|
|
31
|
+
if (ids.includes(4000006) || ids.includes(4000002)) {
|
|
32
|
+
return { status: "invalid_credentials" };
|
|
33
|
+
}
|
|
34
|
+
const errorId = body.error?.id;
|
|
35
|
+
if (errorId === "session_aal2_required") {
|
|
36
|
+
return { status: "second_factor_required" };
|
|
37
|
+
}
|
|
38
|
+
if (errorId === "session_verified_address_required") {
|
|
39
|
+
return { status: "email_not_verified" };
|
|
40
|
+
}
|
|
41
|
+
if (errorId === "browser_location_change_required") {
|
|
42
|
+
return { status: "second_factor_required" };
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
async function verifyAgainstKratos(publicUrl, check, fetchImpl = fetch) {
|
|
47
|
+
const base = publicUrl.replace(/\/$/, "");
|
|
48
|
+
let flow;
|
|
49
|
+
try {
|
|
50
|
+
const started = await fetchImpl(`${base}/self-service/login/api`, {
|
|
51
|
+
method: "GET",
|
|
52
|
+
headers: { Accept: "application/json" },
|
|
53
|
+
signal: AbortSignal.timeout(FLOW_TIMEOUT_MS)
|
|
54
|
+
});
|
|
55
|
+
if (!started.ok) return { status: "unavailable" };
|
|
56
|
+
flow = await started.json();
|
|
57
|
+
if (!flow?.id) return { status: "unavailable" };
|
|
58
|
+
} catch {
|
|
59
|
+
return { status: "unavailable" };
|
|
60
|
+
}
|
|
61
|
+
const csrf = csrfTokenOf(flow);
|
|
62
|
+
try {
|
|
63
|
+
const submitted = await fetchImpl(
|
|
64
|
+
`${base}/self-service/login?flow=${encodeURIComponent(flow.id)}`,
|
|
65
|
+
{
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
method: "password",
|
|
70
|
+
identifier: check.email.trim().toLowerCase(),
|
|
71
|
+
password: check.password,
|
|
72
|
+
...csrf ? { csrf_token: csrf } : {}
|
|
73
|
+
}),
|
|
74
|
+
signal: AbortSignal.timeout(FLOW_TIMEOUT_MS)
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
if (submitted.ok) {
|
|
78
|
+
const body2 = await submitted.json();
|
|
79
|
+
const identity = body2.session?.identity;
|
|
80
|
+
if (!identity?.id) return { status: "unavailable" };
|
|
81
|
+
if (identity.state && identity.state !== "active") {
|
|
82
|
+
return { status: "account_disabled" };
|
|
83
|
+
}
|
|
84
|
+
return { status: "valid", identityId: identity.id };
|
|
85
|
+
}
|
|
86
|
+
if (submitted.status >= 500) return { status: "unavailable" };
|
|
87
|
+
const body = await submitted.json().catch(() => ({}));
|
|
88
|
+
const mapped = outcomeFromMessages(body);
|
|
89
|
+
if (mapped) return mapped;
|
|
90
|
+
if (submitted.status === 400 || submitted.status === 401) {
|
|
91
|
+
return { status: "invalid_credentials" };
|
|
92
|
+
}
|
|
93
|
+
if (submitted.status === 403) return { status: "second_factor_required" };
|
|
94
|
+
return { status: "unavailable" };
|
|
95
|
+
} catch {
|
|
96
|
+
return { status: "unavailable" };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/identity-provisioning.ts
|
|
101
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
102
|
+
var TOKEN_EXPIRY_MARGIN_S = 30;
|
|
103
|
+
var tokenCache = /* @__PURE__ */ new Map();
|
|
104
|
+
async function accessToken(config, fetchImpl) {
|
|
105
|
+
const key = `${config.issuer}|${config.clientId}`;
|
|
106
|
+
const cached = tokenCache.get(key);
|
|
107
|
+
const now = Date.now() / 1e3;
|
|
108
|
+
if (cached && cached.expiresAt > now) return cached.value;
|
|
109
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
110
|
+
const credentials = btoa(`${config.clientId}:${config.clientSecret}`);
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetchImpl(`${base}/oauth2/token`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Basic ${credentials}`,
|
|
116
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
117
|
+
},
|
|
118
|
+
body: new URLSearchParams({
|
|
119
|
+
grant_type: "client_credentials",
|
|
120
|
+
audience: "urbangate",
|
|
121
|
+
scope: "urbangate:identities:provision"
|
|
122
|
+
}).toString(),
|
|
123
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) return void 0;
|
|
126
|
+
const body = await response.json();
|
|
127
|
+
if (!body.access_token) return void 0;
|
|
128
|
+
tokenCache.set(key, {
|
|
129
|
+
value: body.access_token,
|
|
130
|
+
expiresAt: now + (body.expires_in ?? 900) - TOKEN_EXPIRY_MARGIN_S
|
|
131
|
+
});
|
|
132
|
+
return body.access_token;
|
|
133
|
+
} catch {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function provisionIdentity(config, request, fetchImpl = fetch) {
|
|
138
|
+
const token = await accessToken(config, fetchImpl);
|
|
139
|
+
if (!token) return { status: "unavailable" };
|
|
140
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetchImpl(`${base}/api/machine/identities`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Bearer ${token}`,
|
|
146
|
+
"Content-Type": "application/json"
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
email: request.email.trim().toLowerCase(),
|
|
150
|
+
email_verified: true,
|
|
151
|
+
name: request.name ?? "",
|
|
152
|
+
role: config.role,
|
|
153
|
+
product: config.product,
|
|
154
|
+
...request.password ? { password: request.password } : {}
|
|
155
|
+
}),
|
|
156
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
157
|
+
});
|
|
158
|
+
if (response.ok) {
|
|
159
|
+
const body2 = await response.json();
|
|
160
|
+
if (!body2.identity_id) return { status: "unavailable" };
|
|
161
|
+
return {
|
|
162
|
+
status: "provisioned",
|
|
163
|
+
identityId: body2.identity_id,
|
|
164
|
+
created: body2.created === true
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (response.status === 503 || response.status >= 500) {
|
|
168
|
+
return { status: "unavailable" };
|
|
169
|
+
}
|
|
170
|
+
if (response.status === 401) {
|
|
171
|
+
tokenCache.delete(`${config.issuer}|${config.clientId}`);
|
|
172
|
+
return { status: "unavailable" };
|
|
173
|
+
}
|
|
174
|
+
const body = await response.json().catch(() => ({}));
|
|
175
|
+
return {
|
|
176
|
+
status: "rejected",
|
|
177
|
+
reason: body.error ?? body.message ?? `http_${response.status}`
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
return { status: "unavailable" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function updateIdentityPassword(config, request, fetchImpl = fetch) {
|
|
184
|
+
const token = await accessToken(config, fetchImpl);
|
|
185
|
+
if (!token) return { status: "unavailable" };
|
|
186
|
+
const base = config.issuer.replace(/\/$/, "");
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetchImpl(`${base}/api/machine/passwords`, {
|
|
189
|
+
method: "PUT",
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${token}`,
|
|
192
|
+
"Content-Type": "application/json"
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
email: request.email.trim().toLowerCase(),
|
|
196
|
+
password: request.password
|
|
197
|
+
}),
|
|
198
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
199
|
+
});
|
|
200
|
+
if (response.ok) {
|
|
201
|
+
const body2 = await response.json();
|
|
202
|
+
if (!body2.identity_id) return { status: "unavailable" };
|
|
203
|
+
return {
|
|
204
|
+
status: "provisioned",
|
|
205
|
+
identityId: body2.identity_id,
|
|
206
|
+
created: false
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (response.status === 503 || response.status >= 500) {
|
|
210
|
+
return { status: "unavailable" };
|
|
211
|
+
}
|
|
212
|
+
if (response.status === 401) {
|
|
213
|
+
tokenCache.delete(`${config.issuer}|${config.clientId}`);
|
|
214
|
+
return { status: "unavailable" };
|
|
215
|
+
}
|
|
216
|
+
if (response.status === 404) {
|
|
217
|
+
return { status: "rejected", reason: "not_found" };
|
|
218
|
+
}
|
|
219
|
+
const body = await response.json().catch(() => ({}));
|
|
220
|
+
return {
|
|
221
|
+
status: "rejected",
|
|
222
|
+
reason: body.error ?? body.message ?? `http_${response.status}`
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return { status: "unavailable" };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
19
229
|
// src/google-defaults.ts
|
|
20
230
|
function withGoogleDefaults(google) {
|
|
21
231
|
if (typeof google === "function") return google;
|
|
@@ -140,7 +350,8 @@ function createPlatformAuth(config) {
|
|
|
140
350
|
rateLimit,
|
|
141
351
|
twoFactor: twoFactorConfig,
|
|
142
352
|
trustedOrigins,
|
|
143
|
-
sso
|
|
353
|
+
sso,
|
|
354
|
+
kratosPasswords
|
|
144
355
|
} = config;
|
|
145
356
|
const ssoProviderId = sso?.providerId ?? "urbangate";
|
|
146
357
|
const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects };
|
|
@@ -153,7 +364,16 @@ function createPlatformAuth(config) {
|
|
|
153
364
|
rateLimit: resolveRateLimit(rateLimit),
|
|
154
365
|
emailAndPassword: {
|
|
155
366
|
enabled: true,
|
|
156
|
-
requireEmailVerification: true
|
|
367
|
+
requireEmailVerification: true,
|
|
368
|
+
...kratosPasswords ? {
|
|
369
|
+
password: {
|
|
370
|
+
// Sign-in refuses before reaching the verifier when the account
|
|
371
|
+
// carries no hash, so a sign-up must still write one. It is a
|
|
372
|
+
// constant that validates nothing, never a hash of the password.
|
|
373
|
+
hash: kratosHasher(kratosPasswords),
|
|
374
|
+
verify: kratosVerifier(kratosPasswords)
|
|
375
|
+
}
|
|
376
|
+
} : {}
|
|
157
377
|
},
|
|
158
378
|
// Never auto-merge a social identity into an existing account by matching
|
|
159
379
|
// email. Better Auth links by default (email-verified providers are trusted),
|
|
@@ -166,6 +386,17 @@ function createPlatformAuth(config) {
|
|
|
166
386
|
account: {
|
|
167
387
|
accountLinking: sso ? { enabled: true, trustedProviders: [ssoProviderId] } : { enabled: false }
|
|
168
388
|
},
|
|
389
|
+
...kratosPasswords ? {
|
|
390
|
+
user: {
|
|
391
|
+
additionalFields: {
|
|
392
|
+
identityId: {
|
|
393
|
+
type: "string",
|
|
394
|
+
required: false,
|
|
395
|
+
input: false
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} : {},
|
|
169
400
|
// Naming happens here rather than on /sign-up/email so that every way in
|
|
170
401
|
// is covered: a magic link that signs up bypasses the endpoint entirely
|
|
171
402
|
// and calls createUser straight, with `name: name || ""`.
|
|
@@ -176,6 +407,10 @@ function createPlatformAuth(config) {
|
|
|
176
407
|
...databaseHooks?.user,
|
|
177
408
|
create: {
|
|
178
409
|
...databaseHooks?.user?.create,
|
|
410
|
+
after: withIdentityProvisioning(
|
|
411
|
+
databaseHooks?.user?.create?.after,
|
|
412
|
+
kratosPasswords
|
|
413
|
+
),
|
|
179
414
|
before: async (user, ctx) => {
|
|
180
415
|
const named = withSignUpName(user);
|
|
181
416
|
const appHook = databaseHooks?.user?.create?.before;
|
|
@@ -192,6 +427,19 @@ function createPlatformAuth(config) {
|
|
|
192
427
|
hooks: {
|
|
193
428
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
429
|
before: async (ctx) => {
|
|
430
|
+
if (kratosPasswords) {
|
|
431
|
+
const body2 = ctx.body;
|
|
432
|
+
if (ctx.path === "/sign-in/email" && body2?.email && body2?.password) {
|
|
433
|
+
rememberSignInIdentifier(body2.password, body2.email);
|
|
434
|
+
}
|
|
435
|
+
if ((ctx.path === "/sign-up/email" || ctx.path === "/email-otp/reset-password") && body2?.email && body2?.password) {
|
|
436
|
+
rememberPasswordOwner(body2.password, body2.email);
|
|
437
|
+
}
|
|
438
|
+
if (ctx.path === "/change-password" && body2?.newPassword) {
|
|
439
|
+
const email2 = sessionEmail(ctx);
|
|
440
|
+
if (email2) rememberPasswordOwner(body2.newPassword, email2);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
195
443
|
if (!betaMode) return;
|
|
196
444
|
if (ctx.path !== "/sign-up/email") return;
|
|
197
445
|
const body = ctx.body;
|
|
@@ -298,6 +546,25 @@ function createPlatformAuth(config) {
|
|
|
298
546
|
}
|
|
299
547
|
});
|
|
300
548
|
}
|
|
549
|
+
function withIdentityProvisioning(own, config) {
|
|
550
|
+
if (!config) return own;
|
|
551
|
+
return async (user, ctx) => {
|
|
552
|
+
await own?.(user, ctx);
|
|
553
|
+
const record = user;
|
|
554
|
+
if (!record.id || !record.email) return;
|
|
555
|
+
const outcome = await provisionIdentity(config, {
|
|
556
|
+
email: record.email,
|
|
557
|
+
name: record.name
|
|
558
|
+
});
|
|
559
|
+
if (outcome.status === "provisioned" && ctx) {
|
|
560
|
+
await ctx.context.internalAdapter.updateUser(record.id, {
|
|
561
|
+
identityId: outcome.identityId
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
await config.onProvisioningDeferred?.({ userId: record.id, email: record.email });
|
|
566
|
+
};
|
|
567
|
+
}
|
|
301
568
|
function withSsoRoleSync(hooks, sso, providerId) {
|
|
302
569
|
if (!sso) return hooks;
|
|
303
570
|
const sync = async (account, ctx) => {
|
|
@@ -316,7 +583,97 @@ function withSsoRoleSync(hooks, sso, providerId) {
|
|
|
316
583
|
update: { ...hooks?.update, after: chain(hooks?.update?.after) }
|
|
317
584
|
};
|
|
318
585
|
}
|
|
586
|
+
var pendingIdentifiers = /* @__PURE__ */ new Map();
|
|
587
|
+
function rememberSignInIdentifier(password, email) {
|
|
588
|
+
pendingIdentifiers.set(password, email.trim().toLowerCase());
|
|
589
|
+
}
|
|
590
|
+
function takeSignInIdentifier(password) {
|
|
591
|
+
const email = pendingIdentifiers.get(password);
|
|
592
|
+
pendingIdentifiers.delete(password);
|
|
593
|
+
return email;
|
|
594
|
+
}
|
|
595
|
+
var pendingPasswordOwners = /* @__PURE__ */ new Map();
|
|
596
|
+
function rememberPasswordOwner(password, email) {
|
|
597
|
+
pendingPasswordOwners.set(password, email.trim().toLowerCase());
|
|
598
|
+
}
|
|
599
|
+
function takePasswordOwner(password) {
|
|
600
|
+
const email = pendingPasswordOwners.get(password);
|
|
601
|
+
pendingPasswordOwners.delete(password);
|
|
602
|
+
return email;
|
|
603
|
+
}
|
|
604
|
+
function kratosHasher(config) {
|
|
605
|
+
return async (password) => {
|
|
606
|
+
const email = takePasswordOwner(password);
|
|
607
|
+
if (!email) return KRATOS_SENTINEL_HASH;
|
|
608
|
+
const outcome = await provisionIdentity(config, { email, password });
|
|
609
|
+
if (outcome.status === "provisioned") return KRATOS_SENTINEL_HASH;
|
|
610
|
+
if (outcome.status === "unavailable") {
|
|
611
|
+
throw new KratosSignInError(
|
|
612
|
+
"SERVICE_UNAVAILABLE",
|
|
613
|
+
"IDENTITY_PROVIDER_UNAVAILABLE",
|
|
614
|
+
"The identity service is unavailable. Nothing was changed \u2014 try again shortly."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
throw new KratosSignInError(
|
|
618
|
+
"UNPROCESSABLE_ENTITY",
|
|
619
|
+
"IDENTITY_PASSWORD_REFUSED",
|
|
620
|
+
"The identity service refused this password."
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
var KratosSignInError = class extends APIError {
|
|
625
|
+
constructor(status, code, message) {
|
|
626
|
+
super(status, { code, message });
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
function refusalFor(outcome) {
|
|
630
|
+
switch (outcome.status) {
|
|
631
|
+
case "no_credential":
|
|
632
|
+
return new KratosSignInError(
|
|
633
|
+
"FORBIDDEN",
|
|
634
|
+
"IDENTITY_HAS_NO_PASSWORD",
|
|
635
|
+
"This account has no password yet at the identity provider. Use the password recovery to set one."
|
|
636
|
+
);
|
|
637
|
+
case "second_factor_required":
|
|
638
|
+
return new KratosSignInError(
|
|
639
|
+
"FORBIDDEN",
|
|
640
|
+
"SECOND_FACTOR_REQUIRED",
|
|
641
|
+
"A second factor is required to sign in."
|
|
642
|
+
);
|
|
643
|
+
case "account_disabled":
|
|
644
|
+
return new KratosSignInError(
|
|
645
|
+
"FORBIDDEN",
|
|
646
|
+
"ACCOUNT_DISABLED",
|
|
647
|
+
"This account is deactivated."
|
|
648
|
+
);
|
|
649
|
+
case "unavailable":
|
|
650
|
+
return new KratosSignInError(
|
|
651
|
+
"SERVICE_UNAVAILABLE",
|
|
652
|
+
"IDENTITY_PROVIDER_UNAVAILABLE",
|
|
653
|
+
"The identity service is unavailable. Your password has not been refused \u2014 try again shortly."
|
|
654
|
+
);
|
|
655
|
+
default:
|
|
656
|
+
return void 0;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function kratosVerifier(config) {
|
|
660
|
+
return async ({ password }) => {
|
|
661
|
+
const email = takeSignInIdentifier(password);
|
|
662
|
+
if (!email) return false;
|
|
663
|
+
const outcome = await verifyAgainstKratos(config.publicUrl, { email, password });
|
|
664
|
+
if (outcome.status === "valid") return true;
|
|
665
|
+
const refusal = refusalFor(outcome);
|
|
666
|
+
if (refusal) throw refusal;
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function sessionEmail(ctx) {
|
|
671
|
+
const email = ctx?.context?.session?.user?.email;
|
|
672
|
+
return typeof email === "string" ? email : void 0;
|
|
673
|
+
}
|
|
319
674
|
export {
|
|
675
|
+
KRATOS_SENTINEL_HASH,
|
|
676
|
+
KratosSignInError,
|
|
320
677
|
bootstrapFirstAdmin,
|
|
321
678
|
claimInvitation,
|
|
322
679
|
completesSignup,
|
|
@@ -325,8 +682,12 @@ export {
|
|
|
325
682
|
invitationOutcomeCookie,
|
|
326
683
|
inviteTokenFrom,
|
|
327
684
|
isInvitationFailure,
|
|
685
|
+
isKratosSentinel,
|
|
328
686
|
mapSsoProfile,
|
|
329
687
|
pinInviteToken,
|
|
330
|
-
|
|
688
|
+
provisionIdentity,
|
|
689
|
+
releaseInviteTokenCookie,
|
|
690
|
+
rememberSignInIdentifier,
|
|
691
|
+
updateIdentityPassword
|
|
331
692
|
};
|
|
332
693
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/google-defaults.ts","../src/sso-endpoints.ts","../src/rate-limit.ts","../src/bootstrap-admin.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin, magicLink, twoFactor, genericOAuth } from \"better-auth/plugins\"\nimport type { PlatformAuthConfig, PlatformAuthMailerType, PlatformSsoConfig } from \"./types\"\nimport { withGoogleDefaults } from \"./google-defaults\"\nimport { mapSsoProfile, roleFromIdToken, type SsoProfile } from \"./sso-profile\"\nimport { ssoEndpoints } from \"./sso-endpoints\"\nimport { withSignUpName } from \"./signup-name\"\nimport { resolveRateLimit } from \"./rate-limit\"\n\nconst DEFAULT_EMAIL_SUBJECTS: Record<string, string> = {\n \"email-verification\": \"Verify your account\",\n \"forget-password\": \"Reset your password\",\n \"sign-in\": \"Your sign-in code\",\n}\n\nfunction defaultRenderOtpEmail(otp: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your verification code</h2>\n <p style=\"color:#555;margin-bottom:24px\">Use the code below to continue. It expires in 5 minutes.</p>\n <div style=\"background:#f5f5f5;border-radius:8px;padding:24px;text-align:center;letter-spacing:8px;font-size:32px;font-weight:700\">\n ${otp}\n </div>\n <p style=\"color:#999;font-size:12px;margin-top:24px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\nfunction defaultRenderMagicLinkEmail(url: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your sign-in link</h2>\n <p style=\"color:#555;margin-bottom:24px\">Click the button below to sign in. The link expires in 5 minutes and works once.</p>\n <a href=\"${url}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;border-radius:8px;padding:14px 28px;font-weight:600\">Sign in</a>\n <p style=\"color:#999;font-size:12px;margin-top:24px;word-break:break-all\">Or paste this address into your browser:<br>${url}</p>\n <p style=\"color:#999;font-size:12px;margin-top:16px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\n/**\n * Creates a Better Auth instance with platform defaults.\n * Each app calls this with its own config (DB, secret, providers, plugins).\n */\nexport function createPlatformAuth(\n config: PlatformAuthConfig,\n): Auth<BetterAuthOptions> {\n const {\n database,\n baseURL,\n secret,\n appName,\n mailer,\n google,\n github,\n plugins = [],\n databaseHooks,\n betaMode = false,\n isInvited,\n emailSubjects,\n renderOtpEmail,\n magicLink: magicLinkConfig,\n rateLimit,\n twoFactor: twoFactorConfig,\n trustedOrigins,\n sso,\n } = config\n const ssoProviderId = sso?.providerId ?? \"urbangate\"\n\n const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects }\n const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail\n\n // The concrete instance type (with email-otp/admin plugins) is widened to\n // the base Auth type so the published .d.ts stays portable (inferring the\n // full plugin type triggers TS2742 — it can't be named without a zod ref).\n // The admin() plugin's user.role field is re-exposed via module augmentation\n // below, so consumers (e.g. transcript-web me.ts) still see session.user.role.\n return betterAuth({\n database,\n baseURL,\n secret,\n ...(trustedOrigins ? { trustedOrigins } : {}),\n rateLimit: resolveRateLimit(rateLimit),\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n },\n // Never auto-merge a social identity into an existing account by matching\n // email. Better Auth links by default (email-verified providers are trusted),\n // so signing in with Google/GitHub on an email already registered would fold\n // that identity into the existing account. We keep each sign-in method its\n // own account: a social login on a taken email is refused, not linked.\n // The suite's own identity provider is the one exception: it verifies\n // emails itself, and the same person must land on the same account\n // whether they signed in here before the SSO existed or not.\n account: {\n accountLinking: sso\n ? { enabled: true, trustedProviders: [ssoProviderId] }\n : { enabled: false },\n },\n // Naming happens here rather than on /sign-up/email so that every way in\n // is covered: a magic link that signs up bypasses the endpoint entirely\n // and calls createUser straight, with `name: name || \"\"`.\n databaseHooks: {\n ...databaseHooks,\n account: withSsoRoleSync(databaseHooks?.account, sso, ssoProviderId),\n user: {\n ...databaseHooks?.user,\n create: {\n ...databaseHooks?.user?.create,\n before: async (user: Record<string, unknown>, ctx: unknown) => {\n const named = withSignUpName(user)\n const appHook = databaseHooks?.user?.create?.before\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const applied = await appHook?.(named as any, ctx as any)\n if (applied === false) return false\n if (applied && typeof applied === \"object\" && \"data\" in applied) {\n return { data: withSignUpName(applied.data) }\n }\n return { data: named }\n },\n },\n },\n },\n hooks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n before: async (ctx: any) => {\n if (!betaMode) return\n if (ctx.path !== \"/sign-up/email\") return\n const body = ctx.body as { email?: string; inviteToken?: string } | undefined\n const email = body?.email\n const inviteToken = body?.inviteToken\n if (email && inviteToken && isInvited) {\n const ok = await isInvited(email, inviteToken)\n if (ok) return\n }\n throw new APIError(\"FORBIDDEN\", {\n message: \"Registration is invite-only during the private beta.\",\n })\n },\n },\n plugins: [\n emailOTP({\n async sendVerificationOTP({ email, otp, type }) {\n const subject = subjects[type]\n ? `${subjects[type]} - ${appName}`\n : `Your ${appName} code`\n const html = renderEmail(otp, type as PlatformAuthMailerType)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: type as PlatformAuthMailerType,\n otp,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging OTP to stdout for ${email} (${type}): ${otp}`,\n )\n },\n otpLength: 6,\n expiresIn: 300,\n overrideDefaultEmailVerification: true,\n }),\n ...(magicLinkConfig\n ? [\n magicLink({\n expiresIn: magicLinkConfig.expiresIn ?? 300,\n // A magic link that signs up walks past both gates the platform\n // puts on the front door: requireEmailVerification, and the\n // invite-only hook, which only guards /sign-up/email.\n disableSignUp: !magicLinkConfig.allowSignUp,\n async sendMagicLink({ email, url }) {\n const subject =\n magicLinkConfig.subject ?? `Your sign-in link - ${appName}`\n const html = (\n magicLinkConfig.render ?? defaultRenderMagicLinkEmail\n )(url, email)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: \"magic-link\",\n url,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging magic link to stdout for ${email}: ${url}`,\n )\n },\n }),\n ]\n : []),\n admin(),\n ...(sso\n ? [\n genericOAuth({\n config: [\n {\n providerId: ssoProviderId,\n ...ssoEndpoints(sso.issuer),\n clientId: sso.clientId,\n clientSecret: sso.clientSecret,\n scopes: [\"openid\", \"email\", \"profile\", \"offline_access\"],\n pkce: true,\n overrideUserInfo: true,\n disableSignUp: sso.allowSignUp === false,\n mapProfileToUser: (profile) =>\n mapSsoProfile(profile as SsoProfile, sso.adminRole),\n },\n ],\n }),\n ]\n : []),\n ...(twoFactorConfig?.enabled\n ? [\n twoFactor({\n issuer: twoFactorConfig.issuer ?? appName,\n skipVerificationOnEnable:\n twoFactorConfig.skipVerificationOnEnable ?? false,\n }),\n ]\n : []),\n ...plugins, // app-specific plugins (e.g. tanstackStartCookies)\n ],\n socialProviders: {\n // Spread as given rather than rebuilt field by field: anything Better\n // Auth accepts belongs to the app, and a config silently dropped on the\n // way through is how an app ends up writing a plugin to put it back.\n //\n // The two defaults below are the platform's, not Google's: without\n // accessType 'offline' Google never mints a refresh token, and without\n // 'consent' it stops minting one for an account that already consented.\n // A NULL refreshToken means deleting an account can revoke the access\n // token but cannot remove the app from myaccount.google.com/permissions,\n // so the grant outlives the account it belonged to.\n ...(google ? { google: withGoogleDefaults(google) } : {}),\n ...(github ? { github } : {}),\n },\n }) as unknown as Auth<BetterAuthOptions>\n}\n\ntype AccountHooks = NonNullable<NonNullable<BetterAuthOptions[\"databaseHooks\"]>[\"account\"]>\ntype AccountAfterHook = NonNullable<NonNullable<AccountHooks[\"create\"]>[\"after\"]>\n\n// The admin plugin declares `role` as not settable from input, so the role\n// mapProfileToUser returns is dropped when the OAuth path creates the user.\n// The account row, created then refreshed on every sign-in, carries the ID\n// token: its roles claim is what sets the local role, each time.\nfunction withSsoRoleSync(\n hooks: AccountHooks | undefined,\n sso: PlatformSsoConfig | undefined,\n providerId: string,\n): AccountHooks | undefined {\n if (!sso) return hooks\n const sync: AccountAfterHook = async (account, ctx) => {\n if (account.providerId !== providerId || !ctx) return\n const role = roleFromIdToken(account.idToken, sso.adminRole)\n if (!role) return\n await ctx.context.internalAdapter.updateUser(account.userId, { role })\n }\n const chain =\n (own: AccountAfterHook | undefined): AccountAfterHook =>\n async (account, ctx) => {\n await own?.(account, ctx)\n await sync(account, ctx)\n }\n return {\n ...hooks,\n create: { ...hooks?.create, after: chain(hooks?.create?.after) },\n update: { ...hooks?.update, after: chain(hooks?.update?.after) },\n }\n}\n\nexport type PlatformAuth = ReturnType<typeof createPlatformAuth>\n\n// Re-export the session contract from /server so consumers that import the\n// auth factory can type api.getSession() without a second import path.\nexport type {\n PlatformUser,\n PlatformSession,\n PlatformSessionData,\n PlatformRateLimitConfig,\n PlatformRateLimitRule,\n PlatformTwoFactorConfig,\n} from \"./types\"\n\n// Invitation claiming runs on the auth callback, where the session is\n// established — the one place every sign-up flow passes through.\nexport {\n claimInvitation,\n completesSignup,\n holdInviteTokenCookie,\n invitationOutcomeCookie,\n inviteTokenFrom,\n isInvitationFailure,\n pinInviteToken,\n releaseInviteTokenCookie,\n} from \"./invitation\"\nexport type { ClaimOutcome, ClaimInvitationOptions } from \"./invitation\"\n\nexport { mapSsoProfile } from \"./sso-profile\"\nexport type { SsoProfile, SsoMappedUser } from \"./sso-profile\"\n\nexport { bootstrapFirstAdmin } from \"./bootstrap-admin\"\nexport type {\n BootstrapAdminPool,\n BootstrapAdminClient,\n BootstrapFirstAdminInput,\n BootstrapFirstAdminResult,\n} from \"./bootstrap-admin\"\n","import type { PlatformAuthConfig } from \"./types\"\n\n/**\n * Applies the platform's Google defaults without touching what the app set.\n *\n * Better Auth also accepts a function returning the options, which cannot be\n * amended without calling it — such a config is passed straight through and\n * owns its own defaults.\n */\nexport function withGoogleDefaults(\n google: NonNullable<PlatformAuthConfig[\"google\"]>,\n): NonNullable<PlatformAuthConfig[\"google\"]> {\n if (typeof google === \"function\") return google\n return {\n ...google,\n accessType: google.accessType ?? \"offline\",\n prompt: google.prompt ?? \"select_account consent\",\n }\n}\n\n","export interface SsoEndpoints {\n discoveryUrl: string\n accountIssuer: string\n authorizationUrl: string\n tokenUrl: string\n userInfoUrl: string\n}\n\n/**\n * Hydra's endpoints, derived from the issuer so that the provider comes up\n * without a discovery fetch: Better Auth refuses to initialise a provider\n * whose discovery failed unless it already knows the account issuer and the\n * endpoints, and that failure would take the whole app down at boot.\n */\nexport function ssoEndpoints(issuer: string): SsoEndpoints {\n const base = issuer.replace(/\\/$/, \"\")\n return {\n discoveryUrl: `${base}/.well-known/openid-configuration`,\n accountIssuer: base,\n authorizationUrl: `${base}/oauth2/auth`,\n tokenUrl: `${base}/oauth2/token`,\n userInfoUrl: `${base}/userinfo`,\n }\n}\n","import type { PlatformRateLimitConfig, PlatformRateLimitRule } from \"./types\"\n\n// Better Auth keys its own rate limiter on NODE_ENV === \"production\", so a\n// deployment that forgets the variable serves sign-in with no brute-force\n// protection and nothing reports it. These are the platform's rules, applied\n// regardless of the environment.\nconst PLATFORM_RULES: Record<string, PlatformRateLimitRule> = {\n \"/sign-in/email\": { window: 60, max: 5 },\n \"/sign-up/email\": { window: 60, max: 5 },\n \"/email-otp/send-verification-otp\": { window: 60, max: 3 },\n \"/email-otp/verify-email\": { window: 60, max: 5 },\n \"/email-otp/reset-password\": { window: 60, max: 5 },\n \"/forget-password\": { window: 60, max: 3 },\n \"/reset-password\": { window: 60, max: 5 },\n \"/sign-in/magic-link\": { window: 60, max: 3 },\n \"/two-factor/verify-totp\": { window: 60, max: 5 },\n \"/two-factor/verify-otp\": { window: 60, max: 5 },\n \"/two-factor/verify-backup-code\": { window: 60, max: 5 },\n \"/two-factor/send-otp\": { window: 60, max: 3 },\n}\n\nexport interface ResolvedRateLimit {\n enabled: boolean\n window: number\n max: number\n storage?: \"memory\" | \"database\" | \"secondary-storage\"\n modelName?: string\n customRules: Record<string, PlatformRateLimitRule>\n}\n\nexport function resolveRateLimit(\n config?: PlatformRateLimitConfig,\n): ResolvedRateLimit {\n return {\n enabled: config?.enabled ?? true,\n window: config?.window ?? 10,\n max: config?.max ?? 100,\n ...(config?.storage ? { storage: config.storage } : {}),\n ...(config?.modelName ? { modelName: config.modelName } : {}),\n customRules: { ...PLATFORM_RULES, ...config?.customRules },\n }\n}\n\nexport { PLATFORM_RULES }\n","import type { PlatformAuth } from \"./server\"\n\n/**\n * Minimal Postgres pool contract this module needs — kept structural so the\n * package doesn't pull in `pg` as a dependency; any `pg.Pool` satisfies it.\n */\nexport interface BootstrapAdminPool {\n connect(): Promise<BootstrapAdminClient>\n}\n\nexport interface BootstrapAdminClient {\n query(text: string, values?: unknown[]): Promise<{ rowCount: number | null }>\n release(): void\n}\n\nexport interface BootstrapFirstAdminInput {\n email: string\n password: string\n name: string\n}\n\nexport type BootstrapFirstAdminResult =\n | { ok: true }\n | { ok: false; error: \"already_completed\" }\n\n// The admin() plugin's createUser endpoint isn't in PlatformAuth's published\n// type (see the widening note on createPlatformAuth in ./server), even though\n// it's mounted at runtime by every app that enables admin().\ninterface AuthWithAdminCreateUser {\n api: {\n createUser: (input: {\n body: {\n email: string\n password: string\n name: string\n role: string\n data?: Record<string, unknown>\n }\n }) => Promise<unknown>\n }\n}\n\n/**\n * Creates the very first admin for an app, before any admin exists — the one\n * case the admin() plugin's own `createUser` endpoint can't cover, since it\n * requires an already-authenticated admin session to call.\n *\n * Delegates the actual user/account creation to `auth.api.createUser` (called\n * server-side, with no request/session, so the plugin's own auth check is\n * skipped the same way a trusted server script would be) instead of inserting\n * `user`/`account` rows by hand — that keeps this in sync with whatever\n * Better Auth's internal adapter does (account.issuer, password hashing,\n * future schema changes) rather than re-deriving it and letting the two\n * drift apart.\n *\n * Serializes concurrent callers with a Postgres advisory lock: a plain\n * `WHERE NOT EXISTS` check on an empty `user` table lets two racing requests\n * both pass before either has inserted, minting two admins.\n *\n * Callers are expected to have already applied their own access gate (setup\n * token, allowed-emails list, etc.) — this function only enforces \"at most\n * one admin, ever\".\n */\nexport async function bootstrapFirstAdmin(\n auth: PlatformAuth,\n pool: BootstrapAdminPool,\n input: BootstrapFirstAdminInput,\n): Promise<BootstrapFirstAdminResult> {\n const client = await pool.connect()\n try {\n await client.query(\"SELECT pg_advisory_lock(hashtext($1))\", [\"admin-setup\"])\n\n const existing = await client.query(\n `SELECT 1 FROM \"user\" WHERE role = 'admin'`,\n )\n if (existing.rowCount && existing.rowCount > 0) {\n return { ok: false, error: \"already_completed\" }\n }\n\n const adminApi = (auth as unknown as AuthWithAdminCreateUser).api\n await adminApi.createUser({\n body: {\n email: input.email,\n password: input.password,\n name: input.name,\n role: \"admin\",\n data: { emailVerified: true },\n },\n })\n\n return { ok: true }\n } finally {\n await client.query(\"SELECT pg_advisory_unlock(hashtext($1))\", [\"admin-setup\"])\n client.release()\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/kratos-credentials.ts","../src/identity-provisioning.ts","../src/google-defaults.ts","../src/sso-endpoints.ts","../src/rate-limit.ts","../src/bootstrap-admin.ts"],"sourcesContent":["import { betterAuth, APIError, type Auth, type BetterAuthOptions } from \"better-auth\"\nimport { emailOTP, admin, magicLink, twoFactor, genericOAuth } from \"better-auth/plugins\"\nimport type {\n PlatformAuthConfig,\n PlatformAuthMailerType,\n PlatformKratosPasswordConfig,\n PlatformSsoConfig,\n} from \"./types\"\nimport {\n KRATOS_SENTINEL_HASH,\n verifyAgainstKratos,\n type KratosOutcome,\n} from \"./kratos-credentials\"\nimport { provisionIdentity } from \"./identity-provisioning\"\nimport { withGoogleDefaults } from \"./google-defaults\"\nimport { mapSsoProfile, roleFromIdToken, type SsoProfile } from \"./sso-profile\"\nimport { ssoEndpoints } from \"./sso-endpoints\"\nimport { withSignUpName } from \"./signup-name\"\nimport { resolveRateLimit } from \"./rate-limit\"\n\nconst DEFAULT_EMAIL_SUBJECTS: Record<string, string> = {\n \"email-verification\": \"Verify your account\",\n \"forget-password\": \"Reset your password\",\n \"sign-in\": \"Your sign-in code\",\n}\n\nfunction defaultRenderOtpEmail(otp: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your verification code</h2>\n <p style=\"color:#555;margin-bottom:24px\">Use the code below to continue. It expires in 5 minutes.</p>\n <div style=\"background:#f5f5f5;border-radius:8px;padding:24px;text-align:center;letter-spacing:8px;font-size:32px;font-weight:700\">\n ${otp}\n </div>\n <p style=\"color:#999;font-size:12px;margin-top:24px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\nfunction defaultRenderMagicLinkEmail(url: string): string {\n return `\n <div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px\">\n <h2 style=\"font-size:20px;font-weight:600;margin-bottom:16px\">Your sign-in link</h2>\n <p style=\"color:#555;margin-bottom:24px\">Click the button below to sign in. The link expires in 5 minutes and works once.</p>\n <a href=\"${url}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;border-radius:8px;padding:14px 28px;font-weight:600\">Sign in</a>\n <p style=\"color:#999;font-size:12px;margin-top:24px;word-break:break-all\">Or paste this address into your browser:<br>${url}</p>\n <p style=\"color:#999;font-size:12px;margin-top:16px\">If you didn't request this, you can safely ignore this email.</p>\n </div>\n `\n}\n\n/**\n * Creates a Better Auth instance with platform defaults.\n * Each app calls this with its own config (DB, secret, providers, plugins).\n */\nexport function createPlatformAuth(\n config: PlatformAuthConfig,\n): Auth<BetterAuthOptions> {\n const {\n database,\n baseURL,\n secret,\n appName,\n mailer,\n google,\n github,\n plugins = [],\n databaseHooks,\n betaMode = false,\n isInvited,\n emailSubjects,\n renderOtpEmail,\n magicLink: magicLinkConfig,\n rateLimit,\n twoFactor: twoFactorConfig,\n trustedOrigins,\n sso,\n kratosPasswords,\n } = config\n const ssoProviderId = sso?.providerId ?? \"urbangate\"\n\n const subjects = { ...DEFAULT_EMAIL_SUBJECTS, ...emailSubjects }\n const renderEmail = renderOtpEmail ?? defaultRenderOtpEmail\n\n // The concrete instance type (with email-otp/admin plugins) is widened to\n // the base Auth type so the published .d.ts stays portable (inferring the\n // full plugin type triggers TS2742 — it can't be named without a zod ref).\n // The admin() plugin's user.role field is re-exposed via module augmentation\n // below, so consumers (e.g. transcript-web me.ts) still see session.user.role.\n return betterAuth({\n database,\n baseURL,\n secret,\n ...(trustedOrigins ? { trustedOrigins } : {}),\n rateLimit: resolveRateLimit(rateLimit),\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n ...(kratosPasswords\n ? {\n password: {\n // Sign-in refuses before reaching the verifier when the account\n // carries no hash, so a sign-up must still write one. It is a\n // constant that validates nothing, never a hash of the password.\n hash: kratosHasher(kratosPasswords),\n verify: kratosVerifier(kratosPasswords),\n },\n }\n : {}),\n },\n // Never auto-merge a social identity into an existing account by matching\n // email. Better Auth links by default (email-verified providers are trusted),\n // so signing in with Google/GitHub on an email already registered would fold\n // that identity into the existing account. We keep each sign-in method its\n // own account: a social login on a taken email is refused, not linked.\n // The suite's own identity provider is the one exception: it verifies\n // emails itself, and the same person must land on the same account\n // whether they signed in here before the SSO existed or not.\n account: {\n accountLinking: sso\n ? { enabled: true, trustedProviders: [ssoProviderId] }\n : { enabled: false },\n },\n ...(kratosPasswords\n ? {\n user: {\n additionalFields: {\n identityId: {\n type: \"string\",\n required: false,\n input: false,\n },\n },\n },\n }\n : {}),\n // Naming happens here rather than on /sign-up/email so that every way in\n // is covered: a magic link that signs up bypasses the endpoint entirely\n // and calls createUser straight, with `name: name || \"\"`.\n databaseHooks: {\n ...databaseHooks,\n account: withSsoRoleSync(databaseHooks?.account, sso, ssoProviderId),\n user: {\n ...databaseHooks?.user,\n create: {\n ...databaseHooks?.user?.create,\n after: withIdentityProvisioning(\n databaseHooks?.user?.create?.after,\n kratosPasswords,\n ),\n before: async (user: Record<string, unknown>, ctx: unknown) => {\n const named = withSignUpName(user)\n const appHook = databaseHooks?.user?.create?.before\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const applied = await appHook?.(named as any, ctx as any)\n if (applied === false) return false\n if (applied && typeof applied === \"object\" && \"data\" in applied) {\n return { data: withSignUpName(applied.data) }\n }\n return { data: named }\n },\n },\n },\n },\n hooks: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n before: async (ctx: any) => {\n if (kratosPasswords) {\n const body = ctx.body as\n | { email?: string; password?: string; newPassword?: string }\n | undefined\n if (ctx.path === \"/sign-in/email\" && body?.email && body?.password) {\n rememberSignInIdentifier(body.password, body.email)\n }\n // Sign-up and the OTP reset name the address they act on; a change\n // of password only has the session, whose user carries it.\n if (\n (ctx.path === \"/sign-up/email\" ||\n ctx.path === \"/email-otp/reset-password\") &&\n body?.email &&\n body?.password\n ) {\n rememberPasswordOwner(body.password, body.email)\n }\n if (ctx.path === \"/change-password\" && body?.newPassword) {\n const email = sessionEmail(ctx)\n if (email) rememberPasswordOwner(body.newPassword, email)\n }\n }\n if (!betaMode) return\n if (ctx.path !== \"/sign-up/email\") return\n const body = ctx.body as { email?: string; inviteToken?: string } | undefined\n const email = body?.email\n const inviteToken = body?.inviteToken\n if (email && inviteToken && isInvited) {\n const ok = await isInvited(email, inviteToken)\n if (ok) return\n }\n throw new APIError(\"FORBIDDEN\", {\n message: \"Registration is invite-only during the private beta.\",\n })\n },\n },\n plugins: [\n emailOTP({\n async sendVerificationOTP({ email, otp, type }) {\n const subject = subjects[type]\n ? `${subjects[type]} - ${appName}`\n : `Your ${appName} code`\n const html = renderEmail(otp, type as PlatformAuthMailerType)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: type as PlatformAuthMailerType,\n otp,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging OTP to stdout for ${email} (${type}): ${otp}`,\n )\n },\n otpLength: 6,\n expiresIn: 300,\n overrideDefaultEmailVerification: true,\n }),\n ...(magicLinkConfig\n ? [\n magicLink({\n expiresIn: magicLinkConfig.expiresIn ?? 300,\n // A magic link that signs up walks past both gates the platform\n // puts on the front door: requireEmailVerification, and the\n // invite-only hook, which only guards /sign-up/email.\n disableSignUp: !magicLinkConfig.allowSignUp,\n async sendMagicLink({ email, url }) {\n const subject =\n magicLinkConfig.subject ?? `Your sign-in link - ${appName}`\n const html = (\n magicLinkConfig.render ?? defaultRenderMagicLinkEmail\n )(url, email)\n\n if (mailer) {\n await mailer({\n to: email,\n subject,\n html,\n type: \"magic-link\",\n url,\n })\n return\n }\n\n console.warn(\n `[EMAIL] No mailer configured — logging magic link to stdout for ${email}: ${url}`,\n )\n },\n }),\n ]\n : []),\n admin(),\n ...(sso\n ? [\n genericOAuth({\n config: [\n {\n providerId: ssoProviderId,\n ...ssoEndpoints(sso.issuer),\n clientId: sso.clientId,\n clientSecret: sso.clientSecret,\n scopes: [\"openid\", \"email\", \"profile\", \"offline_access\"],\n pkce: true,\n overrideUserInfo: true,\n disableSignUp: sso.allowSignUp === false,\n mapProfileToUser: (profile) =>\n mapSsoProfile(profile as SsoProfile, sso.adminRole),\n },\n ],\n }),\n ]\n : []),\n ...(twoFactorConfig?.enabled\n ? [\n twoFactor({\n issuer: twoFactorConfig.issuer ?? appName,\n skipVerificationOnEnable:\n twoFactorConfig.skipVerificationOnEnable ?? false,\n }),\n ]\n : []),\n ...plugins, // app-specific plugins (e.g. tanstackStartCookies)\n ],\n socialProviders: {\n // Spread as given rather than rebuilt field by field: anything Better\n // Auth accepts belongs to the app, and a config silently dropped on the\n // way through is how an app ends up writing a plugin to put it back.\n //\n // The two defaults below are the platform's, not Google's: without\n // accessType 'offline' Google never mints a refresh token, and without\n // 'consent' it stops minting one for an account that already consented.\n // A NULL refreshToken means deleting an account can revoke the access\n // token but cannot remove the app from myaccount.google.com/permissions,\n // so the grant outlives the account it belonged to.\n ...(google ? { google: withGoogleDefaults(google) } : {}),\n ...(github ? { github } : {}),\n },\n }) as unknown as Auth<BetterAuthOptions>\n}\n\ntype UserHooks = NonNullable<NonNullable<BetterAuthOptions[\"databaseHooks\"]>[\"user\"]>\ntype UserAfterHook = NonNullable<NonNullable<UserHooks[\"create\"]>[\"after\"]>\n\n/**\n * Gives the new local user an identity at the provider and stores its id.\n *\n * This runs after the insert commits, so it cannot be atomic with the\n * sign-up: a provider that is down leaves `identityId` null and the person\n * registered all the same. The endpoint is idempotent on the address, so the\n * repair re-sends without risking a second identity.\n */\nfunction withIdentityProvisioning(\n own: UserAfterHook | undefined,\n config: PlatformKratosPasswordConfig | undefined,\n): UserAfterHook | undefined {\n if (!config) return own\n return async (user, ctx) => {\n await own?.(user, ctx)\n const record = user as { id?: string; email?: string; name?: string }\n if (!record.id || !record.email) return\n\n const outcome = await provisionIdentity(config, {\n email: record.email,\n name: record.name,\n })\n\n if (outcome.status === \"provisioned\" && ctx) {\n await ctx.context.internalAdapter.updateUser(record.id, {\n identityId: outcome.identityId,\n })\n return\n }\n\n await config.onProvisioningDeferred?.({ userId: record.id, email: record.email })\n }\n}\n\ntype AccountHooks = NonNullable<NonNullable<BetterAuthOptions[\"databaseHooks\"]>[\"account\"]>\ntype AccountAfterHook = NonNullable<NonNullable<AccountHooks[\"create\"]>[\"after\"]>\n\n// The admin plugin declares `role` as not settable from input, so the role\n// mapProfileToUser returns is dropped when the OAuth path creates the user.\n// The account row, created then refreshed on every sign-in, carries the ID\n// token: its roles claim is what sets the local role, each time.\nfunction withSsoRoleSync(\n hooks: AccountHooks | undefined,\n sso: PlatformSsoConfig | undefined,\n providerId: string,\n): AccountHooks | undefined {\n if (!sso) return hooks\n const sync: AccountAfterHook = async (account, ctx) => {\n if (account.providerId !== providerId || !ctx) return\n const role = roleFromIdToken(account.idToken, sso.adminRole)\n if (!role) return\n await ctx.context.internalAdapter.updateUser(account.userId, { role })\n }\n const chain =\n (own: AccountAfterHook | undefined): AccountAfterHook =>\n async (account, ctx) => {\n await own?.(account, ctx)\n await sync(account, ctx)\n }\n return {\n ...hooks,\n create: { ...hooks?.create, after: chain(hooks?.create?.after) },\n update: { ...hooks?.update, after: chain(hooks?.update?.after) },\n }\n}\n\n/**\n * Better Auth's verifier is handed the stored hash and the submitted password,\n * never the address, and the same verifier serves sign-in, password change and\n * account deletion. The address of the sign-in being processed is carried here\n * by the route hook, keyed by the submitted password so two concurrent\n * sign-ins cannot read each other's.\n */\nconst pendingIdentifiers = new Map<string, string>()\n\nexport function rememberSignInIdentifier(password: string, email: string): void {\n pendingIdentifiers.set(password, email.trim().toLowerCase())\n}\n\nfunction takeSignInIdentifier(password: string): string | undefined {\n const email = pendingIdentifiers.get(password)\n pendingIdentifiers.delete(password)\n return email\n}\n\n// The same blind spot on the writing side: `hash` is handed the new password\n// and nothing else, and it is the only place Better Auth exposes it before\n// storing a placeholder in its stead. Sign-up and reset carry the address in\n// their body; a password change carries only a session, so the route hook\n// resolves it there.\nconst pendingPasswordOwners = new Map<string, string>()\n\nfunction rememberPasswordOwner(password: string, email: string): void {\n pendingPasswordOwners.set(password, email.trim().toLowerCase())\n}\n\nfunction takePasswordOwner(password: string): string | undefined {\n const email = pendingPasswordOwners.get(password)\n pendingPasswordOwners.delete(password)\n return email\n}\n\n// Writing a password is the one operation that must reach the provider: a\n// placeholder stored against a password Kratos never received is an account\n// its owner can never open. It is relayed before the local write, so a\n// provider that refuses it fails the request instead of stranding the account.\nfunction kratosHasher(config: PlatformKratosPasswordConfig) {\n return async (password: string): Promise<string> => {\n const email = takePasswordOwner(password)\n if (!email) return KRATOS_SENTINEL_HASH\n\n const outcome = await provisionIdentity(config, { email, password })\n if (outcome.status === \"provisioned\") return KRATOS_SENTINEL_HASH\n\n if (outcome.status === \"unavailable\") {\n throw new KratosSignInError(\n \"SERVICE_UNAVAILABLE\",\n \"IDENTITY_PROVIDER_UNAVAILABLE\",\n \"The identity service is unavailable. Nothing was changed — try again shortly.\",\n )\n }\n throw new KratosSignInError(\n \"UNPROCESSABLE_ENTITY\",\n \"IDENTITY_PASSWORD_REFUSED\",\n \"The identity service refused this password.\",\n )\n }\n}\n\nexport class KratosSignInError extends APIError {\n constructor(\n status:\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"SERVICE_UNAVAILABLE\"\n | \"UNPROCESSABLE_ENTITY\",\n code: string,\n message: string,\n ) {\n super(status, { code, message })\n }\n}\n\nfunction refusalFor(outcome: KratosOutcome): KratosSignInError | undefined {\n switch (outcome.status) {\n case \"no_credential\":\n return new KratosSignInError(\n \"FORBIDDEN\",\n \"IDENTITY_HAS_NO_PASSWORD\",\n \"This account has no password yet at the identity provider. Use the password recovery to set one.\",\n )\n case \"second_factor_required\":\n return new KratosSignInError(\n \"FORBIDDEN\",\n \"SECOND_FACTOR_REQUIRED\",\n \"A second factor is required to sign in.\",\n )\n case \"account_disabled\":\n return new KratosSignInError(\n \"FORBIDDEN\",\n \"ACCOUNT_DISABLED\",\n \"This account is deactivated.\",\n )\n case \"unavailable\":\n return new KratosSignInError(\n \"SERVICE_UNAVAILABLE\",\n \"IDENTITY_PROVIDER_UNAVAILABLE\",\n \"The identity service is unavailable. Your password has not been refused — try again shortly.\",\n )\n default:\n return undefined\n }\n}\n\n/**\n * Fails closed: anything other than an explicit success refuses the sign-in,\n * and only an explicit refusal by Kratos reads as a wrong password. An outage\n * answers 503, so nobody is told their password is wrong and rotates a\n * password that was right.\n */\nfunction kratosVerifier(config: PlatformKratosPasswordConfig) {\n return async ({ password }: { hash: string; password: string }): Promise<boolean> => {\n const email = takeSignInIdentifier(password)\n if (!email) return false\n const outcome = await verifyAgainstKratos(config.publicUrl, { email, password })\n if (outcome.status === \"valid\") return true\n const refusal = refusalFor(outcome)\n if (refusal) throw refusal\n return false\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction sessionEmail(ctx: any): string | undefined {\n const email = ctx?.context?.session?.user?.email\n return typeof email === \"string\" ? email : undefined\n}\n\nexport type PlatformAuth = ReturnType<typeof createPlatformAuth>\n\n// Re-export the session contract from /server so consumers that import the\n// auth factory can type api.getSession() without a second import path.\nexport type {\n PlatformUser,\n PlatformSession,\n PlatformSessionData,\n PlatformRateLimitConfig,\n PlatformRateLimitRule,\n PlatformTwoFactorConfig,\n} from \"./types\"\n\n// Invitation claiming runs on the auth callback, where the session is\n// established — the one place every sign-up flow passes through.\nexport {\n claimInvitation,\n completesSignup,\n holdInviteTokenCookie,\n invitationOutcomeCookie,\n inviteTokenFrom,\n isInvitationFailure,\n pinInviteToken,\n releaseInviteTokenCookie,\n} from \"./invitation\"\nexport type { ClaimOutcome, ClaimInvitationOptions } from \"./invitation\"\n\nexport { mapSsoProfile } from \"./sso-profile\"\nexport type { SsoProfile, SsoMappedUser } from \"./sso-profile\"\n\nexport { KRATOS_SENTINEL_HASH, isKratosSentinel } from \"./kratos-credentials\"\nexport type { KratosOutcome } from \"./kratos-credentials\"\n\n// The repair path an app schedules for the sign-ups whose provisioning could\n// not reach the provider.\nexport { provisionIdentity, updateIdentityPassword } from \"./identity-provisioning\"\nexport type {\n IdentityProvisioningConfig,\n ProvisionOutcome,\n} from \"./identity-provisioning\"\n\nexport { bootstrapFirstAdmin } from \"./bootstrap-admin\"\nexport type {\n BootstrapAdminPool,\n BootstrapAdminClient,\n BootstrapFirstAdminInput,\n BootstrapFirstAdminResult,\n} from \"./bootstrap-admin\"\n","/**\n * The value written in place of a password hash once Kratos holds the\n * password. Better Auth's sign-in route refuses the request before reaching\n * the verifier when the credential account carries no hash, so a row must\n * exist and be non-empty for the Kratos check to run at all.\n *\n * It is not a hash and cannot become one: argon2/bcrypt/scrypt verification\n * of this string fails on its format, so a build that ever bypassed the\n * custom verifier refuses everyone instead of admitting anyone.\n */\nexport const KRATOS_SENTINEL_HASH = \"kratos:external-credential:no-local-hash\"\n\nexport function isKratosSentinel(hash: string | null | undefined): boolean {\n return hash === KRATOS_SENTINEL_HASH\n}\n\nexport type KratosOutcome =\n | { status: \"valid\"; identityId: string }\n | { status: \"invalid_credentials\" }\n | { status: \"no_credential\" }\n | { status: \"email_not_verified\" }\n | { status: \"second_factor_required\" }\n | { status: \"account_disabled\" }\n | { status: \"unavailable\" }\n\nexport interface KratosCredentialCheck {\n email: string\n password: string\n}\n\ninterface KratosLoginFlow {\n id: string\n ui?: { nodes?: Array<{ attributes?: { name?: string; value?: string } }> }\n}\n\ninterface KratosErrorBody {\n error?: { id?: string; code?: number; reason?: string; message?: string }\n ui?: { messages?: Array<{ id?: number; text?: string; type?: string }> }\n redirect_browser_to?: string\n}\n\ninterface KratosSuccessBody {\n session?: { identity?: { id?: string; state?: string } }\n session_token?: string\n}\n\nconst FLOW_TIMEOUT_MS = 5000\n\nfunction csrfTokenOf(flow: KratosLoginFlow): string | undefined {\n return flow.ui?.nodes?.find((n) => n.attributes?.name === \"csrf_token\")\n ?.attributes?.value\n}\n\n/**\n * Kratos reports a refusal through numbered UI messages rather than a status\n * code of its own; 4000006 is the generic credential refusal, 4000010 an\n * inactive account, 4000002 a missing field. The address-unverified and\n * second-factor cases arrive as a redirect or an aal2 requirement instead.\n */\nfunction outcomeFromMessages(body: KratosErrorBody): KratosOutcome | undefined {\n const ids = (body.ui?.messages ?? []).map((m) => m.id)\n if (ids.includes(4000010)) return { status: \"account_disabled\" }\n if (ids.includes(4000006) || ids.includes(4000002)) {\n return { status: \"invalid_credentials\" }\n }\n const errorId = body.error?.id\n if (errorId === \"session_aal2_required\") {\n return { status: \"second_factor_required\" }\n }\n if (errorId === \"session_verified_address_required\") {\n return { status: \"email_not_verified\" }\n }\n if (errorId === \"browser_location_change_required\") {\n return { status: \"second_factor_required\" }\n }\n return undefined\n}\n\n/**\n * Validates a password against Kratos through the native (API) login flow,\n * which returns the outcome as JSON instead of driving a browser.\n *\n * Every failure that is not an explicit refusal by Kratos answers\n * `unavailable`, never `invalid_credentials`: a person told their password is\n * wrong during an outage changes a password that was right.\n */\nexport async function verifyAgainstKratos(\n publicUrl: string,\n check: KratosCredentialCheck,\n fetchImpl: typeof fetch = fetch,\n): Promise<KratosOutcome> {\n const base = publicUrl.replace(/\\/$/, \"\")\n let flow: KratosLoginFlow\n try {\n const started = await fetchImpl(`${base}/self-service/login/api`, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n signal: AbortSignal.timeout(FLOW_TIMEOUT_MS),\n })\n if (!started.ok) return { status: \"unavailable\" }\n flow = (await started.json()) as KratosLoginFlow\n if (!flow?.id) return { status: \"unavailable\" }\n } catch {\n return { status: \"unavailable\" }\n }\n\n const csrf = csrfTokenOf(flow)\n try {\n const submitted = await fetchImpl(\n `${base}/self-service/login?flow=${encodeURIComponent(flow.id)}`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n body: JSON.stringify({\n method: \"password\",\n identifier: check.email.trim().toLowerCase(),\n password: check.password,\n ...(csrf ? { csrf_token: csrf } : {}),\n }),\n signal: AbortSignal.timeout(FLOW_TIMEOUT_MS),\n },\n )\n\n if (submitted.ok) {\n const body = (await submitted.json()) as KratosSuccessBody\n const identity = body.session?.identity\n if (!identity?.id) return { status: \"unavailable\" }\n if (identity.state && identity.state !== \"active\") {\n return { status: \"account_disabled\" }\n }\n return { status: \"valid\", identityId: identity.id }\n }\n\n if (submitted.status >= 500) return { status: \"unavailable\" }\n\n const body = (await submitted.json().catch(() => ({}))) as KratosErrorBody\n const mapped = outcomeFromMessages(body)\n if (mapped) return mapped\n if (submitted.status === 400 || submitted.status === 401) {\n return { status: \"invalid_credentials\" }\n }\n if (submitted.status === 403) return { status: \"second_factor_required\" }\n return { status: \"unavailable\" }\n } catch {\n return { status: \"unavailable\" }\n }\n}\n","export interface IdentityProvisioningConfig {\n /** urbangate's issuer URL, e.g. https://id.urbangate.dev */\n issuer: string\n /** The product's provisioner client, e.g. \"spore-provisioner\". */\n clientId: string\n clientSecret: string\n /** The role granted on provisioning, e.g. \"spore:user\". */\n role: string\n /** The product id carried in the request, e.g. \"spore\". */\n product: string\n}\n\nexport type ProvisionOutcome =\n | { status: \"provisioned\"; identityId: string; created: boolean }\n | { status: \"rejected\"; reason: string }\n | { status: \"unavailable\" }\n\nexport interface ProvisionRequest {\n email: string\n name?: string\n /**\n * The password the person just typed on the product's own form. Kratos\n * hashes it with the hasher its configuration declares, so an app cannot\n * hand over one it hashed itself; it is relayed for the length of this\n * request and stored nowhere. Omitted, the identity is created without a\n * credential and its owner sets one through recovery.\n */\n password?: string\n}\n\ninterface TokenResponse {\n access_token?: string\n expires_in?: number\n}\n\ninterface IdentityResponse {\n identity_id?: string\n created?: boolean\n}\n\nconst REQUEST_TIMEOUT_MS = 5000\nconst TOKEN_EXPIRY_MARGIN_S = 30\n\ninterface CachedToken {\n value: string\n expiresAt: number\n}\n\nconst tokenCache = new Map<string, CachedToken>()\n\nexport function resetProvisioningTokenCache(): void {\n tokenCache.clear()\n}\n\nasync function accessToken(\n config: IdentityProvisioningConfig,\n fetchImpl: typeof fetch,\n): Promise<string | undefined> {\n const key = `${config.issuer}|${config.clientId}`\n const cached = tokenCache.get(key)\n const now = Date.now() / 1000\n if (cached && cached.expiresAt > now) return cached.value\n\n const base = config.issuer.replace(/\\/$/, \"\")\n const credentials = btoa(`${config.clientId}:${config.clientSecret}`)\n try {\n const response = await fetchImpl(`${base}/oauth2/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Basic ${credentials}`,\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"client_credentials\",\n audience: \"urbangate\",\n scope: \"urbangate:identities:provision\",\n }).toString(),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n if (!response.ok) return undefined\n const body = (await response.json()) as TokenResponse\n if (!body.access_token) return undefined\n tokenCache.set(key, {\n value: body.access_token,\n expiresAt: now + (body.expires_in ?? 900) - TOKEN_EXPIRY_MARGIN_S,\n })\n return body.access_token\n } catch {\n return undefined\n }\n}\n\n/**\n * Creates or joins the person's identity at urbangate, returning the id the\n * local user row stores.\n *\n * The endpoint is idempotent on the address: a person who already has an\n * identity through another product of the suite gets that same one, with this\n * product's role added. The products therefore never own the identity, only\n * their role on it — a product deleting its local account must drop its role,\n * never deactivate the identity, or it would sign the person out of every\n * other product of the suite.\n */\nexport async function provisionIdentity(\n config: IdentityProvisioningConfig,\n request: ProvisionRequest,\n fetchImpl: typeof fetch = fetch,\n): Promise<ProvisionOutcome> {\n const token = await accessToken(config, fetchImpl)\n if (!token) return { status: \"unavailable\" }\n\n const base = config.issuer.replace(/\\/$/, \"\")\n try {\n const response = await fetchImpl(`${base}/api/machine/identities`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n email: request.email.trim().toLowerCase(),\n email_verified: true,\n name: request.name ?? \"\",\n role: config.role,\n product: config.product,\n ...(request.password ? { password: request.password } : {}),\n }),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n\n if (response.ok) {\n const body = (await response.json()) as IdentityResponse\n if (!body.identity_id) return { status: \"unavailable\" }\n return {\n status: \"provisioned\",\n identityId: body.identity_id,\n created: body.created === true,\n }\n }\n\n // 503 is the endpoint's retryable answer, including the inconclusive\n // lookup that refuses to risk a duplicate identity.\n if (response.status === 503 || response.status >= 500) {\n return { status: \"unavailable\" }\n }\n if (response.status === 401) {\n tokenCache.delete(`${config.issuer}|${config.clientId}`)\n return { status: \"unavailable\" }\n }\n const body = (await response.json().catch(() => ({}))) as {\n error?: string\n message?: string\n }\n return {\n status: \"rejected\",\n reason: body.error ?? body.message ?? `http_${response.status}`,\n }\n } catch {\n return { status: \"unavailable\" }\n }\n}\n\n/**\n * Sets the password of an identity the product already enrols, for a reset or\n * a change made on the product's own form.\n *\n * `rejected` with reason `not_found` is the person having no identity yet —\n * a local account that predates the move, or one whose provisioning is still\n * to be repaired — and is worth provisioning rather than retrying.\n */\nexport async function updateIdentityPassword(\n config: IdentityProvisioningConfig,\n request: { email: string; password: string },\n fetchImpl: typeof fetch = fetch,\n): Promise<ProvisionOutcome> {\n const token = await accessToken(config, fetchImpl)\n if (!token) return { status: \"unavailable\" }\n\n const base = config.issuer.replace(/\\/$/, \"\")\n try {\n const response = await fetchImpl(`${base}/api/machine/passwords`, {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n email: request.email.trim().toLowerCase(),\n password: request.password,\n }),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n\n if (response.ok) {\n const body = (await response.json()) as IdentityResponse\n if (!body.identity_id) return { status: \"unavailable\" }\n return {\n status: \"provisioned\",\n identityId: body.identity_id,\n created: false,\n }\n }\n\n if (response.status === 503 || response.status >= 500) {\n return { status: \"unavailable\" }\n }\n if (response.status === 401) {\n tokenCache.delete(`${config.issuer}|${config.clientId}`)\n return { status: \"unavailable\" }\n }\n if (response.status === 404) {\n return { status: \"rejected\", reason: \"not_found\" }\n }\n const body = (await response.json().catch(() => ({}))) as {\n error?: string\n message?: string\n }\n return {\n status: \"rejected\",\n reason: body.error ?? body.message ?? `http_${response.status}`,\n }\n } catch {\n return { status: \"unavailable\" }\n }\n}\n","import type { PlatformAuthConfig } from \"./types\"\n\n/**\n * Applies the platform's Google defaults without touching what the app set.\n *\n * Better Auth also accepts a function returning the options, which cannot be\n * amended without calling it — such a config is passed straight through and\n * owns its own defaults.\n */\nexport function withGoogleDefaults(\n google: NonNullable<PlatformAuthConfig[\"google\"]>,\n): NonNullable<PlatformAuthConfig[\"google\"]> {\n if (typeof google === \"function\") return google\n return {\n ...google,\n accessType: google.accessType ?? \"offline\",\n prompt: google.prompt ?? \"select_account consent\",\n }\n}\n\n","export interface SsoEndpoints {\n discoveryUrl: string\n accountIssuer: string\n authorizationUrl: string\n tokenUrl: string\n userInfoUrl: string\n}\n\n/**\n * Hydra's endpoints, derived from the issuer so that the provider comes up\n * without a discovery fetch: Better Auth refuses to initialise a provider\n * whose discovery failed unless it already knows the account issuer and the\n * endpoints, and that failure would take the whole app down at boot.\n */\nexport function ssoEndpoints(issuer: string): SsoEndpoints {\n const base = issuer.replace(/\\/$/, \"\")\n return {\n discoveryUrl: `${base}/.well-known/openid-configuration`,\n accountIssuer: base,\n authorizationUrl: `${base}/oauth2/auth`,\n tokenUrl: `${base}/oauth2/token`,\n userInfoUrl: `${base}/userinfo`,\n }\n}\n","import type { PlatformRateLimitConfig, PlatformRateLimitRule } from \"./types\"\n\n// Better Auth keys its own rate limiter on NODE_ENV === \"production\", so a\n// deployment that forgets the variable serves sign-in with no brute-force\n// protection and nothing reports it. These are the platform's rules, applied\n// regardless of the environment.\nconst PLATFORM_RULES: Record<string, PlatformRateLimitRule> = {\n \"/sign-in/email\": { window: 60, max: 5 },\n \"/sign-up/email\": { window: 60, max: 5 },\n \"/email-otp/send-verification-otp\": { window: 60, max: 3 },\n \"/email-otp/verify-email\": { window: 60, max: 5 },\n \"/email-otp/reset-password\": { window: 60, max: 5 },\n \"/forget-password\": { window: 60, max: 3 },\n \"/reset-password\": { window: 60, max: 5 },\n \"/sign-in/magic-link\": { window: 60, max: 3 },\n \"/two-factor/verify-totp\": { window: 60, max: 5 },\n \"/two-factor/verify-otp\": { window: 60, max: 5 },\n \"/two-factor/verify-backup-code\": { window: 60, max: 5 },\n \"/two-factor/send-otp\": { window: 60, max: 3 },\n}\n\nexport interface ResolvedRateLimit {\n enabled: boolean\n window: number\n max: number\n storage?: \"memory\" | \"database\" | \"secondary-storage\"\n modelName?: string\n customRules: Record<string, PlatformRateLimitRule>\n}\n\nexport function resolveRateLimit(\n config?: PlatformRateLimitConfig,\n): ResolvedRateLimit {\n return {\n enabled: config?.enabled ?? true,\n window: config?.window ?? 10,\n max: config?.max ?? 100,\n ...(config?.storage ? { storage: config.storage } : {}),\n ...(config?.modelName ? { modelName: config.modelName } : {}),\n customRules: { ...PLATFORM_RULES, ...config?.customRules },\n }\n}\n\nexport { PLATFORM_RULES }\n","import type { PlatformAuth } from \"./server\"\n\n/**\n * Minimal Postgres pool contract this module needs — kept structural so the\n * package doesn't pull in `pg` as a dependency; any `pg.Pool` satisfies it.\n */\nexport interface BootstrapAdminPool {\n connect(): Promise<BootstrapAdminClient>\n}\n\nexport interface BootstrapAdminClient {\n query(text: string, values?: unknown[]): Promise<{ rowCount: number | null }>\n release(): void\n}\n\nexport interface BootstrapFirstAdminInput {\n email: string\n password: string\n name: string\n}\n\nexport type BootstrapFirstAdminResult =\n | { ok: true }\n | { ok: false; error: \"already_completed\" }\n\n// The admin() plugin's createUser endpoint isn't in PlatformAuth's published\n// type (see the widening note on createPlatformAuth in ./server), even though\n// it's mounted at runtime by every app that enables admin().\ninterface AuthWithAdminCreateUser {\n api: {\n createUser: (input: {\n body: {\n email: string\n password: string\n name: string\n role: string\n data?: Record<string, unknown>\n }\n }) => Promise<unknown>\n }\n}\n\n/**\n * Creates the very first admin for an app, before any admin exists — the one\n * case the admin() plugin's own `createUser` endpoint can't cover, since it\n * requires an already-authenticated admin session to call.\n *\n * Delegates the actual user/account creation to `auth.api.createUser` (called\n * server-side, with no request/session, so the plugin's own auth check is\n * skipped the same way a trusted server script would be) instead of inserting\n * `user`/`account` rows by hand — that keeps this in sync with whatever\n * Better Auth's internal adapter does (account.issuer, password hashing,\n * future schema changes) rather than re-deriving it and letting the two\n * drift apart.\n *\n * Serializes concurrent callers with a Postgres advisory lock: a plain\n * `WHERE NOT EXISTS` check on an empty `user` table lets two racing requests\n * both pass before either has inserted, minting two admins.\n *\n * Callers are expected to have already applied their own access gate (setup\n * token, allowed-emails list, etc.) — this function only enforces \"at most\n * one admin, ever\".\n */\nexport async function bootstrapFirstAdmin(\n auth: PlatformAuth,\n pool: BootstrapAdminPool,\n input: BootstrapFirstAdminInput,\n): Promise<BootstrapFirstAdminResult> {\n const client = await pool.connect()\n try {\n await client.query(\"SELECT pg_advisory_lock(hashtext($1))\", [\"admin-setup\"])\n\n const existing = await client.query(\n `SELECT 1 FROM \"user\" WHERE role = 'admin'`,\n )\n if (existing.rowCount && existing.rowCount > 0) {\n return { ok: false, error: \"already_completed\" }\n }\n\n const adminApi = (auth as unknown as AuthWithAdminCreateUser).api\n await adminApi.createUser({\n body: {\n email: input.email,\n password: input.password,\n name: input.name,\n role: \"admin\",\n data: { emailVerified: true },\n },\n })\n\n return { ok: true }\n } finally {\n await client.query(\"SELECT pg_advisory_unlock(hashtext($1))\", [\"admin-setup\"])\n client.release()\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAAA,SAAS,YAAY,gBAAmD;AACxE,SAAS,UAAU,OAAO,WAAW,WAAW,oBAAoB;;;ACS7D,IAAM,uBAAuB;AAE7B,SAAS,iBAAiB,MAA0C;AACzE,SAAO,SAAS;AAClB;AAgCA,IAAM,kBAAkB;AAExB,SAAS,YAAY,MAA2C;AAC9D,SAAO,KAAK,IAAI,OAAO,KAAK,CAAC,MAAM,EAAE,YAAY,SAAS,YAAY,GAClE,YAAY;AAClB;AAQA,SAAS,oBAAoB,MAAkD;AAC7E,QAAM,OAAO,KAAK,IAAI,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE;AACrD,MAAI,IAAI,SAAS,OAAO,EAAG,QAAO,EAAE,QAAQ,mBAAmB;AAC/D,MAAI,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,OAAO,GAAG;AAClD,WAAO,EAAE,QAAQ,sBAAsB;AAAA,EACzC;AACA,QAAM,UAAU,KAAK,OAAO;AAC5B,MAAI,YAAY,yBAAyB;AACvC,WAAO,EAAE,QAAQ,yBAAyB;AAAA,EAC5C;AACA,MAAI,YAAY,qCAAqC;AACnD,WAAO,EAAE,QAAQ,qBAAqB;AAAA,EACxC;AACA,MAAI,YAAY,oCAAoC;AAClD,WAAO,EAAE,QAAQ,yBAAyB;AAAA,EAC5C;AACA,SAAO;AACT;AAUA,eAAsB,oBACpB,WACA,OACA,YAA0B,OACF;AACxB,QAAM,OAAO,UAAU,QAAQ,OAAO,EAAE;AACxC,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,UAAU,GAAG,IAAI,2BAA2B;AAAA,MAChE,QAAQ;AAAA,MACR,SAAS,EAAE,QAAQ,mBAAmB;AAAA,MACtC,QAAQ,YAAY,QAAQ,eAAe;AAAA,IAC7C,CAAC;AACD,QAAI,CAAC,QAAQ,GAAI,QAAO,EAAE,QAAQ,cAAc;AAChD,WAAQ,MAAM,QAAQ,KAAK;AAC3B,QAAI,CAAC,MAAM,GAAI,QAAO,EAAE,QAAQ,cAAc;AAAA,EAChD,QAAQ;AACN,WAAO,EAAE,QAAQ,cAAc;AAAA,EACjC;AAEA,QAAM,OAAO,YAAY,IAAI;AAC7B,MAAI;AACF,UAAM,YAAY,MAAM;AAAA,MACtB,GAAG,IAAI,4BAA4B,mBAAmB,KAAK,EAAE,CAAC;AAAA,MAC9D;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,QAAQ,mBAAmB;AAAA,QAC1E,MAAM,KAAK,UAAU;AAAA,UACnB,QAAQ;AAAA,UACR,YAAY,MAAM,MAAM,KAAK,EAAE,YAAY;AAAA,UAC3C,UAAU,MAAM;AAAA,UAChB,GAAI,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;AAAA,QACrC,CAAC;AAAA,QACD,QAAQ,YAAY,QAAQ,eAAe;AAAA,MAC7C;AAAA,IACF;AAEA,QAAI,UAAU,IAAI;AAChB,YAAMA,QAAQ,MAAM,UAAU,KAAK;AACnC,YAAM,WAAWA,MAAK,SAAS;AAC/B,UAAI,CAAC,UAAU,GAAI,QAAO,EAAE,QAAQ,cAAc;AAClD,UAAI,SAAS,SAAS,SAAS,UAAU,UAAU;AACjD,eAAO,EAAE,QAAQ,mBAAmB;AAAA,MACtC;AACA,aAAO,EAAE,QAAQ,SAAS,YAAY,SAAS,GAAG;AAAA,IACpD;AAEA,QAAI,UAAU,UAAU,IAAK,QAAO,EAAE,QAAQ,cAAc;AAE5D,UAAM,OAAQ,MAAM,UAAU,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACrD,UAAM,SAAS,oBAAoB,IAAI;AACvC,QAAI,OAAQ,QAAO;AACnB,QAAI,UAAU,WAAW,OAAO,UAAU,WAAW,KAAK;AACxD,aAAO,EAAE,QAAQ,sBAAsB;AAAA,IACzC;AACA,QAAI,UAAU,WAAW,IAAK,QAAO,EAAE,QAAQ,yBAAyB;AACxE,WAAO,EAAE,QAAQ,cAAc;AAAA,EACjC,QAAQ;AACN,WAAO,EAAE,QAAQ,cAAc;AAAA,EACjC;AACF;;;AC1GA,IAAM,qBAAqB;AAC3B,IAAM,wBAAwB;AAO9B,IAAM,aAAa,oBAAI,IAAyB;AAMhD,eAAe,YACb,QACA,WAC6B;AAC7B,QAAM,MAAM,GAAG,OAAO,MAAM,IAAI,OAAO,QAAQ;AAC/C,QAAM,SAAS,WAAW,IAAI,GAAG;AACjC,QAAM,MAAM,KAAK,IAAI,IAAI;AACzB,MAAI,UAAU,OAAO,YAAY,IAAK,QAAO,OAAO;AAEpD,QAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,EAAE;AAC5C,QAAM,cAAc,KAAK,GAAG,OAAO,QAAQ,IAAI,OAAO,YAAY,EAAE;AACpE,MAAI;AACF,UAAM,WAAW,MAAM,UAAU,GAAG,IAAI,iBAAiB;AAAA,MACvD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,SAAS,WAAW;AAAA,QACnC,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,OAAO;AAAA,MACT,CAAC,EAAE,SAAS;AAAA,MACZ,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,QAAO;AACzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,QAAI,CAAC,KAAK,aAAc,QAAO;AAC/B,eAAW,IAAI,KAAK;AAAA,MAClB,OAAO,KAAK;AAAA,MACZ,WAAW,OAAO,KAAK,cAAc,OAAO;AAAA,IAC9C,CAAC;AACD,WAAO,KAAK;AAAA,EACd,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAaA,eAAsB,kBACpB,QACA,SACA,YAA0B,OACC;AAC3B,QAAM,QAAQ,MAAM,YAAY,QAAQ,SAAS;AACjD,MAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,cAAc;AAE3C,QAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,EAAE;AAC5C,MAAI;AACF,UAAM,WAAW,MAAM,UAAU,GAAG,IAAI,2BAA2B;AAAA,MACjE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,QAAQ,MAAM,KAAK,EAAE,YAAY;AAAA,QACxC,gBAAgB;AAAA,QAChB,MAAM,QAAQ,QAAQ;AAAA,QACtB,MAAM,OAAO;AAAA,QACb,SAAS,OAAO;AAAA,QAChB,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,MAC3D,CAAC;AAAA,MACD,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AAED,QAAI,SAAS,IAAI;AACf,YAAMC,QAAQ,MAAM,SAAS,KAAK;AAClC,UAAI,CAACA,MAAK,YAAa,QAAO,EAAE,QAAQ,cAAc;AACtD,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,YAAYA,MAAK;AAAA,QACjB,SAASA,MAAK,YAAY;AAAA,MAC5B;AAAA,IACF;AAIA,QAAI,SAAS,WAAW,OAAO,SAAS,UAAU,KAAK;AACrD,aAAO,EAAE,QAAQ,cAAc;AAAA,IACjC;AACA,QAAI,SAAS,WAAW,KAAK;AAC3B,iBAAW,OAAO,GAAG,OAAO,MAAM,IAAI,OAAO,QAAQ,EAAE;AACvD,aAAO,EAAE,QAAQ,cAAc;AAAA,IACjC;AACA,UAAM,OAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAIpD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ,KAAK,SAAS,KAAK,WAAW,QAAQ,SAAS,MAAM;AAAA,IAC/D;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,QAAQ,cAAc;AAAA,EACjC;AACF;AAUA,eAAsB,uBACpB,QACA,SACA,YAA0B,OACC;AAC3B,QAAM,QAAQ,MAAM,YAAY,QAAQ,SAAS;AACjD,MAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,cAAc;AAE3C,QAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,EAAE;AAC5C,MAAI;AACF,UAAM,WAAW,MAAM,UAAU,GAAG,IAAI,0BAA0B;AAAA,MAChE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,QAAQ,MAAM,KAAK,EAAE,YAAY;AAAA,QACxC,UAAU,QAAQ;AAAA,MACpB,CAAC;AAAA,MACD,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AAED,QAAI,SAAS,IAAI;AACf,YAAMA,QAAQ,MAAM,SAAS,KAAK;AAClC,UAAI,CAACA,MAAK,YAAa,QAAO,EAAE,QAAQ,cAAc;AACtD,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,YAAYA,MAAK;AAAA,QACjB,SAAS;AAAA,MACX;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,OAAO,SAAS,UAAU,KAAK;AACrD,aAAO,EAAE,QAAQ,cAAc;AAAA,IACjC;AACA,QAAI,SAAS,WAAW,KAAK;AAC3B,iBAAW,OAAO,GAAG,OAAO,MAAM,IAAI,OAAO,QAAQ,EAAE;AACvD,aAAO,EAAE,QAAQ,cAAc;AAAA,IACjC;AACA,QAAI,SAAS,WAAW,KAAK;AAC3B,aAAO,EAAE,QAAQ,YAAY,QAAQ,YAAY;AAAA,IACnD;AACA,UAAM,OAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAIpD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ,KAAK,SAAS,KAAK,WAAW,QAAQ,SAAS,MAAM;AAAA,IAC/D;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,QAAQ,cAAc;AAAA,EACjC;AACF;;;ACvNO,SAAS,mBACd,QAC2C;AAC3C,MAAI,OAAO,WAAW,WAAY,QAAO;AACzC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,YAAY,OAAO,cAAc;AAAA,IACjC,QAAQ,OAAO,UAAU;AAAA,EAC3B;AACF;;;ACJO,SAAS,aAAa,QAA8B;AACzD,QAAM,OAAO,OAAO,QAAQ,OAAO,EAAE;AACrC,SAAO;AAAA,IACL,cAAc,GAAG,IAAI;AAAA,IACrB,eAAe;AAAA,IACf,kBAAkB,GAAG,IAAI;AAAA,IACzB,UAAU,GAAG,IAAI;AAAA,IACjB,aAAa,GAAG,IAAI;AAAA,EACtB;AACF;;;ACjBA,IAAM,iBAAwD;AAAA,EAC5D,kBAAkB,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EACvC,kBAAkB,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EACvC,oCAAoC,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EACzD,2BAA2B,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EAChD,6BAA6B,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EAClD,oBAAoB,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EACzC,mBAAmB,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EACxC,uBAAuB,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EAC5C,2BAA2B,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EAChD,0BAA0B,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EAC/C,kCAAkC,EAAE,QAAQ,IAAI,KAAK,EAAE;AAAA,EACvD,wBAAwB,EAAE,QAAQ,IAAI,KAAK,EAAE;AAC/C;AAWO,SAAS,iBACd,QACmB;AACnB,SAAO;AAAA,IACL,SAAS,QAAQ,WAAW;AAAA,IAC5B,QAAQ,QAAQ,UAAU;AAAA,IAC1B,KAAK,QAAQ,OAAO;AAAA,IACpB,GAAI,QAAQ,UAAU,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;AAAA,IACrD,GAAI,QAAQ,YAAY,EAAE,WAAW,OAAO,UAAU,IAAI,CAAC;AAAA,IAC3D,aAAa,EAAE,GAAG,gBAAgB,GAAG,QAAQ,YAAY;AAAA,EAC3D;AACF;;;ACsBA,eAAsB,oBACpB,MACA,MACA,OACoC;AACpC,QAAM,SAAS,MAAM,KAAK,QAAQ;AAClC,MAAI;AACF,UAAM,OAAO,MAAM,yCAAyC,CAAC,aAAa,CAAC;AAE3E,UAAM,WAAW,MAAM,OAAO;AAAA,MAC5B;AAAA,IACF;AACA,QAAI,SAAS,YAAY,SAAS,WAAW,GAAG;AAC9C,aAAO,EAAE,IAAI,OAAO,OAAO,oBAAoB;AAAA,IACjD;AAEA,UAAM,WAAY,KAA4C;AAC9D,UAAM,SAAS,WAAW;AAAA,MACxB,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,UAAU,MAAM;AAAA,QAChB,MAAM,MAAM;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,EAAE,eAAe,KAAK;AAAA,MAC9B;AAAA,IACF,CAAC;AAED,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB,UAAE;AACA,UAAM,OAAO,MAAM,2CAA2C,CAAC,aAAa,CAAC;AAC7E,WAAO,QAAQ;AAAA,EACjB;AACF;;;AN3EA,IAAM,yBAAiD;AAAA,EACrD,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,WAAW;AACb;AAEA,SAAS,sBAAsB,KAAqB;AAClD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,oBAKW,GAAG;AAAA;AAAA;AAAA;AAAA;AAKvB;AAEA,SAAS,4BAA4B,KAAqB;AACxD,SAAO;AAAA;AAAA;AAAA;AAAA,2BAIkB,GAAG;AAAA,wIAC0G,GAAG;AAAA;AAAA;AAAA;AAI3I;AAMO,SAAS,mBACd,QACyB;AACzB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,CAAC;AAAA,IACX;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AACJ,QAAM,gBAAgB,KAAK,cAAc;AAEzC,QAAM,WAAW,EAAE,GAAG,wBAAwB,GAAG,cAAc;AAC/D,QAAM,cAAc,kBAAkB;AAOtC,SAAO,WAAW;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;AAAA,IAC3C,WAAW,iBAAiB,SAAS;AAAA,IACrC,kBAAkB;AAAA,MAChB,SAAS;AAAA,MACT,0BAA0B;AAAA,MAC1B,GAAI,kBACA;AAAA,QACE,UAAU;AAAA;AAAA;AAAA;AAAA,UAIR,MAAM,aAAa,eAAe;AAAA,UAClC,QAAQ,eAAe,eAAe;AAAA,QACxC;AAAA,MACF,IACA,CAAC;AAAA,IACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,SAAS;AAAA,MACP,gBAAgB,MACZ,EAAE,SAAS,MAAM,kBAAkB,CAAC,aAAa,EAAE,IACnD,EAAE,SAAS,MAAM;AAAA,IACvB;AAAA,IACA,GAAI,kBACA;AAAA,MACE,MAAM;AAAA,QACJ,kBAAkB;AAAA,UAChB,YAAY;AAAA,YACV,MAAM;AAAA,YACN,UAAU;AAAA,YACV,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAAA,IACF,IACA,CAAC;AAAA;AAAA;AAAA;AAAA,IAIL,eAAe;AAAA,MACb,GAAG;AAAA,MACH,SAAS,gBAAgB,eAAe,SAAS,KAAK,aAAa;AAAA,MACnE,MAAM;AAAA,QACJ,GAAG,eAAe;AAAA,QAClB,QAAQ;AAAA,UACN,GAAG,eAAe,MAAM;AAAA,UACxB,OAAO;AAAA,YACL,eAAe,MAAM,QAAQ;AAAA,YAC7B;AAAA,UACF;AAAA,UACA,QAAQ,OAAO,MAA+B,QAAiB;AAC7D,kBAAM,QAAQ,eAAe,IAAI;AACjC,kBAAM,UAAU,eAAe,MAAM,QAAQ;AAE7C,kBAAM,UAAU,MAAM,UAAU,OAAc,GAAU;AACxD,gBAAI,YAAY,MAAO,QAAO;AAC9B,gBAAI,WAAW,OAAO,YAAY,YAAY,UAAU,SAAS;AAC/D,qBAAO,EAAE,MAAM,eAAe,QAAQ,IAAI,EAAE;AAAA,YAC9C;AACA,mBAAO,EAAE,MAAM,MAAM;AAAA,UACvB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,OAAO;AAAA;AAAA,MAEL,QAAQ,OAAO,QAAa;AAC1B,YAAI,iBAAiB;AACnB,gBAAMC,QAAO,IAAI;AAGjB,cAAI,IAAI,SAAS,oBAAoBA,OAAM,SAASA,OAAM,UAAU;AAClE,qCAAyBA,MAAK,UAAUA,MAAK,KAAK;AAAA,UACpD;AAGA,eACG,IAAI,SAAS,oBACZ,IAAI,SAAS,gCACfA,OAAM,SACNA,OAAM,UACN;AACA,kCAAsBA,MAAK,UAAUA,MAAK,KAAK;AAAA,UACjD;AACA,cAAI,IAAI,SAAS,sBAAsBA,OAAM,aAAa;AACxD,kBAAMC,SAAQ,aAAa,GAAG;AAC9B,gBAAIA,OAAO,uBAAsBD,MAAK,aAAaC,MAAK;AAAA,UAC1D;AAAA,QACF;AACA,YAAI,CAAC,SAAU;AACf,YAAI,IAAI,SAAS,iBAAkB;AACnC,cAAM,OAAO,IAAI;AACjB,cAAM,QAAQ,MAAM;AACpB,cAAM,cAAc,MAAM;AAC1B,YAAI,SAAS,eAAe,WAAW;AACrC,gBAAM,KAAK,MAAM,UAAU,OAAO,WAAW;AAC7C,cAAI,GAAI;AAAA,QACV;AACA,cAAM,IAAI,SAAS,aAAa;AAAA,UAC9B,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,SAAS;AAAA,QACP,MAAM,oBAAoB,EAAE,OAAO,KAAK,KAAK,GAAG;AAC9C,gBAAM,UAAU,SAAS,IAAI,IACzB,GAAG,SAAS,IAAI,CAAC,MAAM,OAAO,KAC9B,QAAQ,OAAO;AACnB,gBAAM,OAAO,YAAY,KAAK,IAA8B;AAE5D,cAAI,QAAQ;AACV,kBAAM,OAAO;AAAA,cACX,IAAI;AAAA,cACJ;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YACF,CAAC;AACD;AAAA,UACF;AAEA,kBAAQ;AAAA,YACN,iEAA4D,KAAK,KAAK,IAAI,MAAM,GAAG;AAAA,UACrF;AAAA,QACF;AAAA,QACA,WAAW;AAAA,QACX,WAAW;AAAA,QACX,kCAAkC;AAAA,MACpC,CAAC;AAAA,MACD,GAAI,kBACA;AAAA,QACE,UAAU;AAAA,UACR,WAAW,gBAAgB,aAAa;AAAA;AAAA;AAAA;AAAA,UAIxC,eAAe,CAAC,gBAAgB;AAAA,UAChC,MAAM,cAAc,EAAE,OAAO,IAAI,GAAG;AAClC,kBAAM,UACJ,gBAAgB,WAAW,uBAAuB,OAAO;AAC3D,kBAAM,QACJ,gBAAgB,UAAU,6BAC1B,KAAK,KAAK;AAEZ,gBAAI,QAAQ;AACV,oBAAM,OAAO;AAAA,gBACX,IAAI;AAAA,gBACJ;AAAA,gBACA;AAAA,gBACA,MAAM;AAAA,gBACN;AAAA,cACF,CAAC;AACD;AAAA,YACF;AAEA,oBAAQ;AAAA,cACN,wEAAmE,KAAK,KAAK,GAAG;AAAA,YAClF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,IACA,CAAC;AAAA,MACL,MAAM;AAAA,MACN,GAAI,MACA;AAAA,QACE,aAAa;AAAA,UACX,QAAQ;AAAA,YACN;AAAA,cACE,YAAY;AAAA,cACZ,GAAG,aAAa,IAAI,MAAM;AAAA,cAC1B,UAAU,IAAI;AAAA,cACd,cAAc,IAAI;AAAA,cAClB,QAAQ,CAAC,UAAU,SAAS,WAAW,gBAAgB;AAAA,cACvD,MAAM;AAAA,cACN,kBAAkB;AAAA,cAClB,eAAe,IAAI,gBAAgB;AAAA,cACnC,kBAAkB,CAAC,YACjB,cAAc,SAAuB,IAAI,SAAS;AAAA,YACtD;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,IACA,CAAC;AAAA,MACL,GAAI,iBAAiB,UACjB;AAAA,QACE,UAAU;AAAA,UACR,QAAQ,gBAAgB,UAAU;AAAA,UAClC,0BACE,gBAAgB,4BAA4B;AAAA,QAChD,CAAC;AAAA,MACH,IACA,CAAC;AAAA,MACL,GAAG;AAAA;AAAA,IACL;AAAA,IACA,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAWf,GAAI,SAAS,EAAE,QAAQ,mBAAmB,MAAM,EAAE,IAAI,CAAC;AAAA,MACvD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC7B;AAAA,EACF,CAAC;AACH;AAaA,SAAS,yBACP,KACA,QAC2B;AAC3B,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,OAAO,MAAM,QAAQ;AAC1B,UAAM,MAAM,MAAM,GAAG;AACrB,UAAM,SAAS;AACf,QAAI,CAAC,OAAO,MAAM,CAAC,OAAO,MAAO;AAEjC,UAAM,UAAU,MAAM,kBAAkB,QAAQ;AAAA,MAC9C,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,IACf,CAAC;AAED,QAAI,QAAQ,WAAW,iBAAiB,KAAK;AAC3C,YAAM,IAAI,QAAQ,gBAAgB,WAAW,OAAO,IAAI;AAAA,QACtD,YAAY,QAAQ;AAAA,MACtB,CAAC;AACD;AAAA,IACF;AAEA,UAAM,OAAO,yBAAyB,EAAE,QAAQ,OAAO,IAAI,OAAO,OAAO,MAAM,CAAC;AAAA,EAClF;AACF;AASA,SAAS,gBACP,OACA,KACA,YAC0B;AAC1B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAyB,OAAO,SAAS,QAAQ;AACrD,QAAI,QAAQ,eAAe,cAAc,CAAC,IAAK;AAC/C,UAAM,OAAO,gBAAgB,QAAQ,SAAS,IAAI,SAAS;AAC3D,QAAI,CAAC,KAAM;AACX,UAAM,IAAI,QAAQ,gBAAgB,WAAW,QAAQ,QAAQ,EAAE,KAAK,CAAC;AAAA,EACvE;AACA,QAAM,QACJ,CAAC,QACD,OAAO,SAAS,QAAQ;AACtB,UAAM,MAAM,SAAS,GAAG;AACxB,UAAM,KAAK,SAAS,GAAG;AAAA,EACzB;AACF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ,EAAE,GAAG,OAAO,QAAQ,OAAO,MAAM,OAAO,QAAQ,KAAK,EAAE;AAAA,IAC/D,QAAQ,EAAE,GAAG,OAAO,QAAQ,OAAO,MAAM,OAAO,QAAQ,KAAK,EAAE;AAAA,EACjE;AACF;AASA,IAAM,qBAAqB,oBAAI,IAAoB;AAE5C,SAAS,yBAAyB,UAAkB,OAAqB;AAC9E,qBAAmB,IAAI,UAAU,MAAM,KAAK,EAAE,YAAY,CAAC;AAC7D;AAEA,SAAS,qBAAqB,UAAsC;AAClE,QAAM,QAAQ,mBAAmB,IAAI,QAAQ;AAC7C,qBAAmB,OAAO,QAAQ;AAClC,SAAO;AACT;AAOA,IAAM,wBAAwB,oBAAI,IAAoB;AAEtD,SAAS,sBAAsB,UAAkB,OAAqB;AACpE,wBAAsB,IAAI,UAAU,MAAM,KAAK,EAAE,YAAY,CAAC;AAChE;AAEA,SAAS,kBAAkB,UAAsC;AAC/D,QAAM,QAAQ,sBAAsB,IAAI,QAAQ;AAChD,wBAAsB,OAAO,QAAQ;AACrC,SAAO;AACT;AAMA,SAAS,aAAa,QAAsC;AAC1D,SAAO,OAAO,aAAsC;AAClD,UAAM,QAAQ,kBAAkB,QAAQ;AACxC,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,UAAU,MAAM,kBAAkB,QAAQ,EAAE,OAAO,SAAS,CAAC;AACnE,QAAI,QAAQ,WAAW,cAAe,QAAO;AAE7C,QAAI,QAAQ,WAAW,eAAe;AACpC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,oBAAN,cAAgC,SAAS;AAAA,EAC9C,YACE,QAKA,MACA,SACA;AACA,UAAM,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAAA,EACjC;AACF;AAEA,SAAS,WAAW,SAAuD;AACzE,UAAQ,QAAQ,QAAQ;AAAA,IACtB,KAAK;AACH,aAAO,IAAI;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,IAAI;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,IAAI;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,IAAI;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACE,aAAO;AAAA,EACX;AACF;AAQA,SAAS,eAAe,QAAsC;AAC5D,SAAO,OAAO,EAAE,SAAS,MAA4D;AACnF,UAAM,QAAQ,qBAAqB,QAAQ;AAC3C,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,UAAU,MAAM,oBAAoB,OAAO,WAAW,EAAE,OAAO,SAAS,CAAC;AAC/E,QAAI,QAAQ,WAAW,QAAS,QAAO;AACvC,UAAM,UAAU,WAAW,OAAO;AAClC,QAAI,QAAS,OAAM;AACnB,WAAO;AAAA,EACT;AACF;AAGA,SAAS,aAAa,KAA8B;AAClD,QAAM,QAAQ,KAAK,SAAS,SAAS,MAAM;AAC3C,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;","names":["body","body","body","email"]}
|