@lalternative/auth 0.16.0 → 0.17.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.
@@ -0,0 +1,177 @@
1
+ // src/identity-provisioning.ts
2
+ var REQUEST_TIMEOUT_MS = 5e3;
3
+ var TOKEN_EXPIRY_MARGIN_S = 30;
4
+ var tokenCache = /* @__PURE__ */ new Map();
5
+ function resetProvisioningTokenCache() {
6
+ tokenCache.clear();
7
+ }
8
+ async function accessToken(config, fetchImpl) {
9
+ const key = `${config.issuer}|${config.clientId}`;
10
+ const cached = tokenCache.get(key);
11
+ const now = Date.now() / 1e3;
12
+ if (cached && cached.expiresAt > now) return cached.value;
13
+ const base = config.issuer.replace(/\/$/, "");
14
+ const credentials = btoa(`${config.clientId}:${config.clientSecret}`);
15
+ try {
16
+ const response = await fetchImpl(`${base}/oauth2/token`, {
17
+ method: "POST",
18
+ headers: {
19
+ Authorization: `Basic ${credentials}`,
20
+ "Content-Type": "application/x-www-form-urlencoded"
21
+ },
22
+ body: new URLSearchParams({
23
+ grant_type: "client_credentials",
24
+ audience: "urbangate",
25
+ scope: "urbangate:identities:provision"
26
+ }).toString(),
27
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
28
+ });
29
+ if (!response.ok) return void 0;
30
+ const body = await response.json();
31
+ if (!body.access_token) return void 0;
32
+ tokenCache.set(key, {
33
+ value: body.access_token,
34
+ expiresAt: now + (body.expires_in ?? 900) - TOKEN_EXPIRY_MARGIN_S
35
+ });
36
+ return body.access_token;
37
+ } catch {
38
+ return void 0;
39
+ }
40
+ }
41
+ async function provisionIdentity(config, request, fetchImpl = fetch) {
42
+ const token = await accessToken(config, fetchImpl);
43
+ if (!token) return { status: "unavailable" };
44
+ const base = config.issuer.replace(/\/$/, "");
45
+ try {
46
+ const response = await fetchImpl(`${base}/api/machine/identities`, {
47
+ method: "POST",
48
+ headers: {
49
+ Authorization: `Bearer ${token}`,
50
+ "Content-Type": "application/json"
51
+ },
52
+ body: JSON.stringify({
53
+ email: request.email.trim().toLowerCase(),
54
+ email_verified: true,
55
+ name: request.name ?? "",
56
+ role: config.role,
57
+ product: config.product,
58
+ ...request.password ? { password: request.password } : {}
59
+ }),
60
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
61
+ });
62
+ if (response.ok) {
63
+ const body2 = await response.json();
64
+ if (!body2.identity_id) return { status: "unavailable" };
65
+ return {
66
+ status: "provisioned",
67
+ identityId: body2.identity_id,
68
+ created: body2.created === true
69
+ };
70
+ }
71
+ if (response.status === 503 || response.status >= 500) {
72
+ return { status: "unavailable" };
73
+ }
74
+ if (response.status === 401) {
75
+ tokenCache.delete(`${config.issuer}|${config.clientId}`);
76
+ return { status: "unavailable" };
77
+ }
78
+ const body = await response.json().catch(() => ({}));
79
+ return {
80
+ status: "rejected",
81
+ reason: body.error ?? body.message ?? `http_${response.status}`
82
+ };
83
+ } catch {
84
+ return { status: "unavailable" };
85
+ }
86
+ }
87
+ async function updateIdentityPassword(config, request, fetchImpl = fetch) {
88
+ const token = await accessToken(config, fetchImpl);
89
+ if (!token) return { status: "unavailable" };
90
+ const base = config.issuer.replace(/\/$/, "");
91
+ try {
92
+ const response = await fetchImpl(`${base}/api/machine/passwords`, {
93
+ method: "PUT",
94
+ headers: {
95
+ Authorization: `Bearer ${token}`,
96
+ "Content-Type": "application/json"
97
+ },
98
+ body: JSON.stringify({
99
+ email: request.email.trim().toLowerCase(),
100
+ password: request.password
101
+ }),
102
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
103
+ });
104
+ if (response.ok) {
105
+ const body2 = await response.json();
106
+ if (!body2.identity_id) return { status: "unavailable" };
107
+ return {
108
+ status: "provisioned",
109
+ identityId: body2.identity_id,
110
+ created: false
111
+ };
112
+ }
113
+ if (response.status === 503 || response.status >= 500) {
114
+ return { status: "unavailable" };
115
+ }
116
+ if (response.status === 401) {
117
+ tokenCache.delete(`${config.issuer}|${config.clientId}`);
118
+ return { status: "unavailable" };
119
+ }
120
+ if (response.status === 404) {
121
+ return { status: "rejected", reason: "not_found" };
122
+ }
123
+ const body = await response.json().catch(() => ({}));
124
+ return {
125
+ status: "rejected",
126
+ reason: body.error ?? body.message ?? `http_${response.status}`
127
+ };
128
+ } catch {
129
+ return { status: "unavailable" };
130
+ }
131
+ }
132
+ async function requestAccountDeletion(config, request, fetchImpl = fetch) {
133
+ const token = await accessToken(config, fetchImpl);
134
+ if (!token) return { status: "unavailable" };
135
+ const base = config.issuer.replace(/\/$/, "");
136
+ try {
137
+ const response = await fetchImpl(`${base}/api/v1/machine/accounts/deletions`, {
138
+ method: "POST",
139
+ headers: {
140
+ Authorization: `Bearer ${token}`,
141
+ "Content-Type": "application/json"
142
+ },
143
+ // No product field: urbangate reads it from the token's client_id, so a
144
+ // token cannot ask for an account it does not own.
145
+ body: JSON.stringify({
146
+ identity_id: request.identityId,
147
+ ...request.userId ? { user_id: request.userId } : {}
148
+ }),
149
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
150
+ });
151
+ if (response.ok) {
152
+ const body2 = await response.json();
153
+ return { status: "requested", eventId: body2.event_id ?? "" };
154
+ }
155
+ if (response.status >= 500) return { status: "unavailable" };
156
+ if (response.status === 401) {
157
+ tokenCache.delete(`${config.issuer}|${config.clientId}`);
158
+ return { status: "unavailable" };
159
+ }
160
+ if (response.status === 404) return { status: "requested", eventId: "" };
161
+ const body = await response.json().catch(() => ({}));
162
+ return {
163
+ status: "rejected",
164
+ reason: body.error ?? body.message ?? `http_${response.status}`
165
+ };
166
+ } catch {
167
+ return { status: "unavailable" };
168
+ }
169
+ }
170
+
171
+ export {
172
+ resetProvisioningTokenCache,
173
+ provisionIdentity,
174
+ updateIdentityPassword,
175
+ requestAccountDeletion
176
+ };
177
+ //# sourceMappingURL=chunk-UURHL4NP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/identity-provisioning.ts"],"sourcesContent":["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\nexport type DeletionOutcome =\n | { status: \"requested\"; eventId: string }\n | { status: \"rejected\"; reason: string }\n | { status: \"unavailable\" }\n\n/**\n * Asks urbangate to drop this product's role on the identity, so the person\n * stops being one of its users.\n *\n * It drops the role and nothing else. The identity belongs to the person, not\n * to the product that enrolled them: deactivating it would take away every\n * other product of the suite, without either product knowing why. urbangate\n * deletes the identity itself only once no role is left on it.\n *\n * `identityId` is the `identityId` the local user row stores, written there by\n * `provisionIdentity`. A local account that predates the move has none, and is\n * deleted locally without a call here.\n */\nexport async function requestAccountDeletion(\n config: IdentityProvisioningConfig,\n request: { identityId: string; userId?: string },\n fetchImpl: typeof fetch = fetch,\n): Promise<DeletionOutcome> {\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/v1/machine/accounts/deletions`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n // No product field: urbangate reads it from the token's client_id, so a\n // token cannot ask for an account it does not own.\n body: JSON.stringify({\n identity_id: request.identityId,\n ...(request.userId ? { user_id: request.userId } : {}),\n }),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n })\n\n if (response.ok) {\n const body = (await response.json()) as { event_id?: string }\n return { status: \"requested\", eventId: body.event_id ?? \"\" }\n }\n\n if (response.status >= 500) return { status: \"unavailable\" }\n if (response.status === 401) {\n tokenCache.delete(`${config.issuer}|${config.clientId}`)\n return { status: \"unavailable\" }\n }\n // The identity is already gone: an earlier attempt got through, or an\n // administrator removed it. Either way this product has no role left on\n // it, which is what the call was for.\n if (response.status === 404) return { status: \"requested\", eventId: \"\" }\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"],"mappings":"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","names":["body"]}
@@ -0,0 +1,13 @@
1
+ import {
2
+ provisionIdentity,
3
+ requestAccountDeletion,
4
+ resetProvisioningTokenCache,
5
+ updateIdentityPassword
6
+ } from "./chunk-UURHL4NP.js";
7
+ export {
8
+ provisionIdentity,
9
+ requestAccountDeletion,
10
+ resetProvisioningTokenCache,
11
+ updateIdentityPassword
12
+ };
13
+ //# sourceMappingURL=identity-provisioning-GBE6OXR3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/server.d.ts CHANGED
@@ -89,6 +89,105 @@ declare function updateIdentityPassword(config: IdentityProvisioningConfig, requ
89
89
  email: string;
90
90
  password: string;
91
91
  }, fetchImpl?: typeof fetch): Promise<ProvisionOutcome>;
