@lunora/auth 1.0.0-alpha.116 → 1.0.0-alpha.118
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 +18 -10
- package/dist/index.d.mts +51 -2
- package/dist/index.d.ts +51 -2
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/{AUTH_DO_AUDIT_PATH-DiXHh-vn.mjs → AUTH_DO_AUDIT_PATH-CS1FCCoC.mjs} +1 -1
- package/dist/packem_shared/LunoraAuthAdminError-BIpCQR2m.mjs +1 -0
- package/dist/packem_shared/authAuditHook-Bmq06hsp.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-CCvK7g57.mjs +1 -0
- package/dist/packem_shared/{createAuth-CtTKaLZN.mjs → createAuth-BI6qU0c_.mjs} +1 -1
- package/dist/packem_shared/{createDoAuthWiring-Ber4dHOy.mjs → createDoAuthWiring-D0pT8Y9R.mjs} +1 -1
- package/dist/packem_shared/createSignUpInvitation-CqqQc4S8.mjs +1 -0
- package/dist/packem_shared/{invite-only.d-3ui3t5xQ.d.ts → invite-only.d-C9Su82Iq.d.ts} +33 -2
- package/dist/packem_shared/{invite-only.d-B9FBGvtL.d.mts → invite-only.d-DsVdKwcp.d.mts} +33 -2
- package/dist/plugins.d.mts +8 -1
- package/dist/plugins.d.ts +8 -1
- package/dist/plugins.mjs +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/LunoraAuthAdminError-DtAKa22S.mjs +0 -1
- package/dist/packem_shared/authAuditHook-tG8gdR77.mjs +0 -1
- package/dist/packem_shared/compileMigrationsSql-TvwFrqmu.mjs +0 -1
- package/dist/packem_shared/createSignUpInvitation-CxD5Ov8R.mjs +0 -1
package/README.md
CHANGED
|
@@ -57,20 +57,25 @@ pnpm add @lunora/auth
|
|
|
57
57
|
```ts
|
|
58
58
|
import { createAuth, ensureMigrated, handleAuthRequest, lunoraD1Adapter } from "@lunora/auth";
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
const options = { secret: env.AUTH_SECRET, emailAndPassword: { enabled: true } };
|
|
61
|
+
|
|
62
|
+
// Prefer lunoraD1Adapter over passing raw env.DB — the raw binding makes
|
|
63
|
+
// better-auth resolve its Kysely adapter via a dynamic import that hangs
|
|
64
|
+
// under @cloudflare/vite-plugin's dev runner.
|
|
65
|
+
const auth = createAuth({ ...options, database: lunoraD1Adapter(env.DB) });
|
|
66
|
+
|
|
67
|
+
// A SECOND instance, over the RAW binding, only for migrating: better-auth
|
|
68
|
+
// migrates through its Kysely adapter alone and throws on lunoraD1Adapter. Build
|
|
69
|
+
// it once and keep it — `ensureMigrated` single-flights on the options object it
|
|
70
|
+
// is handed, so a `createAuth({...})` built inside `fetch` is a fresh key every
|
|
71
|
+
// request and the migration re-runs on each one.
|
|
72
|
+
const migrationAuth = createAuth({ ...options, database: env.DB });
|
|
68
73
|
|
|
69
74
|
// In your Worker's fetch handler, route /api/auth/* to better-auth and fall
|
|
70
75
|
// through to the Lunora worker for everything else:
|
|
71
76
|
export default {
|
|
72
77
|
async fetch(request, env, ctx) {
|
|
73
|
-
await ensureMigrated(
|
|
78
|
+
await ensureMigrated(migrationAuth); // idempotent schema sync; dev/small deploys
|
|
74
79
|
|
|
75
80
|
const authResponse = await handleAuthRequest(auth, request);
|
|
76
81
|
if (authResponse) return authResponse;
|
|
@@ -131,13 +136,16 @@ const auth = createAuth({
|
|
|
131
136
|
|
|
132
137
|
// …from your own admin-authorized code:
|
|
133
138
|
const invite = await createSignUpInvitation(auth, { email: "ada@example.com" });
|
|
139
|
+
const link = `https://app.example/sign-up?email=${encodeURIComponent(invite.email)}&invite=${invite.token}`;
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
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
143
|
|
|
138
|
-
- **An invitation
|
|
144
|
+
- **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.
|
|
145
|
+
- **The link is a bearer credential.** Whoever holds it takes the seat — send it to the invitee, not a shared inbox, and reissue rather than resend if unsure. `requireEmailVerification` still matters: the user row is written before the verification mail goes out, so verification is what keeps a spent invitation from becoming a usable session. The plugin warns on startup when password sign-up runs without it.
|
|
139
146
|
- **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
147
|
- **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.
|
|
148
|
+
- **The studio's Users page grows a Sign-up invitations section** when the plugin is installed — invite, see pending/accepted/expired, revoke. The same three ops are on `AuthAdmin` and the client (`listAuthSignUpInvitations`, `createAuthSignUpInvitation`, `revokeAuthSignUpInvitation`), with `useSignUpInvitations()` in `@lunora/react`. Nothing prunes the table; `pruneSignUpInvitations(auth)` deletes the invitations that expired unused and returns the count.
|
|
141
149
|
- **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
150
|
|
|
143
151
|
### Security / audit trail
|
package/dist/index.d.mts
CHANGED
|
@@ -11,7 +11,7 @@ export { createSqlAuthStore, d1Executor } from "./sql-store.mjs";
|
|
|
11
11
|
import { BetterAuthOptions } from 'better-auth';
|
|
12
12
|
import { EmailClassification, EmailGateConfig } from "./email-guard.mjs";
|
|
13
13
|
export { type EmailClass, type EmailGateMiddlewareOptions, assertEmailAllowed, classifyEmail, emailGateMiddleware, loadEmailDomainLists } from "./email-guard.mjs";
|
|
14
|
-
export { type I as InviteOnlyOptions, type S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-
|
|
14
|
+
export { type I as InviteOnlyOptions, type a as IssuedSignUpInvitation, type S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, p as pruneSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-DsVdKwcp.mjs";
|
|
15
15
|
export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.mjs";
|
|
16
16
|
export { default as authTables } from "./schema.mjs";
|
|
17
17
|
export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.mjs";
|
|
@@ -99,6 +99,28 @@ interface AuthInvitation {
|
|
|
99
99
|
role?: null | string;
|
|
100
100
|
status?: null | string;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* One sign-up invitation (from the `inviteOnly` plugin). Distinct from
|
|
104
|
+
* {@link AuthInvitation}, which invites an existing account into an organization:
|
|
105
|
+
* this one is what lets an address create an account at all.
|
|
106
|
+
*/
|
|
107
|
+
interface AuthSignUpInvitation {
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
/** When an account was created for this address; `null` while the invitation is unspent. */
|
|
110
|
+
acceptedAt?: AuthTimestamp;
|
|
111
|
+
createdAt?: AuthTimestamp;
|
|
112
|
+
email?: null | string;
|
|
113
|
+
expiresAt?: AuthTimestamp;
|
|
114
|
+
id: string;
|
|
115
|
+
invitedBy?: null | string;
|
|
116
|
+
/**
|
|
117
|
+
* The plaintext invitation token — present **only** on the row
|
|
118
|
+
* {@link AuthAdmin.createSignUpInvitation} returns, never on a listed one.
|
|
119
|
+
* The stored `tokenHash` is in {@link SENSITIVE_FIELDS}, so it cannot leave
|
|
120
|
+
* this plane by accident.
|
|
121
|
+
*/
|
|
122
|
+
token?: string;
|
|
123
|
+
}
|
|
102
124
|
/** One team row (from the `organization` plugin with `teams.enabled`). */
|
|
103
125
|
interface AuthTeam {
|
|
104
126
|
[key: string]: unknown;
|
|
@@ -154,6 +176,8 @@ interface AuthCapabilities {
|
|
|
154
176
|
accounts: boolean;
|
|
155
177
|
/** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
|
|
156
178
|
admin: boolean;
|
|
179
|
+
/** The `inviteOnly` plugin: sign-up invitations. */
|
|
180
|
+
inviteOnly: boolean;
|
|
157
181
|
/** The `organization` plugin: orgs, members, invitations. */
|
|
158
182
|
organization: boolean;
|
|
159
183
|
/** The `@better-auth/passkey` plugin: per-user passkeys. */
|
|
@@ -290,6 +314,17 @@ interface AuthAdmin {
|
|
|
290
314
|
permission: Record<string, string[]>;
|
|
291
315
|
role: string;
|
|
292
316
|
}) => Promise<AuthOrgRole>;
|
|
317
|
+
/**
|
|
318
|
+
* Invite an address to sign up, or refresh an existing invitation for it.
|
|
319
|
+
* Needs the `inviteOnly` plugin — without it the row is written to a table
|
|
320
|
+
* nothing reads, which is why the studio gates the panel on
|
|
321
|
+
* {@link AuthCapabilities.inviteOnly}.
|
|
322
|
+
*/
|
|
323
|
+
createSignUpInvitation: (input: {
|
|
324
|
+
email: string;
|
|
325
|
+
expiresInSeconds?: number;
|
|
326
|
+
invitedBy?: string;
|
|
327
|
+
}) => Promise<AuthSignUpInvitation>;
|
|
293
328
|
/** Create a team under an organization. */
|
|
294
329
|
createTeam: (input: {
|
|
295
330
|
name: string;
|
|
@@ -357,6 +392,16 @@ interface AuthAdmin {
|
|
|
357
392
|
offset?: number;
|
|
358
393
|
userId?: string;
|
|
359
394
|
}) => Promise<AuthPage<AuthAdminSession>>;
|
|
395
|
+
/**
|
|
396
|
+
* Sign-up invitations, newest first. Unfiltered on purpose: "pending" is
|
|
397
|
+
* `acceptedAt === null && expiresAt > now`, and applying that after a page
|
|
398
|
+
* would let page 1 come back empty while pending rows sat on page 2. The
|
|
399
|
+
* caller has both columns and can label each row itself.
|
|
400
|
+
*/
|
|
401
|
+
listSignUpInvitations: (options: {
|
|
402
|
+
limit?: number;
|
|
403
|
+
offset?: number;
|
|
404
|
+
}) => Promise<AuthPage<AuthSignUpInvitation>>;
|
|
360
405
|
/** List a team's members. */
|
|
361
406
|
listTeamMembers: (options: {
|
|
362
407
|
limit?: number;
|
|
@@ -384,6 +429,10 @@ interface AuthAdmin {
|
|
|
384
429
|
removeUser: (input: {
|
|
385
430
|
userId: string;
|
|
386
431
|
}) => Promise<void>;
|
|
432
|
+
/** Withdraw a sign-up invitation. Not retroactive — an account already created keeps existing; use {@link AuthAdmin.removeUser} for that. */
|
|
433
|
+
revokeSignUpInvitation: (input: {
|
|
434
|
+
email: string;
|
|
435
|
+
}) => Promise<void>;
|
|
387
436
|
revokeUserSession: (input: {
|
|
388
437
|
sessionId: string;
|
|
389
438
|
}) => Promise<void>;
|
|
@@ -966,4 +1015,4 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
|
|
|
966
1015
|
* with the same 60s cookie cache as `rolling`.
|
|
967
1016
|
*/
|
|
968
1017
|
declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
|
|
969
|
-
export { READ_AUDIT_PATH as AUTH_DO_AUDIT_PATH, INTERNAL_SECRET_HEADER as AUTH_DO_SECRET_HEADER, RESOLVE_SESSION_PATH as AUTH_DO_SESSION_PATH, type AppendAuthAuditEntry, type AppendAuthAuditOptions, type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthAuditEvent, type AuthAuditHookConfig, type AuthAuditReader, type AuthCapabilities, type AuthConfigInfo, type AuthDoOptions, type AuthDoState, type AuthInvitation, type AuthMember, type AuthNamespaceLike, type AuthOrgRole, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthTeam, type AuthTeamMember, type AuthTimestamp, type AuthUserFieldSpec, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type DoAuthWiring, type DoAuthWiringOptions, type EmailClassification, type EmailGateConfig, type EmailGateHookConfig, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, LunoraAuthDO, type LunoraAuthOptions, type SessionPolicy, type SqlExecutor, authAuditHook, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, compileMigrationsSql, createAuthAdmin, createDoAuthWiring, emailGateDatabaseHooks, ensureMigrated, eventForPath, handleAuthRequest, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
|
|
1018
|
+
export { READ_AUDIT_PATH as AUTH_DO_AUDIT_PATH, INTERNAL_SECRET_HEADER as AUTH_DO_SECRET_HEADER, RESOLVE_SESSION_PATH as AUTH_DO_SESSION_PATH, type AppendAuthAuditEntry, type AppendAuthAuditOptions, type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthAuditEvent, type AuthAuditHookConfig, type AuthAuditReader, type AuthCapabilities, type AuthConfigInfo, type AuthDoOptions, type AuthDoState, type AuthInvitation, type AuthMember, type AuthNamespaceLike, type AuthOrgRole, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthSignUpInvitation, type AuthTeam, type AuthTeamMember, type AuthTimestamp, type AuthUserFieldSpec, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type DoAuthWiring, type DoAuthWiringOptions, type EmailClassification, type EmailGateConfig, type EmailGateHookConfig, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, LunoraAuthDO, type LunoraAuthOptions, type SessionPolicy, type SqlExecutor, authAuditHook, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, compileMigrationsSql, createAuthAdmin, createDoAuthWiring, emailGateDatabaseHooks, ensureMigrated, eventForPath, handleAuthRequest, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { createSqlAuthStore, d1Executor } from "./sql-store.js";
|
|
|
11
11
|
import { BetterAuthOptions } from 'better-auth';
|
|
12
12
|
import { EmailClassification, EmailGateConfig } from "./email-guard.js";
|
|
13
13
|
export { type EmailClass, type EmailGateMiddlewareOptions, assertEmailAllowed, classifyEmail, emailGateMiddleware, loadEmailDomainLists } from "./email-guard.js";
|
|
14
|
-
export { type I as InviteOnlyOptions, type S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-
|
|
14
|
+
export { type I as InviteOnlyOptions, type a as IssuedSignUpInvitation, type S as SignUpInvitation, c as createSignUpInvitation, l as listSignUpInvitations, p as pruneSignUpInvitations, r as revokeSignUpInvitation } from "./packem_shared/invite-only.d-C9Su82Iq.js";
|
|
15
15
|
export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.js";
|
|
16
16
|
export { default as authTables } from "./schema.js";
|
|
17
17
|
export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.js";
|
|
@@ -99,6 +99,28 @@ interface AuthInvitation {
|
|
|
99
99
|
role?: null | string;
|
|
100
100
|
status?: null | string;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* One sign-up invitation (from the `inviteOnly` plugin). Distinct from
|
|
104
|
+
* {@link AuthInvitation}, which invites an existing account into an organization:
|
|
105
|
+
* this one is what lets an address create an account at all.
|
|
106
|
+
*/
|
|
107
|
+
interface AuthSignUpInvitation {
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
/** When an account was created for this address; `null` while the invitation is unspent. */
|
|
110
|
+
acceptedAt?: AuthTimestamp;
|
|
111
|
+
createdAt?: AuthTimestamp;
|
|
112
|
+
email?: null | string;
|
|
113
|
+
expiresAt?: AuthTimestamp;
|
|
114
|
+
id: string;
|
|
115
|
+
invitedBy?: null | string;
|
|
116
|
+
/**
|
|
117
|
+
* The plaintext invitation token — present **only** on the row
|
|
118
|
+
* {@link AuthAdmin.createSignUpInvitation} returns, never on a listed one.
|
|
119
|
+
* The stored `tokenHash` is in {@link SENSITIVE_FIELDS}, so it cannot leave
|
|
120
|
+
* this plane by accident.
|
|
121
|
+
*/
|
|
122
|
+
token?: string;
|
|
123
|
+
}
|
|
102
124
|
/** One team row (from the `organization` plugin with `teams.enabled`). */
|
|
103
125
|
interface AuthTeam {
|
|
104
126
|
[key: string]: unknown;
|
|
@@ -154,6 +176,8 @@ interface AuthCapabilities {
|
|
|
154
176
|
accounts: boolean;
|
|
155
177
|
/** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
|
|
156
178
|
admin: boolean;
|
|
179
|
+
/** The `inviteOnly` plugin: sign-up invitations. */
|
|
180
|
+
inviteOnly: boolean;
|
|
157
181
|
/** The `organization` plugin: orgs, members, invitations. */
|
|
158
182
|
organization: boolean;
|
|
159
183
|
/** The `@better-auth/passkey` plugin: per-user passkeys. */
|
|
@@ -290,6 +314,17 @@ interface AuthAdmin {
|
|
|
290
314
|
permission: Record<string, string[]>;
|
|
291
315
|
role: string;
|
|
292
316
|
}) => Promise<AuthOrgRole>;
|
|
317
|
+
/**
|
|
318
|
+
* Invite an address to sign up, or refresh an existing invitation for it.
|
|
319
|
+
* Needs the `inviteOnly` plugin — without it the row is written to a table
|
|
320
|
+
* nothing reads, which is why the studio gates the panel on
|
|
321
|
+
* {@link AuthCapabilities.inviteOnly}.
|
|
322
|
+
*/
|
|
323
|
+
createSignUpInvitation: (input: {
|
|
324
|
+
email: string;
|
|
325
|
+
expiresInSeconds?: number;
|
|
326
|
+
invitedBy?: string;
|
|
327
|
+
}) => Promise<AuthSignUpInvitation>;
|
|
293
328
|
/** Create a team under an organization. */
|
|
294
329
|
createTeam: (input: {
|
|
295
330
|
name: string;
|
|
@@ -357,6 +392,16 @@ interface AuthAdmin {
|
|
|
357
392
|
offset?: number;
|
|
358
393
|
userId?: string;
|
|
359
394
|
}) => Promise<AuthPage<AuthAdminSession>>;
|
|
395
|
+
/**
|
|
396
|
+
* Sign-up invitations, newest first. Unfiltered on purpose: "pending" is
|
|
397
|
+
* `acceptedAt === null && expiresAt > now`, and applying that after a page
|
|
398
|
+
* would let page 1 come back empty while pending rows sat on page 2. The
|
|
399
|
+
* caller has both columns and can label each row itself.
|
|
400
|
+
*/
|
|
401
|
+
listSignUpInvitations: (options: {
|
|
402
|
+
limit?: number;
|
|
403
|
+
offset?: number;
|
|
404
|
+
}) => Promise<AuthPage<AuthSignUpInvitation>>;
|
|
360
405
|
/** List a team's members. */
|
|
361
406
|
listTeamMembers: (options: {
|
|
362
407
|
limit?: number;
|
|
@@ -384,6 +429,10 @@ interface AuthAdmin {
|
|
|
384
429
|
removeUser: (input: {
|
|
385
430
|
userId: string;
|
|
386
431
|
}) => Promise<void>;
|
|
432
|
+
/** Withdraw a sign-up invitation. Not retroactive — an account already created keeps existing; use {@link AuthAdmin.removeUser} for that. */
|
|
433
|
+
revokeSignUpInvitation: (input: {
|
|
434
|
+
email: string;
|
|
435
|
+
}) => Promise<void>;
|
|
387
436
|
revokeUserSession: (input: {
|
|
388
437
|
sessionId: string;
|
|
389
438
|
}) => Promise<void>;
|
|
@@ -966,4 +1015,4 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
|
|
|
966
1015
|
* with the same 60s cookie cache as `rolling`.
|
|
967
1016
|
*/
|
|
968
1017
|
declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
|
|
969
|
-
export { READ_AUDIT_PATH as AUTH_DO_AUDIT_PATH, INTERNAL_SECRET_HEADER as AUTH_DO_SECRET_HEADER, RESOLVE_SESSION_PATH as AUTH_DO_SESSION_PATH, type AppendAuthAuditEntry, type AppendAuthAuditOptions, type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthAuditEvent, type AuthAuditHookConfig, type AuthAuditReader, type AuthCapabilities, type AuthConfigInfo, type AuthDoOptions, type AuthDoState, type AuthInvitation, type AuthMember, type AuthNamespaceLike, type AuthOrgRole, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthTeam, type AuthTeamMember, type AuthTimestamp, type AuthUserFieldSpec, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type DoAuthWiring, type DoAuthWiringOptions, type EmailClassification, type EmailGateConfig, type EmailGateHookConfig, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, LunoraAuthDO, type LunoraAuthOptions, type SessionPolicy, type SqlExecutor, authAuditHook, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, compileMigrationsSql, createAuthAdmin, createDoAuthWiring, emailGateDatabaseHooks, ensureMigrated, eventForPath, handleAuthRequest, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
|
|
1018
|
+
export { READ_AUDIT_PATH as AUTH_DO_AUDIT_PATH, INTERNAL_SECRET_HEADER as AUTH_DO_SECRET_HEADER, RESOLVE_SESSION_PATH as AUTH_DO_SESSION_PATH, type AppendAuthAuditEntry, type AppendAuthAuditOptions, type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthAuditEvent, type AuthAuditHookConfig, type AuthAuditReader, type AuthCapabilities, type AuthConfigInfo, type AuthDoOptions, type AuthDoState, type AuthInvitation, type AuthMember, type AuthNamespaceLike, type AuthOrgRole, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthSignUpInvitation, type AuthTeam, type AuthTeamMember, type AuthTimestamp, type AuthUserFieldSpec, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type DoAuthWiring, type DoAuthWiringOptions, type EmailClassification, type EmailGateConfig, type EmailGateHookConfig, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, LunoraAuthDO, type LunoraAuthOptions, type SessionPolicy, type SqlExecutor, authAuditHook, authDoColumnAdditions, authDoSchemaStatements, buildAuditEntry, compileMigrationsSql, createAuthAdmin, createDoAuthWiring, emailGateDatabaseHooks, ensureMigrated, eventForPath, handleAuthRequest, sessionPresets, validateSessionPolicy, withAuthAudit, withEmailGate };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{l as r,a as o,b as a}from"./packem_shared/adapter-DA8DdALX.mjs";import{LunoraAuthAdminError as A,createAuthAdmin as u}from"./packem_shared/LunoraAuthAdminError-
|
|
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-Bmq06hsp.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-CS1FCCoC.mjs";import{createAuth as I,resolveAuthOptions as R}from"./packem_shared/createAuth-BI6qU0c_.mjs";import{authDoColumnAdditions as L,authDoSchemaStatements as P}from"./packem_shared/authDoColumnAdditions-BCSs7qaN.mjs";import{createDoAuthWiring as O}from"./packem_shared/createDoAuthWiring-D0pT8Y9R.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-CCvK7g57.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};
|
package/dist/packem_shared/{AUTH_DO_AUDIT_PATH-DiXHh-vn.mjs → AUTH_DO_AUDIT_PATH-CS1FCCoC.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{b as u,d as h}from"./adapter-DA8DdALX.mjs";import{ensureAuthAuditTable as c,createAuthAuditReader as l}from"../audit.mjs";import{resolveAuthOptions as m,createAuth as d}from"./createAuth-
|
|
1
|
+
import{b as u,d as h}from"./adapter-DA8DdALX.mjs";import{ensureAuthAuditTable as c,createAuthAuditReader as l}from"../audit.mjs";import{resolveAuthOptions as m,createAuth as d}from"./createAuth-BI6qU0c_.mjs";import{authDoSchemaStatements as p,authDoColumnAdditions as f}from"./authDoColumnAdditions-BCSs7qaN.mjs";import{handleAuthRequest as A}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const g=(n,e)=>{const s=Math.max(n.length,e.length);let t=n.length^e.length;for(let r=0;r<s;r+=1){const i=r<n.length?n.charCodeAt(r):0,o=r<e.length?e.charCodeAt(r):0;t|=i^o}return t===0},R="/__lunora/auth/session",y="/__lunora/auth/audit",S="x-lunora-auth-do-secret",b=n=>{if(n===null||typeof n!="object"||Array.isArray(n))return{error:"body must be a JSON object"};const e={};for(const[s,t]of Object.entries(n))if(s==="limit"||s==="sinceSeq"){if(typeof t!="number"||!Number.isFinite(t))return{error:`"${s}" must be a finite number`};e[s]=t}else if(s==="actorId"||s==="event"){if(typeof t!="string")return{error:`"${s}" must be a string`};e[s]=t}else return{error:`unknown audit read option "${s}"`};return{options:e}};class v{#s;#r;#t;#e;#n=!1;constructor(e,s,t={}){this.#t=e.storage,this.#r=s,this.#s=t}#o(){if(this.#e!==void 0)return this.#e;const e=this.#r();if(!this.#n){const s=m(e);for(const t of p(s))[...this.#t.sql.exec(t)];for(const t of f(s,r=>this.#a(r)))[...this.#t.sql.exec(t)];this.#n=!0}return this.#e=d({...e,database:u(this.#t)}),this.#e}#a(e){return[...this.#t.sql.exec("SELECT name FROM pragma_table_info(?)",e)].map(t=>String(t.name))}async#u(e){if(!this.#i(e))return Response.json({error:"unauthorized"},{status:401});let s;try{s=await e.json()}catch{return Response.json({error:"invalid body"},{status:400})}const t=b(s??{});if("error"in t)return Response.json({error:t.error},{status:400});const r=h(this.#t);await c(r);const i=await l(r).read(t.options);return Response.json({entries:i})}#i(e){const{internalSecret:s}=this.#s;if(s===void 0||s==="")return!1;const t=e.headers.get(S);return t!==null&&g(t,s)}async#h(e){if(!this.#i(e))return Response.json({error:"unauthorized"},{status:401});const t=await this.#o().api.getSession({headers:e.headers}),r=t?.user.id;if(r===void 0)return Response.json({});const i=t?.session.expiresAt,o=t?.user,a=o?.role;return Response.json({...typeof o?.email=="string"&&o.email.length>0?{email:o.email}:{},...i instanceof Date?{expiresAtMs:i.getTime()}:{},...typeof o?.name=="string"&&o.name.length>0?{name:o.name}:{},...typeof a=="string"&&a.length>0?{role:a}:{},userId:r})}async fetch(e){const s=new URL(e.url);if(s.pathname===R)return this.#h(e);if(s.pathname===y)return this.#u(e);const t=this.#o();return await A(t,e,this.#s.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{S as INTERNAL_SECRET_HEADER,v as LunoraAuthDO,y as READ_AUDIT_PATH,R as RESOLVE_SESSION_PATH};
|
|
@@ -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{createAuthMiddleware as d}from"better-auth/api";import{appendAuthAuditEntry as c}from"../audit.mjs";import{onCloudflareEdge as l}from"./createAuth-BI6qU0c_.mjs";const f=r=>{const t=r.toLowerCase(),e=i=>t===i||t.endsWith(i);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(e("/sign-in/social")||e("/sign-in/magic-link"))return"sign-in-initiated";if(t.includes("/sign-in/")||t.includes("/callback/")||e("/magic-link/verify")||t.includes("/two-factor/verify-"))return"sign-in";if(e("/sign-out"))return"sign-out";if(e("/change-password")||e("/set-password"))return"password-change";if(e("/reset-password")||e("/request-password-reset")||e("/forget-password"))return"password-reset";if(e("/verify-email"))return"email-verification";if(t.includes("/two-factor/enable")||t.includes("/totp/enable"))return"mfa-enable";if(t.includes("/two-factor/disable")||t.includes("/totp/disable"))return"mfa-disable";if(e("/refresh-token")||e("/token"))return"token-refresh";if(e("/revoke-session")||e("/revoke-sessions")||e("/revoke-other-sessions"))return"session-revoke";if(e("/link-social"))return"account-link";if(e("/unlink-account"))return"account-unlink"},a=(r,t)=>r.headers?.get(t)??r.request?.headers.get(t)??void 0,v=(r,t)=>{if(l()){const e=a(r,"cf-connecting-ip");if(e!==void 0)return e}if(t===!0)return a(r,"x-forwarded-for")?.split(",")[0]?.trim()},p=r=>{const t=r.context?.newSession??r.context?.session,e=t?.user?.id??t?.session?.userId,i=t?.user?.email;return{...e===void 0?{}:{actorId:e},...i===void 0?{}:{actorEmail:i}}},h=r=>{const t=r.context?.returned;if(t instanceof Error)return"failure";if(typeof t=="object"&&t!==null&&"status"in t){const e=Number(t.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},u=320,g=(r,t)=>{if(t!=="sign-in"&&t!=="sign-in-initiated")return;const e=r.body?.email??r.body?.username;if(!(typeof e!="string"||e.length===0))return e.length>u?e.slice(0,u):e},m=(r,{now:t=Date.now(),trustProxyHeaders:e}={})=>{const i=r.path===void 0?void 0:f(r.path);if(i===void 0)return;const s=v(r,e),n=a(r,"user-agent"),o=g(r,i);return{...p(r),event:i,outcome:h(r),ts:t,...s===void 0?{}:{ip:s},...o===void 0?{}:{targetEmail:o},...n===void 0?{}:{userAgent:n},detail:{path:r.path}}},w=r=>d(async t=>{try{const e=m(t,{trustProxyHeaders:r.trustProxyHeaders});if(e!==void 0){const i=await c(r.executor,e,{redactDetail:r.redactDetail,retention:r.retention});r.onRecord!==void 0&&await r.onRecord(i)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}}),A=(r,t)=>{const e=w(t),i=r.hooks?.after,s=i?async n=>{const o=await i(n);return await e(n),o}:e;return{...r,hooks:{...r.hooks,after:s}}};export{w as authAuditHook,m as buildAuditEntry,f as eventForPath,A as withAuthAudit};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as g}from"@lunora/errors";import{getMigrations as u}from"better-auth/db/migration";import{resolveAuthOptions as w}from"./createAuth-BI6qU0c_.mjs";const I=/pragma_index_list\s*\(/iu,_=/sqlite_master/iu,y=/^["`[]|["\]`]$/gu,R=/\s+/u,x=/create\s+unique\s+index/iu,M=/\bwhere\b/iu,T=t=>I.test(t)&&_.test(t),D=t=>(t.trim().split(R)[0]??"").replaceAll(y,"").trim(),A=t=>{const n=t.indexOf("(");if(n===-1)return{columns:[],tail:""};let s=0;for(let e=n;e<t.length;e+=1){const o=t[e];if(o==="(")s+=1;else if(o===")"&&(s-=1,s===0))return{columns:t.slice(n+1,e).split(",").map(i=>D(i)).filter(i=>i!==""),tail:t.slice(e+1)}}return{columns:[],tail:""}},P=async t=>{const n=t.prepare("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'"),{results:s}=await n.all(),e=[];for(const o of s??[]){const i=o.name,a=o.tbl_name,r=o.sql;if(typeof i!="string"||typeof a!="string")continue;if(typeof r!="string"){e.push({columnPosition:0,indexName:i,isPartial:0,isUnique:1,tableName:a});continue}const m=x.test(r)?1:0,{columns:f,tail:d}=A(r),h=M.test(d)?1:0;for(const[E,b]of f.entries())e.push({columnName:b,columnPosition:E,indexName:i,isPartial:h,isUnique:m,tableName:a})}return e},N=t=>{const n=async()=>{const e=await P(t);return{meta:{},results:e,success:!0}},s={all:n,bind:()=>s,run:n};return s},U=t=>new Proxy(t,{get(n,s,e){if(s==="prepare")return i=>T(i)?N(n):n.prepare(i);const o=Reflect.get(n,s,e);return typeof o=="function"?o.bind(n):o}}),O=t=>typeof t=="object"&&t!==null&&typeof t.prepare=="function"&&typeof t.batch=="function",l=t=>{const{database:n}=t;if(!(n&&typeof n!="function"))throw new g("AUTH_MIGRATOR_UNSUPPORTED",n?"@lunora/auth: this auth instance's `database` is a custom adapter, which better-auth's migrator cannot drive.":"@lunora/auth: this auth instance has no `database`, so better-auth's migrator has nothing to introspect.")},p=t=>O(t.database)?{...t,database:U(t.database)}:t,c=new WeakMap,H=async t=>{const{options:n}=t;l(n);const s=c.get(n);if(s){await s;return}const e=(async()=>{const{runMigrations:o}=await u(p(n));await o()})();c.set(n,e);try{await e}catch(o){throw c.delete(n),o}},L=async t=>{const n=w(t);l(n);const{compileMigrations:s}=await u(p(n));return s()};export{L as compileMigrationsSql,H as ensureMigrated};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LunoraError as c}from"@lunora/errors";import{betterAuth as l}from"better-auth";import{getIP as u}from"better-auth/api";import{validateSessionPolicy as f}from"./sessionPresets-C867Mlo4.mjs";const n="cf-connecting-ip",h=()=>globalThis.navigator?.userAgent==="Cloudflare-Workers",m=e=>{const t=e!==void 0&&e.length>0;return h()?t?[n,"x-forwarded-for"]:[n]:t?["x-forwarded-for"]:[]},a="/**",p=100,A=e=>{const t=e.rateLimit?.customRules??{};return t[a]!==void 0?t:{...t,[a]:(o,r)=>u(o,e)===null?{max:r.max*p,window:r.window}:r}},d=32,E=e=>{const t=typeof e=="string"?e.trim().length:0;return t>0&&t<d},i=e=>typeof e=="string"?e.toLowerCase().startsWith("http://"):e&&typeof e=="object"?e.protocol==="http"?!0:e.protocol==="https"?!1:typeof e.fallback=="string"&&e.fallback.toLowerCase().startsWith("http://"):!1,g=new Set(["127.0.0.1","localhost"]),L=e=>{if(typeof e!="string")return;let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="http:"||!g.has(t.hostname)))return{allowedHosts:[t.hostname,`${t.hostname}:*`],fallback:e,protocol:"http"}},C=e=>{const t=L(e.baseURL)??e.baseURL;if(E(e.secret)){const s=`@lunora/auth: AUTH_SECRET is only ${String(e.secret?.trim().length)} characters. Use at least ${String(d)} for a brute-force-resistant secret — generate one with \`openssl rand -hex 32\`.`;if(!i(t))throw new c("INTERNAL",s);console.warn(s)}const o=e.advanced??{},r=o.ipAddress??{};return{...e,advanced:{...o,defaultCookieAttributes:o.defaultCookieAttributes??{httpOnly:!0,path:"/",sameSite:"lax"},ipAddress:{...r,...r.ipAddressHeaders===void 0?{ipAddressHeaders:m(r.trustedProxies)}:{}},...o.useSecureCookies===void 0?{useSecureCookies:!i(t)}:{}},baseURL:t}},v=e=>{const t=C(e),o=t.rateLimit?.enabled===void 0,r=t.rateLimit?.storage===void 0&&t.rateLimit?.enabled!==!1,s=t.rateLimit?.enabled===!1?{}:{rateLimit:{...t.rateLimit,...o?{enabled:!0}:{},...r?{storage:"database"}:{},customRules:A(t)}};return{...t,...s,...t.session?.cookieCache===void 0?{session:{...t.session,cookieCache:{enabled:!0,maxAge:60}}}:{}}},H=e=>{if(!e.secret||e.secret.trim()==="")throw new c("INTERNAL",'@lunora/auth: `secret` is required. Set AUTH_SECRET locally in .dev.vars (`lunora env set AUTH_SECRET "$(openssl rand -hex 32)"`), and in production with `wrangler secret put AUTH_SECRET`.');return e.session&&f(e.session),l(v(e))};export{H as createAuth,v as resolveAuthOptions};
|
|
1
|
+
import{LunoraError as c}from"@lunora/errors";import{betterAuth as l}from"better-auth";import{getIP as u}from"better-auth/api";import{validateSessionPolicy as f}from"./sessionPresets-C867Mlo4.mjs";const n="cf-connecting-ip",h=()=>globalThis.navigator?.userAgent==="Cloudflare-Workers",m=e=>{const t=e!==void 0&&e.length>0;return h()?t?[n,"x-forwarded-for"]:[n]:t?["x-forwarded-for"]:[]},a="/**",p=100,A=e=>{const t=e.rateLimit?.customRules??{};return t[a]!==void 0?t:{...t,[a]:(o,r)=>u(o,e)===null?{max:r.max*p,window:r.window}:r}},d=32,E=e=>{const t=typeof e=="string"?e.trim().length:0;return t>0&&t<d},i=e=>typeof e=="string"?e.toLowerCase().startsWith("http://"):e&&typeof e=="object"?e.protocol==="http"?!0:e.protocol==="https"?!1:typeof e.fallback=="string"&&e.fallback.toLowerCase().startsWith("http://"):!1,g=new Set(["127.0.0.1","localhost"]),L=e=>{if(typeof e!="string")return;let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="http:"||!g.has(t.hostname)))return{allowedHosts:[t.hostname,`${t.hostname}:*`],fallback:e,protocol:"http"}},C=e=>{const t=L(e.baseURL)??e.baseURL;if(E(e.secret)){const s=`@lunora/auth: AUTH_SECRET is only ${String(e.secret?.trim().length)} characters. Use at least ${String(d)} for a brute-force-resistant secret — generate one with \`openssl rand -hex 32\`.`;if(!i(t))throw new c("INTERNAL",s);console.warn(s)}const o=e.advanced??{},r=o.ipAddress??{};return{...e,advanced:{...o,defaultCookieAttributes:o.defaultCookieAttributes??{httpOnly:!0,path:"/",sameSite:"lax"},ipAddress:{...r,...r.ipAddressHeaders===void 0?{ipAddressHeaders:m(r.trustedProxies)}:{}},...o.useSecureCookies===void 0?{useSecureCookies:!i(t)}:{}},baseURL:t}},v=e=>{const t=C(e),o=t.rateLimit?.enabled===void 0,r=t.rateLimit?.storage===void 0&&t.rateLimit?.enabled!==!1,s=t.rateLimit?.enabled===!1?{}:{rateLimit:{...t.rateLimit,...o?{enabled:!0}:{},...r?{storage:"database"}:{},customRules:A(t)}};return{...t,...s,...t.session?.cookieCache===void 0?{session:{...t.session,cookieCache:{enabled:!0,maxAge:60}}}:{}}},H=e=>{if(!e.secret||e.secret.trim()==="")throw new c("INTERNAL",'@lunora/auth: `secret` is required. Set AUTH_SECRET locally in .dev.vars (`lunora env set AUTH_SECRET "$(openssl rand -hex 32)"`), and in production with `wrangler secret put AUTH_SECRET`.');return e.session&&f(e.session),l(v(e))};export{H as createAuth,h as onCloudflareEdge,v as resolveAuthOptions};
|
package/dist/packem_shared/{createDoAuthWiring-Ber4dHOy.mjs → createDoAuthWiring-D0pT8Y9R.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as p,READ_AUDIT_PATH as f}from"./AUTH_DO_AUDIT_PATH-
|
|
1
|
+
import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as p,READ_AUDIT_PATH as f}from"./AUTH_DO_AUDIT_PATH-CS1FCCoC.mjs";import{DEFAULT_AUTH_BASE_PATH as h,isAuthRoutePath as A}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const g=l=>{const{basePath:c=h,internalSecret:a,namespace:o,objectName:d="auth"}=l,i=()=>{if(o)return o.get(o.idFromName(d))},m=async(t,n,r)=>{if(!a)return;const s=i();if(!s)return;const e=await s.fetch(new Request(new URL(t,n),{body:JSON.stringify(r),headers:{"content-type":"application/json",[u]:a},method:"POST"}));return e.ok?e:void 0};return{auditReader:{read:async t=>{const n=await m(f,"https://auth-do.invalid",t);return n?(await n.json())?.entries??[]:[]}},authHandler:async t=>{if(A(new URL(t.url).pathname,c))return i()?.fetch(t)},resolveIdentity:async t=>{if(!a)return null;const n=i();if(!n)return null;const r=new Headers(t.headers);r.set(u,a);const s=await n.fetch(new Request(new URL(p,t.url),{headers:r}));if(!s.ok)return null;const e=await s.json();return e?.userId?{...typeof e.email=="string"&&e.email.length>0?{email:e.email}:{},...typeof e.expiresAtMs=="number"&&Number.isFinite(e.expiresAtMs)?{expiresAtMs:e.expiresAtMs}:{},...typeof e.name=="string"&&e.name.length>0?{name:e.name}:{},...typeof e.role=="string"&&e.role.length>0?{role:e.role}:{},userId:e.userId}:null}}};export{g as createDoAuthWiring};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineErrorCodes as S}from"@better-auth/core/utils/error-codes";import{LunoraError as A}from"@lunora/errors";import{createAuthMiddleware as T,APIError as g}from"better-auth/api";const c="signUpInvitation",_=10080*60,v=365*24*60*60,y=500,w=S({SIGN_UP_INVITE_INVALID:"That sign-up invitation is not valid. Ask an administrator for a new invitation link.",SIGN_UP_INVITE_REQUIRED:"Sign-up is invite-only — ask an administrator for an invitation."}),D=32,x=/^[^\s@]+@[^\s@]+$/,O=e=>{if(!x.test(e))return!1;const t=e.slice(e.indexOf("@")+1);return t.includes(".")&&!t.startsWith(".")&&!t.endsWith(".")},p=e=>({acceptedAt:e.acceptedAt instanceof Date?e.acceptedAt:null,createdAt:e.createdAt,email:String(e.email),expiresAt:e.expiresAt,id:String(e.id),invitedBy:typeof e.invitedBy=="string"?e.invitedBy:null}),R=()=>btoa(String.fromCodePoint(...crypto.getRandomValues(new Uint8Array(D)))).replaceAll("+","-").replaceAll("/","_").replaceAll("=",""),E=async e=>{const t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return[...new Uint8Array(t)].map(i=>i.toString(16).padStart(2,"0")).join("")},N=(e,t)=>{if(e.length!==t.length)return!1;let i=0;for(let n=0;n<e.length;n+=1)i|=(e.codePointAt(n)??0)^(t.codePointAt(n)??0);return i===0},m=e=>e.trim().toLowerCase(),I=e=>{if(typeof e.email!="string")return;const t=m(e.email);return t===""?void 0:t},k=async(e,t)=>{const i=await e.findOne({model:c,where:[{field:"email",value:t}]});return i===null||i.acceptedAt instanceof Date?!1:i.expiresAt instanceof Date&&i.expiresAt.getTime()>Date.now()},U=e=>{e.emailAndPassword?.enabled!==!0||e.emailAndPassword.requireEmailVerification===!0||console.warn("@lunora/auth: inviteOnly() is installed with password sign-up but without `emailAndPassword: { requireEmailVerification: true }`. An invitation is keyed by email address alone, so anyone who learns an invited address can sign up as it — and without verification they hold a session the moment they do.")},q=(e={})=>{let t=e.allowFirstUser??!1;const i=async n=>t?await n.count({model:"user"})===0?!0:(t=!1,!1):!1;return{$ERROR_CODES:w,hooks:{before:[{handler:T(async n=>{const r=n.body??{},a=typeof r.email=="string"?m(r.email):"",d=typeof r.inviteToken=="string"?r.inviteToken:"",s=()=>{throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_INVALID)};if(a===""&&s(),await i(n.context.adapter))return;d===""&&s();const o=await n.context.adapter.findOne({model:c,where:[{field:"email",value:a}]}),u=o===null?void 0:o.tokenHash,f=await E(d);(typeof u!="string"||!N(u,f))&&s()}),matcher:n=>n.path==="/sign-up/email"}]},id:"lunora-invite-only",init:n=>{U(n.options);const{adapter:r}=n;return{options:{databaseHooks:{user:{create:{after:async s=>{const o=I(s);o!==void 0&&await r.update({model:c,update:{acceptedAt:new Date},where:[{field:"email",value:o}]})},before:async s=>{const o=I(s);if(!(o!==void 0&&await k(r,o))&&!await i(r))throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_REQUIRED)}}}}}}},schema:{[c]:{fields:{acceptedAt:{required:!1,type:"date"},tokenHash:{required:!1,type:"string"},createdAt:{defaultValue:()=>new Date,required:!0,type:"date"},email:{required:!0,type:"string",unique:!0},expiresAt:{required:!0,type:"date"},invitedBy:{required:!1,type:"string"}}}}}},M=async(e,t)=>{const i=m(t.email);if(!O(i))throw new A("VALIDATION_ERROR",`not an email address to invite: ${JSON.stringify(t.email)}`);const{expiresInSeconds:n=_}=t;if(!Number.isInteger(n)||n<=0||n>v)throw new A("VALIDATION_ERROR",`expiresInSeconds must be a positive integer no greater than ${String(v)}`);const r=new Date(Date.now()+n*1e3),a=R(),d=await E(a),s=t.invitedBy??null,o=await e.$context,u=[{field:"email",value:i}],f=async()=>o.adapter.update({model:c,update:{acceptedAt:null,expiresAt:r,invitedBy:s,tokenHash:d},where:u});if(await o.adapter.findOne({model:c,where:u})){const l=await f();if(l)return{...p(l),token:a}}try{const l=await o.adapter.create({data:{createdAt:new Date,email:i,expiresAt:r,invitedBy:s,tokenHash:d},model:c});return{...p(l),token:a}}catch(l){const h=await f();if(h===null)throw l;return{...p(h),token:a}}},$=async(e,t={})=>{const r=(await(await e.$context).adapter.findMany({limit:y,model:c,sortBy:{direction:"desc",field:"createdAt"}})).map(a=>p(a));return t.pendingOnly===!0?r.filter(a=>a.acceptedAt===null&&a.expiresAt.getTime()>Date.now()):r},C=async(e,t)=>{await(await e.$context).adapter.delete({model:c,where:[{field:"email",value:m(t.email)}]})},b=100,V=async(e,t,i,n)=>{const r=t.filter(a=>a.acceptedAt===null&&a.expiresAt.getTime()<=i).slice(0,n);for(const a of r)await e.delete({model:c,where:[{field:"id",value:a.id}]});return r.length},H=async(e,t={})=>{const i=t.limit??y;if(!Number.isInteger(i)||i<=0)throw new A("VALIDATION_ERROR","limit must be a positive integer");const n=Math.min(i,y),r=await e.$context,a=Date.now();let d=0,s=0;for(;d<n;){const o=await r.adapter.findMany({limit:b,model:c,offset:s,sortBy:{direction:"asc",field:"expiresAt"}});if(o.length===0)break;const u=o.map(l=>p(l)),f=await V(r.adapter,u,a,n-d);if(d+=f,u.some(l=>l.expiresAt.getTime()>a))break;s+=o.length-f}return d};export{M as createSignUpInvitation,q as inviteOnly,$ as listSignUpInvitations,H as pruneSignUpInvitations,C as revokeSignUpInvitation};
|
|
@@ -12,6 +12,16 @@ interface SignUpInvitation {
|
|
|
12
12
|
/** Free-form attribution (a user id, an operator name); never read by the gate. */
|
|
13
13
|
invitedBy: null | string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* What {@link createSignUpInvitation} hands back: the stored row plus the one
|
|
17
|
+
* and only sight of the plaintext `token`. Nothing reads it back afterwards —
|
|
18
|
+
* the database holds a SHA-256 of it — so an invitation link that is lost is
|
|
19
|
+
* reissued, not recovered.
|
|
20
|
+
*/
|
|
21
|
+
interface IssuedSignUpInvitation extends SignUpInvitation {
|
|
22
|
+
/** Put this in the sign-up link as `?invite=…`. Never stored, never logged, never listed. */
|
|
23
|
+
token: string;
|
|
24
|
+
}
|
|
15
25
|
/** Options for {@link inviteOnly}. */
|
|
16
26
|
interface InviteOnlyOptions {
|
|
17
27
|
/**
|
|
@@ -57,12 +67,16 @@ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
|
|
|
57
67
|
* Nothing prunes the table — a spent or expired row stays until you delete it with
|
|
58
68
|
* {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
|
|
59
69
|
* let in.
|
|
70
|
+
*
|
|
71
|
+
* Each call mints a **new token** and returns it in the clear, once. Only its
|
|
72
|
+
* SHA-256 is stored, so re-inviting an address invalidates the previous link, and
|
|
73
|
+
* a link that was never delivered is reissued rather than looked up.
|
|
60
74
|
*/
|
|
61
75
|
declare const createSignUpInvitation: (auth: LunoraAuth, input: {
|
|
62
76
|
email: string;
|
|
63
77
|
expiresInSeconds?: number;
|
|
64
78
|
invitedBy?: string;
|
|
65
|
-
}) => Promise<
|
|
79
|
+
}) => Promise<IssuedSignUpInvitation>;
|
|
66
80
|
/**
|
|
67
81
|
* The most recent invitations, newest first, up to a fixed ceiling of
|
|
68
82
|
* {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
|
|
@@ -92,4 +106,21 @@ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
|
|
|
92
106
|
declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
|
|
93
107
|
email: string;
|
|
94
108
|
}) => Promise<void>;
|
|
95
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Delete invitations that expired without being used, and report how many went.
|
|
111
|
+
*
|
|
112
|
+
* Only the dead ones: a spent invitation is the record of who was let in, and an
|
|
113
|
+
* unexpired one is still live, so both stay. Nothing calls this for you — an app
|
|
114
|
+
* that invites at any volume should put it on a cron; one that doesn't can leave
|
|
115
|
+
* the rows.
|
|
116
|
+
*
|
|
117
|
+
* Bounded by `limit` and therefore incremental: a backlog larger than one pass
|
|
118
|
+
* takes several. It reads a page and deletes row by row rather than issuing one
|
|
119
|
+
* ranged `deleteMany`, because a `lt` comparison against a `date` column is the
|
|
120
|
+
* kind of thing that behaves differently on each of the three adapters this
|
|
121
|
+
* package ships, and a prune job is not where that should be discovered.
|
|
122
|
+
*/
|
|
123
|
+
declare const pruneSignUpInvitations: (auth: LunoraAuth, options?: {
|
|
124
|
+
limit?: number;
|
|
125
|
+
}) => Promise<number>;
|
|
126
|
+
export { InviteOnlyOptions as I, SignUpInvitation as S, IssuedSignUpInvitation as a, createSignUpInvitation as c, inviteOnly as i, listSignUpInvitations as l, pruneSignUpInvitations as p, revokeSignUpInvitation as r };
|
|
@@ -12,6 +12,16 @@ interface SignUpInvitation {
|
|
|
12
12
|
/** Free-form attribution (a user id, an operator name); never read by the gate. */
|
|
13
13
|
invitedBy: null | string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* What {@link createSignUpInvitation} hands back: the stored row plus the one
|
|
17
|
+
* and only sight of the plaintext `token`. Nothing reads it back afterwards —
|
|
18
|
+
* the database holds a SHA-256 of it — so an invitation link that is lost is
|
|
19
|
+
* reissued, not recovered.
|
|
20
|
+
*/
|
|
21
|
+
interface IssuedSignUpInvitation extends SignUpInvitation {
|
|
22
|
+
/** Put this in the sign-up link as `?invite=…`. Never stored, never logged, never listed. */
|
|
23
|
+
token: string;
|
|
24
|
+
}
|
|
15
25
|
/** Options for {@link inviteOnly}. */
|
|
16
26
|
interface InviteOnlyOptions {
|
|
17
27
|
/**
|
|
@@ -57,12 +67,16 @@ declare const inviteOnly: (options?: InviteOnlyOptions) => BetterAuthPlugin;
|
|
|
57
67
|
* Nothing prunes the table — a spent or expired row stays until you delete it with
|
|
58
68
|
* {@link revokeSignUpInvitation}, which is also what keeps it a record of who was
|
|
59
69
|
* let in.
|
|
70
|
+
*
|
|
71
|
+
* Each call mints a **new token** and returns it in the clear, once. Only its
|
|
72
|
+
* SHA-256 is stored, so re-inviting an address invalidates the previous link, and
|
|
73
|
+
* a link that was never delivered is reissued rather than looked up.
|
|
60
74
|
*/
|
|
61
75
|
declare const createSignUpInvitation: (auth: LunoraAuth, input: {
|
|
62
76
|
email: string;
|
|
63
77
|
expiresInSeconds?: number;
|
|
64
78
|
invitedBy?: string;
|
|
65
|
-
}) => Promise<
|
|
79
|
+
}) => Promise<IssuedSignUpInvitation>;
|
|
66
80
|
/**
|
|
67
81
|
* The most recent invitations, newest first, up to a fixed ceiling of
|
|
68
82
|
* {@link MAX_LISTED}. `pendingOnly` drops the spent and the expired, which is the
|
|
@@ -92,4 +106,21 @@ declare const listSignUpInvitations: (auth: LunoraAuth, options?: {
|
|
|
92
106
|
declare const revokeSignUpInvitation: (auth: LunoraAuth, input: {
|
|
93
107
|
email: string;
|
|
94
108
|
}) => Promise<void>;
|
|
95
|
-
|
|
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
|
@@ -12,7 +12,14 @@ type I as InviteOnlyOptions,
|
|
|
12
12
|
* Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
|
|
13
13
|
* re-export.
|
|
14
14
|
*/
|
|
15
|
-
type
|
|
15
|
+
type a as IssuedSignUpInvitation,
|
|
16
|
+
/**
|
|
17
|
+
* Close self-serve sign-up to invited addresses only: an account is created only
|
|
18
|
+
* for an email an administrator has invited with `createSignUpInvitation`.
|
|
19
|
+
* Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
|
|
20
|
+
* re-export.
|
|
21
|
+
*/
|
|
22
|
+
type S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-DsVdKwcp.mjs";
|
|
16
23
|
import { BetterAuthPlugin } from 'better-auth';
|
|
17
24
|
export { apiKey } from '@better-auth/api-key';
|
|
18
25
|
export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
|
package/dist/plugins.d.ts
CHANGED
|
@@ -12,7 +12,14 @@ type I as InviteOnlyOptions,
|
|
|
12
12
|
* Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
|
|
13
13
|
* re-export.
|
|
14
14
|
*/
|
|
15
|
-
type
|
|
15
|
+
type a as IssuedSignUpInvitation,
|
|
16
|
+
/**
|
|
17
|
+
* Close self-serve sign-up to invited addresses only: an account is created only
|
|
18
|
+
* for an email an administrator has invited with `createSignUpInvitation`.
|
|
19
|
+
* Declares the `signUpInvitation` table it reads. Lunora's own, not a better-auth
|
|
20
|
+
* re-export.
|
|
21
|
+
*/
|
|
22
|
+
type S as SignUpInvitation, i as inviteOnly } from "./packem_shared/invite-only.d-C9Su82Iq.js";
|
|
16
23
|
import { BetterAuthPlugin } from 'better-auth';
|
|
17
24
|
export { apiKey } from '@better-auth/api-key';
|
|
18
25
|
export { createMcpProtectedRequestHandler, mcp, requireMcpAuth } from '@better-auth/mcp';
|
package/dist/plugins.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{inviteOnly as e}from"./packem_shared/createSignUpInvitation-
|
|
1
|
+
import{inviteOnly as e}from"./packem_shared/createSignUpInvitation-CqqQc4S8.mjs";import{uiConfig as m}from"./packem_shared/uiConfig-B8pN6pks.mjs";import{apiKey as f}from"@better-auth/api-key";import{createMcpProtectedRequestHandler as i,mcp as n,requireMcpAuth as a}from"@better-auth/mcp";import{oauthDeviceAuthorization as s,oauthProvider as u}from"@better-auth/oauth-provider";import{passkey as d}from"@better-auth/passkey";import{scim as A}from"@better-auth/scim";import{captcha as v,lastLoginMethod as y,oneTap as P}from"better-auth/plugins";import{createAccessControl as T}from"better-auth/plugins/access";import{admin as z}from"better-auth/plugins/admin";import{anonymous as O}from"better-auth/plugins/anonymous";import{bearer as q}from"better-auth/plugins/bearer";import{customSession as L}from"better-auth/plugins/custom-session";import{deviceAuthorization as j}from"better-auth/plugins/device-authorization";import{emailOTP as D}from"better-auth/plugins/email-otp";import{genericOAuth as H}from"better-auth/plugins/generic-oauth";import{haveIBeenPwned as K}from"better-auth/plugins/haveibeenpwned";import{jwt as R}from"better-auth/plugins/jwt";import{magicLink as G}from"better-auth/plugins/magic-link";import{multiSession as Q}from"better-auth/plugins/multi-session";import{oAuthProxy as V}from"better-auth/plugins/oauth-proxy";import{oneTimeToken as X}from"better-auth/plugins/one-time-token";import{organization as Z}from"better-auth/plugins/organization";import{phoneNumber as $}from"better-auth/plugins/phone-number";import{siwe as ro}from"better-auth/plugins/siwe";import{twoFactor as to}from"better-auth/plugins/two-factor";import{username as po}from"better-auth/plugins/username";export{z as admin,O as anonymous,f as apiKey,q as bearer,v as captcha,T as createAccessControl,i as createMcpProtectedRequestHandler,L as customSession,j as deviceAuthorization,D as emailOTP,H as genericOAuth,K as haveIBeenPwned,e as inviteOnly,R as jwt,y as lastLoginMethod,G as magicLink,n as mcp,Q as multiSession,V as oAuthProxy,s as oauthDeviceAuthorization,u as oauthProvider,P as oneTap,X as oneTimeToken,Z as organization,d as passkey,$ as phoneNumber,a as requireMcpAuth,A as scim,ro as siwe,to as twoFactor,m as uiConfig,po as username};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/auth",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.118",
|
|
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.
|
|
107
|
-
"@lunora/values": "1.0.0-alpha.
|
|
106
|
+
"@lunora/errors": "1.0.0-alpha.32",
|
|
107
|
+
"@lunora/values": "1.0.0-alpha.40",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createLocalAccountIssuer as I}from"@better-auth/core/db";import{LunoraError as R}from"@lunora/errors";import{getAuthTables as b}from"better-auth/db";class p extends R{constructor(m,f){super(f,m,{name:"LunoraAuthAdminError"})}}const N=50,E=500,M=3600,D=M*24,U=100*365*24*60*60,T=2880*60*1e3,z=[{field:"userId",model:"member"},{field:"userId",model:"teamMember"},{field:"userId",model:"passkey"},{field:"userId",model:"twoFactor"},{field:"userId",model:"oauthAccessToken"},{field:"userId",model:"oauthRefreshToken"},{field:"userId",model:"oauthConsent"},{field:"userId",model:"deviceCode"},{field:"userId",model:"walletAddress"},{field:"inviterId",model:"invitation"}],L=new Set(["accessToken","backupCodes","idToken","password","publicKey","refreshToken","secret","token"]),k=d=>Math.min(Math.max(Math.trunc(d??N),1),E),C=d=>Math.max(0,Math.trunc(d??0)),u=d=>{const m={};for(const[f,l]of Object.entries(d))L.has(f)||(m[f]=l instanceof Date?l.getTime():l);return m},g=d=>Array.isArray(d)?d.join(","):d,S=d=>d.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),P=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),B={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},F=d=>d==="boolean"||d==="date"||d==="number"?d:"string",G=d=>{const m=[];for(const[f,l]of Object.entries(d))l.input===!1||l.references!==void 0||P.has(f)||m.push({name:f,plugin:B[f],required:l.required===!0,type:F(l.type),unique:l.unique===!0});return m},O=d=>typeof d=="string"&&d.includes("owner"),V=async(d,m)=>{const l=(await d.adapter.findMany({model:"member",where:[{field:"userId",value:m}]})).filter(w=>O(w.role)&&typeof w.organizationId=="string").map(w=>w.organizationId);if(l.length===0)return;const y=await Promise.all(l.map(async w=>d.adapter.findMany({model:"member",where:[{field:"organizationId",value:w}]}))),r=l.find((w,v)=>!y[v]?.some(a=>a.userId!==m&&O(a.role)));if(r!==void 0)throw new p(`user is the last owner of organization ${r} — transfer ownership or delete the organization first`,"LAST_ORGANIZATION_OWNER")},_=d=>{if(d instanceof p)return d;const m=d,f=m?.body?.code??m?.code??"AUTH_ADMIN_ERROR",l=m?.body?.message??m?.message??"auth admin operation failed";return new p(l,f)},$=(d,m={})=>{const f=d.$context,l=m.features??{},y=a=>{const e=new Set((a.plugins??[]).map(i=>i.id)),t=i=>e.has(i);return{accounts:l.accounts??!0,admin:l.admin??t("admin"),organization:l.organization??t("organization"),passkey:l.passkey??t("passkey"),twoFactor:l.twoFactor??t("two-factor")}},r=async a=>{try{return await a(await f)}catch(e){throw _(e)}},w=a=>u(a),v=async(a,e,t)=>{const i=t.where&&t.where.length>0?t.where:void 0,[n,s]=await Promise.all([a.adapter.findMany({limit:k(t.limit),model:e,offset:C(t.offset),sortBy:t.sortBy,where:i}),a.adapter.count({model:e,where:i})]);return{rows:n.map(o=>u(o)),total:s}};return{banUser:({expiresInSeconds:a,reason:e,userId:t})=>r(async i=>{let n=null;if(a!==void 0){if(!Number.isInteger(a)||a<=0)throw new p("expiresInSeconds must be a positive finite integer","INVALID_BAN_SECONDS");const o=Math.min(a,U);n=new Date(Date.now()+o*1e3)}const s=await i.internalAdapter.updateUser(t,{banExpires:n,banned:!0,banReason:e??"No reason"});return await i.internalAdapter.deleteUserSessions(t),w(s)}),cancelInvitation:({invitationId:a})=>r(async e=>{await e.adapter.delete({model:"invitation",where:[{field:"id",value:a}]})}),capabilities:()=>r(a=>Promise.resolve(y(a.options))),addMember:({organizationId:a,role:e,userId:t})=>r(async i=>{const n=await i.adapter.create({data:{createdAt:new Date,organizationId:a,role:e===void 0||e===""?"member":e,userId:t},model:"member"});return u(n)}),addTeamMember:({teamId:a,userId:e})=>r(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,teamId:a,userId:e},model:"teamMember"});return u(i)}),config:()=>r(a=>{const e=a.options,t=y(e),i=new Set((e.plugins??[]).map(c=>c.id)),n=b(e),s=e.session??{},o=e.rateLimit??{};return Promise.resolve({capabilities:t,emailAndPassword:e.emailAndPassword?.enabled??!1,organization:{enabled:t.organization,roles:!!n.organizationRole,teams:!!n.team},plugins:[...i].toSorted((c,h)=>c.localeCompare(h)),rateLimit:{enabled:o.enabled??!1,max:o.max,window:o.window},session:{cookieCache:s.cookieCache?.enabled,expiresIn:s.expiresIn,freshAge:s.freshAge,updateAge:s.updateAge},socialProviders:Object.keys(e.socialProviders??{}).toSorted((c,h)=>c.localeCompare(h)),userFields:G(n.user?.fields??{})})}),createOrganization:({logo:a,metadata:e,name:t,ownerId:i,slug:n})=>r(async s=>{const o=S(n!==void 0&&n!==""?n:t);if(o==="")throw new p("could not derive a slug from the organization name","ORG_SLUG_INVALID");if(await s.adapter.findOne({model:"organization",where:[{field:"slug",value:o}]}))throw new p("an organization with this slug already exists","ORG_SLUG_TAKEN");const h=await s.adapter.create({data:{createdAt:new Date,logo:a===void 0||a===""?void 0:a,metadata:e===void 0?void 0:JSON.stringify(e),name:t,slug:o},model:"organization"});return i!==void 0&&i!==""&&await s.adapter.create({data:{createdAt:new Date,organizationId:h.id,role:"owner",userId:i},model:"member"}),u(h)}),createOrgRole:({organizationId:a,permission:e,role:t})=>r(async i=>{const n=await i.adapter.create({data:{createdAt:new Date,organizationId:a,permission:JSON.stringify(e),role:t},model:"organizationRole"});return u(n)}),createTeam:({name:a,organizationId:e})=>r(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,name:a,organizationId:e},model:"team"});return u(i)}),deleteOrganization:({organizationId:a})=>r(async e=>{const t=b(e.options);if(await e.adapter.deleteMany({model:"member",where:[{field:"organizationId",value:a}]}),await e.adapter.deleteMany({model:"invitation",where:[{field:"organizationId",value:a}]}),t.team){const i=await e.adapter.findMany({model:"team",where:[{field:"organizationId",value:a}]});for(const n of i)await e.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:n.id}]});await e.adapter.deleteMany({model:"team",where:[{field:"organizationId",value:a}]})}t.organizationRole&&await e.adapter.deleteMany({model:"organizationRole",where:[{field:"organizationId",value:a}]}),await e.adapter.delete({model:"organization",where:[{field:"id",value:a}]})}),deleteOrgRole:({roleId:a})=>r(async e=>{await e.adapter.delete({model:"organizationRole",where:[{field:"id",value:a}]})}),inviteMember:({email:a,inviterId:e,organizationId:t,role:i})=>r(async n=>{let s=e;if(s===void 0||s===""){const c=await n.adapter.findMany({model:"member",where:[{field:"organizationId",value:t}]});s=(c.find(A=>typeof A.role=="string"&&A.role.includes("owner"))??c[0])?.userId}if(s===void 0||s==="")throw new p("provide an inviter — the organization has no members to attribute the invitation to","INVITER_REQUIRED");const o=await n.adapter.create({data:{createdAt:new Date,email:a.toLowerCase(),expiresAt:new Date(Date.now()+T),inviterId:s,organizationId:t,role:i===void 0||i===""?"member":i,status:"pending"},model:"invitation"});return u(o)}),listOrgRoles:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"organizationRole",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listTeamMembers:({limit:a,offset:e,teamId:t})=>r(i=>v(i,"teamMember",{limit:a,offset:e,where:[{field:"teamId",value:t}]})),listTeams:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"team",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),removeTeam:({teamId:a})=>r(async e=>{await e.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:a}]}),await e.adapter.delete({model:"team",where:[{field:"id",value:a}]})}),removeTeamMember:({teamMemberId:a})=>r(async e=>{await e.adapter.delete({model:"teamMember",where:[{field:"id",value:a}]})}),updateMemberRole:({memberId:a,role:e})=>r(async t=>{const i=await t.adapter.update({model:"member",update:{role:g(e)},where:[{field:"id",value:a}]});return u(i??{id:a,role:g(e)})}),updateOrganization:({logo:a,metadata:e,name:t,organizationId:i,slug:n})=>r(async s=>{const o={};if(t!==void 0&&(o.name=t),n!==void 0&&n!==""&&(o.slug=S(n)),a!==void 0&&(o.logo=a===""?void 0:a),e!==void 0&&(o.metadata=JSON.stringify(e)),Object.keys(o).length===0)return u({id:i});const c=await s.adapter.update({model:"organization",update:o,where:[{field:"id",value:i}]});return u(c??{id:i})}),updateOrgRole:({permission:a,roleId:e})=>r(async t=>{const i=await t.adapter.update({model:"organizationRole",update:{permission:JSON.stringify(a),updatedAt:new Date},where:[{field:"id",value:e}]});return u(i??{id:e,permission:JSON.stringify(a)})}),updateTeam:({name:a,teamId:e})=>r(async t=>{const i=await t.adapter.update({model:"team",update:{name:a,updatedAt:new Date},where:[{field:"id",value:e}]});return u(i??{id:e,name:a})}),createUser:({data:a,email:e,name:t,password:i,role:n})=>r(async s=>{const o=e.toLowerCase();if(await s.internalAdapter.findUserByEmail(o))throw new p("a user with this email already exists","USER_ALREADY_EXISTS");const c=await s.internalAdapter.createUser({email:o,name:t,role:n===void 0?void 0:g(n),...a},{method:"admin"});if(i!==void 0&&i!==""){const h=await s.password.hash(i);await s.internalAdapter.linkAccount({accountId:c.id,issuer:I("credential"),password:h,providerId:"credential",userId:c.id})}return w(c)}),deletePasskey:({passkeyId:a})=>r(async e=>{await e.adapter.delete({model:"passkey",where:[{field:"id",value:a}]})}),disableTwoFactor:({userId:a})=>r(async e=>{await e.adapter.deleteMany({model:"twoFactor",where:[{field:"userId",value:a}]}),await e.internalAdapter.updateUser(a,{twoFactorEnabled:!1})}),impersonateUser:({userId:a})=>r(async e=>{const t=await e.internalAdapter.findUserById(a);if(!t)throw new p("user not found","USER_NOT_FOUND");const i=m.impersonationSeconds;let n=M;if(i!==void 0){if(!Number.isInteger(i)||!Number.isFinite(i)||i<=0)throw new p("impersonationSeconds must be a positive finite integer","INVALID_IMPERSONATION_SECONDS");n=Math.min(i,D)}const s=new Date(Date.now()+n*1e3),o=await e.internalAdapter.createSession(a,!0,{expiresAt:s,impersonatedBy:m.impersonatedBy??a},!0);return{expiresAt:o.expiresAt instanceof Date?o.expiresAt.getTime():s.getTime(),token:o.token,user:w(t)}}),listAccounts:({userId:a})=>r(async e=>(await e.adapter.findMany({model:"account",where:[{field:"userId",value:a}]})).map(i=>u(i))),listInvitations:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"invitation",{limit:a,offset:e,where:[{field:"organizationId",value:t}]})),listMembers:({limit:a,offset:e,organizationId:t})=>r(i=>v(i,"member",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listOrganizations:({limit:a,offset:e})=>r(t=>v(t,"organization",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"}})),listPasskeys:({userId:a})=>r(async e=>(await e.adapter.findMany({model:"passkey",where:[{field:"userId",value:a}]})).map(i=>u(i))),listSessions:({limit:a,offset:e,userId:t})=>r(i=>v(i,"session",{limit:a,offset:e,sortBy:{direction:"desc",field:"createdAt"},where:t===void 0||t===""?void 0:[{field:"userId",value:t}]})),listUsers:({filterField:a,filterValue:e,limit:t,offset:i,search:n,searchField:s,sortBy:o,sortDirection:c})=>r(h=>{const A=[];return n!==void 0&&n!==""&&A.push({field:s??"email",operator:"contains",value:n}),e!==void 0&&A.push({field:a??"email",operator:"eq",value:e}),v(h,"user",{limit:t,offset:i,sortBy:{direction:c??"desc",field:o??"createdAt"},where:A})}),removeMember:({memberId:a})=>r(async e=>{await e.adapter.delete({model:"member",where:[{field:"id",value:a}]})}),removeUser:({userId:a})=>r(async e=>{const t=b(e.options);t.member&&await V(e,a),await Promise.all(z.filter(({model:i})=>t[i]).map(({field:i,model:n})=>e.adapter.deleteMany({model:n,where:[{field:i,value:a}]}))),await e.internalAdapter.deleteUserSessions(a),await e.internalAdapter.deleteUser(a)}),revokeUserSession:({sessionId:a})=>r(async e=>{const t=await e.adapter.findOne({model:"session",where:[{field:"id",value:a}]});t?.token&&await e.internalAdapter.deleteSession(t.token)}),revokeUserSessions:({userId:a})=>r(async e=>{await e.internalAdapter.deleteUserSessions(a)}),setRole:({role:a,userId:e})=>r(async t=>{const i=await t.internalAdapter.updateUser(e,{role:g(a)});return w(i)}),setUserPassword:({newPassword:a,userId:e})=>r(async t=>{const i=t.password.config.minPasswordLength,n=t.password.config.maxPasswordLength;if(a.length<i)throw new p(`password must be at least ${i.toString()} characters`,"PASSWORD_TOO_SHORT");if(a.length>n)throw new p(`password must be at most ${n.toString()} characters`,"PASSWORD_TOO_LONG");if(!await t.internalAdapter.findUserById(e))throw new p("user not found","USER_NOT_FOUND");const o=(await t.internalAdapter.findAccounts(e)).some(h=>h.providerId==="credential");if(!o&&t.options.emailAndPassword?.enabled!==!0)throw new p("email/password sign-in is disabled for this deployment","EMAIL_PASSWORD_DISABLED");const c=await t.password.hash(a);o?await t.internalAdapter.updatePassword(e,c):await t.internalAdapter.linkAccount({accountId:e,issuer:I("credential"),password:c,providerId:"credential",userId:e})}),unbanUser:({userId:a})=>r(async e=>{const t=await e.internalAdapter.updateUser(a,{banExpires:null,banned:!1,banReason:null});return w(t)}),unlinkAccount:({accountId:a,userId:e})=>r(async t=>{await t.adapter.delete({model:"account",where:[{field:"id",value:a},{connector:"AND",field:"userId",value:e}]})}),updateUser:({data:a,userId:e})=>r(async t=>{const i=await t.internalAdapter.updateUser(e,a);return w(i)})}};export{p as LunoraAuthAdminError,$ as createAuthAdmin};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createAuthMiddleware as d}from"better-auth/api";import{appendAuthAuditEntry as c}from"../audit.mjs";const l=r=>{const t=r.toLowerCase(),e=i=>t===i||t.endsWith(i);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(e("/sign-in/social")||e("/sign-in/magic-link"))return"sign-in-initiated";if(t.includes("/sign-in/")||t.includes("/callback/")||e("/magic-link/verify")||t.includes("/two-factor/verify-"))return"sign-in";if(e("/sign-out"))return"sign-out";if(e("/change-password")||e("/set-password"))return"password-change";if(e("/reset-password")||e("/request-password-reset")||e("/forget-password"))return"password-reset";if(e("/verify-email"))return"email-verification";if(t.includes("/two-factor/enable")||t.includes("/totp/enable"))return"mfa-enable";if(t.includes("/two-factor/disable")||t.includes("/totp/disable"))return"mfa-disable";if(e("/refresh-token")||e("/token"))return"token-refresh";if(e("/revoke-session")||e("/revoke-sessions")||e("/revoke-other-sessions"))return"session-revoke";if(e("/link-social"))return"account-link";if(e("/unlink-account"))return"account-unlink"},a=(r,t)=>r.headers?.get(t)??r.request?.headers.get(t)??void 0,f=(r,t)=>{const e=a(r,"cf-connecting-ip");if(e!==void 0)return e;if(t===!0)return a(r,"x-forwarded-for")?.split(",")[0]?.trim()},v=r=>{const t=r.context?.newSession??r.context?.session,e=t?.user?.id??t?.session?.userId,i=t?.user?.email;return{...e===void 0?{}:{actorId:e},...i===void 0?{}:{actorEmail:i}}},h=r=>{const t=r.context?.returned;if(t instanceof Error)return"failure";if(typeof t=="object"&&t!==null&&"status"in t){const e=Number(t.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},u=320,p=(r,t)=>{if(t!=="sign-in"&&t!=="sign-in-initiated")return;const e=r.body?.email??r.body?.username;if(!(typeof e!="string"||e.length===0))return e.length>u?e.slice(0,u):e},g=(r,{now:t=Date.now(),trustProxyHeaders:e}={})=>{const i=r.path===void 0?void 0:l(r.path);if(i===void 0)return;const s=f(r,e),n=a(r,"user-agent"),o=p(r,i);return{...v(r),event:i,outcome:h(r),ts:t,...s===void 0?{}:{ip:s},...o===void 0?{}:{targetEmail:o},...n===void 0?{}:{userAgent:n},detail:{path:r.path}}},m=r=>d(async t=>{try{const e=g(t,{trustProxyHeaders:r.trustProxyHeaders});if(e!==void 0){const i=await c(r.executor,e,{redactDetail:r.redactDetail,retention:r.retention});r.onRecord!==void 0&&await r.onRecord(i)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}}),y=(r,t)=>{const e=m(t),i=r.hooks?.after,s=i?async n=>{const o=await i(n);return await e(n),o}:e;return{...r,hooks:{...r.hooks,after:s}}};export{m as authAuditHook,g as buildAuditEntry,l as eventForPath,y as withAuthAudit};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{getMigrations as l}from"better-auth/db/migration";import{resolveAuthOptions as I}from"./createAuth-CtTKaLZN.mjs";const w=/pragma_index_list\s*\(/iu,b=/sqlite_master/iu,h=/^["`[]|["\]`]$/gu,y=/\s+/u,_=/create\s+unique\s+index/iu,x=/\bwhere\b/iu,M=t=>w.test(t)&&b.test(t),R=t=>(t.trim().split(y)[0]??"").replaceAll(h,"").trim(),D=t=>{const n=t.indexOf("(");if(n===-1)return{columns:[],tail:""};let s=0;for(let e=n;e<t.length;e+=1){const o=t[e];if(o==="(")s+=1;else if(o===")"&&(s-=1,s===0))return{columns:t.slice(n+1,e).split(",").map(i=>R(i)).filter(i=>i!==""),tail:t.slice(e+1)}}return{columns:[],tail:""}},T=async t=>{const n=t.prepare("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'"),{results:s}=await n.all(),e=[];for(const o of s??[]){const i=o.name,r=o.tbl_name,a=o.sql;if(typeof i!="string"||typeof r!="string")continue;if(typeof a!="string"){e.push({columnPosition:0,indexName:i,isPartial:0,isUnique:1,tableName:r});continue}const p=_.test(a)?1:0,{columns:m,tail:f}=D(a),d=x.test(f)?1:0;for(const[E,g]of m.entries())e.push({columnName:g,columnPosition:E,indexName:i,isPartial:d,isUnique:p,tableName:r})}return e},A=t=>{const n=async()=>{const e=await T(t);return{meta:{},results:e,success:!0}},s={all:n,bind:()=>s,run:n};return s},N=t=>new Proxy(t,{get(n,s,e){if(s==="prepare")return i=>M(i)?A(n):n.prepare(i);const o=Reflect.get(n,s,e);return typeof o=="function"?o.bind(n):o}}),P=t=>typeof t=="object"&&t!==null&&typeof t.prepare=="function"&&typeof t.batch=="function",u=t=>P(t.database)?{...t,database:N(t.database)}:t,c=new WeakMap,S=async t=>{const{options:n}=t,s=c.get(n);if(s){await s;return}const e=(async()=>{const{runMigrations:o}=await l(u(n));await o()})();c.set(n,e);try{await e}catch(o){throw c.delete(n),o}},U=async t=>{const{compileMigrations:n}=await l(u(I(t)));return n()};export{U as compileMigrationsSql,S as ensureMigrated};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{defineErrorCodes as v}from"@better-auth/core/utils/error-codes";import{LunoraError as p}from"@lunora/errors";import{APIError as E}from"better-auth/api";const s="signUpInvitation",I=10080*60,w=365*24*60*60,h=500,A=v({SIGN_UP_INVITE_REQUIRED:"sign-up is invite-only — ask an administrator for an invitation"}),D=/^[^\s@]+@[^\s@]+$/,g=e=>{if(!D.test(e))return!1;const t=e.slice(e.indexOf("@")+1);return t.includes(".")&&!t.startsWith(".")&&!t.endsWith(".")},u=e=>({acceptedAt:e.acceptedAt instanceof Date?e.acceptedAt:null,createdAt:e.createdAt,email:String(e.email),expiresAt:e.expiresAt,id:String(e.id),invitedBy:typeof e.invitedBy=="string"?e.invitedBy:null}),f=e=>e.trim().toLowerCase(),y=e=>{if(typeof e.email!="string")return;const t=f(e.email);return t===""?void 0:t},O=async(e,t)=>{const i=await e.findOne({model:s,where:[{field:"email",value:t}]});return i===null||i.acceptedAt instanceof Date?!1:i.expiresAt instanceof Date&&i.expiresAt.getTime()>Date.now()},S=e=>{e.emailAndPassword?.enabled!==!0||e.emailAndPassword.requireEmailVerification===!0||console.warn("@lunora/auth: inviteOnly() is installed with password sign-up but without `emailAndPassword: { requireEmailVerification: true }`. An invitation is keyed by email address alone, so anyone who learns an invited address can sign up as it — and without verification they hold a session the moment they do.")},T=(e={})=>{const t=e.allowFirstUser??!1;return{$ERROR_CODES:A,id:"lunora-invite-only",init:i=>{S(i.options);const{adapter:n}=i;let a=t;return{options:{databaseHooks:{user:{create:{after:async d=>{const o=y(d);o!==void 0&&await n.update({model:s,update:{acceptedAt:new Date},where:[{field:"email",value:o}]})},before:async d=>{const o=y(d);if(!(o!==void 0&&await O(n,o))){if(a){if(await n.count({model:"user"})===0)return;a=!1}throw new E("BAD_REQUEST",A.SIGN_UP_INVITE_REQUIRED)}}}}}}}},schema:{[s]:{fields:{acceptedAt:{required:!1,type:"date"},createdAt:{defaultValue:()=>new Date,required:!0,type:"date"},email:{required:!0,type:"string",unique:!0},expiresAt:{required:!0,type:"date"},invitedBy:{required:!1,type:"string"}}}}}},U=async(e,t)=>{const i=f(t.email);if(!g(i))throw new p("VALIDATION_ERROR",`not an email address to invite: ${JSON.stringify(t.email)}`);const{expiresInSeconds:n=I}=t;if(!Number.isInteger(n)||n<=0||n>w)throw new p("VALIDATION_ERROR",`expiresInSeconds must be a positive integer no greater than ${String(w)}`);const a=new Date(Date.now()+n*1e3),r=t.invitedBy??null,c=await e.$context,d=[{field:"email",value:i}],o=async()=>c.adapter.update({model:s,update:{acceptedAt:null,expiresAt:a,invitedBy:r},where:d});if(await c.adapter.findOne({model:s,where:d})){const l=await o();if(l)return u(l)}try{return u(await c.adapter.create({data:{createdAt:new Date,email:i,expiresAt:a,invitedBy:r},model:s}))}catch(l){const m=await o();if(m===null)throw l;return u(m)}},N=async(e,t={})=>{const a=(await(await e.$context).adapter.findMany({limit:h,model:s,sortBy:{direction:"desc",field:"createdAt"}})).map(r=>u(r));return t.pendingOnly===!0?a.filter(r=>r.acceptedAt===null&&r.expiresAt.getTime()>Date.now()):a},L=async(e,t)=>{await(await e.$context).adapter.delete({model:s,where:[{field:"email",value:f(t.email)}]})};export{U as createSignUpInvitation,T as inviteOnly,N as listSignUpInvitations,L as revokeSignUpInvitation};
|