@lunora/auth 1.0.0-alpha.21 → 1.0.0-alpha.211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +32 -0
- package/README.md +99 -10
- package/dist/adapter.d.mts +4 -43
- package/dist/adapter.d.ts +4 -43
- package/dist/adapter.mjs +1 -48
- package/dist/audit.d.mts +142 -0
- package/dist/audit.d.ts +142 -0
- package/dist/audit.mjs +12 -0
- package/dist/cimd-node.d.mts +1 -0
- package/dist/cimd-node.d.ts +1 -0
- package/dist/cimd-node.mjs +1 -0
- package/dist/cimd-workers.d.mts +32 -0
- package/dist/cimd-workers.d.ts +32 -0
- package/dist/cimd-workers.mjs +1 -0
- package/dist/email-guard.d.mts +127 -0
- package/dist/email-guard.d.ts +127 -0
- package/dist/email-guard.mjs +1 -0
- package/dist/index.d.mts +769 -173
- package/dist/index.d.ts +769 -173
- package/dist/index.mjs +1 -12
- package/dist/middleware.d.mts +165 -155
- package/dist/middleware.d.ts +165 -155
- package/dist/middleware.mjs +1 -56
- package/dist/packem_shared/AUTH_DO_AUDIT_PATH-1kIGHxKo.mjs +1 -0
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs +1 -0
- package/dist/packem_shared/LunoraAuthAdminError-uhKG1MgQ.mjs +1 -0
- package/dist/packem_shared/adapter-CBZ17jKi.mjs +2 -0
- package/dist/packem_shared/adapter.d-CElAPJ1U.d.ts +88 -0
- package/dist/packem_shared/adapter.d-CaPwF2v3.d.mts +88 -0
- package/dist/packem_shared/auth-do-DEd6xEKe.mjs +1 -0
- package/dist/packem_shared/authAuditHook-DPTKdjBe.mjs +1 -0
- package/dist/packem_shared/authDiscoveryPaths-B14lM14u.mjs +1 -0
- package/dist/packem_shared/authDoColumnAdditions-UwF3Rl41.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-BiYsLz91.mjs +1 -0
- package/dist/packem_shared/create-auth.d-D1CKOFWc.d.mts +142 -0
- package/dist/packem_shared/create-auth.d-D1CKOFWc.d.ts +142 -0
- package/dist/packem_shared/createAuth-BdVltKyf.mjs +1 -0
- package/dist/packem_shared/createDoAuthWiring-B-c6qEcK.mjs +1 -0
- package/dist/packem_shared/createLunoraAuthClient-DRcFNwp9.mjs +1 -0
- package/dist/packem_shared/createSignUpInvitation-DHZL9fNl.mjs +1 -0
- package/dist/packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs +1 -0
- package/dist/packem_shared/invite-only.d-CsPJWAwM.d.ts +126 -0
- package/dist/packem_shared/invite-only.d-DKyHHkkQ.d.mts +126 -0
- package/dist/packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs +1 -0
- package/dist/packem_shared/mcp-CgX1hTOv.mjs +1 -0
- package/dist/packem_shared/on-cloudflare-edge-Dx6gFjBO.mjs +1 -0
- package/dist/packem_shared/quote-identifier-CGiYFBvY.mjs +1 -0
- package/dist/packem_shared/sessionPresets-C867Mlo4.mjs +1 -0
- package/dist/packem_shared/sql-store-uMubPYI9.mjs +1 -0
- package/dist/packem_shared/uiConfig-B8pN6pks.mjs +1 -0
- package/dist/plugins-client.d.mts +250 -1
- package/dist/plugins-client.d.ts +250 -1
- package/dist/plugins-client.mjs +1 -2
- package/dist/plugins-enterprise-client.d.mts +13 -0
- package/dist/plugins-enterprise-client.d.ts +13 -0
- package/dist/plugins-enterprise-client.mjs +1 -0
- package/dist/plugins-enterprise.d.mts +1668 -0
- package/dist/plugins-enterprise.d.ts +1668 -0
- package/dist/plugins-enterprise.mjs +1 -0
- package/dist/plugins.d.mts +167 -2
- package/dist/plugins.d.ts +167 -2
- package/dist/plugins.mjs +1 -22
- package/dist/schema.d.mts +39 -39
- package/dist/schema.d.ts +39 -39
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.d.mts +43 -28
- package/dist/sql-store.d.ts +43 -28
- package/dist/sql-store.mjs +1 -184
- package/dist/store.d.mts +65 -47
- package/dist/store.d.ts +65 -47
- package/dist/store.mjs +1 -183
- package/dist/turnstile-middleware.d.mts +73 -55
- package/dist/turnstile-middleware.d.ts +73 -55
- package/dist/turnstile-middleware.mjs +1 -45
- package/dist/turnstile.d.mts +42 -59
- package/dist/turnstile.d.ts +42 -59
- package/dist/turnstile.mjs +1 -61
- package/package.json +48 -6
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-D4L7n6gN.mjs +0 -510
- package/dist/packem_shared/compileMigrationsSql-B9bj-mJv.mjs +0 -29
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.mts +0 -128
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.ts +0 -128
- package/dist/packem_shared/createAuth-BVMMllTm.mjs +0 -87
- package/dist/packem_shared/sessionPresets-Dwwd74_J.mjs +0 -38
package/dist/index.d.mts
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
import { D as DoStorageLike } from "./packem_shared/adapter.d-CaPwF2v3.mjs";
|
|
2
|
+
export { l as lunoraAuthAdapter, a as lunoraD1Adapter, b as lunoraDoAdapter } from "./packem_shared/adapter.d-CaPwF2v3.mjs";
|
|
2
3
|
import { LunoraError } from '@lunora/errors';
|
|
3
|
-
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-
|
|
4
|
-
export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-
|
|
4
|
+
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-D1CKOFWc.mjs";
|
|
5
|
+
export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-D1CKOFWc.mjs";
|
|
6
|
+
import { AppendAuthAuditEntry, AppendAuthAuditOptions, AuthAuditEvent, AuthAuditReader } from "./audit.mjs";
|
|
7
|
+
export { AUTH_AUDIT_TABLE, type AuthAuditEntry, type AuthAuditOutcome, type ReadAuthAuditOptions, appendAuthAuditEntry, createAuthAuditReader, ensureAuthAuditTable, readAuthAuditLog } from "./audit.mjs";
|
|
8
|
+
import { createAuthMiddleware } from 'better-auth/api';
|
|
9
|
+
import { SqlExecutor } from "./sql-store.mjs";
|
|
10
|
+
export { createSqlAuthStore, d1Executor } from "./sql-store.mjs";
|
|
11
|
+
import { BetterAuthOptions } from 'better-auth';
|
|
12
|
+
import { EmailClassification, EmailGateConfig } from "./email-guard.mjs";
|
|
13
|
+
export { type EmailClass, type EmailGateMiddlewareOptions, assertEmailAllowed, classifyEmail, emailGateMiddleware, loadEmailDomainLists } from "./email-guard.mjs";
|
|
14
|
+
export { type I as InviteOnlyOptions, type a as IssuedSignUpInvitation, type S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, p as pruneSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-DKyHHkkQ.mjs";
|
|
5
15
|
export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.mjs";
|
|
6
16
|
export { default as authTables } from "./schema.mjs";
|
|
7
|
-
import { BetterAuthOptions } from 'better-auth';
|
|
8
|
-
export { type SqlExecutor, createSqlAuthStore, d1Executor } from "./sql-store.mjs";
|
|
9
17
|
export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.mjs";
|
|
10
18
|
export { type FetchLike, TURNSTILE_VERIFY_ENDPOINT, type TurnstileVerifyResult, type VerifyTurnstileOptions, verifyTurnstile } from "./turnstile.mjs";
|
|
11
19
|
export { type VerifyTurnstileMiddlewareOptions, verifyTurnstileMiddleware } from "./turnstile-middleware.mjs";
|
|
12
20
|
import 'better-auth/adapters';
|
|
13
21
|
import '@lunora/server';
|
|
14
22
|
/**
|
|
15
|
-
* A timestamp as it leaves the admin API: epoch-ms (better-auth stores `Date`s,
|
|
16
|
-
* which we normalize on output) or `null` when the column is unset.
|
|
17
|
-
*/
|
|
23
|
+
* A timestamp as it leaves the admin API: epoch-ms (better-auth stores `Date`s,
|
|
24
|
+
* which we normalize on output) or `null` when the column is unset.
|
|
25
|
+
*/
|
|
18
26
|
type AuthTimestamp = null | number;
|
|
19
27
|
/**
|
|
20
|
-
* One user row as the admin API surfaces it. The fixed keys mirror better-auth's
|
|
21
|
-
* core `user` table plus the `admin()` plugin columns (`role`/`banned`/…); the
|
|
22
|
-
* index signature carries any app-defined `user.additionalFields` so callers
|
|
23
|
-
* (the studio) can render them generically. Password material never lives on
|
|
24
|
-
* this row (it's in the `account` table) and is never returned.
|
|
25
|
-
*/
|
|
28
|
+
* One user row as the admin API surfaces it. The fixed keys mirror better-auth's
|
|
29
|
+
* core `user` table plus the `admin()` plugin columns (`role`/`banned`/…); the
|
|
30
|
+
* index signature carries any app-defined `user.additionalFields` so callers
|
|
31
|
+
* (the studio) can render them generically. Password material never lives on
|
|
32
|
+
* this row (it's in the `account` table) and is never returned.
|
|
33
|
+
*/
|
|
26
34
|
interface AuthAdminUser {
|
|
27
35
|
[key: string]: unknown;
|
|
28
36
|
banExpires?: AuthTimestamp;
|
|
@@ -38,12 +46,12 @@ interface AuthAdminUser {
|
|
|
38
46
|
updatedAt?: AuthTimestamp;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
|
-
* One session row as the admin API surfaces it. Mirrors better-auth's `session`
|
|
42
|
-
* table; `impersonatedBy` is set when the session was minted by
|
|
43
|
-
* {@link AuthAdmin.impersonateUser}. The signing `token` is stripped — it's a
|
|
44
|
-
* bearer credential, and the only place we hand one back is the explicit
|
|
45
|
-
* impersonation flow.
|
|
46
|
-
*/
|
|
49
|
+
* One session row as the admin API surfaces it. Mirrors better-auth's `session`
|
|
50
|
+
* table; `impersonatedBy` is set when the session was minted by
|
|
51
|
+
* {@link AuthAdmin.impersonateUser}. The signing `token` is stripped — it's a
|
|
52
|
+
* bearer credential, and the only place we hand one back is the explicit
|
|
53
|
+
* impersonation flow.
|
|
54
|
+
*/
|
|
47
55
|
interface AuthAdminSession {
|
|
48
56
|
[key: string]: unknown;
|
|
49
57
|
createdAt?: AuthTimestamp;
|
|
@@ -91,6 +99,28 @@ interface AuthInvitation {
|
|
|
91
99
|
role?: null | string;
|
|
92
100
|
status?: null | string;
|
|
93
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* One sign-up invitation (from the `inviteOnly` plugin). Distinct from
|
|
104
|
+
* {@link AuthInvitation}, which invites an existing account into an organization:
|
|
105
|
+
* this one is what lets an address create an account at all.
|
|
106
|
+
*/
|
|
107
|
+
interface AuthSignUpInvitation {
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
/** When an account was created for this address; `null` while the invitation is unspent. */
|
|
110
|
+
acceptedAt?: AuthTimestamp;
|
|
111
|
+
createdAt?: AuthTimestamp;
|
|
112
|
+
email?: null | string;
|
|
113
|
+
expiresAt?: AuthTimestamp;
|
|
114
|
+
id: string;
|
|
115
|
+
invitedBy?: null | string;
|
|
116
|
+
/**
|
|
117
|
+
* The plaintext invitation token — present **only** on the row
|
|
118
|
+
* {@link AuthAdmin.createSignUpInvitation} returns, never on a listed one.
|
|
119
|
+
* The stored `tokenHash` is in {@link SENSITIVE_FIELDS}, so it cannot leave
|
|
120
|
+
* this plane by accident.
|
|
121
|
+
*/
|
|
122
|
+
token?: string;
|
|
123
|
+
}
|
|
94
124
|
/** One team row (from the `organization` plugin with `teams.enabled`). */
|
|
95
125
|
interface AuthTeam {
|
|
96
126
|
[key: string]: unknown;
|
|
@@ -108,10 +138,10 @@ interface AuthTeamMember {
|
|
|
108
138
|
userId: string;
|
|
109
139
|
}
|
|
110
140
|
/**
|
|
111
|
-
* One custom organization role (from the organization plugin's dynamic
|
|
112
|
-
* access-control). `permission` is a JSON string of a `resource → actions[]` map
|
|
113
|
-
* as stored; the studio parses it for display/editing.
|
|
114
|
-
*/
|
|
141
|
+
* One custom organization role (from the organization plugin's dynamic
|
|
142
|
+
* access-control). `permission` is a JSON string of a `resource → actions[]` map
|
|
143
|
+
* as stored; the studio parses it for display/editing.
|
|
144
|
+
*/
|
|
115
145
|
interface AuthOrgRole {
|
|
116
146
|
[key: string]: unknown;
|
|
117
147
|
createdAt?: AuthTimestamp;
|
|
@@ -135,17 +165,19 @@ interface AuthPage<T> {
|
|
|
135
165
|
total: number;
|
|
136
166
|
}
|
|
137
167
|
/**
|
|
138
|
-
* Which admin surfaces a given auth instance supports, derived from the enabled
|
|
139
|
-
* better-auth plugins (and any {@link CreateAuthAdminOptions.features} overrides).
|
|
140
|
-
* The studio calls {@link AuthAdmin.capabilities} once and renders only the
|
|
141
|
-
* panels whose capability is `true` — so a deployment that doesn't enable, say,
|
|
142
|
-
* the `organization` plugin never shows an Organizations section.
|
|
143
|
-
*/
|
|
168
|
+
* Which admin surfaces a given auth instance supports, derived from the enabled
|
|
169
|
+
* better-auth plugins (and any {@link CreateAuthAdminOptions.features} overrides).
|
|
170
|
+
* The studio calls {@link AuthAdmin.capabilities} once and renders only the
|
|
171
|
+
* panels whose capability is `true` — so a deployment that doesn't enable, say,
|
|
172
|
+
* the `organization` plugin never shows an Organizations section.
|
|
173
|
+
*/
|
|
144
174
|
interface AuthCapabilities {
|
|
145
175
|
/** Linked-account browsing/unlinking — core (the `account` table always exists). */
|
|
146
176
|
accounts: boolean;
|
|
147
177
|
/** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
|
|
148
178
|
admin: boolean;
|
|
179
|
+
/** The `inviteOnly` plugin: sign-up invitations. */
|
|
180
|
+
inviteOnly: boolean;
|
|
149
181
|
/** The `organization` plugin: orgs, members, invitations. */
|
|
150
182
|
organization: boolean;
|
|
151
183
|
/** The `@better-auth/passkey` plugin: per-user passkeys. */
|
|
@@ -154,12 +186,12 @@ interface AuthCapabilities {
|
|
|
154
186
|
twoFactor: boolean;
|
|
155
187
|
}
|
|
156
188
|
/**
|
|
157
|
-
* One app/plugin-defined field the create-user form should render, derived from
|
|
158
|
-
* the merged better-auth `user` table (core + plugin + `additionalFields`). Only
|
|
159
|
-
* user-settable columns are surfaced — server-managed flags (`input: false`),
|
|
160
|
-
* foreign keys (`references`), and the core columns the form already handles
|
|
161
|
-
* (`email`/`name`/`role`/ban state/…) are filtered out upstream.
|
|
162
|
-
*/
|
|
189
|
+
* One app/plugin-defined field the create-user form should render, derived from
|
|
190
|
+
* the merged better-auth `user` table (core + plugin + `additionalFields`). Only
|
|
191
|
+
* user-settable columns are surfaced — server-managed flags (`input: false`),
|
|
192
|
+
* foreign keys (`references`), and the core columns the form already handles
|
|
193
|
+
* (`email`/`name`/`role`/ban state/…) are filtered out upstream.
|
|
194
|
+
*/
|
|
163
195
|
interface AuthUserFieldSpec {
|
|
164
196
|
/** Logical field name (the key passed back in `createUser`'s `data`). */
|
|
165
197
|
name: string;
|
|
@@ -171,13 +203,13 @@ interface AuthUserFieldSpec {
|
|
|
171
203
|
unique: boolean;
|
|
172
204
|
}
|
|
173
205
|
/**
|
|
174
|
-
* A rich, read-only description of the deployment's auth configuration for the
|
|
175
|
-
* studio's config panel and dynamic create-user form. Unlike
|
|
176
|
-
* {@link AuthCapabilities} (five booleans that gate panels), this exposes *what*
|
|
177
|
-
* is configured — enabled plugins, email/password + social sign-in, the
|
|
178
|
-
* user-settable fields, organization sub-features (teams / custom roles), and
|
|
179
|
-
* the session + rate-limit policy — without ever leaking a secret.
|
|
180
|
-
*/
|
|
206
|
+
* A rich, read-only description of the deployment's auth configuration for the
|
|
207
|
+
* studio's config panel and dynamic create-user form. Unlike
|
|
208
|
+
* {@link AuthCapabilities} (five booleans that gate panels), this exposes *what*
|
|
209
|
+
* is configured — enabled plugins, email/password + social sign-in, the
|
|
210
|
+
* user-settable fields, organization sub-features (teams / custom roles), and
|
|
211
|
+
* the session + rate-limit policy — without ever leaking a secret.
|
|
212
|
+
*/
|
|
181
213
|
interface AuthConfigInfo {
|
|
182
214
|
/** The same capability booleans {@link AuthAdmin.capabilities} returns, embedded so a single call drives the whole panel. */
|
|
183
215
|
capabilities: AuthCapabilities;
|
|
@@ -185,7 +217,8 @@ interface AuthConfigInfo {
|
|
|
185
217
|
emailAndPassword: boolean;
|
|
186
218
|
/** Organization plugin sub-features. */
|
|
187
219
|
organization: {
|
|
188
|
-
enabled: boolean;
|
|
220
|
+
enabled: boolean;
|
|
221
|
+
/** Custom roles / dynamic access control (`organizationRole` table present). */
|
|
189
222
|
roles: boolean;
|
|
190
223
|
/** Teams (`team` table present). */
|
|
191
224
|
teams: boolean;
|
|
@@ -236,14 +269,14 @@ interface ImpersonationResult {
|
|
|
236
269
|
user: AuthAdminUser;
|
|
237
270
|
}
|
|
238
271
|
/**
|
|
239
|
-
* The full read + write surface the studio's auth dashboard drives, backed by
|
|
240
|
-
* better-auth's tables. Returned by {@link createAuthAdmin}. The runtime accepts
|
|
241
|
-
* a structurally-compatible object as its `authAdmin` option and exposes each
|
|
242
|
-
* method behind an admin-token-gated endpoint. Methods whose backing plugin is
|
|
243
|
-
* absent still exist (they're not conditionally omitted) but the studio gates
|
|
244
|
-
* them on {@link AuthAdmin.capabilities}; calling one for an unconfigured plugin
|
|
245
|
-
* surfaces the underlying adapter error.
|
|
246
|
-
*/
|
|
272
|
+
* The full read + write surface the studio's auth dashboard drives, backed by
|
|
273
|
+
* better-auth's tables. Returned by {@link createAuthAdmin}. The runtime accepts
|
|
274
|
+
* a structurally-compatible object as its `authAdmin` option and exposes each
|
|
275
|
+
* method behind an admin-token-gated endpoint. Methods whose backing plugin is
|
|
276
|
+
* absent still exist (they're not conditionally omitted) but the studio gates
|
|
277
|
+
* them on {@link AuthAdmin.capabilities}; calling one for an unconfigured plugin
|
|
278
|
+
* surfaces the underlying adapter error.
|
|
279
|
+
*/
|
|
247
280
|
interface AuthAdmin {
|
|
248
281
|
/** Directly add an existing user as an org member (server-side, no invitation/acceptance). */
|
|
249
282
|
addMember: (input: {
|
|
@@ -281,6 +314,17 @@ interface AuthAdmin {
|
|
|
281
314
|
permission: Record<string, string[]>;
|
|
282
315
|
role: string;
|
|
283
316
|
}) => Promise<AuthOrgRole>;
|
|
317
|
+
/**
|
|
318
|
+
* Invite an address to sign up, or refresh an existing invitation for it.
|
|
319
|
+
* Needs the `inviteOnly` plugin — without it the row is written to a table
|
|
320
|
+
* nothing reads, which is why the studio gates the panel on
|
|
321
|
+
* {@link AuthCapabilities.inviteOnly}.
|
|
322
|
+
*/
|
|
323
|
+
createSignUpInvitation: (input: {
|
|
324
|
+
email: string;
|
|
325
|
+
expiresInSeconds?: number;
|
|
326
|
+
invitedBy?: string;
|
|
327
|
+
}) => Promise<AuthSignUpInvitation>;
|
|
284
328
|
/** Create a team under an organization. */
|
|
285
329
|
createTeam: (input: {
|
|
286
330
|
name: string;
|
|
@@ -348,6 +392,16 @@ interface AuthAdmin {
|
|
|
348
392
|
offset?: number;
|
|
349
393
|
userId?: string;
|
|
350
394
|
}) => Promise<AuthPage<AuthAdminSession>>;
|
|
395
|
+
/**
|
|
396
|
+
* Sign-up invitations, newest first. Unfiltered on purpose: "pending" is
|
|
397
|
+
* `acceptedAt === null && expiresAt > now`, and applying that after a page
|
|
398
|
+
* would let page 1 come back empty while pending rows sat on page 2. The
|
|
399
|
+
* caller has both columns and can label each row itself.
|
|
400
|
+
*/
|
|
401
|
+
listSignUpInvitations: (options: {
|
|
402
|
+
limit?: number;
|
|
403
|
+
offset?: number;
|
|
404
|
+
}) => Promise<AuthPage<AuthSignUpInvitation>>;
|
|
351
405
|
/** List a team's members. */
|
|
352
406
|
listTeamMembers: (options: {
|
|
353
407
|
limit?: number;
|
|
@@ -375,6 +429,10 @@ interface AuthAdmin {
|
|
|
375
429
|
removeUser: (input: {
|
|
376
430
|
userId: string;
|
|
377
431
|
}) => Promise<void>;
|
|
432
|
+
/** Withdraw a sign-up invitation. Not retroactive — an account already created keeps existing; use {@link AuthAdmin.removeUser} for that. */
|
|
433
|
+
revokeSignUpInvitation: (input: {
|
|
434
|
+
email: string;
|
|
435
|
+
}) => Promise<void>;
|
|
378
436
|
revokeUserSession: (input: {
|
|
379
437
|
sessionId: string;
|
|
380
438
|
}) => Promise<void>;
|
|
@@ -427,152 +485,690 @@ interface AuthAdmin {
|
|
|
427
485
|
/** Options for {@link createAuthAdmin}. */
|
|
428
486
|
interface CreateAuthAdminOptions {
|
|
429
487
|
/**
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
488
|
+
* Force individual {@link AuthCapabilities} on or off regardless of which
|
|
489
|
+
* plugins are detected — e.g. `{ impersonate: false }`-style opt-outs by
|
|
490
|
+
* setting `admin: false`, or hiding linked accounts with `accounts: false`.
|
|
491
|
+
* A capability is reported only when both its plugin is enabled *and* its
|
|
492
|
+
* override isn't `false`.
|
|
493
|
+
*/
|
|
436
494
|
features?: Partial<AuthCapabilities>;
|
|
437
495
|
/**
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
496
|
+
* User id recorded as the impersonator on sessions minted by
|
|
497
|
+
* {@link AuthAdmin.impersonateUser}. Defaults to the impersonated user's own
|
|
498
|
+
* id (a self-reference) since the trusted admin plane has no acting user.
|
|
499
|
+
*/
|
|
442
500
|
impersonatedBy?: string;
|
|
443
501
|
/**
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
502
|
+
* How long (in seconds) an impersonation session lives. Must be a positive
|
|
503
|
+
* finite integer. Capped at 24 × {@link DEFAULT_IMPERSONATION_SECONDS}
|
|
504
|
+
* (86 400 s / 24 h). Defaults to {@link DEFAULT_IMPERSONATION_SECONDS}
|
|
505
|
+
* (3 600 s / 1 h).
|
|
506
|
+
*/
|
|
449
507
|
impersonationSeconds?: number;
|
|
450
508
|
}
|
|
451
509
|
/**
|
|
452
|
-
* A normalized failure from an admin operation. better-auth throws `APIError`s
|
|
453
|
-
* carrying a `body.code` (e.g. `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`); we
|
|
454
|
-
* surface that `code` so the runtime can map it onto an HTTP status and the
|
|
455
|
-
* studio can show a meaningful message instead of a generic 500.
|
|
456
|
-
*/
|
|
510
|
+
* A normalized failure from an admin operation. better-auth throws `APIError`s
|
|
511
|
+
* carrying a `body.code` (e.g. `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`); we
|
|
512
|
+
* surface that `code` so the runtime can map it onto an HTTP status and the
|
|
513
|
+
* studio can show a meaningful message instead of a generic 500.
|
|
514
|
+
*/
|
|
457
515
|
declare class LunoraAuthAdminError extends LunoraError {
|
|
458
516
|
constructor(message: string, code: string);
|
|
459
517
|
}
|
|
460
518
|
/**
|
|
461
|
-
* Build the studio's auth user-management plane on top of better-auth.
|
|
462
|
-
*
|
|
463
|
-
* Pass the result as the runtime's `authAdmin` option; the runtime exposes each
|
|
464
|
-
* method behind an admin-token-gated `/_lunora/admin/auth/*` endpoint. The set
|
|
465
|
-
* of usable surfaces is reported by {@link AuthAdmin.capabilities} — derived
|
|
466
|
-
* from the enabled better-auth plugins, so enabling `admin()`, `organization()`,
|
|
467
|
-
* `twoFactor()`, or the passkey plugin in the auth config is what lights up the
|
|
468
|
-
* matching dashboard panels.
|
|
469
|
-
*
|
|
470
|
-
* **Trust model — important.** These operations talk to better-auth's
|
|
471
|
-
* `internalAdapter` (and `adapter`/password hasher) **directly**, deliberately
|
|
472
|
-
* bypassing the plugins' own endpoints, which require the caller to hold an
|
|
473
|
-
* admin-role session. That session check is the wrong gate here: the runtime
|
|
474
|
-
* already authorizes every call with `LUNORA_ADMIN_TOKEN`, so this helper acts
|
|
475
|
-
* as a trusted server-side operator. It is therefore not an end-user-callable
|
|
476
|
-
* API — never expose it on a path that isn't admin-token gated.
|
|
477
|
-
*
|
|
478
|
-
* `auth.$context` is a promise (better-auth resolves the adapter, password
|
|
479
|
-
* config, etc. lazily); we memoize it so the first call pays the cost once.
|
|
480
|
-
*/
|
|
519
|
+
* Build the studio's auth user-management plane on top of better-auth.
|
|
520
|
+
*
|
|
521
|
+
* Pass the result as the runtime's `authAdmin` option; the runtime exposes each
|
|
522
|
+
* method behind an admin-token-gated `/_lunora/admin/auth/*` endpoint. The set
|
|
523
|
+
* of usable surfaces is reported by {@link AuthAdmin.capabilities} — derived
|
|
524
|
+
* from the enabled better-auth plugins, so enabling `admin()`, `organization()`,
|
|
525
|
+
* `twoFactor()`, or the passkey plugin in the auth config is what lights up the
|
|
526
|
+
* matching dashboard panels.
|
|
527
|
+
*
|
|
528
|
+
* **Trust model — important.** These operations talk to better-auth's
|
|
529
|
+
* `internalAdapter` (and `adapter`/password hasher) **directly**, deliberately
|
|
530
|
+
* bypassing the plugins' own endpoints, which require the caller to hold an
|
|
531
|
+
* admin-role session. That session check is the wrong gate here: the runtime
|
|
532
|
+
* already authorizes every call with `LUNORA_ADMIN_TOKEN`, so this helper acts
|
|
533
|
+
* as a trusted server-side operator. It is therefore not an end-user-callable
|
|
534
|
+
* API — never expose it on a path that isn't admin-token gated.
|
|
535
|
+
*
|
|
536
|
+
* `auth.$context` is a promise (better-auth resolves the adapter, password
|
|
537
|
+
* config, etc. lazily); we memoize it so the first call pays the cost once.
|
|
538
|
+
*/
|
|
481
539
|
declare const createAuthAdmin: (auth: LunoraAuth, options?: CreateAuthAdminOptions) => AuthAdmin;
|
|
540
|
+
/** Configuration for {@link authAuditHook}. */
|
|
541
|
+
interface AuthAuditHookConfig extends AppendAuthAuditOptions {
|
|
542
|
+
/**
|
|
543
|
+
* Where to persist the trail — the same {@link SqlExecutor} seam better-auth's
|
|
544
|
+
* store rides (`d1Executor(env.DB)`), so events land in the auth D1 database.
|
|
545
|
+
*/
|
|
546
|
+
executor: SqlExecutor;
|
|
547
|
+
/**
|
|
548
|
+
* Optional export tap (pairs with SIEM forwarding): called with each redacted
|
|
549
|
+
* entry right after it is persisted, so a deployment can fan events out to an
|
|
550
|
+
* external sink. Rejections/throws are swallowed so forwarding can't break an
|
|
551
|
+
* auth request.
|
|
552
|
+
*/
|
|
553
|
+
onRecord?: (entry: AppendAuthAuditEntry) => Promise<void> | void;
|
|
554
|
+
/**
|
|
555
|
+
* When true, `x-forwarded-for` is trusted as a client-IP fallback when
|
|
556
|
+
* `cf-connecting-ip` is absent — only enable this behind a proxy you control,
|
|
557
|
+
* or the recorded IP is attacker-chosen. Defaults to `false`: off Cloudflare,
|
|
558
|
+
* with no trusted proxy configured, the audit record's `ip` is omitted rather
|
|
559
|
+
* than populated from a spoofable header.
|
|
560
|
+
*/
|
|
561
|
+
trustProxyHeaders?: boolean;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Structural view of the fields we read off better-auth's after-hook context —
|
|
565
|
+
* kept loose to avoid coupling to internal types.
|
|
566
|
+
*
|
|
567
|
+
* `body` is the parsed request body better-auth's middleware context exposes
|
|
568
|
+
* as `ctx.body` — pinned present and populated (e.g. `.email`) in the after-hook
|
|
569
|
+
* by `__tests__/audit-hooks.behaviour.test.ts` (plan 280 S0).
|
|
570
|
+
*/
|
|
571
|
+
interface AuditHookContext {
|
|
572
|
+
body?: Record<string, unknown>;
|
|
573
|
+
context?: {
|
|
574
|
+
newSession?: {
|
|
575
|
+
session?: {
|
|
576
|
+
userId?: string;
|
|
577
|
+
};
|
|
578
|
+
user?: {
|
|
579
|
+
email?: string;
|
|
580
|
+
id?: string;
|
|
581
|
+
};
|
|
582
|
+
} | null;
|
|
583
|
+
returned?: unknown;
|
|
584
|
+
session?: {
|
|
585
|
+
session?: {
|
|
586
|
+
userId?: string;
|
|
587
|
+
};
|
|
588
|
+
user?: {
|
|
589
|
+
email?: string;
|
|
590
|
+
id?: string;
|
|
591
|
+
};
|
|
592
|
+
} | null;
|
|
593
|
+
};
|
|
594
|
+
headers?: Headers;
|
|
595
|
+
path?: string;
|
|
596
|
+
request?: Request;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Map a better-auth endpoint path to the security event it represents, or
|
|
600
|
+
* `undefined` for endpoints not worth auditing (session reads, config, …). Match
|
|
601
|
+
* is by suffix so a caller `basePath` prefix (`/api/auth`) never affects it.
|
|
602
|
+
*
|
|
603
|
+
* Sign-in is split by what the endpoint actually DOES (plan 280 §4):
|
|
604
|
+
*
|
|
605
|
+
* - `/sign-in/social`, `/sign-in/magic-link` and `@better-auth/sso`'s
|
|
606
|
+
* `/sign-in/sso` only DISPATCH — the first mints a provider redirect URL, the
|
|
607
|
+
* second sends an email, the third answers `{ url, redirect: true }` pointing
|
|
608
|
+
* at the identity provider's authorization endpoint. Nobody is authenticated
|
|
609
|
+
* yet, so these are `sign-in-initiated`, not `sign-in`. Without the `sso`
|
|
610
|
+
* branch that path fell through to the `/sign-in/` substring below and every
|
|
611
|
+
* SSO redirect mint was recorded as a completed, successful `sign-in` with no
|
|
612
|
+
* actor — in exactly the flow where the "who authenticated" trail matters
|
|
613
|
+
* most.
|
|
614
|
+
* - `/callback/:id` (social + generic-oauth), `/magic-link/verify`,
|
|
615
|
+
* `@better-auth/sso`'s `/sso/saml2/sp/acs/:providerId`, and every
|
|
616
|
+
* `/two-factor/verify-*` (`verify-totp` / `verify-otp` / `verify-backup-code`
|
|
617
|
+
* — all three complete a challenged sign-in the same way) are where a
|
|
618
|
+
* session actually gets issued, so they join credential sign-ins
|
|
619
|
+
* (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`, …) as
|
|
620
|
+
* plain `sign-in`. They were NOT recorded at all before this change.
|
|
621
|
+
*
|
|
622
|
+
* The SAML assertion consumer service needs its own branch because it is the
|
|
623
|
+
* one completion that carries no `/callback/` segment: `processSAMLResponse`
|
|
624
|
+
* validates the assertion, resolves the user and calls `setSessionCookie`, all
|
|
625
|
+
* under `/sso/saml2/sp/acs/:providerId`. Matching neither that substring nor
|
|
626
|
+
* any other branch, it classified as `undefined` — the endpoint that issues
|
|
627
|
+
* every SAML session left no audit row at all.
|
|
628
|
+
*
|
|
629
|
+
* SAML sign-OUT has the mirror-image problem, and two endpoints rather than
|
|
630
|
+
* one. Neither ends in `/sign-out`, and both terminate the local session before
|
|
631
|
+
* redirecting, so both are `sign-out` rather than an initiated event:
|
|
632
|
+
*
|
|
633
|
+
* - `/sso/saml2/logout/:providerId` — SP-initiated. Deletes the SAML session
|
|
634
|
+
* keys, calls `deleteSession` on the current session token and
|
|
635
|
+
* `deleteSessionCookie`, then redirects to the IdP's logout URL.
|
|
636
|
+
* - `/sso/saml2/sp/slo/:providerId` — the SP's single-logout receiver, for the
|
|
637
|
+
* IdP-initiated direction and for the response leg of an SP-initiated one.
|
|
638
|
+
* Both `handleLogoutRequest` and `handleLogoutResponse` call `deleteSession`
|
|
639
|
+
* and `deleteSessionCookie` the same way.
|
|
640
|
+
*
|
|
641
|
+
* Recording the SAML sign-in while leaving these silent is worse than either
|
|
642
|
+
* gap alone: the trail would show a session opening and never closing, so a
|
|
643
|
+
* reader cannot tell "still signed in" from "we stopped watching".
|
|
644
|
+
*
|
|
645
|
+
* There is no dedicated `/oauth2/callback/*` branch because the generic
|
|
646
|
+
* `/callback/` check above already covers it, and covering it is CORRECT: an
|
|
647
|
+
* OAuth callback is a completed sign-in whatever path prefix it arrives on. The
|
|
648
|
+
* same substring also catches `@better-auth/sso`'s `/sso/callback/:providerId`,
|
|
649
|
+
* which is where an SSO sign-in actually completes — `sso` is re-exported from
|
|
650
|
+
* `@lunora/auth/plugins/enterprise`, so that path is live, not hypothetical.
|
|
651
|
+
*
|
|
652
|
+
* Checked against the installed `better-auth` and `@better-auth/*` dist for
|
|
653
|
+
* 1.7.3: generic-oauth registers NO endpoints of its own (there is no
|
|
654
|
+
* `/sign-in/oauth2`) — it registers providers used through the core
|
|
655
|
+
* `/sign-in/social` and `/callback/:id`, and the one dist hit for a literal
|
|
656
|
+
* `/oauth2/callback/` is inside `better-auth/plugins/oauth-popup`, which
|
|
657
|
+
* `plugins.ts` does not re-export — so today the branch fires for the core
|
|
658
|
+
* callback, and stays correct if the other becomes reachable.
|
|
659
|
+
*/
|
|
660
|
+
declare const eventForPath: (path: string) => AuthAuditEvent | undefined;
|
|
661
|
+
/**
|
|
662
|
+
* Build the entry a given after-hook context should record, or `undefined` when
|
|
663
|
+
* the path is not an audited security event. Exported for direct unit testing of
|
|
664
|
+
* the classification/extraction without spinning up better-auth.
|
|
665
|
+
*
|
|
666
|
+
* Does NOT attempt to distinguish a 2FA-challenged credential sign-in from a
|
|
667
|
+
* fully successful one (a `sign-in-challenged` event, as an earlier design for
|
|
668
|
+
* this change proposed) — pinned in `__tests__/audit-hooks.behaviour.test.ts`
|
|
669
|
+
* (plan 280 S0): better-auth runs the APP's own `hooks.after` BEFORE the
|
|
670
|
+
* `twoFactor` plugin's own after-hook that rewrites the response to
|
|
671
|
+
* `{ twoFactorRedirect: true }` and nulls `ctx.context.newSession`. By the time
|
|
672
|
+
* THIS hook runs, `context.returned`/`context.newSession` still reflect the
|
|
673
|
+
* pre-interception, fully-successful sign-in — there is nothing here to detect
|
|
674
|
+
* the challenge from. Distinguishing it would need a different seam (e.g. a
|
|
675
|
+
* plugin-ordered-after-`twoFactor` hook, or reading `twoFactor`'s own
|
|
676
|
+
* database state) and is left to a follow-up.
|
|
677
|
+
*/
|
|
678
|
+
declare const buildAuditEntry: (context: AuditHookContext, { now, trustProxyHeaders }?: {
|
|
679
|
+
now?: number;
|
|
680
|
+
trustProxyHeaders?: boolean;
|
|
681
|
+
}) => AppendAuthAuditEntry | undefined;
|
|
682
|
+
/**
|
|
683
|
+
* Create the better-auth `hooks.after` middleware that records the auth/security
|
|
684
|
+
* audit trail. Assign it to `hooks.after` (or compose via {@link withAuthAudit}).
|
|
685
|
+
*
|
|
686
|
+
* ```ts
|
|
687
|
+
* const auth = createAuth({
|
|
688
|
+
* secret: env.AUTH_SECRET,
|
|
689
|
+
* database: lunoraD1Adapter(env.DB),
|
|
690
|
+
* hooks: { after: authAuditHook({ executor: d1Executor(env.DB), retention: 100_000 }) },
|
|
691
|
+
* });
|
|
692
|
+
* ```
|
|
693
|
+
*
|
|
694
|
+
* ## Behaviour change (plan 280) — `onRecord`/SIEM consumers keyed on `event` strings, read this
|
|
695
|
+
*
|
|
696
|
+
* | Endpoint | Before | After |
|
|
697
|
+
* | -------------------------------------------------- | ------------------- | --------------------- |
|
|
698
|
+
* | `/sign-in/email` (and other credential sign-ins) | `sign-in` | `sign-in` (unchanged) |
|
|
699
|
+
* | `/sign-in/social` | `sign-in` | `sign-in-initiated` |
|
|
700
|
+
* | `/sign-in/magic-link` | `sign-in` | `sign-in-initiated` |
|
|
701
|
+
* | `/callback/:id` (social + generic-oauth) | _(not recorded)_ | `sign-in` |
|
|
702
|
+
* | `/magic-link/verify` | _(not recorded)_ | `sign-in` |
|
|
703
|
+
* | `/two-factor/verify-totp` / `-otp` / `-backup-code`| _(not recorded)_ | `sign-in` |
|
|
704
|
+
*
|
|
705
|
+
* A caller matching on `event === "sign-in"` now sees FEWER events for
|
|
706
|
+
* `/sign-in/social` and `/sign-in/magic-link` (they never actually authenticated
|
|
707
|
+
* anyone) and MORE events for the four previously-unrecorded completion
|
|
708
|
+
* endpoints — the net effect is a more truthful count, not a strictly larger or
|
|
709
|
+
* smaller one. `sign-in-initiated` is a new event name (open `AuthAuditEvent`
|
|
710
|
+
* union, so no wire/type break, but SIEM rules enumerating event names should
|
|
711
|
+
* add it). A failed sign-in now also carries `targetEmail` (the attempted
|
|
712
|
+
* address/username) when the request body supplied one — see
|
|
713
|
+
* {@link AppendAuthAuditEntry.targetEmail} — so credential-stuffing attempts can
|
|
714
|
+
* be grouped by target even though they never produce an `actorEmail`.
|
|
715
|
+
*
|
|
716
|
+
* NOT changed: `/sign-in/email` under an active 2FA challenge still records
|
|
717
|
+
* plain `sign-in` / `success` (not a distinct `sign-in-challenged` event) — see
|
|
718
|
+
* {@link buildAuditEntry}'s docblock for why that distinction turned out not to
|
|
719
|
+
* be buildable from this hook.
|
|
720
|
+
*/
|
|
721
|
+
declare const authAuditHook: (config: AuthAuditHookConfig) => ReturnType<typeof createAuthMiddleware>;
|
|
722
|
+
/**
|
|
723
|
+
* Merge the audit `hooks.after` middleware into a better-auth options object,
|
|
724
|
+
* composing with any `hooks.after` the caller already set (theirs runs first,
|
|
725
|
+
* then the audit record). Returns a new options object.
|
|
726
|
+
*/
|
|
727
|
+
declare const withAuthAudit: <Options extends {
|
|
728
|
+
hooks?: {
|
|
729
|
+
after?: unknown;
|
|
730
|
+
};
|
|
731
|
+
}>(options: Options, config: AuthAuditHookConfig) => Options;
|
|
732
|
+
/**
|
|
733
|
+
* The Durable Object state slice this class needs — structural so unit tests can
|
|
734
|
+
* pass a double without depending on the workers runtime.
|
|
735
|
+
*/
|
|
736
|
+
interface AuthDoState {
|
|
737
|
+
storage: DoStorageLike;
|
|
738
|
+
}
|
|
739
|
+
/** Path the worker calls to resolve a request's identity. Not part of `/api/auth/*`. */
|
|
740
|
+
declare const RESOLVE_SESSION_PATH = "/__lunora/auth/session";
|
|
741
|
+
/**
|
|
742
|
+
* Path the worker calls to read the audit log. Also not part of `/api/auth/*`.
|
|
743
|
+
*
|
|
744
|
+
* The audit table lives in this object like every other auth table, so the worker
|
|
745
|
+
* cannot query it directly — same constraint as the session route, same shared secret.
|
|
746
|
+
*/
|
|
747
|
+
declare const READ_AUDIT_PATH = "/__lunora/auth/audit";
|
|
748
|
+
/** Header carrying the shared secret that authenticates the calling worker. */
|
|
749
|
+
declare const INTERNAL_SECRET_HEADER = "x-lunora-auth-do-secret";
|
|
750
|
+
/**
|
|
751
|
+
* Options for the auth DO, beyond the better-auth options themselves.
|
|
752
|
+
*/
|
|
753
|
+
interface AuthDoOptions {
|
|
754
|
+
/**
|
|
755
|
+
* Shared secret authenticating the worker on {@link RESOLVE_SESSION_PATH}.
|
|
756
|
+
*
|
|
757
|
+
* The DO binding is reachable from any worker bound to the namespace, so the
|
|
758
|
+
* binding alone is not an authorization boundary — same reasoning as
|
|
759
|
+
* `SessionDO`'s `SESSION_DO_SECRET`. When this is unset the internal route is
|
|
760
|
+
* refused outright rather than served unauthenticated: a missing secret is a
|
|
761
|
+
* misconfiguration, and answering identity questions to anyone is the one
|
|
762
|
+
* failure mode worth being loud about. `/api/auth/*` is unaffected — those
|
|
763
|
+
* routes carry their own credentials.
|
|
764
|
+
*/
|
|
765
|
+
internalSecret?: string;
|
|
766
|
+
}
|
|
482
767
|
/**
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
|
|
768
|
+
* Base class for an app's auth Durable Object. Subclass it (or let codegen emit the
|
|
769
|
+
* subclass) and register the subclass in `wrangler.jsonc`.
|
|
770
|
+
*
|
|
771
|
+
* ```ts
|
|
772
|
+
* export class AuthDO extends LunoraAuthDO {
|
|
773
|
+
* public constructor(state: DurableObjectState, env: Env) {
|
|
774
|
+
* super(state, () => ({ secret: env.AUTH_SECRET, plugins: [scim({ … })] }), {
|
|
775
|
+
* internalSecret: env.AUTH_DO_SECRET,
|
|
776
|
+
* });
|
|
777
|
+
* }
|
|
778
|
+
* }
|
|
779
|
+
* ```
|
|
780
|
+
*/
|
|
781
|
+
declare class LunoraAuthDO {
|
|
782
|
+
#private;
|
|
783
|
+
/**
|
|
784
|
+
* @param state The Durable Object state — its `storage` becomes better-auth's database.
|
|
785
|
+
* @param optionsFactory Builds the better-auth options. Called once, lazily, on the first request.
|
|
786
|
+
* @param options Auth-DO specific options (base path, internal secret).
|
|
787
|
+
*/
|
|
788
|
+
constructor(state: AuthDoState, optionsFactory: () => LunoraAuthOptions, options?: AuthDoOptions);
|
|
789
|
+
/**
|
|
790
|
+
* Serve an auth request. Routes under `/api/auth` go to better-auth, and so do
|
|
791
|
+
* the OAuth discovery documents its options configure (`./discovery`); the
|
|
792
|
+
* internal session route is handled here; anything else is a 404.
|
|
793
|
+
*
|
|
794
|
+
* The base path is not configurable. The worker half only ever forwards
|
|
795
|
+
* `/api/auth/*` (`createDoAuthWiring`, which codegen calls with no base path),
|
|
796
|
+
* so a second knob here could only ever disagree with it.
|
|
797
|
+
*/
|
|
798
|
+
fetch(request: Request): Promise<Response>;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The exact `.well-known` paths this auth configuration serves outside its base
|
|
802
|
+
* path: the protected-resource metadata for each MCP resource, and the
|
|
803
|
+
* authorization-server metadata for the issuer. Empty unless an
|
|
804
|
+
* `oauthProvider()` or `mcp()` plugin is configured.
|
|
805
|
+
* @throws LunoraError `AUTH_MCP_RESOURCE_AMBIGUOUS` — see {@link mcpResourceOf}.
|
|
806
|
+
*/
|
|
807
|
+
declare const authDiscoveryPaths: (options: LunoraAuthOptions) => ReadonlyArray<string>;
|
|
808
|
+
/**
|
|
809
|
+
* Derive {@link authDiscoveryPaths} for an `.auth()` declaration, memoised on the
|
|
810
|
+
* declaration object. A framework-hosted worker rebuilds its options on every
|
|
811
|
+
* request, and calling `options(env)` each time would rebuild every plugin.
|
|
812
|
+
*/
|
|
813
|
+
declare const authDiscoveryPathsFor: <Env>(declaration: {
|
|
814
|
+
options: (env: Env) => LunoraAuthOptions;
|
|
815
|
+
}, env: Env) => ReadonlyArray<string>;
|
|
816
|
+
/**
|
|
817
|
+
* Serve an OAuth discovery document from `auth` when `request` asks for one of the
|
|
818
|
+
* paths {@link authDiscoveryPaths} derives from its options; `undefined` for
|
|
819
|
+
* anything else, so the caller's own 404 stands. A 404 from better-auth itself is
|
|
820
|
+
* also passed over as `undefined`.
|
|
821
|
+
*/
|
|
822
|
+
declare const handleAuthDiscoveryRequest: (auth: LunoraAuth, request: Request) => Promise<Response | undefined>;
|
|
823
|
+
/**
|
|
824
|
+
* The `CREATE TABLE` / `CREATE INDEX` statements for a better-auth config, in
|
|
825
|
+
* execution order (every table before any index).
|
|
826
|
+
*
|
|
827
|
+
* All statements are `IF NOT EXISTS`, so this is safe to run on every cold start —
|
|
828
|
+
* which is how `LunoraAuthDO` uses it. It creates; it never alters or drops, so a
|
|
829
|
+
* schema that has already diverged is left alone rather than half-migrated.
|
|
830
|
+
*
|
|
831
|
+
* Physical names throughout: `modelName` for tables, `fieldName ?? key` for columns,
|
|
832
|
+
* and better-auth's own resolved `columns` / `name` for indexes — so an index name
|
|
833
|
+
* here is the name better-auth's introspection expects to find.
|
|
834
|
+
* @param options The better-auth options the DO will run — the plugin list decides which tables exist.
|
|
835
|
+
* @returns SQL statements to execute in order.
|
|
836
|
+
*/
|
|
837
|
+
declare const authDoSchemaStatements: (options: LunoraAuthOptions) => string[];
|
|
838
|
+
/**
|
|
839
|
+
* `ALTER TABLE … ADD COLUMN` statements for columns the live schema is missing.
|
|
840
|
+
*
|
|
841
|
+
* ## Why this is needed at all
|
|
842
|
+
*
|
|
843
|
+
* {@link authDoSchemaStatements} is entirely `IF NOT EXISTS`, which makes it safe to
|
|
844
|
+
* re-run but blind to change: a *new table* appears on the next cold start, a *new
|
|
845
|
+
* column on an existing table* never does. That is not a hypothetical — enabling the
|
|
846
|
+
* `admin` plugin after first deploy adds `role` / `banned` / `banExpires` to `user`,
|
|
847
|
+
* and without this the object would keep serving a `user` table that cannot hold them.
|
|
848
|
+
*
|
|
849
|
+
* Additive only. Nothing here drops, renames, or retypes a column: a column that
|
|
850
|
+
* exists is left exactly as it is, so a schema someone has deliberately diverged is
|
|
851
|
+
* never "corrected" underneath them.
|
|
852
|
+
*
|
|
853
|
+
* ## What SQLite will not let us do
|
|
854
|
+
*
|
|
855
|
+
* `ADD COLUMN` cannot introduce a `NOT NULL` column without a default (existing rows
|
|
856
|
+
* would violate it immediately), and cannot introduce `UNIQUE`. So a required column
|
|
857
|
+
* with a static default is added with both; a required column *without* one is added
|
|
858
|
+
* **nullable**, which differs from what a fresh `CREATE TABLE` would produce. That is
|
|
859
|
+
* the deliberate trade: better-auth writes these fields on every insert, so nullable
|
|
860
|
+
* is harmless, whereas refusing to add the column at all would leave the object
|
|
861
|
+
* broken. Uniqueness is unaffected — better-auth expresses it as a separate index, and
|
|
862
|
+
* those are `IF NOT EXISTS`, so they are created by the statements above.
|
|
863
|
+
* @param options The better-auth options the DO runs, already resolved.
|
|
864
|
+
* @param existingColumns Physical column names currently present on a table; empty/absent for a table that does not exist yet (it will be created instead).
|
|
865
|
+
* @returns SQL statements to execute in order; empty when the live schema is current.
|
|
866
|
+
*/
|
|
867
|
+
declare const authDoColumnAdditions: (options: LunoraAuthOptions, existingColumns: (table: string) => Iterable<string>) => string[];
|
|
868
|
+
/** What one table looked like after a copy call. */
|
|
869
|
+
interface AuthMoveTableReport {
|
|
870
|
+
/** Rows that collided with a row the pinned object holds on its own, kept as they are (only under `force`). */
|
|
871
|
+
conflicts: number;
|
|
872
|
+
/** Rows this call inserted into the target. */
|
|
873
|
+
copied: number;
|
|
874
|
+
/** Rows this call deleted from the target because the source deleted them since they were copied. */
|
|
875
|
+
deleted: number;
|
|
876
|
+
/** Rows in the source object. */
|
|
877
|
+
sourceRows: number;
|
|
878
|
+
table: string;
|
|
879
|
+
/** Rows in the target object after this call. */
|
|
880
|
+
targetRows: number;
|
|
881
|
+
/** Rows this call read that the target already held exactly. */
|
|
882
|
+
unchanged: number;
|
|
883
|
+
/** Rows this call updated in the target because the source changed them since they were copied. */
|
|
884
|
+
updated: number;
|
|
885
|
+
}
|
|
886
|
+
/** The answer to one copy call. */
|
|
887
|
+
interface AuthMoveResult {
|
|
888
|
+
/** `true` once every table is copied and matches the source's fingerprint; otherwise call again. */
|
|
889
|
+
done: boolean;
|
|
890
|
+
tables: AuthMoveTableReport[];
|
|
891
|
+
}
|
|
892
|
+
/** The worker-side move between the un-pinned and the pinned auth object. */
|
|
893
|
+
interface AuthJurisdictionMove {
|
|
894
|
+
/**
|
|
895
|
+
* Copy every table from the un-pinned object into the pinned one. Refuses, unless
|
|
896
|
+
* `force` is set, a pinned object holding users the copy did not write, and a row
|
|
897
|
+
* that collides with one.
|
|
898
|
+
*/
|
|
899
|
+
copy: (options?: {
|
|
900
|
+
force?: boolean;
|
|
901
|
+
}) => Promise<AuthMoveResult>;
|
|
902
|
+
/**
|
|
903
|
+
* Drop every table in the un-pinned object. Refused unless every table was copied
|
|
904
|
+
* and still matches what was copied.
|
|
905
|
+
*/
|
|
906
|
+
purge: () => Promise<{
|
|
907
|
+
dropped: string[];
|
|
908
|
+
}>;
|
|
909
|
+
}
|
|
910
|
+
/** The slice of a Durable Object namespace this needs — structural, so tests need no runtime. */
|
|
911
|
+
interface AuthNamespaceLike {
|
|
912
|
+
get(this: void, id: unknown): {
|
|
913
|
+
fetch: (request: Request) => Promise<Response>;
|
|
914
|
+
};
|
|
915
|
+
idFromName(this: void, name: string): unknown;
|
|
916
|
+
/** Jurisdiction-restricted subnamespace. Optional only so older binding types still fit; {@link createDoAuthWiring} fails closed without it when a jurisdiction is set. */
|
|
917
|
+
jurisdiction?(this: void, jurisdiction: AuthJurisdiction): AuthNamespaceLike;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Cloudflare Durable Object data-residency jurisdiction. Widening union —
|
|
921
|
+
* Cloudflare adds values over time.
|
|
922
|
+
*/
|
|
923
|
+
type AuthJurisdiction = "eu" | "fedramp" | "us";
|
|
924
|
+
/** What {@link createDoAuthWiring} needs, already resolved against `env`. */
|
|
925
|
+
interface DoAuthWiringOptions {
|
|
926
|
+
/**
|
|
927
|
+
* The OAuth discovery paths the object serves outside `/api/auth` —
|
|
928
|
+
* `authDiscoveryPaths(options)` over the same auth options the object builds.
|
|
929
|
+
* Derived in the worker so a probe of any other path never costs a round-trip
|
|
930
|
+
* to the object. Empty or absent: nothing is forwarded.
|
|
931
|
+
*/
|
|
932
|
+
discoveryPaths?: ReadonlyArray<string>;
|
|
933
|
+
/**
|
|
934
|
+
* Shared secret presented on the object's internal session route. `undefined`
|
|
935
|
+
* means identity resolution fails closed — see {@link DoAuthWiring.resolveIdentity}.
|
|
936
|
+
*/
|
|
937
|
+
internalSecret: string | undefined;
|
|
938
|
+
/**
|
|
939
|
+
* Pin the auth object to a data-residency jurisdiction — pass the worker's
|
|
940
|
+
* `jurisdiction`. The object holds users, sessions, and credentials, so it must
|
|
941
|
+
* live where the rest of the app's data does. The same name maps to a different
|
|
942
|
+
* object per jurisdiction, so toggling this on an existing deployment starts a
|
|
943
|
+
* fresh, empty auth object.
|
|
944
|
+
*/
|
|
945
|
+
jurisdiction?: AuthJurisdiction;
|
|
946
|
+
/** The bound namespace, or `undefined` when the binding is absent from `env`. */
|
|
947
|
+
namespace: AuthNamespaceLike | undefined;
|
|
948
|
+
/**
|
|
949
|
+
* Name of the object instance holding the auth tables. Defaults to `"auth"`.
|
|
950
|
+
*
|
|
951
|
+
* One object owns the whole auth schema, so this exists to let an app pick the
|
|
952
|
+
* name (or run separate objects per deployment/tenant) rather than being pinned to
|
|
953
|
+
* a hardcoded one.
|
|
954
|
+
*/
|
|
955
|
+
objectName?: string;
|
|
956
|
+
}
|
|
957
|
+
/** The worker options DO-backed auth replaces. */
|
|
958
|
+
interface DoAuthWiring {
|
|
959
|
+
/**
|
|
960
|
+
* Reads the audit log out of the object, so the studio's audit feed works in DO
|
|
961
|
+
* mode. Answers an empty page rather than throwing when the object is unreachable
|
|
962
|
+
* or no secret is configured — an unavailable feed should read as empty, not 500
|
|
963
|
+
* the studio.
|
|
964
|
+
*/
|
|
965
|
+
auditReader: AuthAuditReader;
|
|
966
|
+
/**
|
|
967
|
+
* Forwards `/api/auth/*` to the object; `undefined` for anything else.
|
|
968
|
+
*
|
|
969
|
+
* The base path is fixed. It was configurable on both halves of DO-backed auth
|
|
970
|
+
* and settable on neither: codegen's `AuthDeclaration` has no field for it, so
|
|
971
|
+
* the emitted `createDoAuthWiring(...)` never passed one and an `AuthDO`
|
|
972
|
+
* subclass that set its own served nothing.
|
|
973
|
+
*/
|
|
974
|
+
authHandler: (request: Request) => Promise<Response | undefined>;
|
|
975
|
+
/**
|
|
976
|
+
* Forwards a `GET`/`HEAD` for one of {@link DoAuthWiringOptions.discoveryPaths}
|
|
977
|
+
* to the object; `undefined` for anything else, and for the object's 404. The
|
|
978
|
+
* worker calls it only after the app's own routes missed.
|
|
979
|
+
*/
|
|
980
|
+
discoveryHandler: (request: Request) => Promise<Response | undefined>;
|
|
981
|
+
/**
|
|
982
|
+
* Copy the auth tables from the un-pinned object into the pinned one, and later
|
|
983
|
+
* purge the un-pinned copy. Present only when a `jurisdiction` is set. Backs the
|
|
984
|
+
* worker's `copyAuthToJurisdiction` and `purgeUnpinnedAuth` admin ops.
|
|
985
|
+
*/
|
|
986
|
+
jurisdictionMove?: AuthJurisdictionMove;
|
|
987
|
+
/**
|
|
988
|
+
* Resolves a request's identity by asking the object. `null` when anonymous,
|
|
989
|
+
* unreachable, or ungated.
|
|
990
|
+
*
|
|
991
|
+
* `expiresAtMs` (epoch ms) is the session's expiry, which the runtime forwards as
|
|
992
|
+
* the socket's credential expiry so the DO can drop a subscriber whose session has
|
|
993
|
+
* lapsed; `role` is better-auth's `admin()` column, which RLS role grants read;
|
|
994
|
+
* `email` and `name` are the profile claims `ctx.auth.getIdentity()` is documented
|
|
995
|
+
* to carry. Each is absent when the session does not carry it.
|
|
996
|
+
*/
|
|
997
|
+
resolveIdentity: (request: Request) => Promise<null | {
|
|
998
|
+
email?: string;
|
|
999
|
+
expiresAtMs?: number;
|
|
1000
|
+
name?: string;
|
|
1001
|
+
role?: string;
|
|
1002
|
+
userId: string;
|
|
1003
|
+
}>;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Build the worker-side wiring for an auth Durable Object.
|
|
1007
|
+
*
|
|
1008
|
+
* Every failure path answers "not authenticated" rather than throwing: this runs on
|
|
1009
|
+
* the request path for every request that touches `ctx.auth`, and a throw there would
|
|
1010
|
+
* turn a misconfiguration into a 500 on traffic that has nothing to do with auth.
|
|
1011
|
+
* The exception is a `jurisdiction` the namespace cannot express, which throws here.
|
|
1012
|
+
* @param options The resolved namespace, secret, and names.
|
|
1013
|
+
* @returns The `authHandler` / `resolveIdentity` pair.
|
|
1014
|
+
*/
|
|
1015
|
+
declare const createDoAuthWiring: (options: DoAuthWiringOptions) => DoAuthWiring;
|
|
1016
|
+
/** better-auth's `databaseHooks` shape, derived so a rename upstream fails to compile rather than silently mis-hooking. */
|
|
1017
|
+
type DatabaseHooks = NonNullable<BetterAuthOptions["databaseHooks"]>;
|
|
1018
|
+
/** Config for the signup gate hooks: the base {@link EmailGateConfig} plus an optional classification tap. */
|
|
1019
|
+
interface EmailGateHookConfig extends EmailGateConfig {
|
|
1020
|
+
/**
|
|
1021
|
+
* Called with the resolved classification once the gate passes, so app policy
|
|
1022
|
+
* can react to `free` vs `business` at signup (e.g. tag the account). Never
|
|
1023
|
+
* fires when the gate rejects. `context` is better-auth's endpoint context
|
|
1024
|
+
* (`null` outside a request, e.g. an internal create).
|
|
1025
|
+
*/
|
|
1026
|
+
onClassify?: (classification: EmailClassification, user: Record<string, unknown>, context: unknown) => void;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Produce a `databaseHooks` fragment that gates better-auth's native signup on
|
|
1030
|
+
* the email-domain policy. Spread it into `createAuth({ databaseHooks: … })`, or
|
|
1031
|
+
* use {@link withEmailGate} to merge it (composing with any existing
|
|
1032
|
+
* `user.create.before`).
|
|
1033
|
+
*
|
|
1034
|
+
* ```ts
|
|
1035
|
+
* const auth = createAuth({
|
|
1036
|
+
* secret: env.AUTH_SECRET,
|
|
1037
|
+
* database: lunoraD1Adapter(env.DB),
|
|
1038
|
+
* databaseHooks: emailGateDatabaseHooks({ blockDisposable: true }),
|
|
1039
|
+
* });
|
|
1040
|
+
* ```
|
|
1041
|
+
*/
|
|
1042
|
+
declare const emailGateDatabaseHooks: (config?: EmailGateHookConfig) => DatabaseHooks;
|
|
1043
|
+
/**
|
|
1044
|
+
* Merge the email-domain signup gate into an existing better-auth options object,
|
|
1045
|
+
* preserving any `databaseHooks` the caller already set. If they already declared
|
|
1046
|
+
* a `user.create.before`, the gate runs first (rejecting disposable signups
|
|
1047
|
+
* before their hook sees them), then theirs runs on the (possibly rewritten) user.
|
|
1048
|
+
*
|
|
1049
|
+
* ```ts
|
|
1050
|
+
* const auth = createAuth(withEmailGate({ secret, database }, { blockDisposable: true }));
|
|
1051
|
+
* ```
|
|
1052
|
+
*/
|
|
1053
|
+
declare const withEmailGate: (options: BetterAuthOptions, config?: EmailGateHookConfig) => BetterAuthOptions;
|
|
1054
|
+
/**
|
|
1055
|
+
* Default basePath used by better-auth's client + handler. Override via the
|
|
1056
|
+
* second argument if you mount the auth routes somewhere else.
|
|
1057
|
+
*/
|
|
486
1058
|
declare const DEFAULT_AUTH_BASE_PATH: string;
|
|
487
1059
|
/**
|
|
488
|
-
* Route an inbound `Request` to better-auth if the path falls under
|
|
489
|
-
* `basePath`; otherwise return `
|
|
490
|
-
*
|
|
491
|
-
* Better-auth handles arbitrarily nested paths (`/api/auth/sign-in/email`,
|
|
492
|
-
* `/api/auth/callback/github`, …), so we use prefix matching instead of the
|
|
493
|
-
* exact-path map `createWorker` consumes for top-level routes.
|
|
494
|
-
*/
|
|
1060
|
+
* Route an inbound `Request` to better-auth if the path falls under
|
|
1061
|
+
* `basePath`; otherwise return `undefined` so the caller can continue dispatching.
|
|
1062
|
+
*
|
|
1063
|
+
* Better-auth handles arbitrarily nested paths (`/api/auth/sign-in/email`,
|
|
1064
|
+
* `/api/auth/callback/github`, …), so we use prefix matching instead of the
|
|
1065
|
+
* exact-path map `createWorker` consumes for top-level routes.
|
|
1066
|
+
*/
|
|
495
1067
|
declare const handleAuthRequest: (auth: LunoraAuth, request: Request, basePath?: string) => Promise<Response | undefined>;
|
|
496
1068
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* `
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1069
|
+
* The cleanup statements, in execution order: every blocking index, then the column.
|
|
1070
|
+
*
|
|
1071
|
+
* Exported for the pre-applied-schema path (`compileMigrationsSql` + `wrangler d1 execute`),
|
|
1072
|
+
* which compiles DDL without ever reading the database and so can neither tell whether the
|
|
1073
|
+
* column is present nor discover the index names — SQLite has no `DROP COLUMN IF EXISTS` to
|
|
1074
|
+
* make either unnecessary. Pass the names from
|
|
1075
|
+
* `SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'account'`, and
|
|
1076
|
+
* run this only against a database that still has the column.
|
|
1077
|
+
* @param accountTable Physical name of the account table (`account` unless renamed via `account.modelName`).
|
|
1078
|
+
* @param indexNames Indexes to drop first, from {@link indexesReferencingIssuer}.
|
|
1079
|
+
* @returns The `DROP INDEX` statements followed by the `DROP COLUMN`.
|
|
1080
|
+
*/
|
|
1081
|
+
declare const legacyIssuerCleanupStatements: (accountTable?: string, indexNames?: Iterable<string>) => string[];
|
|
1082
|
+
/**
|
|
1083
|
+
* Apply better-auth's required schema (`user`, `session`, `account`,
|
|
1084
|
+
* `verification`) to the configured database. Idempotent — better-auth
|
|
1085
|
+
* diffs the existing schema and only runs the missing DDL.
|
|
1086
|
+
*
|
|
1087
|
+
* Cached per `options` reference so the diff cost (one PRAGMA-style sweep
|
|
1088
|
+
* per table) doesn't fire on every request. In Cloudflare Workers the same
|
|
1089
|
+
* `env.DB` binding is reused across invocations within a single isolate, so
|
|
1090
|
+
* caching against the options object — which captures the binding — is
|
|
1091
|
+
* sufficient.
|
|
1092
|
+
*
|
|
1093
|
+
* For production you should prefer pre-applying the schema at deploy time
|
|
1094
|
+
* via `compileMigrationsSql` + `wrangler d1 execute`; this helper exists for
|
|
1095
|
+
* dev/playground and small deployments.
|
|
1096
|
+
*/
|
|
511
1097
|
declare const ensureMigrated: (auth: LunoraAuth | {
|
|
512
1098
|
options: LunoraAuthOptions;
|
|
513
1099
|
}) => Promise<void>;
|
|
514
1100
|
/**
|
|
515
|
-
* Compile better-auth's migrations to a single SQL string. Useful for
|
|
516
|
-
* `wrangler d1 execute --file -` in CI so the deploy step applies the schema
|
|
517
|
-
* before the first user request.
|
|
518
|
-
*
|
|
519
|
-
* Compiles from the SAME resolved options `createAuth` runs with (via
|
|
520
|
-
* `resolveAuthOptions`), not the raw caller options — so the schema includes the
|
|
521
|
-
* `rateLimit` table the worker's default-on durable limiter writes to. Compiling
|
|
522
|
-
* from the raw options would omit it, and the running worker would then write to
|
|
523
|
-
* a table the migration never created.
|
|
524
|
-
|
|
1101
|
+
* Compile better-auth's migrations to a single SQL string. Useful for
|
|
1102
|
+
* `wrangler d1 execute --file -` in CI so the deploy step applies the schema
|
|
1103
|
+
* before the first user request.
|
|
1104
|
+
*
|
|
1105
|
+
* Compiles from the SAME resolved options `createAuth` runs with (via
|
|
1106
|
+
* `resolveAuthOptions`), not the raw caller options — so the schema includes the
|
|
1107
|
+
* `rateLimit` table the worker's default-on durable limiter writes to. Compiling
|
|
1108
|
+
* from the raw options would omit it, and the running worker would then write to
|
|
1109
|
+
* a table the migration never created.
|
|
1110
|
+
*
|
|
1111
|
+
* **Upgrading a database created by an older `@lunora/auth`.** This compiles DDL without
|
|
1112
|
+
* reading the database, so it cannot know whether the `account.issuer` column better-auth
|
|
1113
|
+
* 1.7.0 required and 1.7.3 reverted is present — and SQLite has no `DROP COLUMN IF EXISTS`
|
|
1114
|
+
* to make that moot. `ensureMigrated` handles it automatically; on this path, run
|
|
1115
|
+
* `legacyIssuerCleanupStatements()` once against a database that still has the column.
|
|
1116
|
+
* Leaving it in place fails every sign-up with `NOT NULL constraint failed: account.issuer`.
|
|
1117
|
+
*/
|
|
525
1118
|
declare const compileMigrationsSql: (options: LunoraAuthOptions) => Promise<string>;
|
|
526
1119
|
/**
|
|
527
|
-
* Lunora-friendly view over better-auth's `session` option.
|
|
528
|
-
*
|
|
529
|
-
* This is a typed alias for better-auth's own `session` shape — Lunora stays a
|
|
530
|
-
* thin wrapper, so we don't reimplement the fields, we just give them a named,
|
|
531
|
-
* documented home so callers get autocomplete without reaching into
|
|
532
|
-
* `BetterAuthOptions`. The most relevant fields for session rotation / richer
|
|
533
|
-
* policies are `expiresIn` (absolute session lifetime, in seconds),
|
|
534
|
-
* `updateAge` (rolling-rotation interval, in seconds — how often an active
|
|
535
|
-
* session's expiry is pushed forward; `0` rotates on every use),
|
|
536
|
-
* `disableSessionRefresh` (turn rolling rotation off entirely so sessions
|
|
537
|
-
* expire at the absolute `expiresIn` regardless of activity), `freshAge`
|
|
538
|
-
* (freshness window, in seconds, for sensitive operations like account
|
|
539
|
-
* deletion; `0` treats every session as fresh — not recommended), and
|
|
540
|
-
* `cookieCache` (
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
|
|
1120
|
+
* Lunora-friendly view over better-auth's `session` option.
|
|
1121
|
+
*
|
|
1122
|
+
* This is a typed alias for better-auth's own `session` shape — Lunora stays a
|
|
1123
|
+
* thin wrapper, so we don't reimplement the fields, we just give them a named,
|
|
1124
|
+
* documented home so callers get autocomplete without reaching into
|
|
1125
|
+
* `BetterAuthOptions`. The most relevant fields for session rotation / richer
|
|
1126
|
+
* policies are `expiresIn` (absolute session lifetime, in seconds),
|
|
1127
|
+
* `updateAge` (rolling-rotation interval, in seconds — how often an active
|
|
1128
|
+
* session's expiry is pushed forward; `0` rotates on every use),
|
|
1129
|
+
* `disableSessionRefresh` (turn rolling rotation off entirely so sessions
|
|
1130
|
+
* expire at the absolute `expiresIn` regardless of activity), `freshAge`
|
|
1131
|
+
* (freshness window, in seconds, for sensitive operations like account
|
|
1132
|
+
* deletion; `0` treats every session as fresh — not recommended), and
|
|
1133
|
+
* `cookieCache` (signed-cookie session cache that skips DB reads —
|
|
1134
|
+
* `createAuth` fills `{ enabled: true, maxAge: 60 }` when you leave it unset, so
|
|
1135
|
+
* a revoked session keeps authenticating for up to that many seconds; pass
|
|
1136
|
+
* `{ enabled: false }` where that window is unacceptable).
|
|
1137
|
+
*
|
|
1138
|
+
* See better-auth's `session` option for the full field list.
|
|
1139
|
+
*/
|
|
544
1140
|
type SessionPolicy = NonNullable<BetterAuthOptions["session"]>;
|
|
545
1141
|
/**
|
|
546
|
-
* Validate a {@link SessionPolicy} and return it unchanged (pass-through).
|
|
547
|
-
*
|
|
548
|
-
* better-auth happily accepts the `session` block verbatim, so the only job
|
|
549
|
-
* here is to catch obviously-broken numeric inputs at construction time —
|
|
550
|
-
* negative or non-finite durations — rather than letting them produce
|
|
551
|
-
* surprising cookie expiries at runtime. Field names mirror better-auth's
|
|
552
|
-
* `session` option exactly so the validated object forwards 1:1.
|
|
553
|
-
*/
|
|
1142
|
+
* Validate a {@link SessionPolicy} and return it unchanged (pass-through).
|
|
1143
|
+
*
|
|
1144
|
+
* better-auth happily accepts the `session` block verbatim, so the only job
|
|
1145
|
+
* here is to catch obviously-broken numeric inputs at construction time —
|
|
1146
|
+
* negative or non-finite durations — rather than letting them produce
|
|
1147
|
+
* surprising cookie expiries at runtime. Field names mirror better-auth's
|
|
1148
|
+
* `session` option exactly so the validated object forwards 1:1.
|
|
1149
|
+
*/
|
|
554
1150
|
declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
|
|
555
1151
|
/**
|
|
556
|
-
* Ready-made {@link SessionPolicy} presets covering the common rotation /
|
|
557
|
-
* expiry trade-offs. Spread or override fields as needed:
|
|
558
|
-
*
|
|
559
|
-
* ```ts
|
|
560
|
-
* import { createAuth, sessionPresets } from "@lunora/auth";
|
|
561
|
-
*
|
|
562
|
-
* export const auth = createAuth({
|
|
563
|
-
* secret: env.AUTH_SECRET,
|
|
564
|
-
* database: env.DB,
|
|
565
|
-
* session: { ...sessionPresets.rolling, freshAge: 60 * 5 },
|
|
566
|
-
* });
|
|
567
|
-
* ```
|
|
568
|
-
*
|
|
569
|
-
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
|
|
570
|
-
* with a 60s signed-cookie session cache so bursts of authenticated calls
|
|
571
|
-
* skip the per-request DB session read.
|
|
572
|
-
* - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
|
|
573
|
-
* cookie cache **off** (fast revocation / short freshness is the whole point).
|
|
574
|
-
* - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
|
|
575
|
-
* with the same 60s cookie cache as `rolling`.
|
|
576
|
-
*/
|
|
1152
|
+
* Ready-made {@link SessionPolicy} presets covering the common rotation /
|
|
1153
|
+
* expiry trade-offs. Spread or override fields as needed:
|
|
1154
|
+
*
|
|
1155
|
+
* ```ts
|
|
1156
|
+
* import { createAuth, sessionPresets } from "@lunora/auth";
|
|
1157
|
+
*
|
|
1158
|
+
* export const auth = createAuth({
|
|
1159
|
+
* secret: env.AUTH_SECRET,
|
|
1160
|
+
* database: env.DB,
|
|
1161
|
+
* session: { ...sessionPresets.rolling, freshAge: 60 * 5 },
|
|
1162
|
+
* });
|
|
1163
|
+
* ```
|
|
1164
|
+
*
|
|
1165
|
+
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
|
|
1166
|
+
* with a 60s signed-cookie session cache so bursts of authenticated calls
|
|
1167
|
+
* skip the per-request DB session read.
|
|
1168
|
+
* - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
|
|
1169
|
+
* cookie cache **off** (fast revocation / short freshness is the whole point).
|
|
1170
|
+
* - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
|
|
1171
|
+
* with the same 60s cookie cache as `rolling`.
|
|
1172
|
+
*/
|
|
577
1173
|
declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
|
|
578
|
-
export { type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthCapabilities, type AuthInvitation, type AuthMember, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthTimestamp, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, type LunoraAuthOptions, type SessionPolicy, compileMigrationsSql, createAuthAdmin, ensureMigrated, handleAuthRequest, sessionPresets, validateSessionPolicy };
|
|
1174
|
+
export { READ_AUDIT_PATH as AUTH_DO_AUDIT_PATH, INTERNAL_SECRET_HEADER as AUTH_DO_SECRET_HEADER, RESOLVE_SESSION_PATH as AUTH_DO_SESSION_PATH, type AppendAuthAuditEntry, type AppendAuthAuditOptions, type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthAuditEvent, type AuthAuditHookConfig, type AuthAuditReader, type AuthCapabilities, type AuthConfigInfo, type AuthDoOptions, type AuthDoState, type AuthInvitation, type AuthJurisdiction, type AuthJurisdictionMove, type AuthMember, type AuthMoveResult, type AuthMoveTableReport, type AuthNamespaceLike, type AuthOrgRole, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthSignUpInvitation, type AuthTeam, type AuthTeamMember, type AuthTimestamp, type AuthUserFieldSpec, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type DoAuthWiring, type DoAuthWiringOptions, type EmailClassification, type EmailGateConfig, type EmailGateHookConfig, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, LunoraAuthDO, type LunoraAuthOptions, type SessionPolicy, type SqlExecutor, authAuditHook, authDiscoveryPaths, authDiscoveryPathsFor, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, compileMigrationsSql, createAuthAdmin, createDoAuthWiring, emailGateDatabaseHooks, ensureMigrated, eventForPath, handleAuthDiscoveryRequest, handleAuthRequest, legacyIssuerCleanupStatements, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
|