@lunora/auth 1.0.0-alpha.116 → 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
@@ -131,13 +131,16 @@ const auth = createAuth({
131
131
 
132
132
  // …from your own admin-authorized code:
133
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}`;
134
135
  ```
135
136
 
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.
137
138
 
138
- - **An invitation is keyed by email and nothing else** — no secret token — so anyone who learns an invited address can spend that seat. `requireEmailVerification` does **not** close this: the user row is written before the token is mailed, so the attacker still creates an account and still burns the invitation; what it buys is that they hold no session until someone clicks a link that lands in the invitee's inbox. Recovery is `AuthAdmin.removeUser` plus a fresh invitation. The plugin warns on startup when password sign-up runs without verification.
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.
139
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.
140
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.
141
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.
142
145
 
143
146
  ### Security / audit trail
package/dist/index.d.mts CHANGED
@@ -11,7 +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 S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-B9FBGvtL.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";
15
15
  export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.mjs";
16
16
  export { default as authTables } from "./schema.mjs";
17
17
  export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.mjs";
@@ -99,6 +99,28 @@ interface AuthInvitation {
99
99
  role?: null | string;
100
100
  status?: null | string;
101
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
+ }
102
124
  /** One team row (from the `organization` plugin with `teams.enabled`). */
103
125
  interface AuthTeam {
104
126
  [key: string]: unknown;
@@ -154,6 +176,8 @@ interface AuthCapabilities {
154
176
  accounts: boolean;
155
177
  /** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
156
178
  admin: boolean;
179
+ /** The `inviteOnly` plugin: sign-up invitations. */
180
+ inviteOnly: boolean;
157
181
  /** The `organization` plugin: orgs, members, invitations. */
158
182
  organization: boolean;
159
183
  /** The `@better-auth/passkey` plugin: per-user passkeys. */
@@ -290,6 +314,17 @@ interface AuthAdmin {
290
314
  permission: Record<string, string[]>;
291
315
  role: string;
292
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>;
293
328
  /** Create a team under an organization. */
294
329
  createTeam: (input: {
295
330
  name: string;
@@ -357,6 +392,16 @@ interface AuthAdmin {
357
392
  offset?: number;
358
393
  userId?: string;
359
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>>;
360
405
  /** List a team's members. */
361
406
  listTeamMembers: (options: {
362
407
  limit?: number;
@@ -384,6 +429,10 @@ interface AuthAdmin {
384
429
  removeUser: (input: {
385
430
  userId: string;
386
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>;
387
436
  revokeUserSession: (input: {
388
437
  sessionId: string;
389
438
  }) => Promise<void>;
@@ -966,4 +1015,4 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
966
1015
  * with the same 60s cookie cache as `rolling`.
967
1016
  */
968
1017
  declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
969
- 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,7 +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 S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-3ui3t5xQ.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";
15
15
  export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.js";
16
16
  export { default as authTables } from "./schema.js";
17
17
  export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.js";
@@ -99,6 +99,28 @@ interface AuthInvitation {
99
99
  role?: null | string;
100
100
  status?: null | string;
101
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
+ }
102
124
  /** One team row (from the `organization` plugin with `teams.enabled`). */
103
125
  interface AuthTeam {
104
126
  [key: string]: unknown;
@@ -154,6 +176,8 @@ interface AuthCapabilities {
154
176
  accounts: boolean;
155
177
  /** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
156
178
  admin: boolean;
179
+ /** The `inviteOnly` plugin: sign-up invitations. */
180
+ inviteOnly: boolean;
157
181
  /** The `organization` plugin: orgs, members, invitations. */
158
182
  organization: boolean;
159
183
  /** The `@better-auth/passkey` plugin: per-user passkeys. */
@@ -290,6 +314,17 @@ interface AuthAdmin {
290
314
  permission: Record<string, string[]>;
291
315
  role: string;
292
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>;
293
328
  /** Create a team under an organization. */
294
329
  createTeam: (input: {
295
330
  name: string;
@@ -357,6 +392,16 @@ interface AuthAdmin {
357
392
  offset?: number;
358
393
  userId?: string;
359
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>>;
360
405
  /** List a team's members. */
361
406
  listTeamMembers: (options: {
362
407
  limit?: number;
@@ -384,6 +429,10 @@ interface AuthAdmin {
384
429
  removeUser: (input: {
385
430
  userId: string;
386
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>;
387
436
  revokeUserSession: (input: {
388
437
  sessionId: string;
389
438
  }) => Promise<void>;
@@ -966,4 +1015,4 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
966
1015
  * with the same 60s cookie cache as `rolling`.
967
1016
  */
968
1017
  declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
969
- 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 A,createAuthAdmin as u}from"./packem_shared/LunoraAuthAdminError-DtAKa22S.mjs";import{AUTH_AUDIT_TABLE as n,appendAuthAuditEntry as m,createAuthAuditReader as l,ensureAuthAuditTable as d,readAuthAuditLog as p}from"./audit.mjs";import{authAuditHook as E,buildAuditEntry as T,eventForPath as f,withAuthAudit as _}from"./packem_shared/authAuditHook-tG8gdR77.mjs";import{READ_AUDIT_PATH as S,INTERNAL_SECRET_HEADER as D,RESOLVE_SESSION_PATH as H,LunoraAuthDO as c}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 P,authDoSchemaStatements as v}from"./packem_shared/authDoColumnAdditions-BCSs7qaN.mjs";import{createDoAuthWiring as g}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,revokeSignUpInvitation as j}from"./packem_shared/createSignUpInvitation-CxD5Ov8R.mjs";import{LunoraAuthHeadersError as J,withAuthPlugins as K}from"./middleware.mjs";import{compileMigrationsSql as X,ensureMigrated as Z}from"./packem_shared/compileMigrationsSql-TvwFrqmu.mjs";import{default as tt}from"./schema.mjs";import{sessionPresets as rt,validateSessionPolicy as ot}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{createSqlAuthStore as it,d1Executor as At}from"./sql-store.mjs";import{createMemoryAuthStore as st,matchesWhere as nt}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as lt,verifyTurnstile as dt}from"./turnstile.mjs";import{verifyTurnstileMiddleware as ht}from"./turnstile-middleware.mjs";export{n as AUTH_AUDIT_TABLE,S 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,c as LunoraAuthDO,J as LunoraAuthHeadersError,lt as TURNSTILE_VERIFY_ENDPOINT,m as appendAuthAuditEntry,M as assertEmailAllowed,E as authAuditHook,P as authDoColumnAdditions,v as authDoSchemaStatements,tt as authTables,T as buildAuditEntry,k as classifyEmail,X as compileMigrationsSql,I as createAuth,u as createAuthAdmin,l as createAuthAuditReader,g as createDoAuthWiring,st as createMemoryAuthStore,W as createSignUpInvitation,it as createSqlAuthStore,At as d1Executor,N as emailGateDatabaseHooks,q as emailGateMiddleware,d as ensureAuthAuditTable,Z as ensureMigrated,f as eventForPath,B as handleAuthRequest,Y as listSignUpInvitations,C as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,nt as matchesWhere,p as readAuthAuditLog,R as resolveAuthOptions,j as revokeSignUpInvitation,rt as sessionPresets,ot as validateSessionPolicy,dt as verifyTurnstile,ht as verifyTurnstileMiddleware,_ as withAuthAudit,K as withAuthPlugins,w 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};
@@ -12,6 +12,16 @@ interface SignUpInvitation {
12
12
  /** Free-form attribution (a user id, an operator name); never read by the gate. */
13
13
  invitedBy: null | string;
14
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
+ }
15
25
  /** Options for {@link inviteOnly}. */
16
26
  interface InviteOnlyOptions {
17
27
  /**
@@ -57,12 +67,16 @@ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
57
67
  * Nothing prunes the table — a spent or expired row stays until you delete it with
58
68
  * {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
59
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.
60
74
  */
61
75
  declare const createSignUpInvitation: (auth: LunoraAuth, input: {
62
76
  email: string;
63
77
  expiresInSeconds?: number;
64
78
  invitedBy?: string;
65
- }) => Promise<SignUpInvitation>;
79
+ }) => Promise<IssuedSignUpInvitation>;
66
80
  /**
67
81
  * The most recent invitations, newest first, up to a fixed ceiling of
68
82
  * {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
@@ -92,4 +106,21 @@ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
92
106
  declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
93
107
  email: string;
94
108
  }) => Promise<void>;
95
- export { InviteOnlyOptions as I, SignUpInvitation as S, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, revokeSignUpInvitation as r };
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 };
@@ -12,6 +12,16 @@ interface SignUpInvitation {
12
12
  /** Free-form attribution (a user id, an operator name); never read by the gate. */
13
13
  invitedBy: null | string;
14
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
+ }
15
25
  /** Options for {@link inviteOnly}. */
16
26
  interface InviteOnlyOptions {
17
27
  /**
@@ -57,12 +67,16 @@ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
57
67
  * Nothing prunes the table — a spent or expired row stays until you delete it with
58
68
  * {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
59
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.
60
74
  */
61
75
  declare const createSignUpInvitation: (auth: LunoraAuth, input: {
62
76
  email: string;
63
77
  expiresInSeconds?: number;
64
78
  invitedBy?: string;
65
- }) => Promise<SignUpInvitation>;
79
+ }) => Promise<IssuedSignUpInvitation>;
66
80
  /**
67
81
  * The most recent invitations, newest first, up to a fixed ceiling of
68
82
  * {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
@@ -92,4 +106,21 @@ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
92
106
  declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
93
107
  email: string;
94
108
  }) => Promise<void>;
95
- export { InviteOnlyOptions as I, SignUpInvitation as S, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, revokeSignUpInvitation as r };
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 };
@@ -12,7 +12,14 @@ type I as InviteOnlyOptions,
12
12
  * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
13
13
  * re-export.
14
14
  */
15
- type S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-B9FBGvtL.mjs";
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";
16
23
  import { BetterAuthPlugin } from 'better-auth';
17
24
  export { apiKey } from '@better-auth/api-key';
18
25
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
package/dist/plugins.d.ts CHANGED
@@ -12,7 +12,14 @@ type I as InviteOnlyOptions,
12
12
  * Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
13
13
  * re-export.
14
14
  */
15
- type S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-3ui3t5xQ.js";
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";
16
23
  import { BetterAuthPlugin } from 'better-auth';
17
24
  export { apiKey } from '@better-auth/api-key';
18
25
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
package/dist/plugins.mjs CHANGED
@@ -1 +1 @@
1
- import{inviteOnly as e}from"./packem_shared/createSignUpInvitation-CxD5Ov8R.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};
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.116",
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};
@@ -1 +0,0 @@
1
- import{defineErrorCodes as v}from"@better-auth/core/utils/error-codes";import{LunoraError as p}from"@lunora/errors";import{APIError as E}from"better-auth/api";const s="signUpInvitation",I=10080*60,w=365*24*60*60,h=500,A=v({SIGN_UP_INVITE_REQUIRED:"sign-up is invite-only — ask an administrator for an invitation"}),D=/^[^\s@]+@[^\s@]+$/,g=e=>{if(!D.test(e))return!1;const t=e.slice(e.indexOf("@")+1);return t.includes(".")&&!t.startsWith(".")&&!t.endsWith(".")},u=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}),f=e=>e.trim().toLowerCase(),y=e=>{if(typeof e.email!="string")return;const t=f(e.email);return t===""?void 0:t},O=async(e,t)=>{const i=await e.findOne({model:s,where:[{field:"email",value:t}]});return i===null||i.acceptedAt instanceof Date?!1:i.expiresAt instanceof Date&&i.expiresAt.getTime()>Date.now()},S=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.")},T=(e={})=>{const t=e.allowFirstUser??!1;return{$ERROR_CODES:A,id:"lunora-invite-only",init:i=>{S(i.options);const{adapter:n}=i;let a=t;return{options:{databaseHooks:{user:{create:{after:async d=>{const o=y(d);o!==void 0&&await n.update({model:s,update:{acceptedAt:new Date},where:[{field:"email",value:o}]})},before:async d=>{const o=y(d);if(!(o!==void 0&&await O(n,o))){if(a){if(await n.count({model:"user"})===0)return;a=!1}throw new E("BAD_REQUEST",A.SIGN_UP_INVITE_REQUIRED)}}}}}}}},schema:{[s]:{fields:{acceptedAt:{required:!1,type:"date"},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"}}}}}},U=async(e,t)=>{const i=f(t.email);if(!g(i))throw new p("VALIDATION_ERROR",`not an email address to invite: ${JSON.stringify(t.email)}`);const{expiresInSeconds:n=I}=t;if(!Number.isInteger(n)||n<=0||n>w)throw new p("VALIDATION_ERROR",`expiresInSeconds must be a positive integer no greater than ${String(w)}`);const a=new Date(Date.now()+n*1e3),r=t.invitedBy??null,c=await e.$context,d=[{field:"email",value:i}],o=async()=>c.adapter.update({model:s,update:{acceptedAt:null,expiresAt:a,invitedBy:r},where:d});if(await c.adapter.findOne({model:s,where:d})){const l=await o();if(l)return u(l)}try{return u(await c.adapter.create({data:{createdAt:new Date,email:i,expiresAt:a,invitedBy:r},model:s}))}catch(l){const m=await o();if(m===null)throw l;return u(m)}},N=async(e,t={})=>{const a=(await(await e.$context).adapter.findMany({limit:h,model:s,sortBy:{direction:"desc",field:"createdAt"}})).map(r=>u(r));return t.pendingOnly===!0?a.filter(r=>r.acceptedAt===null&&r.expiresAt.getTime()>Date.now()):a},L=async(e,t)=>{await(await e.$context).adapter.delete({model:s,where:[{field:"email",value:f(t.email)}]})};export{U as createSignUpInvitation,T as inviteOnly,N as listSignUpInvitations,L as revokeSignUpInvitation};