@lunora/auth 1.0.0-alpha.115 → 1.0.0-alpha.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,6 +114,35 @@ A blocked signup fails with the coded error `EMAIL_DOMAIN_BLOCKED` (HTTP 400); a
114
114
  - **Programmatic / non-auth use:** `classifyEmail(email, config)` (sync, pure-data) and `assertEmailAllowed(email, config)` (async; throws the coded error) come from `@lunora/auth/email-guard`, plus `emailGateMiddleware({ email: (ctx) => ctx.args.email })` for a `.use()` gate on your own signup mutations.
115
115
  - **Edge-safety:** on workerd, `await loadEmailDomainLists()` once at worker init (the gate helpers do this for you). The optional `mx: true` deliverability check is loaded via a dynamic import so `node:dns` never enters the default bundle — enable it only with `nodejs_compat` (or a DNS-over-HTTPS shim).
116
116
 
117
+ ### Invite-only sign-up
118
+
119
+ Close self-serve registration: `inviteOnly()` creates an account only for an address an administrator invited. It hooks `user.create.before`, so it gates every path that mints a user — password sign-up, an OAuth callback creating a new account, magic link, `admin.createUser`, and Lunora's own `AuthAdmin.createUser` — and declares the `signUpInvitation` table it reads, so migrations pick it up on their own.
120
+
121
+ ```ts
122
+ import { createAuth, createSignUpInvitation } from "@lunora/auth";
123
+ import { inviteOnly } from "@lunora/auth/plugins";
124
+
125
+ const auth = createAuth({
126
+ secret: env.AUTH_SECRET,
127
+ database: lunoraD1Adapter(env.DB),
128
+ emailAndPassword: { enabled: true, requireEmailVerification: true },
129
+ plugins: [inviteOnly()],
130
+ });
131
+
132
+ // …from your own admin-authorized code:
133
+ const invite = await createSignUpInvitation(auth, { email: "ada@example.com" });
134
+ const link = `https://app.example/sign-up?email=${encodeURIComponent(invite.email)}&invite=${invite.token}`;
135
+ ```
136
+
137
+ An uninvited signup fails with the coded error `SIGN_UP_INVITE_REQUIRED` (HTTP **400** — a 403 from a create hook is swallowed by better-auth's sign-up route and answered with a fabricated success). Leave `emailAndPassword.disableSignUp` **off** — the invitee still uses the ordinary sign-up form, which `@lunora/auth-ui` prefills from `?email=`. `listSignUpInvitations` and `revokeSignUpInvitation` complete the set; all three are trusted server-side calls with no authorization of their own, so gate them like any other admin action.
138
+
139
+ - **An invitation carries a secret token** — 256 CSPRNG bits, returned in the clear exactly once and stored only as a SHA-256. It is checked on `/sign-up/email` and nowhere else, because every other account-minting path (OAuth callback, magic link, email OTP) has already proved the person controls the address. So there are two layers: the database gate requires an unspent invitation whatever created the row, and the route hook additionally requires the token. Missing token, wrong token, expired invitation and never-invited address all answer with one message, so the form is not a directory oracle.
140
+ - **The link is a bearer credential.** Whoever holds it takes the seat — send it to the invitee, not a shared inbox, and reissue rather than resend if unsure. `requireEmailVerification` still matters: the user row is written before the verification mail goes out, so verification is what keeps a spent invitation from becoming a usable session. The plugin warns on startup when password sign-up runs without it.
141
+ - **Plugins that synthesize an address are refused, not admitted.** `anonymous` (`temp-<id>@…`), `siwe` (`<wallet>@<domain>`), and `phoneNumber`'s sign-up-on-verification all create users with a generated email that matches no invitation, so those flows are rejected — but only once a user exists. Under `allowFirstUser: true` the bootstrap runs before the address is compared, so the first anonymous session or wallet sign-in is what claims it. Don't combine them with this.
142
+ - **Nobody signs up before the first invitation exists, including you.** Seed it with `createSignUpInvitation` at worker init or from a one-off internal mutation. `inviteOnly({ allowFirstUser: true })` instead admits the first account uninvited — convenient, but the "is the user table empty" check is racy and open to whoever finds the URL first.
143
+ - **The studio's Users page grows a Sign-up invitations section** when the plugin is installed — invite, see pending/accepted/expired, revoke. The same three ops are on `AuthAdmin` and the client (`listAuthSignUpInvitations`, `createAuthSignUpInvitation`, `revokeAuthSignUpInvitation`), with `useSignUpInvitations()` in `@lunora/react`. Nothing prunes the table; `pruneSignUpInvitations(auth)` deletes the invitations that expired unused and returns the count.
144
+ - **Revocation is not retroactive, and not atomic against a sign-up in flight.** better-auth does not wrap the `before` hook and the user insert in one transaction, and the adapter contract offers no conditional consume, so a revoke landing between the two lets that one account through. `AuthAdmin.removeUser` is how you undo one that already happened.
145
+
117
146
  ### Security / audit trail
118
147
 
119
148
  Record authentication & security events (sign-in, sign-up, password change, MFA enable/disable, token refresh, session revoke, …) to a durable, queryable audit trail. Install the better-auth `hooks.after` recorder with `authAuditHook` (or compose via `withAuthAudit`), backed by the same D1 database as the auth tables:
package/dist/index.d.mts CHANGED
@@ -11,6 +11,7 @@ export { createSqlAuthStore, d1Executor } from "./sql-store.mjs";
11
11
  import { BetterAuthOptions } from 'better-auth';
12
12
  import { EmailClassification, EmailGateConfig } from "./email-guard.mjs";
13
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-DsVdKwcp.mjs";
14
15
  export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.mjs";
15
16
  export { default as authTables } from "./schema.mjs";
16
17
  export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.mjs";
@@ -98,6 +99,28 @@ interface AuthInvitation {
98
99
  role?: null | string;
99
100
  status?: null | string;
100
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
+ }
101
124
  /** One team row (from the `organization` plugin with `teams.enabled`). */
102
125
  interface AuthTeam {
103
126
  [key: string]: unknown;
@@ -153,6 +176,8 @@ interface AuthCapabilities {
153
176
  accounts: boolean;
154
177
  /** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
155
178
  admin: boolean;
179
+ /** The `inviteOnly` plugin: sign-up invitations. */
180
+ inviteOnly: boolean;
156
181
  /** The `organization` plugin: orgs, members, invitations. */
157
182
  organization: boolean;
158
183
  /** The `@better-auth/passkey` plugin: per-user passkeys. */
@@ -289,6 +314,17 @@ interface AuthAdmin {
289
314
  permission: Record<string, string[]>;
290
315
  role: string;
291
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>;
292
328
  /** Create a team under an organization. */
293
329
  createTeam: (input: {
294
330
  name: string;
@@ -356,6 +392,16 @@ interface AuthAdmin {
356
392
  offset?: number;
357
393
  userId?: string;
358
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>>;
359
405
  /** List a team's members. */
360
406
  listTeamMembers: (options: {
361
407
  limit?: number;
@@ -383,6 +429,10 @@ interface AuthAdmin {
383
429
  removeUser: (input: {
384
430
  userId: string;
385
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>;
386
436
  revokeUserSession: (input: {
387
437
  sessionId: string;
388
438
  }) => Promise<void>;
@@ -965,4 +1015,4 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
965
1015
  * with the same 60s cookie cache as `rolling`.
966
1016
  */
967
1017
  declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
968
- 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 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, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
1018
+ 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, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { createSqlAuthStore, d1Executor } from "./sql-store.js";
11
11
  import { BetterAuthOptions } from 'better-auth';
12
12
  import { EmailClassification, EmailGateConfig } from "./email-guard.js";
13
13
  export { type EmailClass, type EmailGateMiddlewareOptions, assertEmailAllowed, classifyEmail, emailGateMiddleware, loadEmailDomainLists } from "./email-guard.js";
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-C9Su82Iq.js";
14
15
  export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.js";
15
16
  export { default as authTables } from "./schema.js";
16
17
  export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.js";
@@ -98,6 +99,28 @@ interface AuthInvitation {
98
99
  role?: null | string;
99
100
  status?: null | string;
100
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
+ }
101
124
  /** One team row (from the `organization` plugin with `teams.enabled`). */
102
125
  interface AuthTeam {
103
126
  [key: string]: unknown;
@@ -153,6 +176,8 @@ interface AuthCapabilities {
153
176
  accounts: boolean;
154
177
  /** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
155
178
  admin: boolean;
179
+ /** The `inviteOnly` plugin: sign-up invitations. */
180
+ inviteOnly: boolean;
156
181
  /** The `organization` plugin: orgs, members, invitations. */
157
182
  organization: boolean;
158
183
  /** The `@better-auth/passkey` plugin: per-user passkeys. */
@@ -289,6 +314,17 @@ interface AuthAdmin {
289
314
  permission: Record<string, string[]>;
290
315
  role: string;
291
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>;
292
328
  /** Create a team under an organization. */
293
329
  createTeam: (input: {
294
330
  name: string;
@@ -356,6 +392,16 @@ interface AuthAdmin {
356
392
  offset?: number;
357
393
  userId?: string;
358
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>>;
359
405
  /** List a team's members. */
360
406
  listTeamMembers: (options: {
361
407
  limit?: number;
@@ -383,6 +429,10 @@ interface AuthAdmin {
383
429
  removeUser: (input: {
384
430
  userId: string;
385
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>;
386
436
  revokeUserSession: (input: {
387
437
  sessionId: string;
388
438
  }) => Promise<void>;
@@ -965,4 +1015,4 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
965
1015
  * with the same 60s cookie cache as `rolling`.
966
1016
  */
967
1017
  declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
968
- 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 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, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
1018
+ 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, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{l as r,a as o,b as a}from"./packem_shared/adapter-DA8DdALX.mjs";import{LunoraAuthAdminError as u,createAuthAdmin as i}from"./packem_shared/LunoraAuthAdminError-DtAKa22S.mjs";import{AUTH_AUDIT_TABLE as m,appendAuthAuditEntry as l,createAuthAuditReader as d,ensureAuthAuditTable as h,readAuthAuditLog as E}from"./audit.mjs";import{authAuditHook as p,buildAuditEntry as T,eventForPath as f,withAuthAudit as _}from"./packem_shared/authAuditHook-tG8gdR77.mjs";import{READ_AUDIT_PATH as D,INTERNAL_SECRET_HEADER as S,RESOLVE_SESSION_PATH as H,LunoraAuthDO as c}from"./packem_shared/AUTH_DO_AUDIT_PATH-DiXHh-vn.mjs";import{createAuth as L,resolveAuthOptions as P}from"./packem_shared/createAuth-CtTKaLZN.mjs";import{authDoColumnAdditions as I,authDoSchemaStatements as O}from"./packem_shared/authDoColumnAdditions-BCSs7qaN.mjs";import{createDoAuthWiring as N}from"./packem_shared/createDoAuthWiring-Ber4dHOy.mjs";import{emailGateDatabaseHooks as b,withEmailGate as g}from"./packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs";import{assertEmailAllowed as M,classifyEmail as q,emailGateMiddleware as C,loadEmailDomainLists as F}from"./email-guard.mjs";import{DEFAULT_AUTH_BASE_PATH as k,handleAuthRequest as B}from"./packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{LunoraAuthHeadersError as W,withAuthPlugins as Y}from"./middleware.mjs";import{compileMigrationsSql as z,ensureMigrated as J}from"./packem_shared/compileMigrationsSql-TvwFrqmu.mjs";import{default as Q}from"./schema.mjs";import{sessionPresets as Z,validateSessionPolicy as $}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{createSqlAuthStore as te,d1Executor as re}from"./sql-store.mjs";import{createMemoryAuthStore as ae,matchesWhere as Ae}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as ie,verifyTurnstile as se}from"./turnstile.mjs";import{verifyTurnstileMiddleware as le}from"./turnstile-middleware.mjs";export{m as AUTH_AUDIT_TABLE,D as AUTH_DO_AUDIT_PATH,S as AUTH_DO_SECRET_HEADER,H as AUTH_DO_SESSION_PATH,k as DEFAULT_AUTH_BASE_PATH,u as LunoraAuthAdminError,c as LunoraAuthDO,W as LunoraAuthHeadersError,ie as TURNSTILE_VERIFY_ENDPOINT,l as appendAuthAuditEntry,M as assertEmailAllowed,p as authAuditHook,I as authDoColumnAdditions,O as authDoSchemaStatements,Q as authTables,T as buildAuditEntry,q as classifyEmail,z as compileMigrationsSql,L as createAuth,i as createAuthAdmin,d as createAuthAuditReader,N as createDoAuthWiring,ae as createMemoryAuthStore,te as createSqlAuthStore,re as d1Executor,b as emailGateDatabaseHooks,C as emailGateMiddleware,h as ensureAuthAuditTable,J as ensureMigrated,f as eventForPath,B as handleAuthRequest,F as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,Ae as matchesWhere,E as readAuthAuditLog,P as resolveAuthOptions,Z as sessionPresets,$ as validateSessionPolicy,se as verifyTurnstile,le as verifyTurnstileMiddleware,_ as withAuthAudit,Y as withAuthPlugins,g as withEmailGate};
1
+ import{l as r,a as o,b as a}from"./packem_shared/adapter-DA8DdALX.mjs";import{LunoraAuthAdminError as A,createAuthAdmin as u}from"./packem_shared/LunoraAuthAdminError-BIpCQR2m.mjs";import{AUTH_AUDIT_TABLE as s,appendAuthAuditEntry as m,createAuthAuditReader as l,ensureAuthAuditTable as p,readAuthAuditLog as d}from"./audit.mjs";import{authAuditHook as E,buildAuditEntry as T,eventForPath as f,withAuthAudit as S}from"./packem_shared/authAuditHook-tG8gdR77.mjs";import{READ_AUDIT_PATH as x,INTERNAL_SECRET_HEADER as D,RESOLVE_SESSION_PATH as H,LunoraAuthDO as U}from"./packem_shared/AUTH_DO_AUDIT_PATH-DiXHh-vn.mjs";import{createAuth as I,resolveAuthOptions as R}from"./packem_shared/createAuth-CtTKaLZN.mjs";import{authDoColumnAdditions as L,authDoSchemaStatements as P}from"./packem_shared/authDoColumnAdditions-BCSs7qaN.mjs";import{createDoAuthWiring as O}from"./packem_shared/createDoAuthWiring-Ber4dHOy.mjs";import{emailGateDatabaseHooks as N,withEmailGate as w}from"./packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs";import{assertEmailAllowed as M,classifyEmail as k,emailGateMiddleware as q,loadEmailDomainLists as C}from"./email-guard.mjs";import{DEFAULT_AUTH_BASE_PATH as G,handleAuthRequest as B}from"./packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{createSignUpInvitation as W,listSignUpInvitations as Y,pruneSignUpInvitations as j,revokeSignUpInvitation as z}from"./packem_shared/createSignUpInvitation-CqqQc4S8.mjs";import{LunoraAuthHeadersError as K,withAuthPlugins as Q}from"./middleware.mjs";import{compileMigrationsSql as Z,ensureMigrated as $}from"./packem_shared/compileMigrationsSql-TvwFrqmu.mjs";import{default as et}from"./schema.mjs";import{sessionPresets as ot,validateSessionPolicy as at}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{createSqlAuthStore as At,d1Executor as ut}from"./sql-store.mjs";import{createMemoryAuthStore as st,matchesWhere as mt}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as pt,verifyTurnstile as dt}from"./turnstile.mjs";import{verifyTurnstileMiddleware as Et}from"./turnstile-middleware.mjs";export{s as AUTH_AUDIT_TABLE,x as AUTH_DO_AUDIT_PATH,D as AUTH_DO_SECRET_HEADER,H as AUTH_DO_SESSION_PATH,G as DEFAULT_AUTH_BASE_PATH,A as LunoraAuthAdminError,U as LunoraAuthDO,K as LunoraAuthHeadersError,pt as TURNSTILE_VERIFY_ENDPOINT,m as appendAuthAuditEntry,M as assertEmailAllowed,E as authAuditHook,L as authDoColumnAdditions,P as authDoSchemaStatements,et as authTables,T as buildAuditEntry,k as classifyEmail,Z as compileMigrationsSql,I as createAuth,u as createAuthAdmin,l as createAuthAuditReader,O as createDoAuthWiring,st as createMemoryAuthStore,W as createSignUpInvitation,At as createSqlAuthStore,ut as d1Executor,N as emailGateDatabaseHooks,q as emailGateMiddleware,p as ensureAuthAuditTable,$ as ensureMigrated,f as eventForPath,B as handleAuthRequest,Y as listSignUpInvitations,C as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,mt as matchesWhere,j as pruneSignUpInvitations,d as readAuthAuditLog,R as resolveAuthOptions,z as revokeSignUpInvitation,ot as sessionPresets,at as validateSessionPolicy,dt as verifyTurnstile,Et as verifyTurnstileMiddleware,S as withAuthAudit,Q as withAuthPlugins,w as withEmailGate};
@@ -0,0 +1 @@
1
+ import{createLocalAccountIssuer as I}from"@better-auth/core/db";import{LunoraError as U}from"@lunora/errors";import{getAuthTables as b}from"better-auth/db";import{revokeSignUpInvitation as R,createSignUpInvitation as N}from"./createSignUpInvitation-CqqQc4S8.mjs";class p extends U{constructor(m,f){super(f,m,{name:"LunoraAuthAdminError"})}}const E=50,D=500,M=3600,T=M*24,z=100*365*24*60*60,k=2880*60*1e3,L=[{field:"userId",model:"member"},{field:"userId",model:"teamMember"},{field:"userId",model:"passkey"},{field:"userId",model:"twoFactor"},{field:"userId",model:"oauthAccessToken"},{field:"userId",model:"oauthRefreshToken"},{field:"userId",model:"oauthConsent"},{field:"userId",model:"deviceCode"},{field:"userId",model:"walletAddress"},{field:"inviterId",model:"invitation"}],C=new Set(["accessToken","backupCodes","idToken","password","publicKey","refreshToken","secret","token","tokenHash"]),P=o=>Math.min(Math.max(Math.trunc(o??E),1),D),B=o=>Math.max(0,Math.trunc(o??0)),u=o=>{const m={};for(const[f,l]of Object.entries(o))C.has(f)||(m[f]=l instanceof Date?l.getTime():l);return m},g=o=>Array.isArray(o)?o.join(","):o,S=o=>o.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),F=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),G={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},V=o=>o==="boolean"||o==="date"||o==="number"?o:"string",_=o=>{const m=[];for(const[f,l]of Object.entries(o))l.input===!1||l.references!==void 0||F.has(f)||m.push({name:f,plugin:G[f],required:l.required===!0,type:V(l.type),unique:l.unique===!0});return m},O=o=>typeof o=="string"&&o.includes("owner"),j=async(o,m)=>{const l=(await o.adapter.findMany({model:"member",where:[{field:"userId",value:m}]})).filter(w=>O(w.role)&&typeof w.organizationId=="string").map(w=>w.organizationId);if(l.length===0)return;const A=await Promise.all(l.map(async w=>o.adapter.findMany({model:"member",where:[{field:"organizationId",value:w}]}))),n=l.find((w,v)=>!A[v]?.some(e=>e.userId!==m&&O(e.role)));if(n!==void 0)throw new p(`user is the last owner of organization ${n} — transfer ownership or delete the organization first`,"LAST_ORGANIZATION_OWNER")},q=o=>{if(o instanceof p)return o;const m=o,f=m?.body?.code??m?.code??"AUTH_ADMIN_ERROR",l=m?.body?.message??m?.message??"auth admin operation failed";return new p(l,f)},H=(o,m={})=>{const f=o.$context,l=m.features??{},A=e=>{const a=new Set((e.plugins??[]).map(i=>i.id)),t=i=>a.has(i);return{accounts:l.accounts??!0,admin:l.admin??t("admin"),inviteOnly:l.inviteOnly??t("lunora-invite-only"),organization:l.organization??t("organization"),passkey:l.passkey??t("passkey"),twoFactor:l.twoFactor??t("two-factor")}},n=async e=>{try{return await e(await f)}catch(a){throw q(a)}},w=e=>u(e),v=async(e,a,t)=>{const i=t.where&&t.where.length>0?t.where:void 0,[r,s]=await Promise.all([e.adapter.findMany({limit:P(t.limit),model:a,offset:B(t.offset),sortBy:t.sortBy,where:i}),e.adapter.count({model:a,where:i})]);return{rows:r.map(d=>u(d)),total:s}};return{banUser:({expiresInSeconds:e,reason:a,userId:t})=>n(async i=>{let r=null;if(e!==void 0){if(!Number.isInteger(e)||e<=0)throw new p("expiresInSeconds must be a positive finite integer","INVALID_BAN_SECONDS");const d=Math.min(e,z);r=new Date(Date.now()+d*1e3)}const s=await i.internalAdapter.updateUser(t,{banExpires:r,banned:!0,banReason:a??"No reason"});return await i.internalAdapter.deleteUserSessions(t),w(s)}),cancelInvitation:({invitationId:e})=>n(async a=>{await a.adapter.delete({model:"invitation",where:[{field:"id",value:e}]})}),capabilities:()=>n(e=>Promise.resolve(A(e.options))),addMember:({organizationId:e,role:a,userId:t})=>n(async i=>{const r=await i.adapter.create({data:{createdAt:new Date,organizationId:e,role:a===void 0||a===""?"member":a,userId:t},model:"member"});return u(r)}),addTeamMember:({teamId:e,userId:a})=>n(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,teamId:e,userId:a},model:"teamMember"});return u(i)}),config:()=>n(e=>{const a=e.options,t=A(a),i=new Set((a.plugins??[]).map(c=>c.id)),r=b(a),s=a.session??{},d=a.rateLimit??{};return Promise.resolve({capabilities:t,emailAndPassword:a.emailAndPassword?.enabled??!1,organization:{enabled:t.organization,roles:!!r.organizationRole,teams:!!r.team},plugins:[...i].toSorted((c,h)=>c.localeCompare(h)),rateLimit:{enabled:d.enabled??!1,max:d.max,window:d.window},session:{cookieCache:s.cookieCache?.enabled,expiresIn:s.expiresIn,freshAge:s.freshAge,updateAge:s.updateAge},socialProviders:Object.keys(a.socialProviders??{}).toSorted((c,h)=>c.localeCompare(h)),userFields:_(r.user?.fields??{})})}),createOrganization:({logo:e,metadata:a,name:t,ownerId:i,slug:r})=>n(async s=>{const d=S(r!==void 0&&r!==""?r:t);if(d==="")throw new p("could not derive a slug from the organization name","ORG_SLUG_INVALID");if(await s.adapter.findOne({model:"organization",where:[{field:"slug",value:d}]}))throw new p("an organization with this slug already exists","ORG_SLUG_TAKEN");const h=await s.adapter.create({data:{createdAt:new Date,logo:e===void 0||e===""?void 0:e,metadata:a===void 0?void 0:JSON.stringify(a),name:t,slug:d},model:"organization"});return i!==void 0&&i!==""&&await s.adapter.create({data:{createdAt:new Date,organizationId:h.id,role:"owner",userId:i},model:"member"}),u(h)}),createOrgRole:({organizationId:e,permission:a,role:t})=>n(async i=>{const r=await i.adapter.create({data:{createdAt:new Date,organizationId:e,permission:JSON.stringify(a),role:t},model:"organizationRole"});return u(r)}),createTeam:({name:e,organizationId:a})=>n(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,name:e,organizationId:a},model:"team"});return u(i)}),deleteOrganization:({organizationId:e})=>n(async a=>{const t=b(a.options);if(await a.adapter.deleteMany({model:"member",where:[{field:"organizationId",value:e}]}),await a.adapter.deleteMany({model:"invitation",where:[{field:"organizationId",value:e}]}),t.team){const i=await a.adapter.findMany({model:"team",where:[{field:"organizationId",value:e}]});for(const r of i)await a.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:r.id}]});await a.adapter.deleteMany({model:"team",where:[{field:"organizationId",value:e}]})}t.organizationRole&&await a.adapter.deleteMany({model:"organizationRole",where:[{field:"organizationId",value:e}]}),await a.adapter.delete({model:"organization",where:[{field:"id",value:e}]})}),deleteOrgRole:({roleId:e})=>n(async a=>{await a.adapter.delete({model:"organizationRole",where:[{field:"id",value:e}]})}),inviteMember:({email:e,inviterId:a,organizationId:t,role:i})=>n(async r=>{let s=a;if(s===void 0||s===""){const c=await r.adapter.findMany({model:"member",where:[{field:"organizationId",value:t}]});s=(c.find(y=>typeof y.role=="string"&&y.role.includes("owner"))??c[0])?.userId}if(s===void 0||s==="")throw new p("provide an inviter — the organization has no members to attribute the invitation to","INVITER_REQUIRED");const d=await r.adapter.create({data:{createdAt:new Date,email:e.toLowerCase(),expiresAt:new Date(Date.now()+k),inviterId:s,organizationId:t,role:i===void 0||i===""?"member":i,status:"pending"},model:"invitation"});return u(d)}),listOrgRoles:({limit:e,offset:a,organizationId:t})=>n(i=>v(i,"organizationRole",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listTeamMembers:({limit:e,offset:a,teamId:t})=>n(i=>v(i,"teamMember",{limit:e,offset:a,where:[{field:"teamId",value:t}]})),listTeams:({limit:e,offset:a,organizationId:t})=>n(i=>v(i,"team",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),removeTeam:({teamId:e})=>n(async a=>{await a.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:e}]}),await a.adapter.delete({model:"team",where:[{field:"id",value:e}]})}),removeTeamMember:({teamMemberId:e})=>n(async a=>{await a.adapter.delete({model:"teamMember",where:[{field:"id",value:e}]})}),updateMemberRole:({memberId:e,role:a})=>n(async t=>{const i=await t.adapter.update({model:"member",update:{role:g(a)},where:[{field:"id",value:e}]});return u(i??{id:e,role:g(a)})}),updateOrganization:({logo:e,metadata:a,name:t,organizationId:i,slug:r})=>n(async s=>{const d={};if(t!==void 0&&(d.name=t),r!==void 0&&r!==""&&(d.slug=S(r)),e!==void 0&&(d.logo=e===""?void 0:e),a!==void 0&&(d.metadata=JSON.stringify(a)),Object.keys(d).length===0)return u({id:i});const c=await s.adapter.update({model:"organization",update:d,where:[{field:"id",value:i}]});return u(c??{id:i})}),updateOrgRole:({permission:e,roleId:a})=>n(async t=>{const i=await t.adapter.update({model:"organizationRole",update:{permission:JSON.stringify(e),updatedAt:new Date},where:[{field:"id",value:a}]});return u(i??{id:a,permission:JSON.stringify(e)})}),updateTeam:({name:e,teamId:a})=>n(async t=>{const i=await t.adapter.update({model:"team",update:{name:e,updatedAt:new Date},where:[{field:"id",value:a}]});return u(i??{id:a,name:e})}),createUser:({data:e,email:a,name:t,password:i,role:r})=>n(async s=>{const d=a.toLowerCase();if(await s.internalAdapter.findUserByEmail(d))throw new p("a user with this email already exists","USER_ALREADY_EXISTS");const c=await s.internalAdapter.createUser({email:d,name:t,role:r===void 0?void 0:g(r),...e},{method:"admin"});if(i!==void 0&&i!==""){const h=await s.password.hash(i);await s.internalAdapter.linkAccount({accountId:c.id,issuer:I("credential"),password:h,providerId:"credential",userId:c.id})}return w(c)}),deletePasskey:({passkeyId:e})=>n(async a=>{await a.adapter.delete({model:"passkey",where:[{field:"id",value:e}]})}),disableTwoFactor:({userId:e})=>n(async a=>{await a.adapter.deleteMany({model:"twoFactor",where:[{field:"userId",value:e}]}),await a.internalAdapter.updateUser(e,{twoFactorEnabled:!1})}),impersonateUser:({userId:e})=>n(async a=>{const t=await a.internalAdapter.findUserById(e);if(!t)throw new p("user not found","USER_NOT_FOUND");const i=m.impersonationSeconds;let r=M;if(i!==void 0){if(!Number.isInteger(i)||!Number.isFinite(i)||i<=0)throw new p("impersonationSeconds must be a positive finite integer","INVALID_IMPERSONATION_SECONDS");r=Math.min(i,T)}const s=new Date(Date.now()+r*1e3),d=await a.internalAdapter.createSession(e,!0,{expiresAt:s,impersonatedBy:m.impersonatedBy??e},!0);return{expiresAt:d.expiresAt instanceof Date?d.expiresAt.getTime():s.getTime(),token:d.token,user:w(t)}}),listAccounts:({userId:e})=>n(async a=>(await a.adapter.findMany({model:"account",where:[{field:"userId",value:e}]})).map(i=>u(i))),listInvitations:({limit:e,offset:a,organizationId:t})=>n(i=>v(i,"invitation",{limit:e,offset:a,where:[{field:"organizationId",value:t}]})),createSignUpInvitation:({email:e,expiresInSeconds:a,invitedBy:t})=>n(async()=>{const i=await N(o,{email:e,expiresInSeconds:a,invitedBy:t});return{...u({...i}),token:i.token}}),listSignUpInvitations:({limit:e,offset:a})=>n(t=>v(t,"signUpInvitation",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"}})),revokeSignUpInvitation:({email:e})=>n(async()=>R(o,{email:e})),listMembers:({limit:e,offset:a,organizationId:t})=>n(i=>v(i,"member",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listOrganizations:({limit:e,offset:a})=>n(t=>v(t,"organization",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"}})),listPasskeys:({userId:e})=>n(async a=>(await a.adapter.findMany({model:"passkey",where:[{field:"userId",value:e}]})).map(i=>u(i))),listSessions:({limit:e,offset:a,userId:t})=>n(i=>v(i,"session",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:t===void 0||t===""?void 0:[{field:"userId",value:t}]})),listUsers:({filterField:e,filterValue:a,limit:t,offset:i,search:r,searchField:s,sortBy:d,sortDirection:c})=>n(h=>{const y=[];return r!==void 0&&r!==""&&y.push({field:s??"email",operator:"contains",value:r}),a!==void 0&&y.push({field:e??"email",operator:"eq",value:a}),v(h,"user",{limit:t,offset:i,sortBy:{direction:c??"desc",field:d??"createdAt"},where:y})}),removeMember:({memberId:e})=>n(async a=>{await a.adapter.delete({model:"member",where:[{field:"id",value:e}]})}),removeUser:({userId:e})=>n(async a=>{const t=b(a.options);t.member&&await j(a,e),await Promise.all(L.filter(({model:i})=>t[i]).map(({field:i,model:r})=>a.adapter.deleteMany({model:r,where:[{field:i,value:e}]}))),await a.internalAdapter.deleteUserSessions(e),await a.internalAdapter.deleteUser(e)}),revokeUserSession:({sessionId:e})=>n(async a=>{const t=await a.adapter.findOne({model:"session",where:[{field:"id",value:e}]});t?.token&&await a.internalAdapter.deleteSession(t.token)}),revokeUserSessions:({userId:e})=>n(async a=>{await a.internalAdapter.deleteUserSessions(e)}),setRole:({role:e,userId:a})=>n(async t=>{const i=await t.internalAdapter.updateUser(a,{role:g(e)});return w(i)}),setUserPassword:({newPassword:e,userId:a})=>n(async t=>{const i=t.password.config.minPasswordLength,r=t.password.config.maxPasswordLength;if(e.length<i)throw new p(`password must be at least ${i.toString()} characters`,"PASSWORD_TOO_SHORT");if(e.length>r)throw new p(`password must be at most ${r.toString()} characters`,"PASSWORD_TOO_LONG");if(!await t.internalAdapter.findUserById(a))throw new p("user not found","USER_NOT_FOUND");const d=(await t.internalAdapter.findAccounts(a)).some(h=>h.providerId==="credential");if(!d&&t.options.emailAndPassword?.enabled!==!0)throw new p("email/password sign-in is disabled for this deployment","EMAIL_PASSWORD_DISABLED");const c=await t.password.hash(e);d?await t.internalAdapter.updatePassword(a,c):await t.internalAdapter.linkAccount({accountId:a,issuer:I("credential"),password:c,providerId:"credential",userId:a})}),unbanUser:({userId:e})=>n(async a=>{const t=await a.internalAdapter.updateUser(e,{banExpires:null,banned:!1,banReason:null});return w(t)}),unlinkAccount:({accountId:e,userId:a})=>n(async t=>{await t.adapter.delete({model:"account",where:[{field:"id",value:e},{connector:"AND",field:"userId",value:a}]})}),updateUser:({data:e,userId:a})=>n(async t=>{const i=await t.internalAdapter.updateUser(a,e);return w(i)})}};export{p as LunoraAuthAdminError,H as createAuthAdmin};
@@ -0,0 +1 @@
1
+ import{defineErrorCodes as S}from"@better-auth/core/utils/error-codes";import{LunoraError as A}from"@lunora/errors";import{createAuthMiddleware as T,APIError as g}from"better-auth/api";const c="signUpInvitation",_=10080*60,v=365*24*60*60,y=500,w=S({SIGN_UP_INVITE_INVALID:"That sign-up invitation is not valid. Ask an administrator for a new invitation link.",SIGN_UP_INVITE_REQUIRED:"Sign-up is invite-only — ask an administrator for an invitation."}),D=32,x=/^[^\s@]+@[^\s@]+$/,O=e=>{if(!x.test(e))return!1;const t=e.slice(e.indexOf("@")+1);return t.includes(".")&&!t.startsWith(".")&&!t.endsWith(".")},p=e=>({acceptedAt:e.acceptedAt instanceof Date?e.acceptedAt:null,createdAt:e.createdAt,email:String(e.email),expiresAt:e.expiresAt,id:String(e.id),invitedBy:typeof e.invitedBy=="string"?e.invitedBy:null}),R=()=>btoa(String.fromCodePoint(...crypto.getRandomValues(new Uint8Array(D)))).replaceAll("+","-").replaceAll("/","_").replaceAll("=",""),E=async e=>{const t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return[...new Uint8Array(t)].map(i=>i.toString(16).padStart(2,"0")).join("")},N=(e,t)=>{if(e.length!==t.length)return!1;let i=0;for(let n=0;n<e.length;n+=1)i|=(e.codePointAt(n)??0)^(t.codePointAt(n)??0);return i===0},m=e=>e.trim().toLowerCase(),I=e=>{if(typeof e.email!="string")return;const t=m(e.email);return t===""?void 0:t},k=async(e,t)=>{const i=await e.findOne({model:c,where:[{field:"email",value:t}]});return i===null||i.acceptedAt instanceof Date?!1:i.expiresAt instanceof Date&&i.expiresAt.getTime()>Date.now()},U=e=>{e.emailAndPassword?.enabled!==!0||e.emailAndPassword.requireEmailVerification===!0||console.warn("@lunora/auth: inviteOnly() is installed with password sign-up but without `emailAndPassword: { requireEmailVerification: true }`. An invitation is keyed by email address alone, so anyone who learns an invited address can sign up as it — and without verification they hold a session the moment they do.")},q=(e={})=>{let t=e.allowFirstUser??!1;const i=async n=>t?await n.count({model:"user"})===0?!0:(t=!1,!1):!1;return{$ERROR_CODES:w,hooks:{before:[{handler:T(async n=>{const r=n.body??{},a=typeof r.email=="string"?m(r.email):"",d=typeof r.inviteToken=="string"?r.inviteToken:"",s=()=>{throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_INVALID)};if(a===""&&s(),await i(n.context.adapter))return;d===""&&s();const o=await n.context.adapter.findOne({model:c,where:[{field:"email",value:a}]}),u=o===null?void 0:o.tokenHash,f=await E(d);(typeof u!="string"||!N(u,f))&&s()}),matcher:n=>n.path==="/sign-up/email"}]},id:"lunora-invite-only",init:n=>{U(n.options);const{adapter:r}=n;return{options:{databaseHooks:{user:{create:{after:async s=>{const o=I(s);o!==void 0&&await r.update({model:c,update:{acceptedAt:new Date},where:[{field:"email",value:o}]})},before:async s=>{const o=I(s);if(!(o!==void 0&&await k(r,o))&&!await i(r))throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_REQUIRED)}}}}}}},schema:{[c]:{fields:{acceptedAt:{required:!1,type:"date"},tokenHash:{required:!1,type:"string"},createdAt:{defaultValue:()=>new Date,required:!0,type:"date"},email:{required:!0,type:"string",unique:!0},expiresAt:{required:!0,type:"date"},invitedBy:{required:!1,type:"string"}}}}}},M=async(e,t)=>{const i=m(t.email);if(!O(i))throw new A("VALIDATION_ERROR",`not an email address to invite: ${JSON.stringify(t.email)}`);const{expiresInSeconds:n=_}=t;if(!Number.isInteger(n)||n<=0||n>v)throw new A("VALIDATION_ERROR",`expiresInSeconds must be a positive integer no greater than ${String(v)}`);const r=new Date(Date.now()+n*1e3),a=R(),d=await E(a),s=t.invitedBy??null,o=await e.$context,u=[{field:"email",value:i}],f=async()=>o.adapter.update({model:c,update:{acceptedAt:null,expiresAt:r,invitedBy:s,tokenHash:d},where:u});if(await o.adapter.findOne({model:c,where:u})){const l=await f();if(l)return{...p(l),token:a}}try{const l=await o.adapter.create({data:{createdAt:new Date,email:i,expiresAt:r,invitedBy:s,tokenHash:d},model:c});return{...p(l),token:a}}catch(l){const h=await f();if(h===null)throw l;return{...p(h),token:a}}},$=async(e,t={})=>{const r=(await(await e.$context).adapter.findMany({limit:y,model:c,sortBy:{direction:"desc",field:"createdAt"}})).map(a=>p(a));return t.pendingOnly===!0?r.filter(a=>a.acceptedAt===null&&a.expiresAt.getTime()>Date.now()):r},C=async(e,t)=>{await(await e.$context).adapter.delete({model:c,where:[{field:"email",value:m(t.email)}]})},b=100,V=async(e,t,i,n)=>{const r=t.filter(a=>a.acceptedAt===null&&a.expiresAt.getTime()<=i).slice(0,n);for(const a of r)await e.delete({model:c,where:[{field:"id",value:a.id}]});return r.length},H=async(e,t={})=>{const i=t.limit??y;if(!Number.isInteger(i)||i<=0)throw new A("VALIDATION_ERROR","limit must be a positive integer");const n=Math.min(i,y),r=await e.$context,a=Date.now();let d=0,s=0;for(;d<n;){const o=await r.adapter.findMany({limit:b,model:c,offset:s,sortBy:{direction:"asc",field:"expiresAt"}});if(o.length===0)break;const u=o.map(l=>p(l)),f=await V(r.adapter,u,a,n-d);if(d+=f,u.some(l=>l.expiresAt.getTime()>a))break;s+=o.length-f}return d};export{M as createSignUpInvitation,q as inviteOnly,$ as listSignUpInvitations,H as pruneSignUpInvitations,C as revokeSignUpInvitation};
@@ -0,0 +1,126 @@
1
+ import { BetterAuthPlugin } from 'better-auth';
2
+ import { L as LunoraAuth } from "./create-auth.d-hkN1GE5-.js";
3
+ /** One pending or spent sign-up invitation. */
4
+ interface SignUpInvitation {
5
+ /** When an account was created for this address; `null` while the invitation is unspent. */
6
+ acceptedAt: Date | null;
7
+ createdAt: Date;
8
+ /** The invited address, lowercased — the only thing an invitation is matched on. */
9
+ email: string;
10
+ expiresAt: Date;
11
+ id: string;
12
+ /** Free-form attribution (a user id, an operator name); never read by the gate. */
13
+ invitedBy: null | string;
14
+ }
15
+ /**
16
+ * What {@link createSignUpInvitation} hands back: the stored row plus the one
17
+ * and only sight of the plaintext `token`. Nothing reads it back afterwards —
18
+ * the database holds a SHA-256 of it — so an invitation link that is lost is
19
+ * reissued, not recovered.
20
+ */
21
+ interface IssuedSignUpInvitation extends SignUpInvitation {
22
+ /** Put this in the sign-up link as `?invite=…`. Never stored, never logged, never listed. */
23
+ token: string;
24
+ }
25
+ /** Options for {@link inviteOnly}. */
26
+ interface InviteOnlyOptions {
27
+ /**
28
+ * Let the very first account through uninvited, so a fresh deployment can be
29
+ * bootstrapped without seeding a row.
30
+ *
31
+ * **Off by default.** The check is "the `user` table is empty", which two
32
+ * concurrent sign-ups can both observe, and the window it opens is the gap
33
+ * between deploying and the owner signing up — whoever finds the URL first
34
+ * gets an account on a deployment whose whole point is that nobody does. Seed
35
+ * the first invitation with {@link createSignUpInvitation} instead (a one-off
36
+ * call at worker init, or an internal mutation you run once).
37
+ * @default false
38
+ */
39
+ allowFirstUser?: boolean;
40
+ }
41
+ /**
42
+ * A better-auth server plugin that refuses to create an account for an address
43
+ * with no unspent invitation, and the `signUpInvitation` table those live in.
44
+ *
45
+ * Issue invitations with {@link createSignUpInvitation}; there is no HTTP
46
+ * endpoint for it on purpose. Who counts as an administrator is your
47
+ * application's question — call it from a mutation you already authorize, the
48
+ * same trust model `createAuthAdmin` documents.
49
+ *
50
+ * The return type is better-auth's own `BetterAuthPlugin` rather than the
51
+ * precise shape of the schema map, for the reason `./ui-config.ts` spells out:
52
+ * an anonymous inferred type is the difference between a build that emits
53
+ * declarations and one that fails in the bundler alone.
54
+ */
55
+ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
56
+ /**
57
+ * Invite `email` to sign up, or refresh an existing invitation for it.
58
+ *
59
+ * Re-inviting an address updates the row in place — a new expiry, and `acceptedAt`
60
+ * cleared — because `email` is unique. That is also how you re-open a seat after
61
+ * deleting the account that took it.
62
+ *
63
+ * This is a trusted server-side call with no authorization of its own; gate it
64
+ * the way you gate any other administrative action. Delivering the invitation is
65
+ * yours too: nothing here sends mail, so the returned row is the whole handoff.
66
+ *
67
+ * Nothing prunes the table — a spent or expired row stays until you delete it with
68
+ * {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
69
+ * let in.
70
+ *
71
+ * Each call mints a **new token** and returns it in the clear, once. Only its
72
+ * SHA-256 is stored, so re-inviting an address invalidates the previous link, and
73
+ * a link that was never delivered is reissued rather than looked up.
74
+ */
75
+ declare const createSignUpInvitation: (auth: LunoraAuth, input: {
76
+ email: string;
77
+ expiresInSeconds?: number;
78
+ invitedBy?: string;
79
+ }) => Promise<IssuedSignUpInvitation>;
80
+ /**
81
+ * The most recent invitations, newest first, up to a fixed ceiling of
82
+ * {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
83
+ * list an operator usually wants; the unfiltered form doubles as the record of who
84
+ * was let in.
85
+ *
86
+ * Deliberately not paged. "Pending" is two conditions, one of them a comparison
87
+ * against `now`, and filtering those after a page would let page 1 come back empty
88
+ * while pending invitations sat on page 2. An operator list that outgrows the
89
+ * ceiling wants a query against the `signUpInvitation` table, not an offset.
90
+ */
91
+ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
92
+ pendingOnly?: boolean;
93
+ }) => Promise<SignUpInvitation[]>;
94
+ /**
95
+ * Withdraw the invitation for `email`. Deletes the row, so it also forgets a spent
96
+ * one — the account it created is untouched, and removing that is
97
+ * `AuthAdmin.removeUser`'s job.
98
+ *
99
+ * Not retroactive, and not atomic against a sign-up already in flight: better-auth
100
+ * creates the user without wrapping the `before` hook and the insert in one
101
+ * transaction, so a revoke landing between the two lets that one account through.
102
+ * There is no conditional consume in the adapter contract to close it with. Treat
103
+ * revocation as "no further sign-ups", and `AuthAdmin.removeUser` as the way to
104
+ * undo one that already happened.
105
+ */
106
+ declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
107
+ email: string;
108
+ }) => Promise<void>;
109
+ /**
110
+ * Delete invitations that expired without being used, and report how many went.
111
+ *
112
+ * Only the dead ones: a spent invitation is the record of who was let in, and an
113
+ * unexpired one is still live, so both stay. Nothing calls this for you — an app
114
+ * that invites at any volume should put it on a cron; one that doesn't can leave
115
+ * the rows.
116
+ *
117
+ * Bounded by `limit` and therefore incremental: a backlog larger than one pass
118
+ * takes several. It reads a page and deletes row by row rather than issuing one
119
+ * ranged `deleteMany`, because a `lt` comparison against a `date` column is the
120
+ * kind of thing that behaves differently on each of the three adapters this
121
+ * package ships, and a prune job is not where that should be discovered.
122
+ */
123
+ declare const pruneSignUpInvitations: (auth: LunoraAuth, options?: {
124
+ limit?: number;
125
+ }) => Promise<number>;
126
+ export { InviteOnlyOptions as I, SignUpInvitation as S, IssuedSignUpInvitation as a, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, pruneSignUpInvitations as p, revokeSignUpInvitation as r };
@@ -0,0 +1,126 @@
1
+ import { BetterAuthPlugin } from 'better-auth';
2
+ import { L as LunoraAuth } from "./create-auth.d-hkN1GE5-.mjs";
3
+ /** One pending or spent sign-up invitation. */
4
+ interface SignUpInvitation {
5
+ /** When an account was created for this address; `null` while the invitation is unspent. */
6
+ acceptedAt: Date | null;
7
+ createdAt: Date;
8
+ /** The invited address, lowercased — the only thing an invitation is matched on. */
9
+ email: string;
10
+ expiresAt: Date;
11
+ id: string;
12
+ /** Free-form attribution (a user id, an operator name); never read by the gate. */
13
+ invitedBy: null | string;
14
+ }
15
+ /**
16
+ * What {@link createSignUpInvitation} hands back: the stored row plus the one
17
+ * and only sight of the plaintext `token`. Nothing reads it back afterwards —
18
+ * the database holds a SHA-256 of it — so an invitation link that is lost is
19
+ * reissued, not recovered.
20
+ */
21
+ interface IssuedSignUpInvitation extends SignUpInvitation {
22
+ /** Put this in the sign-up link as `?invite=…`. Never stored, never logged, never listed. */
23
+ token: string;
24
+ }
25
+ /** Options for {@link inviteOnly}. */
26
+ interface InviteOnlyOptions {
27
+ /**
28
+ * Let the very first account through uninvited, so a fresh deployment can be
29
+ * bootstrapped without seeding a row.
30
+ *
31
+ * **Off by default.** The check is "the `user` table is empty", which two
32
+ * concurrent sign-ups can both observe, and the window it opens is the gap
33
+ * between deploying and the owner signing up — whoever finds the URL first
34
+ * gets an account on a deployment whose whole point is that nobody does. Seed
35
+ * the first invitation with {@link createSignUpInvitation} instead (a one-off
36
+ * call at worker init, or an internal mutation you run once).
37
+ * @default false
38
+ */
39
+ allowFirstUser?: boolean;
40
+ }
41
+ /**
42
+ * A better-auth server plugin that refuses to create an account for an address
43
+ * with no unspent invitation, and the `signUpInvitation` table those live in.
44
+ *
45
+ * Issue invitations with {@link createSignUpInvitation}; there is no HTTP
46
+ * endpoint for it on purpose. Who counts as an administrator is your
47
+ * application's question — call it from a mutation you already authorize, the
48
+ * same trust model `createAuthAdmin` documents.
49
+ *
50
+ * The return type is better-auth's own `BetterAuthPlugin` rather than the
51
+ * precise shape of the schema map, for the reason `./ui-config.ts` spells out:
52
+ * an anonymous inferred type is the difference between a build that emits
53
+ * declarations and one that fails in the bundler alone.
54
+ */
55
+ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
56
+ /**
57
+ * Invite `email` to sign up, or refresh an existing invitation for it.
58
+ *
59
+ * Re-inviting an address updates the row in place — a new expiry, and `acceptedAt`
60
+ * cleared — because `email` is unique. That is also how you re-open a seat after
61
+ * deleting the account that took it.
62
+ *
63
+ * This is a trusted server-side call with no authorization of its own; gate it
64
+ * the way you gate any other administrative action. Delivering the invitation is
65
+ * yours too: nothing here sends mail, so the returned row is the whole handoff.
66
+ *
67
+ * Nothing prunes the table — a spent or expired row stays until you delete it with
68
+ * {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
69
+ * let in.
70
+ *
71
+ * Each call mints a **new token** and returns it in the clear, once. Only its
72
+ * SHA-256 is stored, so re-inviting an address invalidates the previous link, and
73
+ * a link that was never delivered is reissued rather than looked up.
74
+ */
75
+ declare const createSignUpInvitation: (auth: LunoraAuth, input: {
76
+ email: string;
77
+ expiresInSeconds?: number;
78
+ invitedBy?: string;
79
+ }) => Promise<IssuedSignUpInvitation>;
80
+ /**
81
+ * The most recent invitations, newest first, up to a fixed ceiling of
82
+ * {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
83
+ * list an operator usually wants; the unfiltered form doubles as the record of who
84
+ * was let in.
85
+ *
86
+ * Deliberately not paged. "Pending" is two conditions, one of them a comparison
87
+ * against `now`, and filtering those after a page would let page 1 come back empty
88
+ * while pending invitations sat on page 2. An operator list that outgrows the
89
+ * ceiling wants a query against the `signUpInvitation` table, not an offset.
90
+ */
91
+ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
92
+ pendingOnly?: boolean;
93
+ }) => Promise<SignUpInvitation[]>;
94
+ /**
95
+ * Withdraw the invitation for `email`. Deletes the row, so it also forgets a spent
96
+ * one — the account it created is untouched, and removing that is
97
+ * `AuthAdmin.removeUser`'s job.
98
+ *
99
+ * Not retroactive, and not atomic against a sign-up already in flight: better-auth
100
+ * creates the user without wrapping the `before` hook and the insert in one
101
+ * transaction, so a revoke landing between the two lets that one account through.
102
+ * There is no conditional consume in the adapter contract to close it with. Treat
103
+ * revocation as "no further sign-ups", and `AuthAdmin.removeUser` as the way to
104
+ * undo one that already happened.
105
+ */
106
+ declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
107
+ email: string;
108
+ }) => Promise<void>;
109
+ /**
110
+ * Delete invitations that expired without being used, and report how many went.
111
+ *
112
+ * Only the dead ones: a spent invitation is the record of who was let in, and an
113
+ * unexpired one is still live, so both stay. Nothing calls this for you — an app
114
+ * that invites at any volume should put it on a cron; one that doesn't can leave
115
+ * the rows.
116
+ *
117
+ * Bounded by `limit` and therefore incremental: a backlog larger than one pass
118
+ * takes several. It reads a page and deletes row by row rather than issuing one
119
+ * ranged `deleteMany`, because a `lt` comparison against a `date` column is the
120
+ * kind of thing that behaves differently on each of the three adapters this
121
+ * package ships, and a prune job is not where that should be discovered.
122
+ */
123
+ declare const pruneSignUpInvitations: (auth: LunoraAuth, options?: {
124
+ limit?: number;
125
+ }) => Promise<number>;
126
+ export { InviteOnlyOptions as I, SignUpInvitation as S, IssuedSignUpInvitation as a, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, pruneSignUpInvitations as p, revokeSignUpInvitation as r };
@@ -1,3 +1,25 @@
1
+ export {
2
+ /**
3
+ * Close self-serve sign-up to invited addresses only: an account is created only
4
+ * for an email an administrator has invited with `createSignUpInvitation`.
5
+ * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
6
+ * re-export.
7
+ */
8
+ type I as InviteOnlyOptions,
9
+ /**
10
+ * Close self-serve sign-up to invited addresses only: an account is created only
11
+ * for an email an administrator has invited with `createSignUpInvitation`.
12
+ * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
13
+ * re-export.
14
+ */
15
+ type a as IssuedSignUpInvitation,
16
+ /**
17
+ * Close self-serve sign-up to invited addresses only: an account is created only
18
+ * for an email an administrator has invited with `createSignUpInvitation`.
19
+ * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
20
+ * re-export.
21
+ */
22
+ type S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-DsVdKwcp.mjs";
1
23
  import { BetterAuthPlugin } from 'better-auth';
2
24
  export { apiKey } from '@better-auth/api-key';
3
25
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
@@ -24,6 +46,7 @@ export { phoneNumber } from 'better-auth/plugins/phone-number';
24
46
  export { siwe } from 'better-auth/plugins/siwe';
25
47
  export { twoFactor } from 'better-auth/plugins/two-factor';
26
48
  export { username } from 'better-auth/plugins/username';
49
+ import "./packem_shared/create-auth.d-hkN1GE5-.mjs";
27
50
  /** Organization sub-features a UI branches on. */
28
51
  interface UiConfigOrganization {
29
52
  /** Whether an ordinary user may create one at all. */
package/dist/plugins.d.ts CHANGED
@@ -1,3 +1,25 @@
1
+ export {
2
+ /**
3
+ * Close self-serve sign-up to invited addresses only: an account is created only
4
+ * for an email an administrator has invited with `createSignUpInvitation`.
5
+ * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
6
+ * re-export.
7
+ */
8
+ type I as InviteOnlyOptions,
9
+ /**
10
+ * Close self-serve sign-up to invited addresses only: an account is created only
11
+ * for an email an administrator has invited with `createSignUpInvitation`.
12
+ * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
13
+ * re-export.
14
+ */
15
+ type a as IssuedSignUpInvitation,
16
+ /**
17
+ * Close self-serve sign-up to invited addresses only: an account is created only
18
+ * for an email an administrator has invited with `createSignUpInvitation`.
19
+ * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
20
+ * re-export.
21
+ */
22
+ type S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-C9Su82Iq.js";
1
23
  import { BetterAuthPlugin } from 'better-auth';
2
24
  export { apiKey } from '@better-auth/api-key';
3
25
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
@@ -24,6 +46,7 @@ export { phoneNumber } from 'better-auth/plugins/phone-number';
24
46
  export { siwe } from 'better-auth/plugins/siwe';
25
47
  export { twoFactor } from 'better-auth/plugins/two-factor';
26
48
  export { username } from 'better-auth/plugins/username';
49
+ import "./packem_shared/create-auth.d-hkN1GE5-.js";
27
50
  /** Organization sub-features a UI branches on. */
28
51
  interface UiConfigOrganization {
29
52
  /** Whether an ordinary user may create one at all. */
package/dist/plugins.mjs CHANGED
@@ -1 +1 @@
1
- import{uiConfig as e}from"./packem_shared/uiConfig-B8pN6pks.mjs";import{apiKey as m}from"@better-auth/api-key";import{createMcpProtectedRequestHandler as f,mcp as x,requireMcpAuth as i}from"@better-auth/mcp";import{oauthDeviceAuthorization as n,oauthProvider as c}from"@better-auth/oauth-provider";import{passkey as u}from"@better-auth/passkey";import{scim as d}from"@better-auth/scim";import{captcha as g,lastLoginMethod as l,oneTap as P}from"better-auth/plugins";import{createAccessControl as w}from"better-auth/plugins/access";import{admin as T}from"better-auth/plugins/admin";import{anonymous as z}from"better-auth/plugins/anonymous";import{bearer as b}from"better-auth/plugins/bearer";import{customSession as C}from"better-auth/plugins/custom-session";import{deviceAuthorization as O}from"better-auth/plugins/device-authorization";import{emailOTP as j}from"better-auth/plugins/email-otp";import{genericOAuth as D}from"better-auth/plugins/generic-oauth";import{haveIBeenPwned as H}from"better-auth/plugins/haveibeenpwned";import{jwt as K}from"better-auth/plugins/jwt";import{magicLink as R}from"better-auth/plugins/magic-link";import{multiSession as G}from"better-auth/plugins/multi-session";import{oAuthProxy as Q}from"better-auth/plugins/oauth-proxy";import{oneTimeToken as V}from"better-auth/plugins/one-time-token";import{organization as X}from"better-auth/plugins/organization";import{phoneNumber as Z}from"better-auth/plugins/phone-number";import{siwe as $}from"better-auth/plugins/siwe";import{twoFactor as ro}from"better-auth/plugins/two-factor";import{username as to}from"better-auth/plugins/username";export{T as admin,z as anonymous,m as apiKey,b as bearer,g as captcha,w as createAccessControl,f as createMcpProtectedRequestHandler,C as customSession,O as deviceAuthorization,j as emailOTP,D as genericOAuth,H as haveIBeenPwned,K as jwt,l as lastLoginMethod,R as magicLink,x as mcp,G as multiSession,Q as oAuthProxy,n as oauthDeviceAuthorization,c as oauthProvider,P as oneTap,V as oneTimeToken,X as organization,u as passkey,Z as phoneNumber,i as requireMcpAuth,d as scim,$ as siwe,ro as twoFactor,e as uiConfig,to as username};
1
+ import{inviteOnly as e}from"./packem_shared/createSignUpInvitation-CqqQc4S8.mjs";import{uiConfig as m}from"./packem_shared/uiConfig-B8pN6pks.mjs";import{apiKey as f}from"@better-auth/api-key";import{createMcpProtectedRequestHandler as i,mcp as n,requireMcpAuth as a}from"@better-auth/mcp";import{oauthDeviceAuthorization as s,oauthProvider as u}from"@better-auth/oauth-provider";import{passkey as d}from"@better-auth/passkey";import{scim as A}from"@better-auth/scim";import{captcha as v,lastLoginMethod as y,oneTap as P}from"better-auth/plugins";import{createAccessControl as T}from"better-auth/plugins/access";import{admin as z}from"better-auth/plugins/admin";import{anonymous as O}from"better-auth/plugins/anonymous";import{bearer as q}from"better-auth/plugins/bearer";import{customSession as L}from"better-auth/plugins/custom-session";import{deviceAuthorization as j}from"better-auth/plugins/device-authorization";import{emailOTP as D}from"better-auth/plugins/email-otp";import{genericOAuth as H}from"better-auth/plugins/generic-oauth";import{haveIBeenPwned as K}from"better-auth/plugins/haveibeenpwned";import{jwt as R}from"better-auth/plugins/jwt";import{magicLink as G}from"better-auth/plugins/magic-link";import{multiSession as Q}from"better-auth/plugins/multi-session";import{oAuthProxy as V}from"better-auth/plugins/oauth-proxy";import{oneTimeToken as X}from"better-auth/plugins/one-time-token";import{organization as Z}from"better-auth/plugins/organization";import{phoneNumber as $}from"better-auth/plugins/phone-number";import{siwe as ro}from"better-auth/plugins/siwe";import{twoFactor as to}from"better-auth/plugins/two-factor";import{username as po}from"better-auth/plugins/username";export{z as admin,O as anonymous,f as apiKey,q as bearer,v as captcha,T as createAccessControl,i as createMcpProtectedRequestHandler,L as customSession,j as deviceAuthorization,D as emailOTP,H as genericOAuth,K as haveIBeenPwned,e as inviteOnly,R as jwt,y as lastLoginMethod,G as magicLink,n as mcp,Q as multiSession,V as oAuthProxy,s as oauthDeviceAuthorization,u as oauthProvider,P as oneTap,X as oneTimeToken,Z as organization,d as passkey,$ as phoneNumber,a as requireMcpAuth,A as scim,ro as siwe,to as twoFactor,m as uiConfig,po as username};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/auth",
3
- "version": "1.0.0-alpha.115",
3
+ "version": "1.0.0-alpha.117",
4
4
  "description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
5
5
  "keywords": [
6
6
  "auth",
@@ -1 +0,0 @@
1
- import{createLocalAccountIssuer as I}from"@better-auth/core/db";import{LunoraError as R}from"@lunora/errors";import{getAuthTables as b}from"better-auth/db";class p extends R{constructor(m,f){super(f,m,{name:"LunoraAuthAdminError"})}}const N=50,E=500,M=3600,D=M*24,U=100*365*24*60*60,T=2880*60*1e3,z=[{field:"userId",model:"member"},{field:"userId",model:"teamMember"},{field:"userId",model:"passkey"},{field:"userId",model:"twoFactor"},{field:"userId",model:"oauthAccessToken"},{field:"userId",model:"oauthRefreshToken"},{field:"userId",model:"oauthConsent"},{field:"userId",model:"deviceCode"},{field:"userId",model:"walletAddress"},{field:"inviterId",model:"invitation"}],L=new Set(["accessToken","backupCodes","idToken","password","publicKey","refreshToken","secret","token"]),k=d=>Math.min(Math.max(Math.trunc(d??N),1),E),C=d=>Math.max(0,Math.trunc(d??0)),u=d=>{const m={};for(const[f,l]of Object.entries(d))L.has(f)||(m[f]=l instanceof Date?l.getTime():l);return m},g=d=>Array.isArray(d)?d.join(","):d,S=d=>d.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),P=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),B={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},F=d=>d==="boolean"||d==="date"||d==="number"?d:"string",G=d=>{const m=[];for(const[f,l]of Object.entries(d))l.input===!1||l.references!==void 0||P.has(f)||m.push({name:f,plugin:B[f],required:l.required===!0,type:F(l.type),unique:l.unique===!0});return m},O=d=>typeof d=="string"&&d.includes("owner"),V=async(d,m)=>{const l=(await d.adapter.findMany({model:"member",where:[{field:"userId",value:m}]})).filter(w=>O(w.role)&&typeof w.organizationId=="string").map(w=>w.organizationId);if(l.length===0)return;const y=await Promise.all(l.map(async w=>d.adapter.findMany({model:"member",where:[{field:"organizationId",value:w}]}))),r=l.find((w,v)=>!y[v]?.some(a=>a.userId!==m&&O(a.role)));if(r!==void 0)throw new p(`user is the last owner of organization ${r} — transfer ownership or delete the organization first`,"LAST_ORGANIZATION_OWNER")},_=d=>{if(d instanceof p)return d;const m=d,f=m?.body?.code??m?.code??"AUTH_ADMIN_ERROR",l=m?.body?.message??m?.message??"auth admin operation failed";return new p(l,f)},$=(d,m={})=>{const f=d.$context,l=m.features??{},y=a=>{const e=new Set((a.plugins??[]).map(i=>i.id)),t=i=>e.has(i);return{accounts:l.accounts??!0,admin:l.admin??t("admin"),organization:l.organization??t("organization"),passkey:l.passkey??t("passkey"),twoFactor:l.twoFactor??t("two-factor")}},r=async a=>{try{return await a(await f)}catch(e){throw _(e)}},w=a=>u(a),v=async(a,e,t)=>{const i=t.where&&t.where.length>0?t.where:void 0,[n,s]=await Promise.all([a.adapter.findMany({limit:k(t.limit),model:e,offset:C(t.offset),sortBy:t.sortBy,where:i}),a.adapter.count({model:e,where:i})]);return{rows:n.map(o=>u(o)),total:s}};return{banUser:({expiresInSeconds:a,reason:e,userId:t})=>r(async i=>{let n=null;if(a!==void 0){if(!Number.isInteger(a)||a<=0)throw new p("expiresInSeconds must be a positive finite integer","INVALID_BAN_SECONDS");const o=Math.min(a,U);n=new Date(Date.now()+o*1e3)}const s=await i.internalAdapter.updateUser(t,{banExpires:n,banned:!0,banReason:e??"No reason"});return await i.internalAdapter.deleteUserSessions(t),w(s)}),cancelInvitation:({invitationId:a})=>r(async e=>{await e.adapter.delete({model:"invitation",where:[{field:"id",value:a}]})}),capabilities:()=>r(a=>Promise.resolve(y(a.options))),addMember:({organizationId:a,role:e,userId:t})=>r(async i=>{const n=await i.adapter.create({data:{createdAt:new Date,organizationId:a,role:e===void 0||e===""?"member":e,userId:t},model:"member"});return u(n)}),addTeamMember:({teamId:a,userId:e})=>r(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,teamId:a,userId:e},model:"teamMember"});return u(i)}),config:()=>r(a=>{const e=a.options,t=y(e),i=new Set((e.plugins??[]).map(c=>c.id)),n=b(e),s=e.session??{},o=e.rateLimit??{};return Promise.resolve({capabilities:t,emailAndPassword:e.emailAndPassword?.enabled??!1,organization:{enabled:t.organization,roles:!!n.organizationRole,teams:!!n.team},plugins:[...i].toSorted((c,h)=>c.localeCompare(h)),rateLimit:{enabled:o.enabled??!1,max:o.max,window:o.window},session:{cookieCache:s.cookieCache?.enabled,expiresIn:s.expiresIn,freshAge:s.freshAge,updateAge:s.updateAge},socialProviders:Object.keys(e.socialProviders??{}).toSorted((c,h)=>c.localeCompare(h)),userFields:G(n.user?.fields??{})})}),createOrganization:({logo:a,metadata:e,name:t,ownerId:i,slug:n})=>r(async s=>{const o=S(n!==void 0&&n!==""?n:t);if(o==="")throw new p("could not derive a slug from the organization name","ORG_SLUG_INVALID");if(await s.adapter.findOne({model:"organization",where:[{field:"slug",value:o}]}))throw new p("an organization with this slug already exists","ORG_SLUG_TAKEN");const h=await s.adapter.create({data:{createdAt:new Date,logo:a===void 0||a===""?void 0:a,metadata:e===void 0?void 0:JSON.stringify(e),name:t,slug:o},model:"organization"});return i!==void 0&&i!==""&&await s.adapter.create({data:{createdAt:new Date,organizationId:h.id,role:"owner",userId:i},model:"member"}),u(h)}),createOrgRole:({organizationId:a,permission:e,role:t})=>r(async i=>{const n=await i.adapter.create({data:{createdAt:new Date,organizationId:a,permission:JSON.stringify(e),role:t},model:"organizationRole"});return u(n)}),createTeam:({name:a,organizationId:e})=>r(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,name:a,organizationId:e},model:"team"});return u(i)}),deleteOrganization:({organizationId:a})=>r(async e=>{const t=b(e.options);if(await e.adapter.deleteMany({model:"member",where:[{field:"organizationId",value:a}]}),await e.adapter.deleteMany({model:"invitation",where:[{field:"organizationId",value:a}]}),t.team){const i=await e.adapter.findMany({model:"team",where:[{field:"organizationId",value:a}]});for(const n of i)await e.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:n.id}]});await e.adapter.deleteMany({model:"team",where:[{field:"organizationId",value:a}]})}t.organizationRole&&await e.adapter.deleteMany({model:"organizationRole",where:[{field:"organizationId",value:a}]}),await e.adapter.delete({model:"organization",where:[{field:"id",value:a}]})}),deleteOrgRole:({roleId:a})=>r(async e=>{await e.adapter.delete({model:"organizationRole",where:[{field:"id",value:a}]})}),inviteMember:({email:a,inviterId:e,organizationId:t,role:i})=>r(async n=>{let s=e;if(s===void 0||s===""){const c=await n.adapter.findMany({model:"member",where:[{field:"organizationId",value:t}]});s=(c.find(A=>typeof A.role=="string"&&A.role.includes("owner"))??c[0])?.userId}if(s===void 0||s==="")throw new p("provide an inviter — the organization has no members to attribute the invitation to","INVITER_REQUIRED");const o=await n.adapter.create({data:{createdAt:new Date,email:a.toLowerCase(),expiresAt:new Date(Date.now()+T),inviterId:s,organizationId:t,role:i===void 0||i===""?"member":i,status:"pending"},model:"invitation"});return u(o)}),listOrgRoles:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"organizationRole",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listTeamMembers:({limit:a,offset:e,teamId:t})=>r(i=>v(i,"teamMember",{limit:a,offset:e,where:[{field:"teamId",value:t}]})),listTeams:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"team",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),removeTeam:({teamId:a})=>r(async e=>{await e.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:a}]}),await e.adapter.delete({model:"team",where:[{field:"id",value:a}]})}),removeTeamMember:({teamMemberId:a})=>r(async e=>{await e.adapter.delete({model:"teamMember",where:[{field:"id",value:a}]})}),updateMemberRole:({memberId:a,role:e})=>r(async t=>{const i=await t.adapter.update({model:"member",update:{role:g(e)},where:[{field:"id",value:a}]});return u(i??{id:a,role:g(e)})}),updateOrganization:({logo:a,metadata:e,name:t,organizationId:i,slug:n})=>r(async s=>{const o={};if(t!==void 0&&(o.name=t),n!==void 0&&n!==""&&(o.slug=S(n)),a!==void 0&&(o.logo=a===""?void 0:a),e!==void 0&&(o.metadata=JSON.stringify(e)),Object.keys(o).length===0)return u({id:i});const c=await s.adapter.update({model:"organization",update:o,where:[{field:"id",value:i}]});return u(c??{id:i})}),updateOrgRole:({permission:a,roleId:e})=>r(async t=>{const i=await t.adapter.update({model:"organizationRole",update:{permission:JSON.stringify(a),updatedAt:new Date},where:[{field:"id",value:e}]});return u(i??{id:e,permission:JSON.stringify(a)})}),updateTeam:({name:a,teamId:e})=>r(async t=>{const i=await t.adapter.update({model:"team",update:{name:a,updatedAt:new Date},where:[{field:"id",value:e}]});return u(i??{id:e,name:a})}),createUser:({data:a,email:e,name:t,password:i,role:n})=>r(async s=>{const o=e.toLowerCase();if(await s.internalAdapter.findUserByEmail(o))throw new p("a user with this email already exists","USER_ALREADY_EXISTS");const c=await s.internalAdapter.createUser({email:o,name:t,role:n===void 0?void 0:g(n),...a},{method:"admin"});if(i!==void 0&&i!==""){const h=await s.password.hash(i);await s.internalAdapter.linkAccount({accountId:c.id,issuer:I("credential"),password:h,providerId:"credential",userId:c.id})}return w(c)}),deletePasskey:({passkeyId:a})=>r(async e=>{await e.adapter.delete({model:"passkey",where:[{field:"id",value:a}]})}),disableTwoFactor:({userId:a})=>r(async e=>{await e.adapter.deleteMany({model:"twoFactor",where:[{field:"userId",value:a}]}),await e.internalAdapter.updateUser(a,{twoFactorEnabled:!1})}),impersonateUser:({userId:a})=>r(async e=>{const t=await e.internalAdapter.findUserById(a);if(!t)throw new p("user not found","USER_NOT_FOUND");const i=m.impersonationSeconds;let n=M;if(i!==void 0){if(!Number.isInteger(i)||!Number.isFinite(i)||i<=0)throw new p("impersonationSeconds must be a positive finite integer","INVALID_IMPERSONATION_SECONDS");n=Math.min(i,D)}const s=new Date(Date.now()+n*1e3),o=await e.internalAdapter.createSession(a,!0,{expiresAt:s,impersonatedBy:m.impersonatedBy??a},!0);return{expiresAt:o.expiresAt instanceof Date?o.expiresAt.getTime():s.getTime(),token:o.token,user:w(t)}}),listAccounts:({userId:a})=>r(async e=>(await e.adapter.findMany({model:"account",where:[{field:"userId",value:a}]})).map(i=>u(i))),listInvitations:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"invitation",{limit:a,offset:e,where:[{field:"organizationId",value:t}]})),listMembers:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"member",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listOrganizations:({limit:a,offset:e})=>r(t=>v(t,"organization",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"}})),listPasskeys:({userId:a})=>r(async e=>(await e.adapter.findMany({model:"passkey",where:[{field:"userId",value:a}]})).map(i=>u(i))),listSessions:({limit:a,offset:e,userId:t})=>r(i=>v(i,"session",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:t===void 0||t===""?void 0:[{field:"userId",value:t}]})),listUsers:({filterField:a,filterValue:e,limit:t,offset:i,search:n,searchField:s,sortBy:o,sortDirection:c})=>r(h=>{const A=[];return n!==void 0&&n!==""&&A.push({field:s??"email",operator:"contains",value:n}),e!==void 0&&A.push({field:a??"email",operator:"eq",value:e}),v(h,"user",{limit:t,offset:i,sortBy:{direction:c??"desc",field:o??"createdAt"},where:A})}),removeMember:({memberId:a})=>r(async e=>{await e.adapter.delete({model:"member",where:[{field:"id",value:a}]})}),removeUser:({userId:a})=>r(async e=>{const t=b(e.options);t.member&&await V(e,a),await Promise.all(z.filter(({model:i})=>t[i]).map(({field:i,model:n})=>e.adapter.deleteMany({model:n,where:[{field:i,value:a}]}))),await e.internalAdapter.deleteUserSessions(a),await e.internalAdapter.deleteUser(a)}),revokeUserSession:({sessionId:a})=>r(async e=>{const t=await e.adapter.findOne({model:"session",where:[{field:"id",value:a}]});t?.token&&await e.internalAdapter.deleteSession(t.token)}),revokeUserSessions:({userId:a})=>r(async e=>{await e.internalAdapter.deleteUserSessions(a)}),setRole:({role:a,userId:e})=>r(async t=>{const i=await t.internalAdapter.updateUser(e,{role:g(a)});return w(i)}),setUserPassword:({newPassword:a,userId:e})=>r(async t=>{const i=t.password.config.minPasswordLength,n=t.password.config.maxPasswordLength;if(a.length<i)throw new p(`password must be at least ${i.toString()} characters`,"PASSWORD_TOO_SHORT");if(a.length>n)throw new p(`password must be at most ${n.toString()} characters`,"PASSWORD_TOO_LONG");if(!await t.internalAdapter.findUserById(e))throw new p("user not found","USER_NOT_FOUND");const o=(await t.internalAdapter.findAccounts(e)).some(h=>h.providerId==="credential");if(!o&&t.options.emailAndPassword?.enabled!==!0)throw new p("email/password sign-in is disabled for this deployment","EMAIL_PASSWORD_DISABLED");const c=await t.password.hash(a);o?await t.internalAdapter.updatePassword(e,c):await t.internalAdapter.linkAccount({accountId:e,issuer:I("credential"),password:c,providerId:"credential",userId:e})}),unbanUser:({userId:a})=>r(async e=>{const t=await e.internalAdapter.updateUser(a,{banExpires:null,banned:!1,banReason:null});return w(t)}),unlinkAccount:({accountId:a,userId:e})=>r(async t=>{await t.adapter.delete({model:"account",where:[{field:"id",value:a},{connector:"AND",field:"userId",value:e}]})}),updateUser:({data:a,userId:e})=>r(async t=>{const i=await t.internalAdapter.updateUser(e,a);return w(i)})}};export{p as LunoraAuthAdminError,$ as createAuthAdmin};