92
+ type DeletionOutcome = {
93
+ status: "requested";
94
+ eventId: string;
95
+ } | {
96
+ status: "rejected";
97
+ reason: string;
98
+ } | {
99
+ status: "unavailable";
100
+ };
101
+ /**
102
+ * Asks urbangate to drop this product's role on the identity, so the person
103
+ * stops being one of its users.
104
+ *
105
+ * It drops the role and nothing else. The identity belongs to the person, not
106
+ * to the product that enrolled them: deactivating it would take away every
107
+ * other product of the suite, without either product knowing why. urbangate
108
+ * deletes the identity itself only once no role is left on it.
109
+ *
110
+ * `identityId` is the `identityId` the local user row stores, written there by
111
+ * `provisionIdentity`. A local account that predates the move has none, and is
112
+ * deleted locally without a call here.
113
+ */
114
+ declare function requestAccountDeletion(config: IdentityProvisioningConfig, request: {
115
+ identityId: string;
116
+ userId?: string;
117
+ }, fetchImpl?: typeof fetch): Promise<DeletionOutcome>;
118
+
119
+ interface DeleteAccountConfig {
120
+ /**
121
+ * Ends the person's subscription. Called first and its failure is fatal:
122
+ * an account that is deleted but keeps being charged is far worse than a
123
+ * deletion its owner has to retry. Must be idempotent — no subscription is
124
+ * a success, not an error, because a retried deletion reaches it again.
125
+ *
126
+ * Omit on a product that bills nobody.
127
+ */
128
+ cancelBilling?: (userId: string) => Promise<void>;
129
+ /**
130
+ * Purges the product's own data: the domain tables keyed on the user, the
131
+ * object storage prefix, whatever else only this product knows about.
132
+ * Fatal, and called before anything that cannot be undone, so a failure
133
+ * leaves an account its owner can still sign into and delete again.
134
+ *
135
+ * Must be idempotent: `DELETE ... WHERE user_id` on already-purged rows
136
+ * affects none, which is what makes a retry safe.
137
+ */
138
+ purgeDomain?: (userId: string) => Promise<void>;
139
+ /**
140
+ * Revokes what third parties still hold: OAuth grants on the person's
141
+ * Google or Bluesky account, API keys, webhook endpoints. Best-effort —
142
+ * the tokens die with the rows anyway, and a right to erasure cannot
143
+ * depend on another company's uptime.
144
+ */
145
+ revokeExternal?: (userId: string) => Promise<void>;
146
+ /**
147
+ * Drops this product's role at urbangate. Omit to leave the identity
148
+ * untouched, which is right for a product that does not enrol one.
149
+ */
150
+ identity?: IdentityProvisioningConfig;
151
+ /** Reports a step that failed without stopping the deletion. */
152
+ onWarning?: (step: string, error: unknown) => void;
153
+ /**
154
+ * The call that asks urbangate to drop the role. Defaults to
155
+ * `requestAccountDeletion`; an app overrides it only in a test.
156
+ */
157
+ requestDeletion?: (identity: IdentityProvisioningConfig, request: {
158
+ identityId: string;
159
+ userId?: string;
160
+ }) => Promise<DeletionOutcome>;
161
+ }
162
+ interface DeleteAccountRequest {
163
+ userId: string;
164
+ /** The `identityId` on the local user row; absent on accounts that predate urbangate. */
165
+ identityId?: string | null;
166
+ }
167
+ type DeleteAccountResult = {
168
+ status: "deleted";
169
+ warnings: Array<string>;
170
+ } | {
171
+ status: "failed";
172
+ step: DeleteAccountStep;
173
+ cause: unknown;
174
+ };
175
+ type DeleteAccountStep = "cancel_billing" | "purge_domain" | "drop_identity_role" | "delete_login";
176
+ /**
177
+ * Deletes one product's account, in the order that leaves the least damage
178
+ * when a step fails.
179
+ *
180
+ * No step can be rolled back once the next one has run, and no transaction
181
+ * spans a payment provider, a domain database and an identity provider. What
182
+ * the order buys is that a failure is always recoverable by retrying: billing
183
+ * stops first because a charge that outlives the account is the one outcome
184
+ * nobody notices, the domain data goes next while the account still exists to
185
+ * try again from, and the login row goes last because it is what the person
186
+ * would need to come back.
187
+ *
188
+ * Every step is idempotent, so the retry is safe.
189
+ */
190
+ declare function deleteAccount(config: DeleteAccountConfig, request: DeleteAccountRequest, deleteLogin: (userId: string) => Promise<void>): Promise<DeleteAccountResult>;
92
191
 
93
192
  /**
94
193
  * Minimal Postgres pool contract this module needs — kept structural so the
@@ -148,4 +247,4 @@ declare class KratosSignInError extends APIError {
148
247
  }
149
248
  type PlatformAuth = ReturnType<typeof createPlatformAuth>;
150
249
 
151
- export { type BootstrapAdminClient, type BootstrapAdminPool, type BootstrapFirstAdminInput, type BootstrapFirstAdminResult, type IdentityProvisioningConfig, KRATOS_SENTINEL_HASH, type KratosOutcome, KratosSignInError, type PlatformAuth, type ProvisionOutcome, bootstrapFirstAdmin, createPlatformAuth, isKratosSentinel, provisionIdentity, rememberSignInIdentifier, updateIdentityPassword };
250
+ export { type BootstrapAdminClient, type BootstrapAdminPool, type BootstrapFirstAdminInput, type BootstrapFirstAdminResult, type DeleteAccountConfig, type DeleteAccountRequest, type DeleteAccountResult, type DeleteAccountStep, type DeletionOutcome, type IdentityProvisioningConfig, KRATOS_SENTINEL_HASH, type KratosOutcome, KratosSignInError, type PlatformAuth, type ProvisionOutcome, bootstrapFirstAdmin, createPlatformAuth, deleteAccount, isKratosSentinel, provisionIdentity, rememberSignInIdentifier, requestAccountDeletion, updateIdentityPassword };
package/dist/server.js CHANGED
@@ -11,6 +11,11 @@ import {
11
11
  roleFromIdToken,
12
12
  withSignUpName
13
13
  } from "./chunk-6OADPUHL.js";
14
+ import {
15
+ provisionIdentity,
16
+ requestAccountDeletion,
17
+ updateIdentityPassword
18
+ } from "./chunk-UURHL4NP.js";
14
19
 
15
20
  // src/server.ts
16
21
  import { betterAuth, APIError } from "better-auth";
@@ -97,135 +102,6 @@ async function verifyAgainstKratos(publicUrl, check, fetchImpl = fetch) {
97
102
  }
98
103
  }
99
104
 
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
-
229
105
  // src/google-defaults.ts
230
106
  function withGoogleDefaults(google) {
231
107
  if (typeof google === "function") return google;
@@ -274,6 +150,59 @@ function resolveRateLimit(config) {
274
150
  };
275
151
  }
276
152
 
153
+ // src/delete-account.ts
154
+ async function deleteAccount(config, request, deleteLogin) {
155
+ const warnings = [];
156
+ const warn = (step, error) => {
157
+ warnings.push(step);
158
+ config.onWarning?.(step, error);
159
+ };
160
+ if (config.cancelBilling) {
161
+ try {
162
+ await config.cancelBilling(request.userId);
163
+ } catch (cause) {
164
+ return { status: "failed", step: "cancel_billing", cause };
165
+ }
166
+ }
167
+ if (config.purgeDomain) {
168
+ try {
169
+ await config.purgeDomain(request.userId);
170
+ } catch (cause) {
171
+ return { status: "failed", step: "purge_domain", cause };
172
+ }
173
+ }
174
+ if (config.revokeExternal) {
175
+ try {
176
+ await config.revokeExternal(request.userId);
177
+ } catch (cause) {
178
+ warn("revoke_external", cause);
179
+ }
180
+ }
181
+ if (config.identity && request.identityId) {
182
+ const call = config.requestDeletion ?? (await import("./identity-provisioning-GBE6OXR3.js")).requestAccountDeletion;
183
+ const outcome = await call(config.identity, {
184
+ identityId: request.identityId,
185
+ userId: request.userId
186
+ });
187
+ if (outcome.status === "unavailable") {
188
+ return {
189
+ status: "failed",
190
+ step: "drop_identity_role",
191
+ cause: new Error("urbangate unavailable")
192
+ };
193
+ }
194
+ if (outcome.status === "rejected") {
195
+ warn("drop_identity_role", new Error(outcome.reason));
196
+ }
197
+ }
198
+ try {
199
+ await deleteLogin(request.userId);
200
+ } catch (cause) {
201
+ return { status: "failed", step: "delete_login", cause };
202
+ }
203
+ return { status: "deleted", warnings };
204
+ }
205
+
277
206
  // src/bootstrap-admin.ts
278
207
  async function bootstrapFirstAdmin(auth, pool, input) {
279
208
  const client = await pool.connect();
@@ -691,6 +620,7 @@ export {
691
620
  claimInvitation,
692
621
  completesSignup,
693
622
  createPlatformAuth,
623
+ deleteAccount,
694
624
  holdInviteTokenCookie,
695
625
  invitationOutcomeCookie,
696
626
  inviteTokenFrom,
@@ -701,6 +631,7 @@ export {
701
631
  provisionIdentity,
702
632
  releaseInviteTokenCookie,
703
633
  rememberSignInIdentifier,
634
+ requestAccountDeletion,
704
635
  updateIdentityPassword
705
636
  };
706
637
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
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 update: {\n ...databaseHooks?.user?.update,\n // A password sign-up is created unverified and confirmed by its OTP\n // a moment later; a social sign-up may be confirmed by the provider\n // later still. Enrolment follows the address becoming verified,\n // whenever that happens, and is idempotent so it never doubles.\n after: withIdentityProvisioning(\n databaseHooks?.user?.update?.after,\n kratosPasswords,\n ),\n },\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 {\n id?: string\n email?: string\n name?: string\n emailVerified?: boolean\n identityId?: unknown\n }\n if (!record.id || !record.email) return\n // Already enrolled: every later update of the row would otherwise call the\n // provider again for nothing.\n if (typeof record.identityId === \"string\" && record.identityId) return\n // An address nobody proved belongs to this person must not reach the\n // provider: enrolment is idempotent on the address, so an unverified one\n // would join them to the identity of whoever actually owns it. A social\n // sign-up whose provider reports the address unverified, and a password\n // sign-up before its OTP, are enrolled once the address is confirmed.\n if (record.emailVerified !== true) 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":"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","names":["body","body","body","email"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/kratos-credentials.ts","../src/google-defaults.ts","../src/sso-endpoints.ts","../src/rate-limit.ts","../src/delete-account.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 update: {\n ...databaseHooks?.user?.update,\n // A password sign-up is created unverified and confirmed by its OTP\n // a moment later; a social sign-up may be confirmed by the provider\n // later still. Enrolment follows the address becoming verified,\n // whenever that happens, and is idempotent so it never doubles.\n after: withIdentityProvisioning(\n databaseHooks?.user?.update?.after,\n kratosPasswords,\n ),\n },\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 {\n id?: string\n email?: string\n name?: string\n emailVerified?: boolean\n identityId?: unknown\n }\n if (!record.id || !record.email) return\n // Already enrolled: every later update of the row would otherwise call the\n // provider again for nothing.\n if (typeof record.identityId === \"string\" && record.identityId) return\n // An address nobody proved belongs to this person must not reach the\n // provider: enrolment is idempotent on the address, so an unverified one\n // would join them to the identity of whoever actually owns it. A social\n // sign-up whose provider reports the address unverified, and a password\n // sign-up before its OTP, are enrolled once the address is confirmed.\n if (record.emailVerified !== true) 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 {\n provisionIdentity,\n updateIdentityPassword,\n requestAccountDeletion,\n} from \"./identity-provisioning\"\nexport type {\n IdentityProvisioningConfig,\n ProvisionOutcome,\n DeletionOutcome,\n} from \"./identity-provisioning\"\n\nexport { deleteAccount } from \"./delete-account\"\nexport type {\n DeleteAccountConfig,\n DeleteAccountRequest,\n DeleteAccountResult,\n DeleteAccountStep,\n} from \"./delete-account\"\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","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 {\n DeletionOutcome,\n IdentityProvisioningConfig,\n} from \"./identity-provisioning\"\n\nexport interface DeleteAccountConfig {\n /**\n * Ends the person's subscription. Called first and its failure is fatal:\n * an account that is deleted but keeps being charged is far worse than a\n * deletion its owner has to retry. Must be idempotent — no subscription is\n * a success, not an error, because a retried deletion reaches it again.\n *\n * Omit on a product that bills nobody.\n */\n cancelBilling?: (userId: string) => Promise<void>\n /**\n * Purges the product's own data: the domain tables keyed on the user, the\n * object storage prefix, whatever else only this product knows about.\n * Fatal, and called before anything that cannot be undone, so a failure\n * leaves an account its owner can still sign into and delete again.\n *\n * Must be idempotent: `DELETE ... WHERE user_id` on already-purged rows\n * affects none, which is what makes a retry safe.\n */\n purgeDomain?: (userId: string) => Promise<void>\n /**\n * Revokes what third parties still hold: OAuth grants on the person's\n * Google or Bluesky account, API keys, webhook endpoints. Best-effort —\n * the tokens die with the rows anyway, and a right to erasure cannot\n * depend on another company's uptime.\n */\n revokeExternal?: (userId: string) => Promise<void>\n /**\n * Drops this product's role at urbangate. Omit to leave the identity\n * untouched, which is right for a product that does not enrol one.\n */\n identity?: IdentityProvisioningConfig\n /** Reports a step that failed without stopping the deletion. */\n onWarning?: (step: string, error: unknown) => void\n /**\n * The call that asks urbangate to drop the role. Defaults to\n * `requestAccountDeletion`; an app overrides it only in a test.\n */\n requestDeletion?: (\n identity: IdentityProvisioningConfig,\n request: { identityId: string; userId?: string },\n ) => Promise<DeletionOutcome>\n}\n\nexport interface DeleteAccountRequest {\n userId: string\n /** The `identityId` on the local user row; absent on accounts that predate urbangate. */\n identityId?: string | null\n}\n\nexport type DeleteAccountResult =\n | { status: \"deleted\"; warnings: Array<string> }\n | { status: \"failed\"; step: DeleteAccountStep; cause: unknown }\n\nexport type DeleteAccountStep =\n | \"cancel_billing\"\n | \"purge_domain\"\n | \"drop_identity_role\"\n | \"delete_login\"\n\n/**\n * Deletes one product's account, in the order that leaves the least damage\n * when a step fails.\n *\n * No step can be rolled back once the next one has run, and no transaction\n * spans a payment provider, a domain database and an identity provider. What\n * the order buys is that a failure is always recoverable by retrying: billing\n * stops first because a charge that outlives the account is the one outcome\n * nobody notices, the domain data goes next while the account still exists to\n * try again from, and the login row goes last because it is what the person\n * would need to come back.\n *\n * Every step is idempotent, so the retry is safe.\n */\nexport async function deleteAccount(\n config: DeleteAccountConfig,\n request: DeleteAccountRequest,\n deleteLogin: (userId: string) => Promise<void>,\n): Promise<DeleteAccountResult> {\n const warnings: Array<string> = []\n const warn = (step: string, error: unknown) => {\n warnings.push(step)\n config.onWarning?.(step, error)\n }\n\n if (config.cancelBilling) {\n try {\n await config.cancelBilling(request.userId)\n } catch (cause) {\n return { status: \"failed\", step: \"cancel_billing\", cause }\n }\n }\n\n if (config.purgeDomain) {\n try {\n await config.purgeDomain(request.userId)\n } catch (cause) {\n return { status: \"failed\", step: \"purge_domain\", cause }\n }\n }\n\n // Before the login row goes: the tokens live on it, and a row that is gone\n // takes with it the only way to find what to revoke.\n if (config.revokeExternal) {\n try {\n await config.revokeExternal(request.userId)\n } catch (cause) {\n warn(\"revoke_external\", cause)\n }\n }\n\n if (config.identity && request.identityId) {\n // Imported here rather than at the top so this module carries no runtime\n // dependency on the provisioning client: a caller that injects its own\n // never loads it.\n const call =\n config.requestDeletion ??\n (await import(\"./identity-provisioning\")).requestAccountDeletion\n const outcome = await call(config.identity, {\n identityId: request.identityId,\n userId: request.userId,\n })\n if (outcome.status === \"unavailable\") {\n return {\n status: \"failed\",\n step: \"drop_identity_role\",\n cause: new Error(\"urbangate unavailable\"),\n }\n }\n // A rejection is urbangate refusing this request, not a transient fault:\n // retrying sends the same one. The role survives, and the warning is what\n // says so.\n if (outcome.status === \"rejected\") {\n warn(\"drop_identity_role\", new Error(outcome.reason))\n }\n }\n\n try {\n await deleteLogin(request.userId)\n } catch (cause) {\n return { status: \"failed\", step: \"delete_login\", cause }\n }\n\n return { status: \"deleted\", warnings }\n}\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":["body","body","email"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalternative/auth",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Shared Better Auth wrapper for L'Alternative apps (server + React client + auth UI)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",