@lunora/auth 1.0.0-alpha.140 → 1.0.0-alpha.142

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/dist/audit.d.mts CHANGED
@@ -79,7 +79,11 @@ interface ReadAuthAuditOptions {
79
79
  event?: string;
80
80
  /** Max rows to return, clamped to [1, 10000]. Defaults to 1000. */
81
81
  limit?: number;
82
- /** Return only events with `seq` strictly greater than this (forward paging). */
82
+ /**
83
+ * Return only events with `seq` strictly greater than this. Supplying it
84
+ * switches the page to oldest-first so the cursor walks forward; omit it for
85
+ * the newest-first feed.
86
+ */
83
87
  sinceSeq?: number;
84
88
  }
85
89
  declare const ensureAuthAuditTable: (executor: SqlExecutor) => Promise<void>;
@@ -91,10 +95,17 @@ declare const ensureAuthAuditTable: (executor: SqlExecutor) => Promise<void>;
91
95
  */
92
96
  declare const appendAuthAuditEntry: (executor: SqlExecutor, entry: AppendAuthAuditEntry, options?: AppendAuthAuditOptions) => Promise<AppendAuthAuditEntry>;
93
97
  /**
94
- * Read audit events newest-first, optionally filtered by `actorId` / `event` and
95
- * paged past `sinceSeq`, up to `limit` (clamped to [1, 10000]). Parses each row's
96
- * `detail` JSON back into an object. Creates the table first so reads on a
97
- * never-audited database return `[]` instead of throwing.
98
+ * Read audit events, optionally filtered by `actorId` / `event` and paged past
99
+ * `sinceSeq`, up to `limit` (clamped to [1, 10000]). Parses each row's `detail`
100
+ * JSON back into an object. Creates the table first so reads on a never-audited
101
+ * database return `[]` instead of throwing.
102
+ *
103
+ * Ordering follows the cursor: **without** `sinceSeq` the page is newest-first
104
+ * (`seq DESC`) — the feed the studio's Security panel renders. **With**
105
+ * `sinceSeq` it is oldest-first (`seq ASC`), because a lower bound is a forward
106
+ * walk: a descending page above the bound would hand back the newest rows and
107
+ * leave everything between the cursor and them unreachable once the caller
108
+ * advanced past it.
98
109
  *
99
110
  * `limit` is NaN-safe: a non-finite/non-number value (e.g. a caller passing
100
111
  * `Number.NaN`, or an upstream boundary that failed to reject one) falls back
package/dist/audit.d.ts CHANGED
@@ -79,7 +79,11 @@ interface ReadAuthAuditOptions {
79
79
  event?: string;
80
80
  /** Max rows to return, clamped to [1, 10000]. Defaults to 1000. */
81
81
  limit?: number;
82
- /** Return only events with `seq` strictly greater than this (forward paging). */
82
+ /**
83
+ * Return only events with `seq` strictly greater than this. Supplying it
84
+ * switches the page to oldest-first so the cursor walks forward; omit it for
85
+ * the newest-first feed.
86
+ */
83
87
  sinceSeq?: number;
84
88
  }
85
89
  declare const ensureAuthAuditTable: (executor: SqlExecutor) => Promise<void>;
@@ -91,10 +95,17 @@ declare const ensureAuthAuditTable: (executor: SqlExecutor) => Promise<void>;
91
95
  */
92
96
  declare const appendAuthAuditEntry: (executor: SqlExecutor, entry: AppendAuthAuditEntry, options?: AppendAuthAuditOptions) => Promise<AppendAuthAuditEntry>;
