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