@lunora/auth 1.0.0-alpha.24 → 1.0.0-alpha.240

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.
Files changed (93) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +101 -11
  3. package/dist/adapter.d.mts +4 -43
  4. package/dist/adapter.d.ts +4 -43
  5. package/dist/adapter.mjs +1 -48
  6. package/dist/audit.d.mts +142 -0
  7. package/dist/audit.d.ts +142 -0
  8. package/dist/audit.mjs +12 -0
  9. package/dist/cimd-node.d.mts +1 -0
  10. package/dist/cimd-node.d.ts +1 -0
  11. package/dist/cimd-node.mjs +1 -0
  12. package/dist/cimd-workers.d.mts +64 -0
  13. package/dist/cimd-workers.d.ts +64 -0
  14. package/dist/cimd-workers.mjs +1 -0
  15. package/dist/email-guard.d.mts +143 -0
  16. package/dist/email-guard.d.ts +143 -0
  17. package/dist/email-guard.mjs +1 -0
  18. package/dist/index.d.mts +415 -545
  19. package/dist/index.d.ts +415 -545
  20. package/dist/index.mjs +1 -12
  21. package/dist/middleware.d.mts +165 -155
  22. package/dist/middleware.d.ts +165 -155
  23. package/dist/middleware.mjs +1 -56
  24. package/dist/migrate.d.mts +47 -0
  25. package/dist/migrate.d.ts +47 -0
  26. package/dist/migrate.mjs +1 -0
  27. package/dist/packem_shared/AUTH_DO_AUDIT_PATH-DNM3G5GI.mjs +1 -0
  28. package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs +1 -0
  29. package/dist/packem_shared/LunoraAuthAdminError-uhKG1MgQ.mjs +1 -0
  30. package/dist/packem_shared/adapter-CBZ17jKi.mjs +2 -0
  31. package/dist/packem_shared/adapter.d-CElAPJ1U.d.ts +88 -0
  32. package/dist/packem_shared/adapter.d-CaPwF2v3.d.mts +88 -0
  33. package/dist/packem_shared/auth-do-nKx0bUTe.mjs +1 -0
  34. package/dist/packem_shared/authAuditHook-DPTKdjBe.mjs +1 -0
  35. package/dist/packem_shared/authDiscoveryPaths-B14lM14u.mjs +1 -0
  36. package/dist/packem_shared/authDoColumnAdditions-UwF3Rl41.mjs +1 -0
  37. package/dist/packem_shared/create-auth.d-D1CKOFWc.d.mts +142 -0
  38. package/dist/packem_shared/create-auth.d-D1CKOFWc.d.ts +142 -0
  39. package/dist/packem_shared/createAuth-B-Pq7nmN.mjs +1 -0
  40. package/dist/packem_shared/createDoAuthWiring-az8FcbYw.mjs +1 -0
  41. package/dist/packem_shared/createLunoraAuthClient-DRcFNwp9.mjs +1 -0
  42. package/dist/packem_shared/createSignUpInvitation-DHZL9fNl.mjs +1 -0
  43. package/dist/packem_shared/emailGateDatabaseHooks-BYLUpiN_.mjs +1 -0
  44. package/dist/packem_shared/invite-only.d-CsPJWAwM.d.ts +126 -0
  45. package/dist/packem_shared/invite-only.d-DKyHHkkQ.d.mts +126 -0
  46. package/dist/packem_shared/isDevEnvironment-yLbPngwn.mjs +1 -0
  47. package/dist/packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs +1 -0
  48. package/dist/packem_shared/mcp-CgX1hTOv.mjs +1 -0
  49. package/dist/packem_shared/on-cloudflare-edge-Dx6gFjBO.mjs +1 -0
  50. package/dist/packem_shared/quote-identifier-CGiYFBvY.mjs +1 -0
  51. package/dist/packem_shared/sessionPresets-C867Mlo4.mjs +1 -0
  52. package/dist/packem_shared/sql-store-uMubPYI9.mjs +1 -0
  53. package/dist/packem_shared/uiConfig-B8pN6pks.mjs +1 -0
  54. package/dist/packem_shared/worker.d-B0mER8nz.d.mts +709 -0
  55. package/dist/packem_shared/worker.d-CGYer33D.d.ts +709 -0
  56. package/dist/plugins-client.d.mts +250 -1
  57. package/dist/plugins-client.d.ts +250 -1
  58. package/dist/plugins-client.mjs +1 -2
  59. package/dist/plugins-enterprise-client.d.mts +13 -0
  60. package/dist/plugins-enterprise-client.d.ts +13 -0
  61. package/dist/plugins-enterprise-client.mjs +1 -0
  62. package/dist/plugins-enterprise.d.mts +1668 -0
  63. package/dist/plugins-enterprise.d.ts +1668 -0
  64. package/dist/plugins-enterprise.mjs +1 -0
  65. package/dist/plugins.d.mts +167 -2
  66. package/dist/plugins.d.ts +167 -2
  67. package/dist/plugins.mjs +1 -22
  68. package/dist/schema.d.mts +39 -39
  69. package/dist/schema.d.ts +39 -39
  70. package/dist/schema.mjs +1 -62
  71. package/dist/sql-store.d.mts +43 -28
  72. package/dist/sql-store.d.ts +43 -28
  73. package/dist/sql-store.mjs +1 -184
  74. package/dist/store.d.mts +65 -47
  75. package/dist/store.d.ts +65 -47
  76. package/dist/store.mjs +1 -183
  77. package/dist/turnstile-middleware.d.mts +73 -55
  78. package/dist/turnstile-middleware.d.ts +73 -55
  79. package/dist/turnstile-middleware.mjs +1 -45
  80. package/dist/turnstile.d.mts +42 -59
  81. package/dist/turnstile.d.ts +42 -59
  82. package/dist/turnstile.mjs +1 -61
  83. package/dist/worker.d.mts +9 -0
  84. package/dist/worker.d.ts +9 -0
  85. package/dist/worker.mjs +1 -0
  86. package/package.json +57 -6
  87. package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
  88. package/dist/packem_shared/LunoraAuthAdminError-D4L7n6gN.mjs +0 -510
  89. package/dist/packem_shared/compileMigrationsSql-B9bj-mJv.mjs +0 -29
  90. package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.mts +0 -128
  91. package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.ts +0 -128
  92. package/dist/packem_shared/createAuth-BVMMllTm.mjs +0 -87
  93. package/dist/packem_shared/sessionPresets-Dwwd74_J.mjs +0 -38
