@lunora/auth 1.0.0-alpha.114 → 1.0.0-alpha.116

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,32 @@ 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
+ ```
135
+
136
+ 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
+ - **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
+ - **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
+ - **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.
141
+ - **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
+
117
143
  ### Security / audit trail
118
144
 
119
145
  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 S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-B9FBGvtL.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";
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 S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-3ui3t5xQ.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";
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-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};
@@ -0,0 +1 @@
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};
@@ -0,0 +1,95 @@
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
+ /** Options for {@link inviteOnly}. */
16
+ interface InviteOnlyOptions {
17
+ /**
18
+ * Let the very first account through uninvited, so a fresh deployment can be
19
+ * bootstrapped without seeding a row.
20
+ *
21
+ * **Off by default.** The check is "the `user` table is empty", which two
22
+ * concurrent sign-ups can both observe, and the window it opens is the gap
23
+ * between deploying and the owner signing up — whoever finds the URL first
24
+ * gets an account on a deployment whose whole point is that nobody does. Seed
25
+ * the first invitation with {@link createSignUpInvitation} instead (a one-off
26
+ * call at worker init, or an internal mutation you run once).
27
+ * @default false
28
+ */
29
+ allowFirstUser?: boolean;
30
+ }
31
+ /**
32
+ * A better-auth server plugin that refuses to create an account for an address
33
+ * with no unspent invitation, and the `signUpInvitation` table those live in.
34
+ *
35
+ * Issue invitations with {@link createSignUpInvitation}; there is no HTTP
36
+ * endpoint for it on purpose. Who counts as an administrator is your
37
+ * application's question — call it from a mutation you already authorize, the
38
+ * same trust model `createAuthAdmin` documents.
39
+ *
40
+ * The return type is better-auth's own `BetterAuthPlugin` rather than the
41
+ * precise shape of the schema map, for the reason `./ui-config.ts` spells out:
42
+ * an anonymous inferred type is the difference between a build that emits
43
+ * declarations and one that fails in the bundler alone.
44
+ */
45
+ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
46
+ /**
47
+ * Invite `email` to sign up, or refresh an existing invitation for it.
48
+ *
49
+ * Re-inviting an address updates the row in place — a new expiry, and `acceptedAt`
50
+ * cleared — because `email` is unique. That is also how you re-open a seat after
51
+ * deleting the account that took it.
52
+ *
53
+ * This is a trusted server-side call with no authorization of its own; gate it
54
+ * the way you gate any other administrative action. Delivering the invitation is
55
+ * yours too: nothing here sends mail, so the returned row is the whole handoff.
56
+ *
57
+ * Nothing prunes the table — a spent or expired row stays until you delete it with
58
+ * {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
59
+ * let in.
60
+ */
61
+ declare const createSignUpInvitation: (auth: LunoraAuth, input: {
62
+ email: string;
63
+ expiresInSeconds?: number;
64
+ invitedBy?: string;
65
+ }) => Promise<SignUpInvitation>;
66
+ /**
67
+ * The most recent invitations, newest first, up to a fixed ceiling of
68
+ * {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
69
+ * list an operator usually wants; the unfiltered form doubles as the record of who
70
+ * was let in.
71
+ *
72
+ * Deliberately not paged. "Pending" is two conditions, one of them a comparison
73
+ * against `now`, and filtering those after a page would let page 1 come back empty
74
+ * while pending invitations sat on page 2. An operator list that outgrows the
75
+ * ceiling wants a query against the `signUpInvitation` table, not an offset.
76
+ */
77
+ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
78
+ pendingOnly?: boolean;
79
+ }) => Promise<SignUpInvitation[]>;
80
+ /**
81
+ * Withdraw the invitation for `email`. Deletes the row, so it also forgets a spent
82
+ * one — the account it created is untouched, and removing that is
83
+ * `AuthAdmin.removeUser`'s job.
84
+ *
85
+ * Not retroactive, and not atomic against a sign-up already in flight: better-auth
86
+ * creates the user without wrapping the `before` hook and the insert in one
87
+ * transaction, so a revoke landing between the two lets that one account through.
88
+ * There is no conditional consume in the adapter contract to close it with. Treat
89
+ * revocation as "no further sign-ups", and `AuthAdmin.removeUser` as the way to
90
+ * undo one that already happened.
91
+ */
92
+ declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
93
+ email: string;
94
+ }) => Promise<void>;
95
+ export { InviteOnlyOptions as I, SignUpInvitation as S, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, revokeSignUpInvitation as r };
@@ -0,0 +1,95 @@
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
+ /** Options for {@link inviteOnly}. */
16
+ interface InviteOnlyOptions {
17
+ /**
18
+ * Let the very first account through uninvited, so a fresh deployment can be
19
+ * bootstrapped without seeding a row.
20
+ *
21
+ * **Off by default.** The check is "the `user` table is empty", which two
22
+ * concurrent sign-ups can both observe, and the window it opens is the gap
23
+ * between deploying and the owner signing up — whoever finds the URL first
24
+ * gets an account on a deployment whose whole point is that nobody does. Seed
25
+ * the first invitation with {@link createSignUpInvitation} instead (a one-off
26
+ * call at worker init, or an internal mutation you run once).
27
+ * @default false
28
+ */
29
+ allowFirstUser?: boolean;
30
+ }
31
+ /**
32
+ * A better-auth server plugin that refuses to create an account for an address
33
+ * with no unspent invitation, and the `signUpInvitation` table those live in.
34
+ *
35
+ * Issue invitations with {@link createSignUpInvitation}; there is no HTTP
36
+ * endpoint for it on purpose. Who counts as an administrator is your
37
+ * application's question — call it from a mutation you already authorize, the
38
+ * same trust model `createAuthAdmin` documents.
39
+ *
40
+ * The return type is better-auth's own `BetterAuthPlugin` rather than the
41
+ * precise shape of the schema map, for the reason `./ui-config.ts` spells out:
42
+ * an anonymous inferred type is the difference between a build that emits
43
+ * declarations and one that fails in the bundler alone.
44
+ */
45
+ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
46
+ /**
47
+ * Invite `email` to sign up, or refresh an existing invitation for it.
48
+ *
49
+ * Re-inviting an address updates the row in place — a new expiry, and `acceptedAt`
50
+ * cleared — because `email` is unique. That is also how you re-open a seat after
51
+ * deleting the account that took it.
52
+ *
53
+ * This is a trusted server-side call with no authorization of its own; gate it
54
+ * the way you gate any other administrative action. Delivering the invitation is
55
+ * yours too: nothing here sends mail, so the returned row is the whole handoff.
56
+ *
57
+ * Nothing prunes the table — a spent or expired row stays until you delete it with
58
+ * {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
59
+ * let in.
60
+ */
61
+ declare const createSignUpInvitation: (auth: LunoraAuth, input: {
62
+ email: string;
63
+ expiresInSeconds?: number;
64
+ invitedBy?: string;
65
+ }) => Promise<SignUpInvitation>;
66
+ /**
67
+ * The most recent invitations, newest first, up to a fixed ceiling of
68
+ * {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
69
+ * list an operator usually wants; the unfiltered form doubles as the record of who
70
+ * was let in.
71
+ *
72
+ * Deliberately not paged. "Pending" is two conditions, one of them a comparison
73
+ * against `now`, and filtering those after a page would let page 1 come back empty
74
+ * while pending invitations sat on page 2. An operator list that outgrows the
75
+ * ceiling wants a query against the `signUpInvitation` table, not an offset.
76
+ */
77
+ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
78
+ pendingOnly?: boolean;
79
+ }) => Promise<SignUpInvitation[]>;
80
+ /**
81
+ * Withdraw the invitation for `email`. Deletes the row, so it also forgets a spent
82
+ * one — the account it created is untouched, and removing that is
83
+ * `AuthAdmin.removeUser`'s job.
84
+ *
85
+ * Not retroactive, and not atomic against a sign-up already in flight: better-auth
86
+ * creates the user without wrapping the `before` hook and the insert in one
87
+ * transaction, so a revoke landing between the two lets that one account through.
88
+ * There is no conditional consume in the adapter contract to close it with. Treat
89
+ * revocation as "no further sign-ups", and `AuthAdmin.removeUser` as the way to
90
+ * undo one that already happened.
91
+ */
92
+ declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
93
+ email: string;
94
+ }) => Promise<void>;
95
+ export { InviteOnlyOptions as I, SignUpInvitation as S, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, revokeSignUpInvitation as r };
@@ -1,3 +1,18 @@
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 S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-B9FBGvtL.mjs";
1
16
  import { BetterAuthPlugin } from 'better-auth';
2
17
  export { apiKey } from '@better-auth/api-key';
3
18
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
@@ -24,6 +39,7 @@ export { phoneNumber } from 'better-auth/plugins/phone-number';
24
39
  export { siwe } from 'better-auth/plugins/siwe';
25
40
  export { twoFactor } from 'better-auth/plugins/two-factor';
26
41
  export { username } from 'better-auth/plugins/username';
42
+ import "./packem_shared/create-auth.d-hkN1GE5-.mjs";
27
43
  /** Organization sub-features a UI branches on. */
28
44
  interface UiConfigOrganization {
29
45
  /** Whether an ordinary user may create one at all. */
package/dist/plugins.d.ts CHANGED
@@ -1,3 +1,18 @@
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 S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-3ui3t5xQ.js";
1
16
  import { BetterAuthPlugin } from 'better-auth';
2
17
  export { apiKey } from '@better-auth/api-key';
3
18
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
@@ -24,6 +39,7 @@ export { phoneNumber } from 'better-auth/plugins/phone-number';
24
39
  export { siwe } from 'better-auth/plugins/siwe';
25
40
  export { twoFactor } from 'better-auth/plugins/two-factor';
26
41
  export { username } from 'better-auth/plugins/username';
42
+ import "./packem_shared/create-auth.d-hkN1GE5-.js";
27
43
  /** Organization sub-features a UI branches on. */
28
44
  interface UiConfigOrganization {
29
45
  /** 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-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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/auth",
3
- "version": "1.0.0-alpha.114",
3
+ "version": "1.0.0-alpha.116",
4
4
  "description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
5
5
  "keywords": [
6
6
  "auth",
@@ -103,8 +103,8 @@
103
103
  "@better-auth/oauth-provider": "1.7.1",
104
104
  "@better-auth/passkey": "1.7.1",
105
105
  "@better-auth/scim": "1.7.1",
106
- "@lunora/errors": "1.0.0-alpha.30",
107
- "@lunora/values": "1.0.0-alpha.38",
106
+ "@lunora/errors": "1.0.0-alpha.31",
107
+ "@lunora/values": "1.0.0-alpha.39",
108
108
  "@visulima/disposable-email-domains": "1.1.0",
109
109
  "@visulima/email-verifier": "1.0.2",
110
110
  "@visulima/free-email-domains": "1.0.1",