93
97
  /**
94
- * Read audit events newest-first, optionally filtered by `actorId` / `event` and
95
- * paged past `sinceSeq`, up to `limit` (clamped to [1, 10000]). Parses each row's
96
- * `detail` JSON back into an object. Creates the table first so reads on a
97
- * never-audited database return `[]` instead of throwing.
98
+ * Read audit events, optionally filtered by `actorId` / `event` and paged past
99
+ * `sinceSeq`, up to `limit` (clamped to [1, 10000]). Parses each row's `detail`
100
+ * JSON back into an object. Creates the table first so reads on a never-audited
101
+ * database return `[]` instead of throwing.
102
+ *
103
+ * Ordering follows the cursor: **without** `sinceSeq` the page is newest-first
104
+ * (`seq DESC`) — the feed the studio's Security panel renders. **With**
105
+ * `sinceSeq` it is oldest-first (`seq ASC`), because a lower bound is a forward
106
+ * walk: a descending page above the bound would hand back the newest rows and
107
+ * leave everything between the cursor and them unreachable once the caller
108
+ * advanced past it.
98
109
  *
99
110
  * `limit` is NaN-safe: a non-finite/non-number value (e.g. a caller passing
100
111
  * `Number.NaN`, or an upstream boundary that failed to reject one) falls back
package/dist/audit.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import{standardRules as L,piiRules as f,redact as g}from"@visulima/redact";const o="__lunora_auth_audit__",R=[...L,...f],p=1e3,N=1e4,n=null,l=e=>{if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="bigint"||typeof e=="boolean")return String(e)},h=async e=>{await e.run(`CREATE TABLE IF NOT EXISTS "${o}" (
1
+ import{standardRules as f,piiRules as g,redact as R}from"@visulima/redact";const c="__lunora_auth_audit__",N=[...f,...g],h=1e3,p=1e4,n=null,d=e=>{if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="bigint"||typeof e=="boolean")return String(e)},I=async e=>{await e.run(`CREATE TABLE IF NOT EXISTS "${c}" (
2
2
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
3
3
  ts REAL NOT NULL,
4
4
  event TEXT NOT NULL,
@@ -9,4 +9,4 @@ import{standardRules as L,piiRules as f,redact as g}from"@visulima/redact";const
9
9
  ip TEXT,
10
10
  user_agent TEXT,
11
11
  detail TEXT
12
- )`,[]);try{await e.run(`ALTER TABLE "${o}" ADD COLUMN target_email TEXT`,[])}catch(t){if(!(t instanceof Error?t.message.toLowerCase():String(t).toLowerCase()).includes("duplicate column"))throw t}},E=new WeakMap,m=e=>{const t=E.get(e);if(t)return t;const a=h(e);return E.set(e,a),a.catch(()=>E.delete(e)),a},D=async(e,t,a={})=>{await m(e);let i;return t.detail!==void 0&&(i=a.redactDetail===!1?t.detail:g(t.detail,R)),await e.run(`INSERT INTO "${o}" (ts, event, outcome, actor_id, actor_email, target_email, ip, user_agent, detail) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,[t.ts,t.event,t.outcome,t.actorId??n,t.actorEmail??n,t.targetEmail??n,t.ip??n,t.userAgent??n,i===void 0?n:JSON.stringify(i)]),typeof a.retention=="number"&&a.retention>0&&await e.run(`DELETE FROM "${o}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${o}")`,[a.retention]),{...t,detail:i}},I=async(e,t={})=>{await m(e);const a=Number.isFinite(t.limit)?Math.max(1,Math.min(t.limit,N)):p,i=["seq > ?"],c=[t.sinceSeq??0];return t.actorId!==void 0&&(i.push("actor_id = ?"),c.push(t.actorId)),t.event!==void 0&&(i.push("event = ?"),c.push(t.event)),c.push(a),(await e.all(`SELECT seq, ts, event, outcome, actor_id, actor_email, target_email, ip, user_agent, detail FROM "${o}" WHERE ${i.join(" AND ")} ORDER BY seq DESC LIMIT ?`,c)).map(r=>{const u={event:l(r.event)??"",outcome:r.outcome==="failure"?"failure":"success",seq:Number(r.seq),ts:Number(r.ts)},A=[["actorId","actor_id"],["actorEmail","actor_email"],["targetEmail","target_email"],["ip","ip"],["userAgent","user_agent"]];for(const[s,_]of A){const T=l(r[_]);T!==void 0&&(u[s]=T)}const d=l(r.detail);if(d!==void 0)try{const s=JSON.parse(d);typeof s=="object"&&s!==null&&!Array.isArray(s)&&(u.detail=s)}catch{}return u})},O=e=>({read:t=>I(e,t)});export{o as AUTH_AUDIT_TABLE,D as appendAuthAuditEntry,O as createAuthAuditReader,m as ensureAuthAuditTable,I as readAuthAuditLog};
12
+ )`,[]);try{await e.run(`ALTER TABLE "${c}" ADD COLUMN target_email TEXT`,[])}catch(t){if(!(t instanceof Error?t.message.toLowerCase():String(t).toLowerCase()).includes("duplicate column"))throw t}},l=new WeakMap,m=e=>{const t=l.get(e);if(t)return t;const a=I(e);return l.set(e,a),a.catch(()=>l.delete(e)),a},D=async(e,t,a={})=>{await m(e);let i;return t.detail!==void 0&&(i=a.redactDetail===!1?t.detail:R(t.detail,N)),await e.run(`INSERT INTO "${c}" (ts, event, outcome, actor_id, actor_email, target_email, ip, user_agent, detail) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,[t.ts,t.event,t.outcome,t.actorId??n,t.actorEmail??n,t.targetEmail??n,t.ip??n,t.userAgent??n,i===void 0?n:JSON.stringify(i)]),typeof a.retention=="number"&&a.retention>0&&await e.run(`DELETE FROM "${c}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${c}")`,[a.retention]),{...t,detail:i}},S=async(e,t={})=>{await m(e);const a=Number.isFinite(t.limit)?Math.max(1,Math.min(t.limit,p)):h,i=["seq > ?"],o=[t.sinceSeq??0],A=t.sinceSeq===void 0?"DESC":"ASC";return t.actorId!==void 0&&(i.push("actor_id = ?"),o.push(t.actorId)),t.event!==void 0&&(i.push("event = ?"),o.push(t.event)),o.push(a),(await e.all(`SELECT seq, ts, event, outcome, actor_id, actor_email, target_email, ip, user_agent, detail FROM "${c}" WHERE ${i.join(" AND ")} ORDER BY seq ${A} LIMIT ?`,o)).map(r=>{const u={event:d(r.event)??"",outcome:r.outcome==="failure"?"failure":"success",seq:Number(r.seq),ts:Number(r.ts)},_=[["actorId","actor_id"],["actorEmail","actor_email"],["targetEmail","target_email"],["ip","ip"],["userAgent","user_agent"]];for(const[s,L]of _){const T=d(r[L]);T!==void 0&&(u[s]=T)}const E=d(r.detail);if(E!==void 0)try{const s=JSON.parse(E);typeof s=="object"&&s!==null&&!Array.isArray(s)&&(u.detail=s)}catch{}return u})},O=e=>({read:t=>S(e,t)});export{c as AUTH_AUDIT_TABLE,D as appendAuthAuditEntry,O as createAuthAuditReader,m as ensureAuthAuditTable,S as readAuthAuditLog};
package/dist/index.d.mts CHANGED
@@ -602,28 +602,60 @@ interface AuditHookContext {
602
602
  *
603
603
  * Sign-in is split by what the endpoint actually DOES (plan 280 §4):
604
604
  *
605
- * - `/sign-in/social`, `/sign-in/magic-link` only DISPATCH — the first mints a
606
- * provider redirect URL, the second sends an email. Nobody is authenticated
607
- * yet, so these are `sign-in-initiated`, not `sign-in`.
608
- * - `/callback/:id` (social + generic-oauth), `/magic-link/verify`, and every
605
+ * - `/sign-in/social`, `/sign-in/magic-link` and `@better-auth/sso`'s
606
+ * `/sign-in/sso` only DISPATCH the first mints a provider redirect URL, the
607
+ * second sends an email, the third answers `{ url, redirect: true }` pointing
608
+ * at the identity provider's authorization endpoint. Nobody is authenticated
609
+ * yet, so these are `sign-in-initiated`, not `sign-in`. Without the `sso`
610
+ * branch that path fell through to the `/sign-in/` substring below and every
611
+ * SSO redirect mint was recorded as a completed, successful `sign-in` with no
612
+ * actor — in exactly the flow where the "who authenticated" trail matters
613
+ * most.
614
+ * - `/callback/:id` (social + generic-oauth), `/magic-link/verify`,
615
+ * `@better-auth/sso`'s `/sso/saml2/sp/acs/:providerId`, and every
609
616
  * `/two-factor/verify-*` (`verify-totp` / `verify-otp` / `verify-backup-code`
610
617
  * — all three complete a challenged sign-in the same way) are where a
611
618
  * session actually gets issued, so they join credential sign-ins
612
619
  * (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`, …) as
613
620
  * plain `sign-in`. They were NOT recorded at all before this change.
614
621
  *
622
+ * The SAML assertion consumer service needs its own branch because it is the
623
+ * one completion that carries no `/callback/` segment: `processSAMLResponse`
624
+ * validates the assertion, resolves the user and calls `setSessionCookie`, all
625
+ * under `/sso/saml2/sp/acs/:providerId`. Matching neither that substring nor
626
+ * any other branch, it classified as `undefined` — the endpoint that issues
627
+ * every SAML session left no audit row at all.
628
+ *
629
+ * SAML sign-OUT has the mirror-image problem, and two endpoints rather than
630
+ * one. Neither ends in `/sign-out`, and both terminate the local session before
631
+ * redirecting, so both are `sign-out` rather than an initiated event:
632
+ *
633
+ * - `/sso/saml2/logout/:providerId` — SP-initiated. Deletes the SAML session
634
+ * keys, calls `deleteSession` on the current session token and
635
+ * `deleteSessionCookie`, then redirects to the IdP's logout URL.
636
+ * - `/sso/saml2/sp/slo/:providerId` — the SP's single-logout receiver, for the
637
+ * IdP-initiated direction and for the response leg of an SP-initiated one.
638
+ * Both `handleLogoutRequest` and `handleLogoutResponse` call `deleteSession`
639
+ * and `deleteSessionCookie` the same way.
640
+ *
641
+ * Recording the SAML sign-in while leaving these silent is worse than either
642
+ * gap alone: the trail would show a session opening and never closing, so a
643
+ * reader cannot tell "still signed in" from "we stopped watching".
644
+ *
615
645
  * There is no dedicated `/oauth2/callback/*` branch because the generic
616
646
  * `/callback/` check above already covers it, and covering it is CORRECT: an
617
647
  * OAuth callback is a completed sign-in whatever path prefix it arrives on. The
618
648
  * same substring also catches `@better-auth/sso`'s `/sso/callback/:providerId`,
619
- * so if `plugins.ts` ever re-exports `sso` (plan 280 §9 Q1) that endpoint is
620
- * classified rather than silently unrecorded. Checked against the installed
621
- * `better-auth` and `@better-auth/*` dist for 1.7.1: generic-oauth reuses the
622
- * core `/callback/:id` endpoint rather than registering its own, and the one
623
- * dist hit for a literal `/oauth2/callback/` is inside
624
- * `better-auth/plugins/oauth-popup`, which `plugins.ts` does not re-export
625
- * so today the branch fires for the core callback, and stays correct if either
626
- * of the others becomes reachable. `__tests__/audit.test.ts` pins all three.
649
+ * which is where an SSO sign-in actually completes — `sso` is re-exported from
650
+ * `@lunora/auth/plugins/enterprise`, so that path is live, not hypothetical.
651
+ *
652
+ * Checked against the installed `better-auth` and `@better-auth/*` dist for
653
+ * 1.7.3: generic-oauth registers NO endpoints of its own (there is no
654
+ * `/sign-in/oauth2`) it registers providers used through the core
655
+ * `/sign-in/social` and `/callback/:id`, and the one dist hit for a literal
656
+ * `/oauth2/callback/` is inside `better-auth/plugins/oauth-popup`, which
657
+ * `plugins.ts` does not re-export — so today the branch fires for the core
658
+ * callback, and stays correct if the other becomes reachable.
627
659
  */
628
660
  declare const eventForPath: (path: string) => AuthAuditEvent | undefined;
629
661
  /**
@@ -720,8 +752,6 @@ declare const INTERNAL_SECRET_HEADER = "x-lunora-auth-do-secret";
720
752
  * @experimental
721
753
  */
722
754
  interface AuthDoOptions {
723
- /** Base path the auth routes are served under. Must match the worker's. */
724
- basePath?: string;
725
755
  /**
726
756
  * Shared secret authenticating the worker on {@link RESOLVE_SESSION_PATH}.
727
757
  *
@@ -759,8 +789,12 @@ declare class LunoraAuthDO {
759
789
  */
760
790
  constructor(state: AuthDoState, optionsFactory: () => LunoraAuthOptions, options?: AuthDoOptions);
761
791
  /**
762
- * Serve an auth request. Routes under `basePath` go to better-auth; the internal
792
+ * Serve an auth request. Routes under `/api/auth` go to better-auth; the internal
763
793
  * session route is handled here; anything else is a 404.
794
+ *
795
+ * The base path is not configurable. The worker half only ever forwards
796
+ * `/api/auth/*` (`createDoAuthWiring`, which codegen calls with no base path),
797
+ * so a second knob here could only ever disagree with it.
764
798
  */
765
799
  fetch(request: Request): Promise<Response>;
766
800
  }
@@ -820,8 +854,6 @@ interface AuthNamespaceLike {
820
854
  }
821
855
  /** What {@link createDoAuthWiring} needs, already resolved against `env`. */
822
856
  interface DoAuthWiringOptions {
823
- /** Base path the auth routes are served under. Defaults to `/api/auth`. */
824
- basePath?: string;
825
857
  /**
826
858
  * Shared secret presented on the object's internal session route. `undefined`
827
859
  * means identity resolution fails closed — see {@link DoAuthWiring.resolveIdentity}.
@@ -847,7 +879,14 @@ interface DoAuthWiring {
847
879
  * the studio.
848
880
  */
849
881
  auditReader: AuthAuditReader;
850
- /** Forwards `/api/auth/*` to the object; `undefined` for anything else. */
882
+ /**
883
+ * Forwards `/api/auth/*` to the object; `undefined` for anything else.
884
+ *
885
+ * The base path is fixed. It was configurable on both halves of DO-backed auth
886
+ * and settable on neither: codegen's `AuthDeclaration` has no field for it, so
887
+ * the emitted `createDoAuthWiring(...)` never passed one and an `AuthDO`
888
+ * subclass that set its own served nothing.
889
+ */
851
890
  authHandler: (request: Request) => Promise<Response | undefined>;
852
891
  /**
853
892
  * Resolves a request's identity by asking the object. `null` when anonymous,
package/dist/index.d.ts CHANGED
@@ -602,28 +602,60 @@ interface AuditHookContext {
602
602
  *
603
603
  * Sign-in is split by what the endpoint actually DOES (plan 280 §4):
604
604
  *
605
- * - `/sign-in/social`, `/sign-in/magic-link` only DISPATCH — the first mints a
606
- * provider redirect URL, the second sends an email. Nobody is authenticated
607
- * yet, so these are `sign-in-initiated`, not `sign-in`.
608
- * - `/callback/:id` (social + generic-oauth), `/magic-link/verify`, and every
605
+ * - `/sign-in/social`, `/sign-in/magic-link` and `@better-auth/sso`'s
606
+ * `/sign-in/sso` only DISPATCH the first mints a provider redirect URL, the
607
+ * second sends an email, the third answers `{ url, redirect: true }` pointing
608
+ * at the identity provider's authorization endpoint. Nobody is authenticated
609
+ * yet, so these are `sign-in-initiated`, not `sign-in`. Without the `sso`
610
+ * branch that path fell through to the `/sign-in/` substring below and every
611
+ * SSO redirect mint was recorded as a completed, successful `sign-in` with no
612
+ * actor — in exactly the flow where the "who authenticated" trail matters
613
+ * most.
614
+ * - `/callback/:id` (social + generic-oauth), `/magic-link/verify`,
615
+ * `@better-auth/sso`'s `/sso/saml2/sp/acs/:providerId`, and every
609
616
  * `/two-factor/verify-*` (`verify-totp` / `verify-otp` / `verify-backup-code`
610
617
  * — all three complete a challenged sign-in the same way) are where a
611
618
  * session actually gets issued, so they join credential sign-ins
612
619
  * (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`, …) as
613
620
  * plain `sign-in`. They were NOT recorded at all before this change.
614
621
  *
622
+ * The SAML assertion consumer service needs its own branch because it is the
623
+ * one completion that carries no `/callback/` segment: `processSAMLResponse`
624
+ * validates the assertion, resolves the user and calls `setSessionCookie`, all
625
+ * under `/sso/saml2/sp/acs/:providerId`. Matching neither that substring nor
626
+ * any other branch, it classified as `undefined` — the endpoint that issues
627
+ * every SAML session left no audit row at all.
628
+ *
629
+ * SAML sign-OUT has the mirror-image problem, and two endpoints rather than
630
+ * one. Neither ends in `/sign-out`, and both terminate the local session before
631
+ * redirecting, so both are `sign-out` rather than an initiated event:
632
+ *
633
+ * - `/sso/saml2/logout/:providerId` — SP-initiated. Deletes the SAML session
634
+ * keys, calls `deleteSession` on the current session token and
635
+ * `deleteSessionCookie`, then redirects to the IdP's logout URL.
636
+ * - `/sso/saml2/sp/slo/:providerId` — the SP's single-logout receiver, for the
637
+ * IdP-initiated direction and for the response leg of an SP-initiated one.
638
+ * Both `handleLogoutRequest` and `handleLogoutResponse` call `deleteSession`
639
+ * and `deleteSessionCookie` the same way.
640
+ *
641
+ * Recording the SAML sign-in while leaving these silent is worse than either
642
+ * gap alone: the trail would show a session opening and never closing, so a
643
+ * reader cannot tell "still signed in" from "we stopped watching".
644
+ *
615
645
  * There is no dedicated `/oauth2/callback/*` branch because the generic
616
646
  * `/callback/` check above already covers it, and covering it is CORRECT: an
617
647
  * OAuth callback is a completed sign-in whatever path prefix it arrives on. The
618
648
  * same substring also catches `@better-auth/sso`'s `/sso/callback/:providerId`,
619
- * so if `plugins.ts` ever re-exports `sso` (plan 280 §9 Q1) that endpoint is
620
- * classified rather than silently unrecorded. Checked against the installed
621
- * `better-auth` and `@better-auth/*` dist for 1.7.1: generic-oauth reuses the
622
- * core `/callback/:id` endpoint rather than registering its own, and the one
623
- * dist hit for a literal `/oauth2/callback/` is inside
624
- * `better-auth/plugins/oauth-popup`, which `plugins.ts` does not re-export
625
- * so today the branch fires for the core callback, and stays correct if either
626
- * of the others becomes reachable. `__tests__/audit.test.ts` pins all three.
649
+ * which is where an SSO sign-in actually completes — `sso` is re-exported from
650
+ * `@lunora/auth/plugins/enterprise`, so that path is live, not hypothetical.
651
+ *
652
+ * Checked against the installed `better-auth` and `@better-auth/*` dist for
653
+ * 1.7.3: generic-oauth registers NO endpoints of its own (there is no
654
+ * `/sign-in/oauth2`) it registers providers used through the core
655
+ * `/sign-in/social` and `/callback/:id`, and the one dist hit for a literal
656
+ * `/oauth2/callback/` is inside `better-auth/plugins/oauth-popup`, which
657
+ * `plugins.ts` does not re-export — so today the branch fires for the core
658
+ * callback, and stays correct if the other becomes reachable.
627
659
  */
628
660
  declare const eventForPath: (path: string) => AuthAuditEvent | undefined;
629
661
  /**
@@ -720,8 +752,6 @@ declare const INTERNAL_SECRET_HEADER = "x-lunora-auth-do-secret";
720
752
  * @experimental
721
753
  */
722
754
  interface AuthDoOptions {
723
- /** Base path the auth routes are served under. Must match the worker's. */
724
- basePath?: string;
725
755
  /**
726
756
  * Shared secret authenticating the worker on {@link RESOLVE_SESSION_PATH}.
727
757
  *
@@ -759,8 +789,12 @@ declare class LunoraAuthDO {
759
789
  */
760
790
  constructor(state: AuthDoState, optionsFactory: () => LunoraAuthOptions, options?: AuthDoOptions);
761
791
  /**
762
- * Serve an auth request. Routes under `basePath` go to better-auth; the internal
792
+ * Serve an auth request. Routes under `/api/auth` go to better-auth; the internal
763
793
  * session route is handled here; anything else is a 404.
794
+ *
795
+ * The base path is not configurable. The worker half only ever forwards
796
+ * `/api/auth/*` (`createDoAuthWiring`, which codegen calls with no base path),
797
+ * so a second knob here could only ever disagree with it.
764
798
  */
765
799
  fetch(request: Request): Promise<Response>;
766
800
  }
@@ -820,8 +854,6 @@ interface AuthNamespaceLike {
820
854
  }
821
855
  /** What {@link createDoAuthWiring} needs, already resolved against `env`. */
822
856
  interface DoAuthWiringOptions {
823
- /** Base path the auth routes are served under. Defaults to `/api/auth`. */
824
- basePath?: string;
825
857
  /**
826
858
  * Shared secret presented on the object's internal session route. `undefined`
827
859
  * means identity resolution fails closed — see {@link DoAuthWiring.resolveIdentity}.
@@ -847,7 +879,14 @@ interface DoAuthWiring {
847
879
  * the studio.
848
880
  */
849
881
  auditReader: AuthAuditReader;
850
- /** Forwards `/api/auth/*` to the object; `undefined` for anything else. */
882
+ /**
883
+ * Forwards `/api/auth/*` to the object; `undefined` for anything else.
884
+ *
885
+ * The base path is fixed. It was configurable on both halves of DO-backed auth
886
+ * and settable on neither: codegen's `AuthDeclaration` has no field for it, so
887
+ * the emitted `createDoAuthWiring(...)` never passed one and an `AuthDO`
888
+ * subclass that set its own served nothing.
889
+ */
851
890
  authHandler: (request: Request) => Promise<Response | undefined>;
852
891
  /**
853
892
  * Resolves a request's identity by asking the object. `null` when anonymous,
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{l as r,a as o,b as a}from"./packem_shared/adapter-CG0Yv85f.mjs";import{LunoraAuthAdminError as A,createAuthAdmin as u}from"./packem_shared/LunoraAuthAdminError-4r29Kj_y.mjs";import{AUTH_AUDIT_TABLE as n,appendAuthAuditEntry as m,createAuthAuditReader as l,ensureAuthAuditTable as p,readAuthAuditLog as d}from"./audit.mjs";import{authAuditHook as E,buildAuditEntry as f,eventForPath as T,withAuthAudit as S}from"./packem_shared/authAuditHook-C9Gt2iYp.mjs";import{READ_AUDIT_PATH as _,INTERNAL_SECRET_HEADER as D,RESOLVE_SESSION_PATH as c,LunoraAuthDO as H}from"./packem_shared/AUTH_DO_AUDIT_PATH-th2-Hb30.mjs";import{createAuth as U,resolveAuthOptions as R}from"./packem_shared/createAuth-8aUw8v8D.mjs";import{authDoColumnAdditions as v,authDoSchemaStatements as L}from"./packem_shared/authDoColumnAdditions-UwF3Rl41.mjs";import{createDoAuthWiring as O}from"./packem_shared/createDoAuthWiring-C8BRUjf6.mjs";import{emailGateDatabaseHooks as N,withEmailGate as w}from"./packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs";import{assertEmailAllowed as M,classifyEmail as C,emailGateMiddleware as k,loadEmailDomainLists as q}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-ClvskwuB.mjs";import{legacyIssuerCleanupStatements as K}from"./packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs";import{LunoraAuthHeadersError as X,withAuthPlugins as Z}from"./middleware.mjs";import{compileMigrationsSql as tt,ensureMigrated as et}from"./packem_shared/compileMigrationsSql-Ds2snsWg.mjs";import{default as ot}from"./schema.mjs";import{sessionPresets as it,validateSessionPolicy as At}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{c as st,d as nt}from"./packem_shared/sql-store-CYa4HJsO.mjs";import{createMemoryAuthStore as lt,matchesWhere as pt}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as ht,verifyTurnstile as Et}from"./turnstile.mjs";import{verifyTurnstileMiddleware as Tt}from"./turnstile-middleware.mjs";export{n as AUTH_AUDIT_TABLE,_ as AUTH_DO_AUDIT_PATH,D as AUTH_DO_SECRET_HEADER,c as AUTH_DO_SESSION_PATH,G as DEFAULT_AUTH_BASE_PATH,A as LunoraAuthAdminError,H as LunoraAuthDO,X as LunoraAuthHeadersError,ht as TURNSTILE_VERIFY_ENDPOINT,m as appendAuthAuditEntry,M as assertEmailAllowed,E as authAuditHook,v as authDoColumnAdditions,L as authDoSchemaStatements,ot as authTables,f as buildAuditEntry,C as classifyEmail,tt as compileMigrationsSql,U as createAuth,u as createAuthAdmin,l as createAuthAuditReader,O as createDoAuthWiring,lt as createMemoryAuthStore,W as createSignUpInvitation,st as createSqlAuthStore,nt as d1Executor,N as emailGateDatabaseHooks,k as emailGateMiddleware,p as ensureAuthAuditTable,et as ensureMigrated,T as eventForPath,B as handleAuthRequest,K as legacyIssuerCleanupStatements,Y as listSignUpInvitations,q as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,pt as matchesWhere,j as pruneSignUpInvitations,d as readAuthAuditLog,R as resolveAuthOptions,z as revokeSignUpInvitation,it as sessionPresets,At as validateSessionPolicy,Et as verifyTurnstile,Tt as verifyTurnstileMiddleware,S as withAuthAudit,Z as withAuthPlugins,w as withEmailGate};
1
+ import{l as r,a as o,b as a}from"./packem_shared/adapter-CG0Yv85f.mjs";import{LunoraAuthAdminError as A,createAuthAdmin as u}from"./packem_shared/LunoraAuthAdminError-uhKG1MgQ.mjs";import{AUTH_AUDIT_TABLE as n,appendAuthAuditEntry as m,createAuthAuditReader as l,ensureAuthAuditTable as p,readAuthAuditLog as d}from"./audit.mjs";import{authAuditHook as E,buildAuditEntry as f,eventForPath as T,withAuthAudit as S}from"./packem_shared/authAuditHook-DPTKdjBe.mjs";import{READ_AUDIT_PATH as _,INTERNAL_SECRET_HEADER as D,RESOLVE_SESSION_PATH as c,LunoraAuthDO as H}from"./packem_shared/AUTH_DO_AUDIT_PATH-_FUWZltp.mjs";import{createAuth as U,resolveAuthOptions as R}from"./packem_shared/createAuth-8aUw8v8D.mjs";import{authDoColumnAdditions as v,authDoSchemaStatements as L}from"./packem_shared/authDoColumnAdditions-UwF3Rl41.mjs";import{createDoAuthWiring as O}from"./packem_shared/createDoAuthWiring-DEVTK4ED.mjs";import{emailGateDatabaseHooks as N,withEmailGate as w}from"./packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs";import{assertEmailAllowed as M,classifyEmail as C,emailGateMiddleware as k,loadEmailDomainLists as q}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-DHZL9fNl.mjs";import{legacyIssuerCleanupStatements as K}from"./packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs";import{LunoraAuthHeadersError as X,withAuthPlugins as Z}from"./middleware.mjs";import{compileMigrationsSql as tt,ensureMigrated as et}from"./packem_shared/compileMigrationsSql-Ds2snsWg.mjs";import{default as ot}from"./schema.mjs";import{sessionPresets as it,validateSessionPolicy as At}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{c as st,d as nt}from"./packem_shared/sql-store-CYa4HJsO.mjs";import{createMemoryAuthStore as lt,matchesWhere as pt}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as ht,verifyTurnstile as Et}from"./turnstile.mjs";import{verifyTurnstileMiddleware as Tt}from"./turnstile-middleware.mjs";export{n as AUTH_AUDIT_TABLE,_ as AUTH_DO_AUDIT_PATH,D as AUTH_DO_SECRET_HEADER,c as AUTH_DO_SESSION_PATH,G as DEFAULT_AUTH_BASE_PATH,A as LunoraAuthAdminError,H as LunoraAuthDO,X as LunoraAuthHeadersError,ht as TURNSTILE_VERIFY_ENDPOINT,m as appendAuthAuditEntry,M as assertEmailAllowed,E as authAuditHook,v as authDoColumnAdditions,L as authDoSchemaStatements,ot as authTables,f as buildAuditEntry,C as classifyEmail,tt as compileMigrationsSql,U as createAuth,u as createAuthAdmin,l as createAuthAuditReader,O as createDoAuthWiring,lt as createMemoryAuthStore,W as createSignUpInvitation,st as createSqlAuthStore,nt as d1Executor,N as emailGateDatabaseHooks,k as emailGateMiddleware,p as ensureAuthAuditTable,et as ensureMigrated,T as eventForPath,B as handleAuthRequest,K as legacyIssuerCleanupStatements,Y as listSignUpInvitations,q as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,pt as matchesWhere,j as pruneSignUpInvitations,d as readAuthAuditLog,R as resolveAuthOptions,z as revokeSignUpInvitation,it as sessionPresets,At as validateSessionPolicy,Et as verifyTurnstile,Tt as verifyTurnstileMiddleware,S as withAuthAudit,Z as withAuthPlugins,w as withEmailGate};
@@ -1 +1 @@
1
- import{LunoraError as i}from"@lunora/errors";const d=e=>{if(e===void 0)return!1;if(typeof e!="object"||e===null)return!0;const{headers:t}=e;return t!=null},c=e=>{const t=()=>e;return new Proxy(e,{get(s,r,a){if(r==="withoutHeaders"&&!(r in s))return t;const n=Reflect.get(s,r,a);if(typeof n!="function"||typeof r!="string")return n;const u=r;return(...o)=>d(o[0])?Reflect.apply(n,s,o):Promise.reject(new h(u))}})};class h extends i{method;constructor(t){super("AUTH_HEADERS_MISSING",`@lunora/auth: ctx.authApi.${t}(…) was called without \`headers\`. better-auth treats a header-less call as a trusted server-to-server invocation and skips session authorization entirely — an authorization bypass. Pass the inbound request headers: ctx.authApi.${t}({ body, headers: request.headers }). If you genuinely intend an unauthenticated server-to-server call, opt out explicitly via ctx.authApi.withoutHeaders().<method>(…), or disable the guard for the whole middleware with withAuthPlugins(auth, { enforceHeaders: false }).`,{name:"LunoraAuthHeadersError"}),this.method=t}}const f=(e,t={})=>{const r=t.enforceHeaders??!0?c(e.api):e.api;return async({next:a})=>await a({ctx:{authApi:r}})};export{h as LunoraAuthHeadersError,f as withAuthPlugins};
1
+ import{LunoraError as d}from"@lunora/errors";const h=e=>{if(e===void 0)return!1;if(typeof e!="object"||e===null)return!0;const{headers:t}=e;return t!=null},c=(e,t)=>{const n=()=>e;return new Proxy(e,{get(s,r,o){if(r==="withoutHeaders"&&!(r in s))return n;const a=Reflect.get(s,r,o);if(typeof a!="function"||typeof r!="string")return a;const i=r;return(...u)=>t&&!h(u[0])?Promise.reject(new l(i)):Reflect.apply(a,s,u)}})};class l extends d{method;constructor(t){super("AUTH_HEADERS_MISSING",`@lunora/auth: ctx.authApi.${t}(…) was called without \`headers\`. better-auth treats a header-less call as a trusted server-to-server invocation and skips session authorization entirely — an authorization bypass. Pass the inbound request headers: ctx.authApi.${t}({ body, headers: request.headers }). If you genuinely intend an unauthenticated server-to-server call, opt out explicitly via ctx.authApi.withoutHeaders().<method>(…), or disable the guard for the whole middleware with withAuthPlugins(auth, { enforceHeaders: false }).`,{name:"LunoraAuthHeadersError"}),this.method=t}}const w=(e,t={})=>{const n=t.enforceHeaders??!0,s=c(e.api,n);return async({next:r})=>await r({ctx:{authApi:s}})};export{l as LunoraAuthHeadersError,w as withAuthPlugins};
@@ -0,0 +1 @@
1
+ import{getAuthTablesWithResolvedIndexes as u}from"@better-auth/core/db/internal";import{d as c,b as l}from"./adapter-CG0Yv85f.mjs";import{ensureAuthAuditTable as h,createAuthAuditReader as m}from"../audit.mjs";import{resolveAuthOptions as d,createAuth as p}from"./createAuth-8aUw8v8D.mjs";import{authDoSchemaStatements as f,authDoColumnAdditions as g}from"./authDoColumnAdditions-UwF3Rl41.mjs";import{handleAuthRequest as A}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{schemaDeclaresIssuer as y,legacyIssuerCleanupStatements as R,indexesReferencingIssuer as x}from"./legacyIssuerCleanupStatements-D9Kf8OD1.mjs";const E=(o,t)=>{const s=Math.max(o.length,t.length);let e=o.length^t.length;for(let n=0;n<s;n+=1){const i=n<o.length?o.charCodeAt(n):0,r=n<t.length?t.charCodeAt(n):0;e|=i^r}return e===0},S="/__lunora/auth/session",b="/__lunora/auth/audit",v="x-lunora-auth-do-secret",_=o=>{if(o===null||typeof o!="object"||Array.isArray(o))return{error:"body must be a JSON object"};const t={};for(const[s,e]of Object.entries(o))if(s==="limit"||s==="sinceSeq"){if(typeof e!="number"||!Number.isFinite(e))return{error:`"${s}" must be a finite number`};t[s]=e}else if(s==="actorId"||s==="event"){if(typeof e!="string")return{error:`"${s}" must be a string`};t[s]=e}else return{error:`unknown audit read option "${s}"`};return{options:t}};class q{#s;#r;#n;#e;#t;#o=!1;constructor(t,s,e={}){this.#e=t.storage,this.#s=c(t.storage),this.#n=s,this.#r=e}#i(){if(this.#t!==void 0)return this.#t;const t=this.#n();if(!this.#o){const s=d(t);for(const e of f(s))[...this.#e.sql.exec(e)];for(const e of g(s,n=>this.#a(n)))[...this.#e.sql.exec(e)];this.#c(s),this.#o=!0}return this.#t=p({...t,database:l(this.#e)}),this.#t}#a(t){return[...this.#e.sql.exec("SELECT name FROM pragma_table_info(?)",t)].map(e=>String(e.name))}#c(t){const{tables:s}=u(t),{account:e}=s;if(e===void 0)return;const n=Object.entries(e.fields).map(([i,r])=>r.fieldName??i);if(!(y(n)||!this.#a(e.modelName).includes("issuer")))try{const i=[...this.#e.sql.exec("SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ?",e.modelName)].map(r=>({name:String(r.name),sql:typeof r.sql=="string"?r.sql:void 0}));for(const r of R(e.modelName,x(i)))[...this.#e.sql.exec(r)]}catch(i){console.error("@lunora/auth: could not drop the reverted `account.issuer` column; sign-ups will fail until it is removed.",i)}}async#l(t){if(!this.#u(t))return Response.json({error:"unauthorized"},{status:401});let s;try{s=await t.json()}catch{return Response.json({error:"invalid body"},{status:400})}const e=_(s??{});if("error"in e)return Response.json({error:e.error},{status:400});await h(this.#s);const n=await m(this.#s).read(e.options);return Response.json({entries:n})}#u(t){const{internalSecret:s}=this.#r;if(s===void 0||s==="")return!1;const e=t.headers.get(v);return e!==null&&E(e,s)}async#h(t){if(!this.#u(t))return Response.json({error:"unauthorized"},{status:401});const e=await this.#i().api.getSession({headers:t.headers}),n=e?.user.id;if(n===void 0)return Response.json({});const i=e?.session.expiresAt,r=e?.user,a=r?.role;return Response.json({...typeof r?.email=="string"&&r.email.length>0?{email:r.email}:{},...i instanceof Date?{expiresAtMs:i.getTime()}:{},...typeof r?.name=="string"&&r.name.length>0?{name:r.name}:{},...typeof a=="string"&&a.length>0?{role:a}:{},userId:n})}async fetch(t){const s=new URL(t.url);if(s.pathname===S)return this.#h(t);if(s.pathname===b)return this.#l(t);const e=this.#i();return await A(e,t)??Response.json({error:"not an auth route"},{status:404})}}export{v as INTERNAL_SECRET_HEADER,q as LunoraAuthDO,b as READ_AUDIT_PATH,S as RESOLVE_SESSION_PATH};
@@ -1 +1 @@
1
- import{LunoraError as M}from"@lunora/errors";import{getAuthTables as b}from"better-auth/db";import{revokeSignUpInvitation as U,createSignUpInvitation as R}from"./createSignUpInvitation-ClvskwuB.mjs";class f extends M{constructor(c,h){super(h,c,{name:"LunoraAuthAdminError"})}}const N=50,D=500,O=3600,E=O*24,T=100*365*24*60*60,z=2880*60*1e3,k=[{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","tokenHash"]),C=o=>Math.min(Math.max(Math.trunc(o??N),1),D),P=o=>Math.max(0,Math.trunc(o??0)),u=o=>{const c={};for(const[h,l]of Object.entries(o))L.has(h)||(c[h]=l instanceof Date?l.getTime():l);return c},g=o=>Array.isArray(o)?o.join(","):o,S=o=>o.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),B=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),F={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},G=o=>o==="boolean"||o==="date"||o==="number"?o:"string",V=o=>{const c=[];for(const[h,l]of Object.entries(o))l.input===!1||l.references!==void 0||B.has(h)||c.push({name:h,plugin:F[h],required:l.required===!0,type:G(l.type),unique:l.unique===!0});return c},I=o=>typeof o=="string"&&o.includes("owner"),_=async(o,c)=>{const l=(await o.adapter.findMany({model:"member",where:[{field:"userId",value:c}]})).filter(p=>I(p.role)&&typeof p.organizationId=="string").map(p=>p.organizationId);if(l.length===0)return;const A=await Promise.all(l.map(async p=>o.adapter.findMany({model:"member",where:[{field:"organizationId",value:p}]}))),n=l.find((p,v)=>!A[v]?.some(e=>e.userId!==c&&I(e.role)));if(n!==void 0)throw new f(`user is the last owner of organization ${n} — transfer ownership or delete the organization first`,"LAST_ORGANIZATION_OWNER")},j=o=>{if(o instanceof f)return o;const c=o,h=c?.body?.code??c?.code??"AUTH_ADMIN_ERROR",l=c?.body?.message??c?.message??"auth admin operation failed";return new f(l,h)},W=(o,c={})=>{const h=o.$context,l=c.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 h)}catch(a){throw j(a)}},p=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:C(t.limit),model:a,offset:P(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 f("expiresInSeconds must be a positive finite integer","INVALID_BAN_SECONDS");const d=Math.min(e,T);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),p(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(m=>m.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((m,w)=>m.localeCompare(w)),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((m,w)=>m.localeCompare(w)),userFields:V(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 f("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 f("an organization with this slug already exists","ORG_SLUG_TAKEN");const w=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:w.id,role:"owner",userId:i},model:"member"}),u(w)}),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 m=await r.adapter.findMany({model:"member",where:[{field:"organizationId",value:t}]});s=(m.find(y=>typeof y.role=="string"&&y.role.includes("owner"))??m[0])?.userId}if(s===void 0||s==="")throw new f("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()+z),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 m=await s.adapter.update({model:"organization",update:d,where:[{field:"id",value:i}]});return u(m??{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 f("a user with this email already exists","USER_ALREADY_EXISTS");const m={...e};delete m.isAnonymous;const w=await s.internalAdapter.createUser({email:d,name:t,role:r===void 0?void 0:g(r),...m},{method:"admin"});if(i!==void 0&&i!==""){const y=await s.password.hash(i);await s.internalAdapter.linkAccount({accountId:w.id,password:y,providerId:"credential",userId:w.id})}return p(w)}),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 f("user not found","USER_NOT_FOUND");const i=c.impersonationSeconds;let r=O;if(i!==void 0){if(!Number.isInteger(i)||!Number.isFinite(i)||i<=0)throw new f("impersonationSeconds must be a positive finite integer","INVALID_IMPERSONATION_SECONDS");r=Math.min(i,E)}const s=new Date(Date.now()+r*1e3),d=await a.internalAdapter.createSession(e,!0,{expiresAt:s,impersonatedBy:c.impersonatedBy??e},!0);return{expiresAt:d.expiresAt instanceof Date?d.expiresAt.getTime():s.getTime(),token:d.token,user:p(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 R(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()=>U(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:m})=>n(w=>{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(w,"user",{limit:t,offset:i,sortBy:{direction:m??"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 _(a,e),await Promise.all(k.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 p(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 f(`password must be at least ${i.toString()} characters`,"PASSWORD_TOO_SHORT");if(e.length>r)throw new f(`password must be at most ${r.toString()} characters`,"PASSWORD_TOO_LONG");if(!await t.internalAdapter.findUserById(a))throw new f("user not found","USER_NOT_FOUND");const d=(await t.internalAdapter.findAccounts(a)).some(w=>w.providerId==="credential");if(!d&&t.options.emailAndPassword?.enabled!==!0)throw new f("email/password sign-in is disabled for this deployment","EMAIL_PASSWORD_DISABLED");const m=await t.password.hash(e);d?await t.internalAdapter.updatePassword(a,m):await t.internalAdapter.linkAccount({accountId:a,password:m,providerId:"credential",userId:a})}),unbanUser:({userId:e})=>n(async a=>{const t=await a.internalAdapter.updateUser(e,{banExpires:null,banned:!1,banReason:null});return p(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 p(i)})}};export{f as LunoraAuthAdminError,W as createAuthAdmin};
1
+ import{LunoraError as M}from"@lunora/errors";import{getAuthTables as b}from"better-auth/db";import{revokeSignUpInvitation as U,createSignUpInvitation as R}from"./createSignUpInvitation-DHZL9fNl.mjs";class f extends M{constructor(c,h){super(h,c,{name:"LunoraAuthAdminError"})}}const N=50,D=500,O=3600,E=O*24,T=100*365*24*60*60,z=2880*60*1e3,k=[{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","tokenHash"]),C=o=>Math.min(Math.max(Math.trunc(o??N),1),D),P=o=>Math.max(0,Math.trunc(o??0)),u=o=>{const c={};for(const[h,l]of Object.entries(o))L.has(h)||(c[h]=l instanceof Date?l.getTime():l);return c},g=o=>Array.isArray(o)?o.join(","):o,S=o=>o.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),B=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),F={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},G=o=>o==="boolean"||o==="date"||o==="number"?o:"string",V=o=>{const c=[];for(const[h,l]of Object.entries(o))l.input===!1||l.references!==void 0||B.has(h)||c.push({name:h,plugin:F[h],required:l.required===!0,type:G(l.type),unique:l.unique===!0});return c},I=o=>typeof o=="string"&&o.includes("owner"),_=async(o,c)=>{const l=(await o.adapter.findMany({model:"member",where:[{field:"userId",value:c}]})).filter(p=>I(p.role)&&typeof p.organizationId=="string").map(p=>p.organizationId);if(l.length===0)return;const A=await Promise.all(l.map(async p=>o.adapter.findMany({model:"member",where:[{field:"organizationId",value:p}]}))),n=l.find((p,v)=>!A[v]?.some(e=>e.userId!==c&&I(e.role)));if(n!==void 0)throw new f(`user is the last owner of organization ${n} — transfer ownership or delete the organization first`,"LAST_ORGANIZATION_OWNER")},j=o=>{if(o instanceof f)return o;const c=o,h=c?.body?.code??c?.code??"AUTH_ADMIN_ERROR",l=c?.body?.message??c?.message??"auth admin operation failed";return new f(l,h)},W=(o,c={})=>{const h=o.$context,l=c.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 h)}catch(a){throw j(a)}},p=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:C(t.limit),model:a,offset:P(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 f("expiresInSeconds must be a positive finite integer","INVALID_BAN_SECONDS");const d=Math.min(e,T);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),p(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(m=>m.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((m,w)=>m.localeCompare(w)),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((m,w)=>m.localeCompare(w)),userFields:V(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 f("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 f("an organization with this slug already exists","ORG_SLUG_TAKEN");const w=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:w.id,role:"owner",userId:i},model:"member"}),u(w)}),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 m=await r.adapter.findMany({model:"member",where:[{field:"organizationId",value:t}]});s=(m.find(y=>typeof y.role=="string"&&y.role.includes("owner"))??m[0])?.userId}if(s===void 0||s==="")throw new f("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()+z),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 m=await s.adapter.update({model:"organization",update:d,where:[{field:"id",value:i}]});return u(m??{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 f("a user with this email already exists","USER_ALREADY_EXISTS");const m={...e};delete m.isAnonymous;const w=await s.internalAdapter.createUser({email:d,name:t,role:r===void 0?void 0:g(r),...m},{method:"admin"});if(i!==void 0&&i!==""){const y=await s.password.hash(i);await s.internalAdapter.linkAccount({accountId:w.id,password:y,providerId:"credential",userId:w.id})}return p(w)}),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 f("user not found","USER_NOT_FOUND");const i=c.impersonationSeconds;let r=O;if(i!==void 0){if(!Number.isInteger(i)||!Number.isFinite(i)||i<=0)throw new f("impersonationSeconds must be a positive finite integer","INVALID_IMPERSONATION_SECONDS");r=Math.min(i,E)}const s=new Date(Date.now()+r*1e3),d=await a.internalAdapter.createSession(e,!0,{expiresAt:s,impersonatedBy:c.impersonatedBy??e},!0);return{expiresAt:d.expiresAt instanceof Date?d.expiresAt.getTime():s.getTime(),token:d.token,user:p(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 R(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()=>U(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:m})=>n(w=>{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(w,"user",{limit:t,offset:i,sortBy:{direction:m??"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 _(a,e),await Promise.all(k.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 p(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 f(`password must be at least ${i.toString()} characters`,"PASSWORD_TOO_SHORT");if(e.length>r)throw new f(`password must be at most ${r.toString()} characters`,"PASSWORD_TOO_LONG");if(!await t.internalAdapter.findUserById(a))throw new f("user not found","USER_NOT_FOUND");const d=(await t.internalAdapter.findAccounts(a)).some(w=>w.providerId==="credential");if(!d&&t.options.emailAndPassword?.enabled!==!0)throw new f("email/password sign-in is disabled for this deployment","EMAIL_PASSWORD_DISABLED");const m=await t.password.hash(e);d?await t.internalAdapter.updatePassword(a,m):await t.internalAdapter.linkAccount({accountId:a,password:m,providerId:"credential",userId:a})}),unbanUser:({userId:e})=>n(async a=>{const t=await a.internalAdapter.updateUser(e,{banExpires:null,banned:!1,banReason:null});return p(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 p(i)})}};export{f as LunoraAuthAdminError,W as createAuthAdmin};
@@ -0,0 +1 @@
1
+ import{createAuthMiddleware as d}from"better-auth/api";import{o as c}from"./on-cloudflare-edge-Dx6gFjBO.mjs";import{appendAuthAuditEntry as l}from"../audit.mjs";const f=t=>{const r=t.toLowerCase(),e=s=>r===s||r.endsWith(s);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(e("/sign-in/social")||e("/sign-in/magic-link")||e("/sign-in/sso"))return"sign-in-initiated";if(r.includes("/sign-in/")||r.includes("/callback/")||r.includes("/saml2/sp/acs/")||e("/magic-link/verify")||r.includes("/two-factor/verify-"))return"sign-in";if(e("/sign-out")||r.includes("/saml2/logout/")||r.includes("/saml2/sp/slo/"))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(r.includes("/two-factor/enable")||r.includes("/totp/enable"))return"mfa-enable";if(r.includes("/two-factor/disable")||r.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=(t,r)=>t.headers?.get(r)??t.request?.headers.get(r)??void 0,v=(t,r)=>{if(c()){const e=a(t,"cf-connecting-ip");if(e!==void 0)return e}if(r===!0)return a(t,"x-forwarded-for")?.split(",")[0]?.trim()},p=t=>{const r=t.context?.newSession??t.context?.session,e=r?.user?.id??r?.session?.userId,s=r?.user?.email;return{...e===void 0?{}:{actorId:e},...s===void 0?{}:{actorEmail:s}}},g=t=>{const r=t.context?.returned;if(r instanceof Error)return"failure";if(typeof r=="object"&&r!==null&&"status"in r){const e=Number(r.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},u=320,h=(t,r)=>{if(r!=="sign-in"&&r!=="sign-in-initiated")return;const e=t.body?.email??t.body?.username;if(!(typeof e!="string"||e.length===0))return e.length>u?e.slice(0,u):e},m=(t,{now:r=Date.now(),trustProxyHeaders:e}={})=>{const s=t.path===void 0?void 0:f(t.path);if(s===void 0)return;const n=v(t,e),i=a(t,"user-agent"),o=h(t,s);return{...p(t),event:s,outcome:g(t),ts:r,...n===void 0?{}:{ip:n},...o===void 0?{}:{targetEmail:o},...i===void 0?{}:{userAgent:i},detail:{path:t.path}}},w=t=>d(async r=>{try{const e=m(r,{trustProxyHeaders:t.trustProxyHeaders});if(e!==void 0){const s=await l(t.executor,e,{redactDetail:t.redactDetail,retention:t.retention});t.onRecord!==void 0&&await t.onRecord(s)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}}),A=(t,r)=>{const e=w(r),s=t.hooks?.after,n=s?async i=>{const o=await s(i);return await e(i),o}:e;return{...t,hooks:{...t.hooks,after:n}}};export{w as authAuditHook,m as buildAuditEntry,f as eventForPath,A as withAuthAudit};
@@ -0,0 +1 @@
1
+ import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as m,READ_AUDIT_PATH as p}from"./AUTH_DO_AUDIT_PATH-_FUWZltp.mjs";import{isAuthRoutePath as f,DEFAULT_AUTH_BASE_PATH as h}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const R=l=>{const{internalSecret:o,namespace:a,objectName:c="auth"}=l,i=()=>{if(a)return a.get(a.idFromName(c))},d=async(t,n,r)=>{if(!o)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]:o},method:"POST"}));return e.ok?e:void 0};return{auditReader:{read:async t=>{const n=await d(p,"https://auth-do.invalid",t);return n?(await n.json())?.entries??[]:[]}},authHandler:async t=>{if(f(new URL(t.url).pathname,h))return i()?.fetch(t)},resolveIdentity:async t=>{if(!o)return null;const n=i();if(!n)return null;const r=new Headers(t.headers);r.set(u,o);const s=await n.fetch(new Request(new URL(m,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{R as createDoAuthWiring};
@@ -0,0 +1 @@
1
+ import{defineErrorCodes as T}from"@better-auth/core/utils/error-codes";import{LunoraError as A}from"@lunora/errors";import{createAuthMiddleware as D,APIError as g}from"better-auth/api";const l="signUpInvitation",O=10080*60,v=365*24*60*60,y=500,w=T({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."}),x=32,N=/^[^\s@]+@[^\s@]+$/,R=e=>{if(!N.test(e))return!1;const t=e.slice(e.indexOf("@")+1);return t.includes(".")&&!t.startsWith(".")&&!t.endsWith(".")},m=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}),U=()=>btoa(String.fromCodePoint(...crypto.getRandomValues(new Uint8Array(x)))).replaceAll("+","-").replaceAll("/","_").replaceAll("=",""),S=async e=>{const t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return[...new Uint8Array(t)].map(a=>a.toString(16).padStart(2,"0")).join("")},b=(e,t)=>{if(e.length!==t.length)return!1;let a=0;for(let o=0;o<e.length;o+=1)a|=(e.codePointAt(o)??0)^(t.codePointAt(o)??0);return a===0},p=e=>e.trim().toLowerCase(),I=e=>{if(typeof e.email!="string")return;const t=p(e.email);return t===""?void 0:t},k="anonymous",P=e=>(e.plugins??[]).some(t=>t.id===k),E=e=>e.isAnonymous===!0,_=e=>e===null||e.acceptedAt instanceof Date?!1:e.expiresAt instanceof Date&&e.expiresAt.getTime()>Date.now(),L=async(e,t)=>_(await e.findOne({model:l,where:[{field:"email",value:t}]})),V=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 }`. The invitation link is a bearer credential, so anyone it reaches — a forwarded mail, a shared inbox — can sign up as that address, and without verification they hold a session the moment they do.")},H=(e={})=>{let t=e.allowFirstUser??!1,a=!1;const o=async i=>{if(!t)return!1;const n=await i.count({model:"user"}),d=a&&n>0?await i.count({model:"user",where:[{field:"isAnonymous",value:!0}]}):0;return n-d<=0?!0:(t=!1,!1)};return{$ERROR_CODES:w,hooks:{before:[{handler:D(async i=>{const n=i.body??{},d=typeof n.email=="string"?p(n.email):"",u=typeof n.inviteToken=="string"?n.inviteToken:"",r=()=>{throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_INVALID)};if(d===""&&r(),await o(i.context.adapter))return;u===""&&r();const s=await i.context.adapter.findOne({model:l,where:[{field:"email",value:d}]}),f=s===null?void 0:s.tokenHash,c=await S(u);(typeof f!="string"||!b(f,c)||!_(s))&&r()}),matcher:i=>i.path==="/sign-up/email"}]},id:"lunora-invite-only",init:i=>{V(i.options);const{adapter:n}=i;return a=P(i.options),{options:{databaseHooks:{user:{create:{after:async r=>{const s=I(r);s===void 0||a&&E(r)||await n.update({model:l,update:{acceptedAt:new Date},where:[{field:"email",value:s}]})},before:async r=>{if(a&&E(r))return;const s=I(r);if(!(s!==void 0&&await L(n,s))&&!await o(n))throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_REQUIRED)}}}}}}},schema:{[l]:{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"}}}}}},G=async(e,t)=>{const a=p(t.email);if(!R(a))throw new A("VALIDATION_ERROR",`not an email address to invite: ${JSON.stringify(t.email)}`);const{expiresInSeconds:o=O}=t;if(!Number.isInteger(o)||o<=0||o>v)throw new A("VALIDATION_ERROR",`expiresInSeconds must be a positive integer no greater than ${String(v)}`);const i=new Date(Date.now()+o*1e3),n=U(),d=await S(n),u=t.invitedBy??null,r=await e.$context,s=[{field:"email",value:a}],f=async()=>r.adapter.update({model:l,update:{acceptedAt:null,expiresAt:i,invitedBy:u,tokenHash:d},where:s});if(await r.adapter.findOne({model:l,where:s})){const c=await f();if(c)return{...m(c),token:n}}try{const c=await r.adapter.create({data:{createdAt:new Date,email:a,expiresAt:i,invitedBy:u,tokenHash:d},model:l});return{...m(c),token:n}}catch(c){const h=await f();if(h===null)throw c;return{...m(h),token:n}}},Q=async(e,t={})=>{const i=(await(await e.$context).adapter.findMany({limit:y,model:l,sortBy:{direction:"desc",field:"createdAt"}})).map(n=>m(n));return t.pendingOnly===!0?i.filter(n=>n.acceptedAt===null&&n.expiresAt.getTime()>Date.now()):i},W=async(e,t)=>{await(await e.$context).adapter.delete({model:l,where:[{field:"email",value:p(t.email)}]})},B=100,q=async(e,t,a,o)=>{const i=t.filter(n=>n.acceptedAt===null&&n.expiresAt.getTime()<=a).slice(0,o);for(const n of i)await e.delete({model:l,where:[{field:"id",value:n.id}]});return i.length},z=async(e,t={})=>{const a=t.limit??y;if(!Number.isInteger(a)||a<=0)throw new A("VALIDATION_ERROR","limit must be a positive integer");const o=Math.min(a,y),i=await e.$context,n=Date.now();let d=0,u=0;for(;d<o;){const r=await i.adapter.findMany({limit:B,model:l,offset:u,sortBy:{direction:"asc",field:"expiresAt"}});if(r.length===0)break;const s=r.map(c=>m(c)),f=await q(i.adapter,s,n,o-d);if(d+=f,s.some(c=>c.expiresAt.getTime()>n))break;u+=r.length-f}return d};export{G as createSignUpInvitation,H as inviteOnly,Q as listSignUpInvitations,z as pruneSignUpInvitations,W as revokeSignUpInvitation};
package/dist/plugins.mjs CHANGED
@@ -1 +1 @@
1
- import{inviteOnly as e}from"./packem_shared/createSignUpInvitation-ClvskwuB.mjs";import{uiConfig as m}from"./packem_shared/uiConfig-B8pN6pks.mjs";import{apiKey as f}from"@better-auth/api-key";import{createMcpProtectedRequestHandler as i,mcp as n,requireMcpAuth as a}from"@better-auth/mcp";import{oauthDeviceAuthorization as s,oauthProvider as u}from"@better-auth/oauth-provider";import{passkey as d}from"@better-auth/passkey";import{scim as A}from"@better-auth/scim";import{captcha as v,lastLoginMethod as y,oneTap as P}from"better-auth/plugins";import{createAccessControl as T}from"better-auth/plugins/access";import{admin as z}from"better-auth/plugins/admin";import{anonymous as O}from"better-auth/plugins/anonymous";import{bearer as q}from"better-auth/plugins/bearer";import{customSession as L}from"better-auth/plugins/custom-session";import{deviceAuthorization as j}from"better-auth/plugins/device-authorization";import{emailOTP as D}from"better-auth/plugins/email-otp";import{genericOAuth as H}from"better-auth/plugins/generic-oauth";import{haveIBeenPwned as K}from"better-auth/plugins/haveibeenpwned";import{jwt as R}from"better-auth/plugins/jwt";import{magicLink as G}from"better-auth/plugins/magic-link";import{multiSession as Q}from"better-auth/plugins/multi-session";import{oAuthProxy as V}from"better-auth/plugins/oauth-proxy";import{oneTimeToken as X}from"better-auth/plugins/one-time-token";import{organization as Z}from"better-auth/plugins/organization";import{phoneNumber as $}from"better-auth/plugins/phone-number";import{siwe as ro}from"better-auth/plugins/siwe";import{twoFactor as to}from"better-auth/plugins/two-factor";import{username as po}from"better-auth/plugins/username";export{z as admin,O as anonymous,f as apiKey,q as bearer,v as captcha,T as createAccessControl,i as createMcpProtectedRequestHandler,L as customSession,j as deviceAuthorization,D as emailOTP,H as genericOAuth,K as haveIBeenPwned,e as inviteOnly,R as jwt,y as lastLoginMethod,G as magicLink,n as mcp,Q as multiSession,V as oAuthProxy,s as oauthDeviceAuthorization,u as oauthProvider,P as oneTap,X as oneTimeToken,Z as organization,d as passkey,$ as phoneNumber,a as requireMcpAuth,A as scim,ro as siwe,to as twoFactor,m as uiConfig,po as username};
1
+ import{inviteOnly as e}from"./packem_shared/createSignUpInvitation-DHZL9fNl.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.140",
3
+ "version": "1.0.0-alpha.142",
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.3",
104
104
  "@better-auth/passkey": "1.7.3",
105
105
  "@better-auth/scim": "1.7.3",
106
- "@lunora/errors": "1.0.0-alpha.36",
107
- "@lunora/values": "1.0.0-alpha.46",
106
+ "@lunora/errors": "1.0.0-alpha.38",
107
+ "@lunora/values": "1.0.0-alpha.48",
108
108
  "@visulima/disposable-email-domains": "1.1.1",
109
109
  "@visulima/email-verifier": "1.0.9",
110
110
  "@visulima/free-email-domains": "1.0.1",
@@ -1 +0,0 @@
1
- import{getAuthTablesWithResolvedIndexes as u}from"@better-auth/core/db/internal";import{b as c,d as l}from"./adapter-CG0Yv85f.mjs";import{ensureAuthAuditTable as h,createAuthAuditReader as m}from"../audit.mjs";import{resolveAuthOptions as d,createAuth as p}from"./createAuth-8aUw8v8D.mjs";import{authDoSchemaStatements as f,authDoColumnAdditions as A}from"./authDoColumnAdditions-UwF3Rl41.mjs";import{handleAuthRequest as g}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{schemaDeclaresIssuer as y,legacyIssuerCleanupStatements as R,indexesReferencingIssuer as x}from"./legacyIssuerCleanupStatements-D9Kf8OD1.mjs";const b=(i,s)=>{const t=Math.max(i.length,s.length);let e=i.length^s.length;for(let r=0;r<t;r+=1){const o=r<i.length?i.charCodeAt(r):0,n=r<s.length?s.charCodeAt(r):0;e|=o^n}return e===0},E="/__lunora/auth/session",S="/__lunora/auth/audit",v="x-lunora-auth-do-secret",_=i=>{if(i===null||typeof i!="object"||Array.isArray(i))return{error:"body must be a JSON object"};const s={};for(const[t,e]of Object.entries(i))if(t==="limit"||t==="sinceSeq"){if(typeof e!="number"||!Number.isFinite(e))return{error:`"${t}" must be a finite number`};s[t]=e}else if(t==="actorId"||t==="event"){if(typeof e!="string")return{error:`"${t}" must be a string`};s[t]=e}else return{error:`unknown audit read option "${t}"`};return{options:s}};class q{#s;#r;#e;#t;#n=!1;constructor(s,t,e={}){this.#e=s.storage,this.#r=t,this.#s=e}#o(){if(this.#t!==void 0)return this.#t;const s=this.#r();if(!this.#n){const t=d(s);for(const e of f(t))[...this.#e.sql.exec(e)];for(const e of A(t,r=>this.#i(r)))[...this.#e.sql.exec(e)];this.#u(t),this.#n=!0}return this.#t=p({...s,database:c(this.#e)}),this.#t}#i(s){return[...this.#e.sql.exec("SELECT name FROM pragma_table_info(?)",s)].map(e=>String(e.name))}#u(s){const{tables:t}=u(s),{account:e}=t;if(e===void 0)return;const r=Object.entries(e.fields).map(([o,n])=>n.fieldName??o);if(!(y(r)||!this.#i(e.modelName).includes("issuer")))try{const o=[...this.#e.sql.exec("SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ?",e.modelName)].map(n=>({name:String(n.name),sql:typeof n.sql=="string"?n.sql:void 0}));for(const n of R(e.modelName,x(o)))[...this.#e.sql.exec(n)]}catch(o){console.error("@lunora/auth: could not drop the reverted `account.issuer` column; sign-ups will fail until it is removed.",o)}}async#c(s){if(!this.#a(s))return Response.json({error:"unauthorized"},{status:401});let t;try{t=await s.json()}catch{return Response.json({error:"invalid body"},{status:400})}const e=_(t??{});if("error"in e)return Response.json({error:e.error},{status:400});const r=l(this.#e);await h(r);const o=await m(r).read(e.options);return Response.json({entries:o})}#a(s){const{internalSecret:t}=this.#s;if(t===void 0||t==="")return!1;const e=s.headers.get(v);return e!==null&&b(e,t)}async#l(s){if(!this.#a(s))return Response.json({error:"unauthorized"},{status:401});const e=await this.#o().api.getSession({headers:s.headers}),r=e?.user.id;if(r===void 0)return Response.json({});const o=e?.session.expiresAt,n=e?.user,a=n?.role;return Response.json({...typeof n?.email=="string"&&n.email.length>0?{email:n.email}:{},...o instanceof Date?{expiresAtMs:o.getTime()}:{},...typeof n?.name=="string"&&n.name.length>0?{name:n.name}:{},...typeof a=="string"&&a.length>0?{role:a}:{},userId:r})}async fetch(s){const t=new URL(s.url);if(t.pathname===E)return this.#l(s);if(t.pathname===S)return this.#c(s);const e=this.#o();return await g(e,s,this.#s.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{v as INTERNAL_SECRET_HEADER,q as LunoraAuthDO,S as READ_AUDIT_PATH,E as RESOLVE_SESSION_PATH};
@@ -1 +0,0 @@
1
- import{createAuthMiddleware as d}from"better-auth/api";import{o as c}from"./on-cloudflare-edge-Dx6gFjBO.mjs";import{appendAuthAuditEntry as l}from"../audit.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(c()){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 l(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};
@@ -1 +0,0 @@
1
- import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as p,READ_AUDIT_PATH as f}from"./AUTH_DO_AUDIT_PATH-th2-Hb30.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};
@@ -1 +0,0 @@
1
- import{defineErrorCodes as _}from"@better-auth/core/utils/error-codes";import{LunoraError as A}from"@lunora/errors";import{createAuthMiddleware as D,APIError as g}from"better-auth/api";const c="signUpInvitation",T=10080*60,v=365*24*60*60,y=500,w=_({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."}),O=32,N=/^[^\s@]+@[^\s@]+$/,x=e=>{if(!N.test(e))return!1;const t=e.slice(e.indexOf("@")+1);return t.includes(".")&&!t.startsWith(".")&&!t.endsWith(".")},m=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(O)))).replaceAll("+","-").replaceAll("/","_").replaceAll("=",""),S=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("")},U=(e,t)=>{if(e.length!==t.length)return!1;let i=0;for(let o=0;o<e.length;o+=1)i|=(e.codePointAt(o)??0)^(t.codePointAt(o)??0);return i===0},p=e=>e.trim().toLowerCase(),I=e=>{if(typeof e.email!="string")return;const t=p(e.email);return t===""?void 0:t},k="anonymous",b=e=>(e.plugins??[]).some(t=>t.id===k),E=e=>e.isAnonymous===!0,P=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()},L=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.")},C=(e={})=>{let t=e.allowFirstUser??!1,i=!1;const o=async a=>{if(!t)return!1;const n=await a.count({model:"user"}),d=i&&n>0?await a.count({model:"user",where:[{field:"isAnonymous",value:!0}]}):0;return n-d<=0?!0:(t=!1,!1)};return{$ERROR_CODES:w,hooks:{before:[{handler:D(async a=>{const n=a.body??{},d=typeof n.email=="string"?p(n.email):"",u=typeof n.inviteToken=="string"?n.inviteToken:"",r=()=>{throw new g("BAD_REQUEST",w.SIGN_UP_INVITE_INVALID)};if(d===""&&r(),await o(a.context.adapter))return;u===""&&r();const s=await a.context.adapter.findOne({model:c,where:[{field:"email",value:d}]}),f=s===null?void 0:s.tokenHash,l=await S(u);(typeof f!="string"||!U(f,l))&&r()}),matcher:a=>a.path==="/sign-up/email"}]},id:"lunora-invite-only",init:a=>{L(a.options);const{adapter:n}=a;return i=b(a.options),{options:{databaseHooks:{user:{create:{after:async r=>{const s=I(r);s===void 0||i&&E(r)||await n.update({model:c,update:{acceptedAt:new Date},where:[{field:"email",value:s}]})},before:async r=>{if(i&&E(r))return;const s=I(r);if(!(s!==void 0&&await P(n,s))&&!await o(n))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"}}}}}},H=async(e,t)=>{const i=p(t.email);if(!x(i))throw new A("VALIDATION_ERROR",`not an email address to invite: ${JSON.stringify(t.email)}`);const{expiresInSeconds:o=T}=t;if(!Number.isInteger(o)||o<=0||o>v)throw new A("VALIDATION_ERROR",`expiresInSeconds must be a positive integer no greater than ${String(v)}`);const a=new Date(Date.now()+o*1e3),n=R(),d=await S(n),u=t.invitedBy??null,r=await e.$context,s=[{field:"email",value:i}],f=async()=>r.adapter.update({model:c,update:{acceptedAt:null,expiresAt:a,invitedBy:u,tokenHash:d},where:s});if(await r.adapter.findOne({model:c,where:s})){const l=await f();if(l)return{...m(l),token:n}}try{const l=await r.adapter.create({data:{createdAt:new Date,email:i,expiresAt:a,invitedBy:u,tokenHash:d},model:c});return{...m(l),token:n}}catch(l){const h=await f();if(h===null)throw l;return{...m(h),token:n}}},G=async(e,t={})=>{const a=(await(await e.$context).adapter.findMany({limit:y,model:c,sortBy:{direction:"desc",field:"createdAt"}})).map(n=>m(n));return t.pendingOnly===!0?a.filter(n=>n.acceptedAt===null&&n.expiresAt.getTime()>Date.now()):a},Q=async(e,t)=>{await(await e.$context).adapter.delete({model:c,where:[{field:"email",value:p(t.email)}]})},V=100,B=async(e,t,i,o)=>{const a=t.filter(n=>n.acceptedAt===null&&n.expiresAt.getTime()<=i).slice(0,o);for(const n of a)await e.delete({model:c,where:[{field:"id",value:n.id}]});return a.length},W=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 o=Math.min(i,y),a=await e.$context,n=Date.now();let d=0,u=0;for(;d<o;){const r=await a.adapter.findMany({limit:V,model:c,offset:u,sortBy:{direction:"asc",field:"expiresAt"}});if(r.length===0)break;const s=r.map(l=>m(l)),f=await B(a.adapter,s,n,o-d);if(d+=f,s.some(l=>l.expiresAt.getTime()>n))break;u+=r.length-f}return d};export{H as createSignUpInvitation,C as inviteOnly,G as listSignUpInvitations,W as pruneSignUpInvitations,Q as revokeSignUpInvitation};