package/dist/index.d.mts CHANGED
@@ -1,578 +1,448 @@
1
- export { lunoraAuthAdapter, lunoraD1Adapter } from "./adapter.mjs";
2
- import { LunoraError } from '@lunora/errors';
3
- import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.mjs";
4
- export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.mjs";
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";
3
+ export { type A as AuthAccount, type a as AuthAdmin, type b as AuthAdminSession, type c as AuthAdminUser, type d as AuthCapabilities, type e as AuthConfigInfo, type f as AuthInvitation, type g as AuthJurisdiction, type h as AuthJurisdictionMove, type i as AuthMember, type j as AuthMoveResult, type k as AuthMoveTableReport, type l as AuthNamespaceLike, type m as AuthOrgRole, type n as AuthOrganization, type o as AuthPage, type p as AuthPasskey, type q as AuthSignUpInvitation, type r as AuthTeam, type s as AuthTeamMember, type t as AuthTimestamp, type u as AuthUserFieldSpec, type C as CreateAuthAdminOptions, D as DEFAULT_AUTH_BASE_PATH, type v as DoAuthWiring, type w as DoAuthWiringOptions, type I as ImpersonationResult, type L as ListUsersOptions, x as LunoraAuthAdminError, y as authDiscoveryPaths, z as authDiscoveryPathsFor, B as createAuthAdmin, E as createDoAuthWiring, F as handleAuthDiscoveryRequest, G as handleAuthRequest } from "./packem_shared/worker.d-B0mER8nz.mjs";
4
+ import { AppendAuthAuditEntry, AppendAuthAuditOptions, AuthAuditEvent } from "./audit.mjs";
5
+ export { AUTH_AUDIT_TABLE, type AuthAuditEntry, type AuthAuditOutcome, type AuthAuditReader, type ReadAuthAuditOptions, appendAuthAuditEntry, createAuthAuditReader, ensureAuthAuditTable, readAuthAuditLog } from "./audit.mjs";
6
+ import { createAuthMiddleware } from 'better-auth/api';
7
+ import { SqlExecutor } from "./sql-store.mjs";
8
+ export { createSqlAuthStore, d1Executor } from "./sql-store.mjs";
9
+ import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-D1CKOFWc.mjs";
10
+ export { type L as LunoraAuth, c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-D1CKOFWc.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';
21
+ import '@lunora/errors';
13
22
  import '@lunora/server';
14
- /**
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
- */
18
- type AuthTimestamp = null | number;
19
- /**
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
- */
26
- interface AuthAdminUser {
27
- [key: string]: unknown;
28
- banExpires?: AuthTimestamp;
29
- banned?: boolean | null;
30
- banReason?: null | string;
31
- createdAt?: AuthTimestamp;
32
- email?: null | string;
33
- emailVerified?: boolean | null;
34
- id: string;
35
- image?: null | string;
36
- name?: null | string;
37
- role?: null | string;
38
- updatedAt?: AuthTimestamp;
23
+ /** Configuration for {@link authAuditHook}. */
24
+ interface AuthAuditHookConfig extends AppendAuthAuditOptions {
25
+ /**
26
+ * Where to persist the trail — the same {@link SqlExecutor} seam better-auth's
27
+ * store rides (`d1Executor(env.DB)`), so events land in the auth D1 database.
28
+ */
29
+ executor: SqlExecutor;
30
+ /**
31
+ * Optional export tap (pairs with SIEM forwarding): called with each redacted
32
+ * entry right after it is persisted, so a deployment can fan events out to an
33
+ * external sink. Rejections/throws are swallowed so forwarding can't break an
34
+ * auth request.
35
+ */
36
+ onRecord?: (entry: AppendAuthAuditEntry) => Promise<void> | void;
37
+ /**
38
+ * When true, `x-forwarded-for` is trusted as a client-IP fallback when
39
+ * `cf-connecting-ip` is absent — only enable this behind a proxy you control,
40
+ * or the recorded IP is attacker-chosen. Defaults to `false`: off Cloudflare,
41
+ * with no trusted proxy configured, the audit record's `ip` is omitted rather
42
+ * than populated from a spoofable header.
43
+ */
44
+ trustProxyHeaders?: boolean;
39
45
  }
40
46
  /**
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
- */
47
- interface AuthAdminSession {
48
- [key: string]: unknown;
49
- createdAt?: AuthTimestamp;
50
- expiresAt?: AuthTimestamp;
51
- id: string;
52
- impersonatedBy?: null | string;
53
- ipAddress?: null | string;
54
- userAgent?: null | string;
55
- userId: string;
56
- }
57
- /** One linked account (a credential or OAuth provider). All token material is stripped. */
58
- interface AuthAccount {
59
- [key: string]: unknown;
60
- accountId?: null | string;
61
- createdAt?: AuthTimestamp;
62
- id: string;
63
- providerId?: null | string;
64
- scope?: null | string;
65
- userId: string;
66
- }
67
- /** One organization row (from the `organization` plugin). */
68
- interface AuthOrganization {
69
- [key: string]: unknown;
70
- createdAt?: AuthTimestamp;
71
- id: string;
72
- name?: null | string;
73
- slug?: null | string;
74
- }
75
- /** One organization membership row. */
76
- interface AuthMember {
77
- [key: string]: unknown;
78
- createdAt?: AuthTimestamp;
79
- id: string;
80
- organizationId: string;
81
- role?: null | string;
82
- userId: string;
83
- }
84
- /** One pending organization invitation. */
85
- interface AuthInvitation {
86
- [key: string]: unknown;
87
- email?: null | string;
88
- expiresAt?: AuthTimestamp;
89
- id: string;
90
- organizationId: string;
91
- role?: null | string;
92
- status?: null | string;
93
- }
94
- /** One team row (from the `organization` plugin with `teams.enabled`). */
95
- interface AuthTeam {
96
- [key: string]: unknown;
97
- createdAt?: AuthTimestamp;
98
- id: string;
99
- name?: null | string;
100
- organizationId: string;
101
- }
102
- /** One team-membership row (teams). */
103
- interface AuthTeamMember {
104
- [key: string]: unknown;
105
- createdAt?: AuthTimestamp;
106
- id: string;
107
- teamId: string;
108
- userId: string;
47
+ * Structural view of the fields we read off better-auth's after-hook context —
48
+ * kept loose to avoid coupling to internal types.
49
+ *
50
+ * `body` is the parsed request body better-auth's middleware context exposes
51
+ * as `ctx.body` — pinned present and populated (e.g. `.email`) in the after-hook
52
+ * by `__tests__/audit-hooks.behaviour.test.ts` (plan 280 S0).
53
+ */
54
+ interface AuditHookContext {
55
+ body?: Record<string, unknown>;
56
+ context?: {
57
+ newSession?: {
58
+ session?: {
59
+ userId?: string;
60
+ };
61
+ user?: {
62
+ email?: string;
63
+ id?: string;
64
+ };
65
+ } | null;
66
+ returned?: unknown;
67
+ session?: {
68
+ session?: {
69
+ userId?: string;
70
+ };
71
+ user?: {
72
+ email?: string;
73
+ id?: string;
74
+ };
75
+ } | null;
76
+ };
77
+ headers?: Headers;
78
+ path?: string;
79
+ request?: Request;
109
80
  }
110
81
  /**
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
- */
115
- interface AuthOrgRole {
116
- [key: string]: unknown;
117
- createdAt?: AuthTimestamp;
118
- id: string;
119
- organizationId: string;
120
- permission?: null | string;
121
- role?: null | string;
122
- }
123
- /** One registered passkey. Credential secrets (`publicKey`) are stripped. */
124
- interface AuthPasskey {
125
- [key: string]: unknown;
126
- createdAt?: AuthTimestamp;
127
- deviceType?: null | string;
128
- id: string;
129
- name?: null | string;
130
- userId: string;
131
- }
132
- /** A page of rows plus the unpaginated total. */
133
- interface AuthPage<T> {
134
- rows: T[];
135
- total: number;
136
- }
82
+ * Map a better-auth endpoint path to the security event it represents, or
83
+ * `undefined` for endpoints not worth auditing (session reads, config, …). Match
84
+ * is by suffix so a caller `basePath` prefix (`/api/auth`) never affects it.
85
+ *
86
+ * Sign-in is split by what the endpoint actually DOES (plan 280 §4):
87
+ *
88
+ * - `/sign-in/social`, `/sign-in/magic-link` and `@better-auth/sso`'s
89
+ * `/sign-in/sso` only DISPATCH — the first mints a provider redirect URL, the
90
+ * second sends an email, the third answers `{ url, redirect: true }` pointing
91
+ * at the identity provider's authorization endpoint. Nobody is authenticated
92
+ * yet, so these are `sign-in-initiated`, not `sign-in`. Without the `sso`
93
+ * branch that path fell through to the `/sign-in/` substring below and every
94
+ * SSO redirect mint was recorded as a completed, successful `sign-in` with no
95
+ * actor — in exactly the flow where the "who authenticated" trail matters
96
+ * most.
97
+ * - `/callback/:id` (social + generic-oauth), `/magic-link/verify`,
98
+ * `@better-auth/sso`'s `/sso/saml2/sp/acs/:providerId`, and every
99
+ * `/two-factor/verify-*` (`verify-totp` / `verify-otp` / `verify-backup-code`
100
+ * — all three complete a challenged sign-in the same way) are where a
101
+ * session actually gets issued, so they join credential sign-ins
102
+ * (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`, …) as
103
+ * plain `sign-in`. They were NOT recorded at all before this change.
104
+ *
105
+ * The SAML assertion consumer service needs its own branch because it is the
106
+ * one completion that carries no `/callback/` segment: `processSAMLResponse`
107
+ * validates the assertion, resolves the user and calls `setSessionCookie`, all
108
+ * under `/sso/saml2/sp/acs/:providerId`. Matching neither that substring nor
109
+ * any other branch, it classified as `undefined` — the endpoint that issues
110
+ * every SAML session left no audit row at all.
111
+ *
112
+ * SAML sign-OUT has the mirror-image problem, and two endpoints rather than
113
+ * one. Neither ends in `/sign-out`, and both terminate the local session before
114
+ * redirecting, so both are `sign-out` rather than an initiated event:
115
+ *
116
+ * - `/sso/saml2/logout/:providerId` — SP-initiated. Deletes the SAML session
117
+ * keys, calls `deleteSession` on the current session token and
118
+ * `deleteSessionCookie`, then redirects to the IdP's logout URL.
119
+ * - `/sso/saml2/sp/slo/:providerId` — the SP's single-logout receiver, for the
120
+ * IdP-initiated direction and for the response leg of an SP-initiated one.
121
+ * Both `handleLogoutRequest` and `handleLogoutResponse` call `deleteSession`
122
+ * and `deleteSessionCookie` the same way.
123
+ *
124
+ * Recording the SAML sign-in while leaving these silent is worse than either
125
+ * gap alone: the trail would show a session opening and never closing, so a
126
+ * reader cannot tell "still signed in" from "we stopped watching".
127
+ *
128
+ * There is no dedicated `/oauth2/callback/*` branch because the generic
129
+ * `/callback/` check above already covers it, and covering it is CORRECT: an
130
+ * OAuth callback is a completed sign-in whatever path prefix it arrives on. The
131
+ * same substring also catches `@better-auth/sso`'s `/sso/callback/:providerId`,
132
+ * which is where an SSO sign-in actually completes — `sso` is re-exported from
133
+ * `@lunora/auth/plugins/enterprise`, so that path is live, not hypothetical.
134
+ *
135
+ * Checked against the installed `better-auth` and `@better-auth/*` dist for
136
+ * 1.7.3: generic-oauth registers NO endpoints of its own (there is no
137
+ * `/sign-in/oauth2`) — it registers providers used through the core
138
+ * `/sign-in/social` and `/callback/:id`, and the one dist hit for a literal
139
+ * `/oauth2/callback/` is inside `better-auth/plugins/oauth-popup`, which
140
+ * `plugins.ts` does not re-export — so today the branch fires for the core
141
+ * callback, and stays correct if the other becomes reachable.
142
+ */
143
+ declare const eventForPath: (path: string) => AuthAuditEvent | undefined;
137
144
  /**
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
- */
144
- interface AuthCapabilities {
145
- /** Linked-account browsing/unlinking — core (the `account` table always exists). */
146
- accounts: boolean;
147
- /** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
148
- admin: boolean;
149
- /** The `organization` plugin: orgs, members, invitations. */
150
- organization: boolean;
151
- /** The `@better-auth/passkey` plugin: per-user passkeys. */
152
- passkey: boolean;
153
- /** The `two-factor` plugin: per-user 2FA status / disable. */
154
- twoFactor: boolean;
155
- }
145
+ * Build the entry a given after-hook context should record, or `undefined` when
146
+ * the path is not an audited security event. Exported for direct unit testing of
147
+ * the classification/extraction without spinning up better-auth.
148
+ *
149
+ * Does NOT attempt to distinguish a 2FA-challenged credential sign-in from a
150
+ * fully successful one (a `sign-in-challenged` event, as an earlier design for
151
+ * this change proposed) — pinned in `__tests__/audit-hooks.behaviour.test.ts`
152
+ * (plan 280 S0): better-auth runs the APP's own `hooks.after` BEFORE the
153
+ * `twoFactor` plugin's own after-hook that rewrites the response to
154
+ * `{ twoFactorRedirect: true }` and nulls `ctx.context.newSession`. By the time
155
+ * THIS hook runs, `context.returned`/`context.newSession` still reflect the
156
+ * pre-interception, fully-successful sign-in — there is nothing here to detect
157
+ * the challenge from. Distinguishing it would need a different seam (e.g. a
158
+ * plugin-ordered-after-`twoFactor` hook, or reading `twoFactor`'s own
159
+ * database state) and is left to a follow-up.
160
+ */
161
+ declare const buildAuditEntry: (context: AuditHookContext, { now, trustProxyHeaders }?: {
162
+ now?: number;
163
+ trustProxyHeaders?: boolean;
164
+ }) => AppendAuthAuditEntry | undefined;
156
165
  /**
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
- */
163
- interface AuthUserFieldSpec {
164
- /** Logical field name (the key passed back in `createUser`'s `data`). */
165
- name: string;
166
- /** Best-effort plugin id the field originates from (`username`, `phone-number`, …); `undefined` for app `additionalFields`. */
167
- plugin?: string;
168
- required: boolean;
169
- /** Coarse input kind the studio maps to a control (checkbox / number / date / text). */
170
- type: "boolean" | "date" | "number" | "string";
171
- unique: boolean;
172
- }
166
+ * Create the better-auth `hooks.after` middleware that records the auth/security
167
+ * audit trail. Assign it to `hooks.after` (or compose via {@link withAuthAudit}).
168
+ *
169
+ * ```ts
170
+ * const auth = createAuth({
171
+ * secret: env.AUTH_SECRET,
172
+ * database: lunoraD1Adapter(env.DB),
173
+ * hooks: { after: authAuditHook({ executor: d1Executor(env.DB), retention: 100_000 }) },
174
+ * });
175
+ * ```
176
+ *
177
+ * ## Behaviour change (plan 280) — `onRecord`/SIEM consumers keyed on `event` strings, read this
178
+ *
179
+ * | Endpoint | Before | After |
180
+ * | -------------------------------------------------- | ------------------- | --------------------- |
181
+ * | `/sign-in/email` (and other credential sign-ins) | `sign-in` | `sign-in` (unchanged) |
182
+ * | `/sign-in/social` | `sign-in` | `sign-in-initiated` |
183
+ * | `/sign-in/magic-link` | `sign-in` | `sign-in-initiated` |
184
+ * | `/callback/:id` (social + generic-oauth) | _(not recorded)_ | `sign-in` |
185
+ * | `/magic-link/verify` | _(not recorded)_ | `sign-in` |
186
+ * | `/two-factor/verify-totp` / `-otp` / `-backup-code`| _(not recorded)_ | `sign-in` |
187
+ *
188
+ * A caller matching on `event === "sign-in"` now sees FEWER events for
189
+ * `/sign-in/social` and `/sign-in/magic-link` (they never actually authenticated
190
+ * anyone) and MORE events for the four previously-unrecorded completion
191
+ * endpoints — the net effect is a more truthful count, not a strictly larger or
192
+ * smaller one. `sign-in-initiated` is a new event name (open `AuthAuditEvent`
193
+ * union, so no wire/type break, but SIEM rules enumerating event names should
194
+ * add it). A failed sign-in now also carries `targetEmail` (the attempted
195
+ * address/username) when the request body supplied one — see
196
+ * {@link AppendAuthAuditEntry.targetEmail} — so credential-stuffing attempts can
197
+ * be grouped by target even though they never produce an `actorEmail`.
198
+ *
199
+ * NOT changed: `/sign-in/email` under an active 2FA challenge still records
200
+ * plain `sign-in` / `success` (not a distinct `sign-in-challenged` event) — see
201
+ * {@link buildAuditEntry}'s docblock for why that distinction turned out not to
202
+ * be buildable from this hook.
203
+ */
204
+ declare const authAuditHook: (config: AuthAuditHookConfig) => ReturnType<typeof createAuthMiddleware>;
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
- */
181
- interface AuthConfigInfo {
182
- /** The same capability booleans {@link AuthAdmin.capabilities} returns, embedded so a single call drives the whole panel. */
183
- capabilities: AuthCapabilities;
184
- /** Whether email + password sign-in is enabled. */
185
- emailAndPassword: boolean;
186
- /** Organization plugin sub-features. */
187
- organization: {
188
- enabled: boolean; /** Custom roles / dynamic access control (`organizationRole` table present). */
189
- roles: boolean;
190
- /** Teams (`team` table present). */
191
- teams: boolean;
192
- };
193
- /** Enabled better-auth plugin ids, sorted. */
194
- plugins: string[];
195
- /** Rate-limit policy (window is in seconds). */
196
- rateLimit: {
197
- enabled: boolean;
198
- max?: number;
199
- window?: number;
206
+ * Merge the audit `hooks.after` middleware into a better-auth options object,
207
+ * composing with any `hooks.after` the caller already set (theirs runs first,
208
+ * then the audit record). Returns a new options object.
209
+ */
210
+ declare const withAuthAudit: <Options extends {
211
+ hooks?: {
212
+ after?: unknown;
200
213
  };
201
- /** Session policy (all durations in seconds). */
202
- session: {
203
- cookieCache?: boolean;
204
- expiresIn?: number;
205
- freshAge?: number;
206
- updateAge?: number;
207
- };
208
- /** Configured social/OAuth provider ids, sorted. */
209
- socialProviders: string[];
210
- /** User-settable extra fields for the create-user form (plugin + app `additionalFields`). */
211
- userFields: AuthUserFieldSpec[];
212
- }
213
- /** A scalar value usable in an adapter `where` clause / filter. */
214
- type WhereValue = boolean | number | string;
215
- /** Filtering / paging options for {@link AuthAdmin.listUsers}. */
216
- interface ListUsersOptions {
217
- /** Column to filter on with `=` (defaults to `email` when `filterValue` is set). */
218
- filterField?: string;
219
- /** Exact-match filter value. */
220
- filterValue?: WhereValue;
221
- limit?: number;
222
- offset?: number;
223
- /** Substring search value (matched with `contains`). */
224
- search?: string;
225
- /** Column the `search` substring matches against (default `email`). */
226
- searchField?: string;
227
- /** Column to order by (default `createdAt`). */
228
- sortBy?: string;
229
- sortDirection?: "asc" | "desc";
230
- }
231
- /** The result of {@link AuthAdmin.impersonateUser}: a fresh session token to act as the target user. */
232
- interface ImpersonationResult {
233
- expiresAt: AuthTimestamp;
234
- /** Bearer session token for the impersonated user. The caller is responsible for using it (e.g. setting the cookie). */
235
- token: string;
236
- user: AuthAdminUser;
237
- }
214
+ }>(options: Options, config: AuthAuditHookConfig) => Options;
238
215
  /**
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
- */
247
- interface AuthAdmin {
248
- /** Directly add an existing user as an org member (server-side, no invitation/acceptance). */
249
- addMember: (input: {
250
- organizationId: string;
251
- role?: string;
252
- userId: string;
253
- }) => Promise<AuthMember>;
254
- /** Add a user to a team. */
255
- addTeamMember: (input: {
256
- teamId: string;
257
- userId: string;
258
- }) => Promise<AuthTeamMember>;
259
- banUser: (input: {
260
- expiresInSeconds?: number;
261
- reason?: string;
262
- userId: string;
263
- }) => Promise<AuthAdminUser>;
264
- cancelInvitation: (input: {
265
- invitationId: string;
266
- }) => Promise<void>;
267
- capabilities: () => Promise<AuthCapabilities>;
268
- /** Rich, read-only description of the auth configuration (plugins, fields, session policy, …). */
269
- config: () => Promise<AuthConfigInfo>;
270
- /** Create an organization; optionally seed an `owner` member for `ownerId`. */
271
- createOrganization: (input: {
272
- logo?: string;
273
- metadata?: Record<string, unknown>;
274
- name: string;
275
- ownerId?: string;
276
- slug?: string;
277
- }) => Promise<AuthOrganization>;
278
- /** Create a custom org role with a permission grant (a `resource → actions[]` map). */
279
- createOrgRole: (input: {
280
- organizationId: string;
281
- permission: Record<string, string[]>;
282
- role: string;
283
- }) => Promise<AuthOrgRole>;
284
- /** Create a team under an organization. */
285
- createTeam: (input: {
286
- name: string;
287
- organizationId: string;
288
- }) => Promise<AuthTeam>;
289
- createUser: (input: {
290
- data?: Record<string, unknown>;
291
- email: string;
292
- name: string;
293
- password?: string;
294
- role?: string | string[];
295
- }) => Promise<AuthAdminUser>;
296
- /** Delete an organization and cascade-delete its members, invitations, teams, and custom roles. */
297
- deleteOrganization: (input: {
298
- organizationId: string;
299
- }) => Promise<void>;
300
- /** Delete a custom org role. */
301
- deleteOrgRole: (input: {
302
- roleId: string;
303
- }) => Promise<void>;
304
- deletePasskey: (input: {
305
- passkeyId: string;
306
- }) => Promise<void>;
307
- disableTwoFactor: (input: {
308
- userId: string;
309
- }) => Promise<void>;
310
- impersonateUser: (input: {
311
- userId: string;
312
- }) => Promise<ImpersonationResult>;
313
- /** Create a pending email invitation to an org (no acceptance side effects). */
314
- inviteMember: (input: {
315
- email: string;
316
- inviterId?: string;
317
- organizationId: string;
318
- role?: string;
319
- }) => Promise<AuthInvitation>;
320
- listAccounts: (input: {
321
- userId: string;
322
- }) => Promise<AuthAccount[]>;
323
- listInvitations: (options: {
324
- limit?: number;
325
- offset?: number;
326
- organizationId: string;
327
- }) => Promise<AuthPage<AuthInvitation>>;
328
- listMembers: (options: {
329
- limit?: number;
330
- offset?: number;
331
- organizationId: string;
332
- }) => Promise<AuthPage<AuthMember>>;
333
- listOrganizations: (options: {
334
- limit?: number;
335
- offset?: number;
336
- }) => Promise<AuthPage<AuthOrganization>>;
337
- /** List an org's custom roles. */
338
- listOrgRoles: (options: {
339
- limit?: number;
340
- offset?: number;
341
- organizationId: string;
342
- }) => Promise<AuthPage<AuthOrgRole>>;
343
- listPasskeys: (input: {
344
- userId: string;
345
- }) => Promise<AuthPasskey[]>;
346
- listSessions: (options: {
347
- limit?: number;
348
- offset?: number;
349
- userId?: string;
350
- }) => Promise<AuthPage<AuthAdminSession>>;
351
- /** List a team's members. */
352
- listTeamMembers: (options: {
353
- limit?: number;
354
- offset?: number;
355
- teamId: string;
356
- }) => Promise<AuthPage<AuthTeamMember>>;
357
- /** List an org's teams. */
358
- listTeams: (options: {
359
- limit?: number;
360
- offset?: number;
361
- organizationId: string;
362
- }) => Promise<AuthPage<AuthTeam>>;
363
- listUsers: (options: ListUsersOptions) => Promise<AuthPage<AuthAdminUser>>;
364
- removeMember: (input: {
365
- memberId: string;
366
- }) => Promise<void>;
367
- /** Delete a team and its memberships. */
368
- removeTeam: (input: {
369
- teamId: string;
370
- }) => Promise<void>;
371
- /** Remove a member from a team. */
372
- removeTeamMember: (input: {
373
- teamMemberId: string;
374
- }) => Promise<void>;
375
- removeUser: (input: {
376
- userId: string;
377
- }) => Promise<void>;
378
- revokeUserSession: (input: {
379
- sessionId: string;
380
- }) => Promise<void>;
381
- revokeUserSessions: (input: {
382
- userId: string;
383
- }) => Promise<void>;
384
- setRole: (input: {
385
- role: string | string[];
386
- userId: string;
387
- }) => Promise<AuthAdminUser>;
388
- setUserPassword: (input: {
389
- newPassword: string;
390
- userId: string;
391
- }) => Promise<void>;
392
- unbanUser: (input: {
393
- userId: string;
394
- }) => Promise<AuthAdminUser>;
395
- unlinkAccount: (input: {
396
- accountId: string;
397
- userId: string;
398
- }) => Promise<void>;
399
- /** Change a member's role. */
400
- updateMemberRole: (input: {
401
- memberId: string;
402
- role: string | string[];
403
- }) => Promise<AuthMember>;
404
- /** Update an organization's name/slug/logo/metadata. */
405
- updateOrganization: (input: {
406
- logo?: string;
407
- metadata?: Record<string, unknown>;
408
- name?: string;
409
- organizationId: string;
410
- slug?: string;
411
- }) => Promise<AuthOrganization>;
412
- /** Replace a custom org role's permission grant. */
413
- updateOrgRole: (input: {
414
- permission: Record<string, string[]>;
415
- roleId: string;
416
- }) => Promise<AuthOrgRole>;
417
- /** Rename a team. */
418
- updateTeam: (input: {
419
- name: string;
420
- teamId: string;
421
- }) => Promise<AuthTeam>;
422
- updateUser: (input: {
423
- data: Record<string, unknown>;
424
- userId: string;
425
- }) => Promise<AuthAdminUser>;
216
+ * The Durable Object state slice this class needs — structural so unit tests can
217
+ * pass a double without depending on the workers runtime.
218
+ */
219
+ interface AuthDoState {
220
+ storage: DoStorageLike;
426
221
  }
427
- /** Options for {@link createAuthAdmin}. */
428
- interface CreateAuthAdminOptions {
222
+ /** Path the worker calls to resolve a request's identity. Not part of `/api/auth/*`. */
223
+ declare const RESOLVE_SESSION_PATH = "/__lunora/auth/session";
224
+ /**
225
+ * Path the worker calls to read the audit log. Also not part of `/api/auth/*`.
226
+ *
227
+ * The audit table lives in this object like every other auth table, so the worker
228
+ * cannot query it directly — same constraint as the session route, same shared secret.
229
+ */
230
+ declare const READ_AUDIT_PATH = "/__lunora/auth/audit";
231
+ /** Header carrying the shared secret that authenticates the calling worker. */
232
+ declare const INTERNAL_SECRET_HEADER = "x-lunora-auth-do-secret";
233
+ /**
234
+ * Options for the auth DO, beyond the better-auth options themselves.
235
+ */
236
+ interface AuthDoOptions {
429
237
  /**
430
- * Force individual {@link AuthCapabilities} on or off regardless of which
431
- * plugins are detected — e.g. `{ impersonate: false }`-style opt-outs by
432
- * setting `admin: false`, or hiding linked accounts with `accounts: false`.
433
- * A capability is reported only when both its plugin is enabled *and* its
434
- * override isn't `false`.
435
- */
436
- features?: Partial<AuthCapabilities>;
238
+ * Shared secret authenticating the worker on {@link RESOLVE_SESSION_PATH}.
239
+ *
240
+ * The DO binding is reachable from any worker bound to the namespace, so the
241
+ * binding alone is not an authorization boundary — same reasoning as
242
+ * `SessionDO`'s `SESSION_DO_SECRET`. When this is unset the internal route is
243
+ * refused outright rather than served unauthenticated: a missing secret is a
244
+ * misconfiguration, and answering identity questions to anyone is the one
245
+ * failure mode worth being loud about. `/api/auth/*` is unaffected — those
246
+ * routes carry their own credentials.
247
+ */
248
+ internalSecret?: string;
249
+ }
250
+ /**
251
+ * Base class for an app's auth Durable Object. Subclass it (or let codegen emit the
252
+ * subclass) and register the subclass in `wrangler.jsonc`.
253
+ *
254
+ * ```ts
255
+ * export class AuthDO extends LunoraAuthDO {
256
+ * public constructor(state: DurableObjectState, env: Env) {
257
+ * super(state, () => ({ secret: env.AUTH_SECRET, plugins: [scim({ … })] }), {
258
+ * internalSecret: env.AUTH_DO_SECRET,
259
+ * });
260
+ * }
261
+ * }
262
+ * ```
263
+ */
264
+ declare class LunoraAuthDO {
265
+ #private;
437
266
  /**
438
- * User id recorded as the impersonator on sessions minted by
439
- * {@link AuthAdmin.impersonateUser}. Defaults to the impersonated user's own
440
- * id (a self-reference) since the trusted admin plane has no acting user.
441
- */
442
- impersonatedBy?: string;
267
+ * @param state The Durable Object state — its `storage` becomes better-auth's database.
268
+ * @param optionsFactory Builds the better-auth options. Called once, lazily, on the first request.
269
+ * @param options Auth-DO specific options (base path, internal secret).
270
+ */
271
+ constructor(state: AuthDoState, optionsFactory: () => LunoraAuthOptions, options?: AuthDoOptions);
443
272
  /**
444
- * How long (in seconds) an impersonation session lives. Must be a positive
445
- * finite integer. Capped at 24 × {@link DEFAULT_IMPERSONATION_SECONDS}
446
- * (86 400 s / 24 h). Defaults to {@link DEFAULT_IMPERSONATION_SECONDS}
447
- * (3 600 s / 1 h).
448
- */
449
- impersonationSeconds?: number;
273
+ * Serve an auth request. Routes under `/api/auth` go to better-auth, and so do
274
+ * the OAuth discovery documents its options configure (`./discovery`); the
275
+ * internal session route is handled here; anything else is a 404.
276
+ *
277
+ * The base path is not configurable. The worker half only ever forwards
278
+ * `/api/auth/*` (`createDoAuthWiring`, which codegen calls with no base path),
279
+ * so a second knob here could only ever disagree with it.
280
+ */
281
+ fetch(request: Request): Promise<Response>;
450
282
  }
451
283
  /**
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
- */
457
- declare class LunoraAuthAdminError extends LunoraError {
458
- constructor(message: string, code: string);
459
- }
284
+ * The `CREATE TABLE` / `CREATE INDEX` statements for a better-auth config, in
285
+ * execution order (every table before any index).
286
+ *
287
+ * All statements are `IF NOT EXISTS`, so this is safe to run on every cold start —
288
+ * which is how `LunoraAuthDO` uses it. It creates; it never alters or drops, so a
289
+ * schema that has already diverged is left alone rather than half-migrated.
290
+ *
291
+ * Physical names throughout: `modelName` for tables, `fieldName ?? key` for columns,
292
+ * and better-auth's own resolved `columns` / `name` for indexes — so an index name
293
+ * here is the name better-auth's introspection expects to find.
294
+ * @param options The better-auth options the DO will run — the plugin list decides which tables exist.
295
+ * @returns SQL statements to execute in order.
296
+ */
297
+ declare const authDoSchemaStatements: (options: LunoraAuthOptions) => string[];
460
298
  /**
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
- */
481
- declare const createAuthAdmin: (auth: LunoraAuth, options?: CreateAuthAdminOptions) => AuthAdmin;
299
+ * `ALTER TABLE … ADD COLUMN` statements for columns the live schema is missing.
300
+ *
301
+ * ## Why this is needed at all
302
+ *
303
+ * {@link authDoSchemaStatements} is entirely `IF NOT EXISTS`, which makes it safe to
304
+ * re-run but blind to change: a *new table* appears on the next cold start, a *new
305
+ * column on an existing table* never does. That is not a hypothetical — enabling the
306
+ * `admin` plugin after first deploy adds `role` / `banned` / `banExpires` to `user`,
307
+ * and without this the object would keep serving a `user` table that cannot hold them.
308
+ *
309
+ * Additive only. Nothing here drops, renames, or retypes a column: a column that
310
+ * exists is left exactly as it is, so a schema someone has deliberately diverged is
311
+ * never "corrected" underneath them.
312
+ *
313
+ * ## What SQLite will not let us do
314
+ *
315
+ * `ADD COLUMN` cannot introduce a `NOT NULL` column without a default (existing rows
316
+ * would violate it immediately), and cannot introduce `UNIQUE`. So a required column
317
+ * with a static default is added with both; a required column *without* one is added
318
+ * **nullable**, which differs from what a fresh `CREATE TABLE` would produce. That is
319
+ * the deliberate trade: better-auth writes these fields on every insert, so nullable
320
+ * is harmless, whereas refusing to add the column at all would leave the object
321
+ * broken. Uniqueness is unaffected — better-auth expresses it as a separate index, and
322
+ * those are `IF NOT EXISTS`, so they are created by the statements above.
323
+ * @param options The better-auth options the DO runs, already resolved.
324
+ * @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).
325
+ * @returns SQL statements to execute in order; empty when the live schema is current.
326
+ */
327
+ declare const authDoColumnAdditions: (options: LunoraAuthOptions, existingColumns: (table: string) => Iterable<string>) => string[];
328
+ /** better-auth's `databaseHooks` shape, derived so a rename upstream fails to compile rather than silently mis-hooking. */
329
+ type DatabaseHooks = NonNullable<BetterAuthOptions["databaseHooks"]>;
330
+ /** Config for the signup gate hooks: the base {@link EmailGateConfig} plus an optional classification tap. */
331
+ interface EmailGateHookConfig extends EmailGateConfig {
332
+ /**
333
+ * Called with the resolved classification once the gate passes, so app policy
334
+ * can react to `free` vs `business` at signup (e.g. tag the account). Never
335
+ * fires when the gate rejects. `context` is better-auth's endpoint context
336
+ * (`null` outside a request, e.g. an internal create).
337
+ */
338
+ onClassify?: (classification: EmailClassification, user: Record<string, unknown>, context: unknown) => void;
339
+ }
482
340
  /**
483
- * Default basePath used by better-auth's client + handler. Override via the
484
- * second argument if you mount the auth routes somewhere else.
485
- */
486
- declare const DEFAULT_AUTH_BASE_PATH: string;
341
+ * Produce a `databaseHooks` fragment that gates better-auth's native signup on
342
+ * the email-domain policy. Spread it into `createAuth({ databaseHooks: … })`, or
343
+ * use {@link withEmailGate} to merge it (composing with any existing
344
+ * `user.create.before`).
345
+ *
346
+ * ```ts
347
+ * const auth = createAuth({
348
+ * secret: env.AUTH_SECRET,
349
+ * database: lunoraD1Adapter(env.DB),
350
+ * databaseHooks: emailGateDatabaseHooks({ blockDisposable: true }),
351
+ * });
352
+ * ```
353
+ */
354
+ declare const emailGateDatabaseHooks: (config?: EmailGateHookConfig) => DatabaseHooks;
487
355
  /**
488
- * Route an inbound `Request` to better-auth if the path falls under
489
- * `basePath`; otherwise return `null` so the caller can continue dispatching.
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
- */
495
- declare const handleAuthRequest: (auth: LunoraAuth, request: Request, basePath?: string) => Promise<Response | undefined>;
356
+ * Merge the email-domain signup gate into an existing better-auth options object,
357
+ * preserving any `databaseHooks` the caller already set. If they already declared
358
+ * a `user.create.before`, the gate runs first (rejecting disposable signups
359
+ * before their hook sees them), then theirs runs on the (possibly rewritten) user.
360
+ *
361
+ * ```ts
362
+ * const auth = createAuth(withEmailGate({ secret, database }, { blockDisposable: true }));
363
+ * ```
364
+ */
365
+ declare const withEmailGate: (options: BetterAuthOptions, config?: EmailGateHookConfig) => BetterAuthOptions;
496
366
  /**
497
- * Apply better-auth's required schema (`user`, `session`, `account`,
498
- * `verification`) to the configured database. Idempotent — better-auth
499
- * diffs the existing schema and only runs the missing DDL.
500
- *
501
- * Cached per `options` reference so the diff cost (one PRAGMA-style sweep
502
- * per table) doesn't fire on every request. In Cloudflare Workers the same
503
- * `env.DB` binding is reused across invocations within a single isolate, so
504
- * caching against the options object — which captures the binding — is
505
- * sufficient.
506
- *
507
- * For production you should prefer pre-applying the schema at deploy time
508
- * via `compileMigrationsSql` + `wrangler d1 execute`; this helper exists for
509
- * dev/playground and small deployments.
510
- */
511
- declare const ensureMigrated: (auth: LunoraAuth | {
512
- options: LunoraAuthOptions;
513
- }) => Promise<void>;
367
+ * Whether the Worker is running in development: at least one of `CF_ENV`,
368
+ * `ENVIRONMENT`, `NODE_ENV` or `WORKER_ENV` is set, and EVERY one that is set
369
+ * names a dev environment. A set-but-empty var counts as set, so it vetoes dev.
370
+ * `@lunora/mail`, `@lunora/queue` and `@lunora/shard-engine` still treat an empty
371
+ * var as unset; this copy is stricter on purpose.
372
+ *
373
+ * `every`, not `some`: a production deploy that also carries a leftover dev
374
+ * `NODE_ENV` must stay production, or it would log auth links (bearer
375
+ * credentials) and run migrations on cold start. A deploy that sets none of the
376
+ * vars is production too — fail closed either way.
377
+ */
378
+ declare const isDevEnvironment: (env: Record<string, unknown>) => boolean;
514
379
  /**
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
- */
525
- declare const compileMigrationsSql: (options: LunoraAuthOptions) => Promise<string>;
380
+ * The cleanup statements, in execution order: every blocking index, then the column.
381
+ *
382
+ * Exported for the pre-applied-schema path (`compileMigrationsSql` + `wrangler d1 execute`),
383
+ * which compiles DDL without ever reading the database and so can neither tell whether the
384
+ * column is present nor discover the index names — SQLite has no `DROP COLUMN IF EXISTS` to
385
+ * make either unnecessary. Pass the names from
386
+ * `SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'account'`, and
387
+ * run this only against a database that still has the column.
388
+ * @param accountTable Physical name of the account table (`account` unless renamed via `account.modelName`).
389
+ * @param indexNames Indexes to drop first, from {@link indexesReferencingIssuer}.
390
+ * @returns The `DROP INDEX` statements followed by the `DROP COLUMN`.
391
+ */
392
+ declare const legacyIssuerCleanupStatements: (accountTable?: string, indexNames?: Iterable<string>) => string[];
526
393
  /**
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` (opt-in signed-cookie session cache to skip DB reads).
541
- *
542
- * See better-auth's `session` option for the full field list.
543
- */
394
+ * Lunora-friendly view over better-auth's `session` option.
395
+ *
396
+ * This is a typed alias for better-auth's own `session` shape — Lunora stays a
397
+ * thin wrapper, so we don't reimplement the fields, we just give them a named,
398
+ * documented home so callers get autocomplete without reaching into
399
+ * `BetterAuthOptions`. The most relevant fields for session rotation / richer
400
+ * policies are `expiresIn` (absolute session lifetime, in seconds),
401
+ * `updateAge` (rolling-rotation interval, in seconds — how often an active
402
+ * session's expiry is pushed forward; `0` rotates on every use),
403
+ * `disableSessionRefresh` (turn rolling rotation off entirely so sessions
404
+ * expire at the absolute `expiresIn` regardless of activity), `freshAge`
405
+ * (freshness window, in seconds, for sensitive operations like account
406
+ * deletion; `0` treats every session as fresh — not recommended), and
407
+ * `cookieCache` (signed-cookie session cache that skips DB reads —
408
+ * `createAuth` fills `{ enabled: true, maxAge: 60 }` when you leave it unset, so
409
+ * a revoked session keeps authenticating for up to that many seconds; pass
410
+ * `{ enabled: false }` where that window is unacceptable).
411
+ *
412
+ * See better-auth's `session` option for the full field list.
413
+ */
544
414
  type SessionPolicy = NonNullable<BetterAuthOptions["session"]>;
545
415
  /**
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
- */
416
+ * Validate a {@link SessionPolicy} and return it unchanged (pass-through).
417
+ *
418
+ * better-auth happily accepts the `session` block verbatim, so the only job
419
+ * here is to catch obviously-broken numeric inputs at construction time —
420
+ * negative or non-finite durations — rather than letting them produce
421
+ * surprising cookie expiries at runtime. Field names mirror better-auth's
422
+ * `session` option exactly so the validated object forwards 1:1.
423
+ */
554
424
  declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
555
425
  /**
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
- */
426
+ * Ready-made {@link SessionPolicy} presets covering the common rotation /
427
+ * expiry trade-offs. Spread or override fields as needed:
428
+ *
429
+ * ```ts
430
+ * import { createAuth, sessionPresets } from "@lunora/auth";
431
+ *
432
+ * export const auth = createAuth({
433
+ * secret: env.AUTH_SECRET,
434
+ * database: env.DB,
435
+ * session: { ...sessionPresets.rolling, freshAge: 60 * 5 },
436
+ * });
437
+ * ```
438
+ *
439
+ * - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
440
+ * with a 60s signed-cookie session cache so bursts of authenticated calls
441
+ * skip the per-request DB session read.
442
+ * - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
443
+ * cookie cache **off** (fast revocation / short freshness is the whole point).
444
+ * - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
445
+ * with the same 60s cookie cache as `rolling`.
446
+ */
577
447
  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 };
448
+ 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 AuthAuditEvent, type AuthAuditHookConfig, type AuthDoOptions, type AuthDoState, type EmailClassification, type EmailGateConfig, type EmailGateHookConfig, LunoraAuthDO, type LunoraAuthOptions, type SessionPolicy, type SqlExecutor, authAuditHook, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, emailGateDatabaseHooks, eventForPath, isDevEnvironment, legacyIssuerCleanupStatements, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };