@lunora/auth 1.0.0-alpha.16 → 1.0.0-alpha.161

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 (76) hide show
  1. package/LICENSE.md +38 -0
  2. package/README.md +99 -10
  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/email-guard.d.mts +127 -0
  10. package/dist/email-guard.d.ts +127 -0
  11. package/dist/email-guard.mjs +1 -0
  12. package/dist/index.d.mts +838 -155
  13. package/dist/index.d.ts +838 -155
  14. package/dist/index.mjs +1 -12
  15. package/dist/middleware.d.mts +165 -155
  16. package/dist/middleware.d.ts +165 -155
  17. package/dist/middleware.mjs +1 -56
  18. package/dist/packem_shared/AUTH_DO_AUDIT_PATH-u9QgGb_v.mjs +1 -0
  19. package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs +1 -0
  20. package/dist/packem_shared/LunoraAuthAdminError-uhKG1MgQ.mjs +1 -0
  21. package/dist/packem_shared/adapter-CG0Yv85f.mjs +2 -0
  22. package/dist/packem_shared/adapter.d-BW_ovHul.d.mts +89 -0
  23. package/dist/packem_shared/adapter.d-BWw-tC_U.d.ts +89 -0
  24. package/dist/packem_shared/authAuditHook-DPTKdjBe.mjs +1 -0
  25. package/dist/packem_shared/authDoColumnAdditions-UwF3Rl41.mjs +1 -0
  26. package/dist/packem_shared/compileMigrationsSql-DKGrw7Lf.mjs +1 -0
  27. package/dist/packem_shared/create-auth.d-D1CKOFWc.d.mts +142 -0
  28. package/dist/packem_shared/create-auth.d-D1CKOFWc.d.ts +142 -0
  29. package/dist/packem_shared/createAuth-8aUw8v8D.mjs +1 -0
  30. package/dist/packem_shared/createDoAuthWiring-BekpJYhd.mjs +1 -0
  31. package/dist/packem_shared/createLunoraAuthClient-DhjhCwmM.mjs +1 -0
  32. package/dist/packem_shared/createSignUpInvitation-DHZL9fNl.mjs +1 -0
  33. package/dist/packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs +1 -0
  34. package/dist/packem_shared/invite-only.d-CsPJWAwM.d.ts +126 -0
  35. package/dist/packem_shared/invite-only.d-DKyHHkkQ.d.mts +126 -0
  36. package/dist/packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs +1 -0
  37. package/dist/packem_shared/on-cloudflare-edge-Dx6gFjBO.mjs +1 -0
  38. package/dist/packem_shared/quote-identifier-CGiYFBvY.mjs +1 -0
  39. package/dist/packem_shared/sessionPresets-C867Mlo4.mjs +1 -0
  40. package/dist/packem_shared/sql-store-CYa4HJsO.mjs +1 -0
  41. package/dist/packem_shared/uiConfig-B8pN6pks.mjs +1 -0
  42. package/dist/plugins-client.d.mts +165 -1
  43. package/dist/plugins-client.d.ts +165 -1
  44. package/dist/plugins-client.mjs +1 -2
  45. package/dist/plugins-enterprise-client.d.mts +15 -0
  46. package/dist/plugins-enterprise-client.d.ts +15 -0
  47. package/dist/plugins-enterprise-client.mjs +1 -0
  48. package/dist/plugins-enterprise.d.mts +1670 -0
  49. package/dist/plugins-enterprise.d.ts +1670 -0
  50. package/dist/plugins-enterprise.mjs +1 -0
  51. package/dist/plugins.d.mts +125 -2
  52. package/dist/plugins.d.ts +125 -2
  53. package/dist/plugins.mjs +1 -22
  54. package/dist/schema.d.mts +39 -39
  55. package/dist/schema.d.ts +39 -39
  56. package/dist/schema.mjs +1 -62
  57. package/dist/sql-store.d.mts +28 -28
  58. package/dist/sql-store.d.ts +28 -28
  59. package/dist/sql-store.mjs +1 -184
  60. package/dist/store.d.mts +65 -47
  61. package/dist/store.d.ts +65 -47
  62. package/dist/store.mjs +1 -183
  63. package/dist/turnstile-middleware.d.mts +73 -55
  64. package/dist/turnstile-middleware.d.ts +73 -55
  65. package/dist/turnstile-middleware.mjs +1 -45
  66. package/dist/turnstile.d.mts +42 -59
  67. package/dist/turnstile.d.ts +42 -59
  68. package/dist/turnstile.mjs +1 -61
  69. package/package.json +39 -6
  70. package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
  71. package/dist/packem_shared/LunoraAuthAdminError-BpVImgEy.mjs +0 -248
  72. package/dist/packem_shared/compileMigrationsSql-B9bj-mJv.mjs +0 -29
  73. package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.mts +0 -128
  74. package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.ts +0 -128
  75. package/dist/packem_shared/createAuth-BVMMllTm.mjs +0 -87
  76. package/dist/packem_shared/sessionPresets-Dwwd74_J.mjs +0 -38
package/dist/index.d.mts CHANGED
@@ -1,28 +1,36 @@
1
- export { lunoraAuthAdapter, lunoraD1Adapter } from "./adapter.mjs";
1
+ import { D as DoStorageLike } from "./packem_shared/adapter.d-BW_ovHul.mjs";
2
+ export { l as lunoraAuthAdapter, a as lunoraD1Adapter, b as lunoraDoAdapter } from "./packem_shared/adapter.d-BW_ovHul.mjs";
2
3
  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";
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,57 @@ 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
+ }
124
+ /** One team row (from the `organization` plugin with `teams.enabled`). */
125
+ interface AuthTeam {
126
+ [key: string]: unknown;
127
+ createdAt?: AuthTimestamp;
128
+ id: string;
129
+ name?: null | string;
130
+ organizationId: string;
131
+ }
132
+ /** One team-membership row (teams). */
133
+ interface AuthTeamMember {
134
+ [key: string]: unknown;
135
+ createdAt?: AuthTimestamp;
136
+ id: string;
137
+ teamId: string;
138
+ userId: string;
139
+ }
140
+ /**
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
+ */
145
+ interface AuthOrgRole {
146
+ [key: string]: unknown;
147
+ createdAt?: AuthTimestamp;
148
+ id: string;
149
+ organizationId: string;
150
+ permission?: null | string;
151
+ role?: null | string;
152
+ }
94
153
  /** One registered passkey. Credential secrets (`publicKey`) are stripped. */
95
154
  interface AuthPasskey {
96
155
  [key: string]: unknown;
@@ -106,17 +165,19 @@ interface AuthPage<T> {
106
165
  total: number;
107
166
  }
108
167
  /**
109
- * Which admin surfaces a given auth instance supports, derived from the enabled
110
- * better-auth plugins (and any {@link CreateAuthAdminOptions.features} overrides).
111
- * The studio calls {@link AuthAdmin.capabilities} once and renders only the
112
- * panels whose capability is `true` — so a deployment that doesn't enable, say,
113
- * the `organization` plugin never shows an Organizations section.
114
- */
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
+ */
115
174
  interface AuthCapabilities {
116
175
  /** Linked-account browsing/unlinking — core (the `account` table always exists). */
117
176
  accounts: boolean;
118
177
  /** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
119
178
  admin: boolean;
179
+ /** The `inviteOnly` plugin: sign-up invitations. */
180
+ inviteOnly: boolean;
120
181
  /** The `organization` plugin: orgs, members, invitations. */
121
182
  organization: boolean;
122
183
  /** The `@better-auth/passkey` plugin: per-user passkeys. */
@@ -124,6 +185,64 @@ interface AuthCapabilities {
124
185
  /** The `two-factor` plugin: per-user 2FA status / disable. */
125
186
  twoFactor: boolean;
126
187
  }
188
+ /**
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
+ */
195
+ interface AuthUserFieldSpec {
196
+ /** Logical field name (the key passed back in `createUser`'s `data`). */
197
+ name: string;
198
+ /** Best-effort plugin id the field originates from (`username`, `phone-number`, …); `undefined` for app `additionalFields`. */
199
+ plugin?: string;
200
+ required: boolean;
201
+ /** Coarse input kind the studio maps to a control (checkbox / number / date / text). */
202
+ type: "boolean" | "date" | "number" | "string";
203
+ unique: boolean;
204
+ }
205
+ /**
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
+ */
213
+ interface AuthConfigInfo {
214
+ /** The same capability booleans {@link AuthAdmin.capabilities} returns, embedded so a single call drives the whole panel. */
215
+ capabilities: AuthCapabilities;
216
+ /** Whether email + password sign-in is enabled. */
217
+ emailAndPassword: boolean;
218
+ /** Organization plugin sub-features. */
219
+ organization: {
220
+ enabled: boolean;
221
+ /** Custom roles / dynamic access control (`organizationRole` table present). */
222
+ roles: boolean;
223
+ /** Teams (`team` table present). */
224
+ teams: boolean;
225
+ };
226
+ /** Enabled better-auth plugin ids, sorted. */
227
+ plugins: string[];
228
+ /** Rate-limit policy (window is in seconds). */
229
+ rateLimit: {
230
+ enabled: boolean;
231
+ max?: number;
232
+ window?: number;
233
+ };
234
+ /** Session policy (all durations in seconds). */
235
+ session: {
236
+ cookieCache?: boolean;
237
+ expiresIn?: number;
238
+ freshAge?: number;
239
+ updateAge?: number;
240
+ };
241
+ /** Configured social/OAuth provider ids, sorted. */
242
+ socialProviders: string[];
243
+ /** User-settable extra fields for the create-user form (plugin + app `additionalFields`). */
244
+ userFields: AuthUserFieldSpec[];
245
+ }
127
246
  /** A scalar value usable in an adapter `where` clause / filter. */
128
247
  type WhereValue = boolean | number | string;
129
248
  /** Filtering / paging options for {@link AuthAdmin.listUsers}. */
@@ -150,15 +269,26 @@ interface ImpersonationResult {
150
269
  user: AuthAdminUser;
151
270
  }
152
271
  /**
153
- * The full read + write surface the studio's auth dashboard drives, backed by
154
- * better-auth's tables. Returned by {@link createAuthAdmin}. The runtime accepts
155
- * a structurally-compatible object as its `authAdmin` option and exposes each
156
- * method behind an admin-token-gated endpoint. Methods whose backing plugin is
157
- * absent still exist (they're not conditionally omitted) but the studio gates
158
- * them on {@link AuthAdmin.capabilities}; calling one for an unconfigured plugin
159
- * surfaces the underlying adapter error.
160
- */
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
+ */
161
280
  interface AuthAdmin {
281
+ /** Directly add an existing user as an org member (server-side, no invitation/acceptance). */
282
+ addMember: (input: {
283
+ organizationId: string;
284
+ role?: string;
285
+ userId: string;
286
+ }) => Promise<AuthMember>;
287
+ /** Add a user to a team. */
288
+ addTeamMember: (input: {
289
+ teamId: string;
290
+ userId: string;
291
+ }) => Promise<AuthTeamMember>;
162
292
  banUser: (input: {
163
293
  expiresInSeconds?: number;
164
294
  reason?: string;
@@ -168,6 +298,38 @@ interface AuthAdmin {
168
298
  invitationId: string;
169
299
  }) => Promise<void>;
170
300
  capabilities: () => Promise<AuthCapabilities>;
301
+ /** Rich, read-only description of the auth configuration (plugins, fields, session policy, …). */
302
+ config: () => Promise<AuthConfigInfo>;
303
+ /** Create an organization; optionally seed an `owner` member for `ownerId`. */
304
+ createOrganization: (input: {
305
+ logo?: string;
306
+ metadata?: Record<string, unknown>;
307
+ name: string;
308
+ ownerId?: string;
309
+ slug?: string;
310
+ }) => Promise<AuthOrganization>;
311
+ /** Create a custom org role with a permission grant (a `resource → actions[]` map). */
312
+ createOrgRole: (input: {
313
+ organizationId: string;
314
+ permission: Record<string, string[]>;
315
+ role: string;
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>;
328
+ /** Create a team under an organization. */
329
+ createTeam: (input: {
330
+ name: string;
331
+ organizationId: string;
332
+ }) => Promise<AuthTeam>;
171
333
  createUser: (input: {
172
334
  data?: Record<string, unknown>;
173
335
  email: string;
@@ -175,6 +337,14 @@ interface AuthAdmin {
175
337
  password?: string;
176
338
  role?: string | string[];
177
339
  }) => Promise<AuthAdminUser>;
340
+ /** Delete an organization and cascade-delete its members, invitations, teams, and custom roles. */
341
+ deleteOrganization: (input: {
342
+ organizationId: string;
343
+ }) => Promise<void>;
344
+ /** Delete a custom org role. */
345
+ deleteOrgRole: (input: {
346
+ roleId: string;
347
+ }) => Promise<void>;
178
348
  deletePasskey: (input: {
179
349
  passkeyId: string;
180
350
  }) => Promise<void>;
@@ -184,6 +354,13 @@ interface AuthAdmin {
184
354
  impersonateUser: (input: {
185
355
  userId: string;
186
356
  }) => Promise<ImpersonationResult>;
357
+ /** Create a pending email invitation to an org (no acceptance side effects). */
358
+ inviteMember: (input: {
359
+ email: string;
360
+ inviterId?: string;
361
+ organizationId: string;
362
+ role?: string;
363
+ }) => Promise<AuthInvitation>;
187
364
  listAccounts: (input: {
188
365
  userId: string;
189
366
  }) => Promise<AuthAccount[]>;
@@ -201,6 +378,12 @@ interface AuthAdmin {
201
378
  limit?: number;
202
379
  offset?: number;
203
380
  }) => Promise<AuthPage<AuthOrganization>>;
381
+ /** List an org's custom roles. */
382
+ listOrgRoles: (options: {
383
+ limit?: number;
384
+ offset?: number;
385
+ organizationId: string;
386
+ }) => Promise<AuthPage<AuthOrgRole>>;
204
387
  listPasskeys: (input: {
205
388
  userId: string;
206
389
  }) => Promise<AuthPasskey[]>;
@@ -209,13 +392,47 @@ interface AuthAdmin {
209
392
  offset?: number;
210
393
  userId?: string;
211
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>>;
405
+ /** List a team's members. */
406
+ listTeamMembers: (options: {
407
+ limit?: number;
408
+ offset?: number;
409
+ teamId: string;
410
+ }) => Promise<AuthPage<AuthTeamMember>>;
411
+ /** List an org's teams. */
412
+ listTeams: (options: {
413
+ limit?: number;
414
+ offset?: number;
415
+ organizationId: string;
416
+ }) => Promise<AuthPage<AuthTeam>>;
212
417
  listUsers: (options: ListUsersOptions) => Promise<AuthPage<AuthAdminUser>>;
213
418
  removeMember: (input: {
214
419
  memberId: string;
215
420
  }) => Promise<void>;
421
+ /** Delete a team and its memberships. */
422
+ removeTeam: (input: {
423
+ teamId: string;
424
+ }) => Promise<void>;
425
+ /** Remove a member from a team. */
426
+ removeTeamMember: (input: {
427
+ teamMemberId: string;
428
+ }) => Promise<void>;
216
429
  removeUser: (input: {
217
430
  userId: string;
218
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>;
219
436
  revokeUserSession: (input: {
220
437
  sessionId: string;
221
438
  }) => Promise<void>;
@@ -237,6 +454,29 @@ interface AuthAdmin {
237
454
  accountId: string;
238
455
  userId: string;
239
456
  }) => Promise<void>;
457
+ /** Change a member's role. */
458
+ updateMemberRole: (input: {
459
+ memberId: string;
460
+ role: string | string[];
461
+ }) => Promise<AuthMember>;
462
+ /** Update an organization's name/slug/logo/metadata. */
463
+ updateOrganization: (input: {
464
+ logo?: string;
465
+ metadata?: Record<string, unknown>;
466
+ name?: string;
467
+ organizationId: string;
468
+ slug?: string;
469
+ }) => Promise<AuthOrganization>;
470
+ /** Replace a custom org role's permission grant. */
471
+ updateOrgRole: (input: {
472
+ permission: Record<string, string[]>;
473
+ roleId: string;
474
+ }) => Promise<AuthOrgRole>;
475
+ /** Rename a team. */
476
+ updateTeam: (input: {
477
+ name: string;
478
+ teamId: string;
479
+ }) => Promise<AuthTeam>;
240
480
  updateUser: (input: {
241
481
  data: Record<string, unknown>;
242
482
  userId: string;
@@ -245,152 +485,595 @@ interface AuthAdmin {
245
485
  /** Options for {@link createAuthAdmin}. */
246
486
  interface CreateAuthAdminOptions {
247
487
  /**
248
- * Force individual {@link AuthCapabilities} on or off regardless of which
249
- * plugins are detected — e.g. `{ impersonate: false }`-style opt-outs by
250
- * setting `admin: false`, or hiding linked accounts with `accounts: false`.
251
- * A capability is reported only when both its plugin is enabled *and* its
252
- * override isn't `false`.
253
- */
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
+ */
254
494
  features?: Partial<AuthCapabilities>;
255
495
  /**
256
- * User id recorded as the impersonator on sessions minted by
257
- * {@link AuthAdmin.impersonateUser}. Defaults to the impersonated user's own
258
- * id (a self-reference) since the trusted admin plane has no acting user.
259
- */
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
+ */
260
500
  impersonatedBy?: string;
261
501
  /**
262
- * How long (in seconds) an impersonation session lives. Must be a positive
263
- * finite integer. Capped at 24 × {@link DEFAULT_IMPERSONATION_SECONDS}
264
- * (86 400 s / 24 h). Defaults to {@link DEFAULT_IMPERSONATION_SECONDS}
265
- * (3 600 s / 1 h).
266
- */
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
+ */
267
507
  impersonationSeconds?: number;
268
508
  }
269
509
  /**
270
- * A normalized failure from an admin operation. better-auth throws `APIError`s
271
- * carrying a `body.code` (e.g. `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`); we
272
- * surface that `code` so the runtime can map it onto an HTTP status and the
273
- * studio can show a meaningful message instead of a generic 500.
274
- */
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
+ */
275
515
  declare class LunoraAuthAdminError extends LunoraError {
276
516
  constructor(message: string, code: string);
277
517
  }
278
518
  /**
279
- * Build the studio's auth user-management plane on top of better-auth.
280
- *
281
- * Pass the result as the runtime's `authAdmin` option; the runtime exposes each
282
- * method behind an admin-token-gated `/_lunora/admin/auth/*` endpoint. The set
283
- * of usable surfaces is reported by {@link AuthAdmin.capabilities} — derived
284
- * from the enabled better-auth plugins, so enabling `admin()`, `organization()`,
285
- * `twoFactor()`, or the passkey plugin in the auth config is what lights up the
286
- * matching dashboard panels.
287
- *
288
- * **Trust model — important.** These operations talk to better-auth's
289
- * `internalAdapter` (and `adapter`/password hasher) **directly**, deliberately
290
- * bypassing the plugins' own endpoints, which require the caller to hold an
291
- * admin-role session. That session check is the wrong gate here: the runtime
292
- * already authorizes every call with `LUNORA_ADMIN_TOKEN`, so this helper acts
293
- * as a trusted server-side operator. It is therefore not an end-user-callable
294
- * API — never expose it on a path that isn't admin-token gated.
295
- *
296
- * `auth.$context` is a promise (better-auth resolves the adapter, password
297
- * config, etc. lazily); we memoize it so the first call pays the cost once.
298
- */
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
+ */
299
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";
300
741
  /**
301
- * Default basePath used by better-auth's client + handler. Override via the
302
- * second argument if you mount the auth routes somewhere else.
303
- */
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
+ * @experimental
753
+ */
754
+ interface AuthDoOptions {
755
+ /**
756
+ * Shared secret authenticating the worker on {@link RESOLVE_SESSION_PATH}.
757
+ *
758
+ * The DO binding is reachable from any worker bound to the namespace, so the
759
+ * binding alone is not an authorization boundary — same reasoning as
760
+ * `SessionDO`'s `SESSION_DO_SECRET`. When this is unset the internal route is
761
+ * refused outright rather than served unauthenticated: a missing secret is a
762
+ * misconfiguration, and answering identity questions to anyone is the one
763
+ * failure mode worth being loud about. `/api/auth/*` is unaffected — those
764
+ * routes carry their own credentials.
765
+ */
766
+ internalSecret?: string;
767
+ }
768
+ /**
769
+ * Base class for an app's auth Durable Object. Subclass it (or let codegen emit the
770
+ * subclass) and register the subclass in `wrangler.jsonc`.
771
+ *
772
+ * ```ts
773
+ * export class AuthDO extends LunoraAuthDO {
774
+ * public constructor(state: DurableObjectState, env: Env) {
775
+ * super(state, () => ({ secret: env.AUTH_SECRET, plugins: [scim({ … })] }), {
776
+ * internalSecret: env.AUTH_DO_SECRET,
777
+ * });
778
+ * }
779
+ * }
780
+ * ```
781
+ * @experimental
782
+ */
783
+ declare class LunoraAuthDO {
784
+ #private;
785
+ /**
786
+ * @param state The Durable Object state — its `storage` becomes better-auth's database.
787
+ * @param optionsFactory Builds the better-auth options. Called once, lazily, on the first request.
788
+ * @param options Auth-DO specific options (base path, internal secret).
789
+ */
790
+ constructor(state: AuthDoState, optionsFactory: () => LunoraAuthOptions, options?: AuthDoOptions);
791
+ /**
792
+ * Serve an auth request. Routes under `/api/auth` go to better-auth; the internal
793
+ * session route is handled here; anything else is a 404.
794
+ *
795
+ * The base path is not configurable. The worker half only ever forwards
796
+ * `/api/auth/*` (`createDoAuthWiring`, which codegen calls with no base path),
797
+ * so a second knob here could only ever disagree with it.
798
+ */
799
+ fetch(request: Request): Promise<Response>;
800
+ }
801
+ /**
802
+ * The `CREATE TABLE` / `CREATE INDEX` statements for a better-auth config, in
803
+ * execution order (every table before any index).
804
+ *
805
+ * All statements are `IF NOT EXISTS`, so this is safe to run on every cold start —
806
+ * which is how `LunoraAuthDO` uses it. It creates; it never alters or drops, so a
807
+ * schema that has already diverged is left alone rather than half-migrated.
808
+ *
809
+ * Physical names throughout: `modelName` for tables, `fieldName ?? key` for columns,
810
+ * and better-auth's own resolved `columns` / `name` for indexes — so an index name
811
+ * here is the name better-auth's introspection expects to find.
812
+ * @param options The better-auth options the DO will run — the plugin list decides which tables exist.
813
+ * @returns SQL statements to execute in order.
814
+ * @experimental
815
+ */
816
+ declare const authDoSchemaStatements: (options: LunoraAuthOptions) => string[];
817
+ /**
818
+ * `ALTER TABLE … ADD COLUMN` statements for columns the live schema is missing.
819
+ *
820
+ * ## Why this is needed at all
821
+ *
822
+ * {@link authDoSchemaStatements} is entirely `IF NOT EXISTS`, which makes it safe to
823
+ * re-run but blind to change: a *new table* appears on the next cold start, a *new
824
+ * column on an existing table* never does. That is not a hypothetical — enabling the
825
+ * `admin` plugin after first deploy adds `role` / `banned` / `banExpires` to `user`,
826
+ * and without this the object would keep serving a `user` table that cannot hold them.
827
+ *
828
+ * Additive only. Nothing here drops, renames, or retypes a column: a column that
829
+ * exists is left exactly as it is, so a schema someone has deliberately diverged is
830
+ * never "corrected" underneath them.
831
+ *
832
+ * ## What SQLite will not let us do
833
+ *
834
+ * `ADD COLUMN` cannot introduce a `NOT NULL` column without a default (existing rows
835
+ * would violate it immediately), and cannot introduce `UNIQUE`. So a required column
836
+ * with a static default is added with both; a required column *without* one is added
837
+ * **nullable**, which differs from what a fresh `CREATE TABLE` would produce. That is
838
+ * the deliberate trade: better-auth writes these fields on every insert, so nullable
839
+ * is harmless, whereas refusing to add the column at all would leave the object
840
+ * broken. Uniqueness is unaffected — better-auth expresses it as a separate index, and
841
+ * those are `IF NOT EXISTS`, so they are created by the statements above.
842
+ * @param options The better-auth options the DO runs, already resolved.
843
+ * @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).
844
+ * @returns SQL statements to execute in order; empty when the live schema is current.
845
+ * @experimental
846
+ */
847
+ declare const authDoColumnAdditions: (options: LunoraAuthOptions, existingColumns: (table: string) => Iterable<string>) => string[];
848
+ /** The slice of a Durable Object namespace this needs — structural, so tests need no runtime. */
849
+ interface AuthNamespaceLike {
850
+ get: (id: unknown) => {
851
+ fetch: (request: Request) => Promise<Response>;
852
+ };
853
+ idFromName: (name: string) => unknown;
854
+ }
855
+ /** What {@link createDoAuthWiring} needs, already resolved against `env`. */
856
+ interface DoAuthWiringOptions {
857
+ /**
858
+ * Shared secret presented on the object's internal session route. `undefined`
859
+ * means identity resolution fails closed — see {@link DoAuthWiring.resolveIdentity}.
860
+ */
861
+ internalSecret: string | undefined;
862
+ /** The bound namespace, or `undefined` when the binding is absent from `env`. */
863
+ namespace: AuthNamespaceLike | undefined;
864
+ /**
865
+ * Name of the object instance holding the auth tables. Defaults to `"auth"`.
866
+ *
867
+ * One object owns the whole auth schema, so this exists to let an app pick the
868
+ * name (or run separate objects per deployment/tenant) rather than being pinned to
869
+ * a hardcoded one.
870
+ */
871
+ objectName?: string;
872
+ }
873
+ /** The worker options DO-backed auth replaces. */
874
+ interface DoAuthWiring {
875
+ /**
876
+ * Reads the audit log out of the object, so the studio's audit feed works in DO
877
+ * mode. Answers an empty page rather than throwing when the object is unreachable
878
+ * or no secret is configured — an unavailable feed should read as empty, not 500
879
+ * the studio.
880
+ */
881
+ auditReader: AuthAuditReader;
882
+ /**
883
+ * Forwards `/api/auth/*` to the object; `undefined` for anything else.
884
+ *
885
+ * The base path is fixed. It was configurable on both halves of DO-backed auth
886
+ * and settable on neither: codegen's `AuthDeclaration` has no field for it, so
887
+ * the emitted `createDoAuthWiring(...)` never passed one and an `AuthDO`
888
+ * subclass that set its own served nothing.
889
+ */
890
+ authHandler: (request: Request) => Promise<Response | undefined>;
891
+ /**
892
+ * Resolves a request's identity by asking the object. `null` when anonymous,
893
+ * unreachable, or ungated.
894
+ *
895
+ * `expiresAtMs` (epoch ms) is the session's expiry, which the runtime forwards as
896
+ * the socket's credential expiry so the DO can drop a subscriber whose session has
897
+ * lapsed; `role` is better-auth's `admin()` column, which RLS role grants read;
898
+ * `email` and `name` are the profile claims `ctx.auth.getIdentity()` is documented
899
+ * to carry. Each is absent when the session does not carry it.
900
+ */
901
+ resolveIdentity: (request: Request) => Promise<null | {
902
+ email?: string;
903
+ expiresAtMs?: number;
904
+ name?: string;
905
+ role?: string;
906
+ userId: string;
907
+ }>;
908
+ }
909
+ /**
910
+ * Build the worker-side wiring for an auth Durable Object.
911
+ *
912
+ * Every failure path answers "not authenticated" rather than throwing: this runs on
913
+ * the request path for every request that touches `ctx.auth`, and a throw there would
914
+ * turn a misconfiguration into a 500 on traffic that has nothing to do with auth.
915
+ * @param options The resolved namespace, secret, and names.
916
+ * @returns The `authHandler` / `resolveIdentity` pair.
917
+ * @experimental
918
+ */
919
+ declare const createDoAuthWiring: (options: DoAuthWiringOptions) => DoAuthWiring;
920
+ /** better-auth's `databaseHooks` shape, derived so a rename upstream fails to compile rather than silently mis-hooking. */
921
+ type DatabaseHooks = NonNullable<BetterAuthOptions["databaseHooks"]>;
922
+ /** Config for the signup gate hooks: the base {@link EmailGateConfig} plus an optional classification tap. */
923
+ interface EmailGateHookConfig extends EmailGateConfig {
924
+ /**
925
+ * Called with the resolved classification once the gate passes, so app policy
926
+ * can react to `free` vs `business` at signup (e.g. tag the account). Never
927
+ * fires when the gate rejects. `context` is better-auth's endpoint context
928
+ * (`null` outside a request, e.g. an internal create).
929
+ */
930
+ onClassify?: (classification: EmailClassification, user: Record<string, unknown>, context: unknown) => void;
931
+ }
932
+ /**
933
+ * Produce a `databaseHooks` fragment that gates better-auth's native signup on
934
+ * the email-domain policy. Spread it into `createAuth({ databaseHooks: … })`, or
935
+ * use {@link withEmailGate} to merge it (composing with any existing
936
+ * `user.create.before`).
937
+ *
938
+ * ```ts
939
+ * const auth = createAuth({
940
+ * secret: env.AUTH_SECRET,
941
+ * database: lunoraD1Adapter(env.DB),
942
+ * databaseHooks: emailGateDatabaseHooks({ blockDisposable: true }),
943
+ * });
944
+ * ```
945
+ */
946
+ declare const emailGateDatabaseHooks: (config?: EmailGateHookConfig) => DatabaseHooks;
947
+ /**
948
+ * Merge the email-domain signup gate into an existing better-auth options object,
949
+ * preserving any `databaseHooks` the caller already set. If they already declared
950
+ * a `user.create.before`, the gate runs first (rejecting disposable signups
951
+ * before their hook sees them), then theirs runs on the (possibly rewritten) user.
952
+ *
953
+ * ```ts
954
+ * const auth = createAuth(withEmailGate({ secret, database }, { blockDisposable: true }));
955
+ * ```
956
+ */
957
+ declare const withEmailGate: (options: BetterAuthOptions, config?: EmailGateHookConfig) => BetterAuthOptions;
958
+ /**
959
+ * Default basePath used by better-auth's client + handler. Override via the
960
+ * second argument if you mount the auth routes somewhere else.
961
+ */
304
962
  declare const DEFAULT_AUTH_BASE_PATH: string;
305
963
  /**
306
- * Route an inbound `Request` to better-auth if the path falls under
307
- * `basePath`; otherwise return `null` so the caller can continue dispatching.
308
- *
309
- * Better-auth handles arbitrarily nested paths (`/api/auth/sign-in/email`,
310
- * `/api/auth/callback/github`, …), so we use prefix matching instead of the
311
- * exact-path map `createWorker` consumes for top-level routes.
312
- */
964
+ * Route an inbound `Request` to better-auth if the path falls under
965
+ * `basePath`; otherwise return `undefined` so the caller can continue dispatching.
966
+ *
967
+ * Better-auth handles arbitrarily nested paths (`/api/auth/sign-in/email`,
968
+ * `/api/auth/callback/github`, …), so we use prefix matching instead of the
969
+ * exact-path map `createWorker` consumes for top-level routes.
970
+ */
313
971
  declare const handleAuthRequest: (auth: LunoraAuth, request: Request, basePath?: string) => Promise<Response | undefined>;
314
972
  /**
315
- * Apply better-auth's required schema (`user`, `session`, `account`,
316
- * `verification`) to the configured database. Idempotent — better-auth
317
- * diffs the existing schema and only runs the missing DDL.
318
- *
319
- * Cached per `options` reference so the diff cost (one PRAGMA-style sweep
320
- * per table) doesn't fire on every request. In Cloudflare Workers the same
321
- * `env.DB` binding is reused across invocations within a single isolate, so
322
- * caching against the options object — which captures the binding — is
323
- * sufficient.
324
- *
325
- * For production you should prefer pre-applying the schema at deploy time
326
- * via `compileMigrationsSql` + `wrangler d1 execute`; this helper exists for
327
- * dev/playground and small deployments.
328
- */
973
+ * The cleanup statements, in execution order: every blocking index, then the column.
974
+ *
975
+ * Exported for the pre-applied-schema path (`compileMigrationsSql` + `wrangler d1 execute`),
976
+ * which compiles DDL without ever reading the database and so can neither tell whether the
977
+ * column is present nor discover the index names — SQLite has no `DROP COLUMN IF EXISTS` to
978
+ * make either unnecessary. Pass the names from
979
+ * `SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'account'`, and
980
+ * run this only against a database that still has the column.
981
+ * @param accountTable Physical name of the account table (`account` unless renamed via `account.modelName`).
982
+ * @param indexNames Indexes to drop first, from {@link indexesReferencingIssuer}.
983
+ * @returns The `DROP INDEX` statements followed by the `DROP COLUMN`.
984
+ * @experimental
985
+ */
986
+ declare const legacyIssuerCleanupStatements: (accountTable?: string, indexNames?: Iterable<string>) => string[];
987
+ /**
988
+ * Apply better-auth's required schema (`user`, `session`, `account`,
989
+ * `verification`) to the configured database. Idempotent — better-auth
990
+ * diffs the existing schema and only runs the missing DDL.
991
+ *
992
+ * Cached per `options` reference so the diff cost (one PRAGMA-style sweep
993
+ * per table) doesn't fire on every request. In Cloudflare Workers the same
994
+ * `env.DB` binding is reused across invocations within a single isolate, so
995
+ * caching against the options object — which captures the binding — is
996
+ * sufficient.
997
+ *
998
+ * For production you should prefer pre-applying the schema at deploy time
999
+ * via `compileMigrationsSql` + `wrangler d1 execute`; this helper exists for
1000
+ * dev/playground and small deployments.
1001
+ */
329
1002
  declare const ensureMigrated: (auth: LunoraAuth | {
330
1003
  options: LunoraAuthOptions;
331
1004
  }) => Promise<void>;
332
1005
  /**
333
- * Compile better-auth's migrations to a single SQL string. Useful for
334
- * `wrangler d1 execute --file -` in CI so the deploy step applies the schema
335
- * before the first user request.
336
- *
337
- * Compiles from the SAME resolved options `createAuth` runs with (via
338
- * `resolveAuthOptions`), not the raw caller options — so the schema includes the
339
- * `rateLimit` table the worker's default-on durable limiter writes to. Compiling
340
- * from the raw options would omit it, and the running worker would then write to
341
- * a table the migration never created.
342
- */
1006
+ * Compile better-auth's migrations to a single SQL string. Useful for
1007
+ * `wrangler d1 execute --file -` in CI so the deploy step applies the schema
1008
+ * before the first user request.
1009
+ *
1010
+ * Compiles from the SAME resolved options `createAuth` runs with (via
1011
+ * `resolveAuthOptions`), not the raw caller options — so the schema includes the
1012
+ * `rateLimit` table the worker's default-on durable limiter writes to. Compiling
1013
+ * from the raw options would omit it, and the running worker would then write to
1014
+ * a table the migration never created.
1015
+ *
1016
+ * **Upgrading a database created by an older `@lunora/auth`.** This compiles DDL without
1017
+ * reading the database, so it cannot know whether the `account.issuer` column better-auth
1018
+ * 1.7.0 required and 1.7.3 reverted is present — and SQLite has no `DROP COLUMN IF EXISTS`
1019
+ * to make that moot. `ensureMigrated` handles it automatically; on this path, run
1020
+ * `legacyIssuerCleanupStatements()` once against a database that still has the column.
1021
+ * Leaving it in place fails every sign-up with `NOT NULL constraint failed: account.issuer`.
1022
+ */
343
1023
  declare const compileMigrationsSql: (options: LunoraAuthOptions) => Promise<string>;
344
1024
  /**
345
- * Lunora-friendly view over better-auth's `session` option.
346
- *
347
- * This is a typed alias for better-auth's own `session` shape — Lunora stays a
348
- * thin wrapper, so we don't reimplement the fields, we just give them a named,
349
- * documented home so callers get autocomplete without reaching into
350
- * `BetterAuthOptions`. The most relevant fields for session rotation / richer
351
- * policies are `expiresIn` (absolute session lifetime, in seconds),
352
- * `updateAge` (rolling-rotation interval, in seconds — how often an active
353
- * session's expiry is pushed forward; `0` rotates on every use),
354
- * `disableSessionRefresh` (turn rolling rotation off entirely so sessions
355
- * expire at the absolute `expiresIn` regardless of activity), `freshAge`
356
- * (freshness window, in seconds, for sensitive operations like account
357
- * deletion; `0` treats every session as fresh — not recommended), and
358
- * `cookieCache` (opt-in signed-cookie session cache to skip DB reads).
359
- *
360
- * See better-auth's `session` option for the full field list.
361
- */
1025
+ * Lunora-friendly view over better-auth's `session` option.
1026
+ *
1027
+ * This is a typed alias for better-auth's own `session` shape — Lunora stays a
1028
+ * thin wrapper, so we don't reimplement the fields, we just give them a named,
1029
+ * documented home so callers get autocomplete without reaching into
1030
+ * `BetterAuthOptions`. The most relevant fields for session rotation / richer
1031
+ * policies are `expiresIn` (absolute session lifetime, in seconds),
1032
+ * `updateAge` (rolling-rotation interval, in seconds — how often an active
1033
+ * session's expiry is pushed forward; `0` rotates on every use),
1034
+ * `disableSessionRefresh` (turn rolling rotation off entirely so sessions
1035
+ * expire at the absolute `expiresIn` regardless of activity), `freshAge`
1036
+ * (freshness window, in seconds, for sensitive operations like account
1037
+ * deletion; `0` treats every session as fresh — not recommended), and
1038
+ * `cookieCache` (signed-cookie session cache that skips DB reads —
1039
+ * `createAuth` fills `{ enabled: true, maxAge: 60 }` when you leave it unset, so
1040
+ * a revoked session keeps authenticating for up to that many seconds; pass
1041
+ * `{ enabled: false }` where that window is unacceptable).
1042
+ *
1043
+ * See better-auth's `session` option for the full field list.
1044
+ */
362
1045
  type SessionPolicy = NonNullable<BetterAuthOptions["session"]>;
363
1046
  /**
364
- * Validate a {@link SessionPolicy} and return it unchanged (pass-through).
365
- *
366
- * better-auth happily accepts the `session` block verbatim, so the only job
367
- * here is to catch obviously-broken numeric inputs at construction time —
368
- * negative or non-finite durations — rather than letting them produce
369
- * surprising cookie expiries at runtime. Field names mirror better-auth's
370
- * `session` option exactly so the validated object forwards 1:1.
371
- */
1047
+ * Validate a {@link SessionPolicy} and return it unchanged (pass-through).
1048
+ *
1049
+ * better-auth happily accepts the `session` block verbatim, so the only job
1050
+ * here is to catch obviously-broken numeric inputs at construction time —
1051
+ * negative or non-finite durations — rather than letting them produce
1052
+ * surprising cookie expiries at runtime. Field names mirror better-auth's
1053
+ * `session` option exactly so the validated object forwards 1:1.
1054
+ */
372
1055
  declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
373
1056
  /**
374
- * Ready-made {@link SessionPolicy} presets covering the common rotation /
375
- * expiry trade-offs. Spread or override fields as needed:
376
- *
377
- * ```ts
378
- * import { createAuth, sessionPresets } from "@lunora/auth";
379
- *
380
- * export const auth = createAuth({
381
- * secret: env.AUTH_SECRET,
382
- * database: env.DB,
383
- * session: { ...sessionPresets.rolling, freshAge: 60 * 5 },
384
- * });
385
- * ```
386
- *
387
- * - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
388
- * with a 60s signed-cookie session cache so bursts of authenticated calls
389
- * skip the per-request DB session read.
390
- * - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
391
- * cookie cache **off** (fast revocation / short freshness is the whole point).
392
- * - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
393
- * with the same 60s cookie cache as `rolling`.
394
- */
1057
+ * Ready-made {@link SessionPolicy} presets covering the common rotation /
1058
+ * expiry trade-offs. Spread or override fields as needed:
1059
+ *
1060
+ * ```ts
1061
+ * import { createAuth, sessionPresets } from "@lunora/auth";
1062
+ *
1063
+ * export const auth = createAuth({
1064
+ * secret: env.AUTH_SECRET,
1065
+ * database: env.DB,
1066
+ * session: { ...sessionPresets.rolling, freshAge: 60 * 5 },
1067
+ * });
1068
+ * ```
1069
+ *
1070
+ * - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
1071
+ * with a 60s signed-cookie session cache so bursts of authenticated calls
1072
+ * skip the per-request DB session read.
1073
+ * - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
1074
+ * cookie cache **off** (fast revocation / short freshness is the whole point).
1075
+ * - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
1076
+ * with the same 60s cookie cache as `rolling`.
1077
+ */
395
1078
  declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
396
- 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 };
1079
+ 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 AuthMember, 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, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, compileMigrationsSql, createAuthAdmin, createDoAuthWiring, emailGateDatabaseHooks, ensureMigrated, eventForPath, handleAuthRequest, legacyIssuerCleanupStatements, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };