@lunora/auth 1.0.0-alpha.110 → 1.0.0-alpha.112

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.
@@ -1 +1 @@
1
- import{LunoraError as o}from"@lunora/errors";import{isDisposableDomain as c,setDomains as d}from"@visulima/disposable-email-domains";import{extractDomain as u,isFreeDomain as w,setDomains as D}from"@visulima/free-email-domains";const r=a=>{const s=a.default??a;return Array.isArray(s)?s:[]};let n;const f=async()=>(n??=(async()=>{const[a,s]=await Promise.all([import("@visulima/disposable-email-domains/domains"),import("@visulima/free-email-domains/domains")]);d(r(a)),D(r(s))})(),n),m=a=>{try{return new URL(`https://${a}`).hostname}catch{return a.toLowerCase()}},l=a=>a&&a.length>0?new Set(a.map(s=>m(s))):void 0,h=(a,s={})=>{const t=u(a);if(t===void 0)return{domain:void 0,emailClass:"business"};const e=m(t),i=l(s.allowDomains);return c(e,{allowDomains:i,customDomains:l(s.denyDomains)})?{domain:e,emailClass:"disposable"}:w(e,{allowDomains:i})?{domain:e,emailClass:"free"}:{domain:e,emailClass:"business"}},p=async a=>{const{checkMxRecords:s}=await import("@visulima/email-verifier/checks/mx");return(await s(a)).valid},y=async(a,s={})=>{if(s.requireValidSyntax!==!1){const{validateSyntax:e}=await import("@visulima/email-verifier/checks/syntax");if(!e(a))throw new o("VALIDATION_ERROR",`@lunora/auth: "${a}" is not a valid email address.`)}await f();const t=h(a,s);if(t.emailClass==="disposable"&&s.blockDisposable!==!1)throw new o("EMAIL_DOMAIN_BLOCKED",`@lunora/auth: signups from the disposable/throwaway domain "${t.domain??a}" are not allowed.`);if(s.mx===!0&&t.domain!==void 0&&!await p(t.domain))throw new o("EMAIL_UNDELIVERABLE",`@lunora/auth: the domain "${t.domain}" publishes no MX records, so mail to it cannot be delivered.`);return t},b=a=>async({ctx:s,next:t})=>{const e=a.email(s);if(e===void 0||e==="")throw new o("VALIDATION_ERROR","@lunora/auth: emailGateMiddleware received no email to check.");const i=await y(e,a);return a.onClassify?.(i,s),t()};export{y as assertEmailAllowed,h as classifyEmail,b as emailGateMiddleware,f as loadEmailDomainLists};
1
+ import{LunoraError as r}from"@lunora/errors";import{isDisposableDomain as c,setDomains as d}from"@visulima/disposable-email-domains";import{extractDomain as u,isFreeDomain as w,setDomains as h}from"@visulima/free-email-domains";const n=a=>{const s=a.default??a;return Array.isArray(s)?s:[]};let o;const D=async()=>{if(o)return o;const a=(async()=>{const[s,t]=await Promise.all([import("@visulima/disposable-email-domains/domains"),import("@visulima/free-email-domains/domains",{with:{type:"json"}})]);d(n(s)),h(n(t))})();return o=a,a.catch(()=>{o=void 0}),a},m=a=>{try{return new URL(`https://${a}`).hostname}catch{return a.toLowerCase()}},l=a=>a&&a.length>0?new Set(a.map(s=>m(s))):void 0,f=(a,s={})=>{const t=u(a);if(t===void 0)return{domain:void 0,emailClass:"business"};const e=m(t),i=l(s.allowDomains);return c(e,{allowDomains:i,customDomains:l(s.denyDomains)})?{domain:e,emailClass:"disposable"}:w(e,{allowDomains:i})?{domain:e,emailClass:"free"}:{domain:e,emailClass:"business"}},p=async a=>{const{checkMxRecords:s}=await import("@visulima/email-verifier/checks/mx");return(await s(a)).valid},y=async(a,s={})=>{if(s.requireValidSyntax!==!1){const{validateSyntax:e}=await import("@visulima/email-verifier/checks/syntax");if(!e(a))throw new r("VALIDATION_ERROR",`@lunora/auth: "${a}" is not a valid email address.`)}await D();const t=f(a,s);if(t.emailClass==="disposable"&&s.blockDisposable!==!1)throw new r("EMAIL_DOMAIN_BLOCKED",`@lunora/auth: signups from the disposable/throwaway domain "${t.domain??a}" are not allowed.`);if(s.mx===!0&&t.domain!==void 0&&!await p(t.domain))throw new r("EMAIL_UNDELIVERABLE",`@lunora/auth: the domain "${t.domain}" publishes no MX records, so mail to it cannot be delivered.`);return t},b=a=>async({ctx:s,next:t})=>{const e=a.email(s);if(e===void 0||e==="")throw new r("VALIDATION_ERROR","@lunora/auth: emailGateMiddleware received no email to check.");const i=await y(e,a);return a.onClassify?.(i,s),t()};export{y as assertEmailAllowed,f as classifyEmail,b as emailGateMiddleware,D as loadEmailDomainLists};
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { D as DoStorageLike } from "./packem_shared/adapter.d-DrD3bb1u.mjs";
2
2
  export { l as lunoraAuthAdapter, a as lunoraD1Adapter, b as lunoraDoAdapter } from "./packem_shared/adapter.d-DrD3bb1u.mjs";
3
3
  import { LunoraError } from '@lunora/errors';
4
- import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-De6IOirt.mjs";
5
- export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-De6IOirt.mjs";
4
+ import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-hkN1GE5-.mjs";
5
+ export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-hkN1GE5-.mjs";
6
6
  import { AppendAuthAuditEntry, AppendAuthAuditOptions, AuthAuditEvent, AuthAuditReader } from "./audit.mjs";
7
7
  export { AUTH_AUDIT_TABLE, type AuthAuditEntry, type AuthAuditOutcome, type ReadAuthAuditOptions, appendAuthAuditEntry, createAuthAuditReader, ensureAuthAuditTable, readAuthAuditLog } from "./audit.mjs";
8
8
  import { createAuthMiddleware } from 'better-auth/api';
@@ -799,8 +799,18 @@ interface DoAuthWiring {
799
799
  auditReader: AuthAuditReader;
800
800
  /** Forwards `/api/auth/*` to the object; `undefined` for anything else. */
801
801
  authHandler: (request: Request) => Promise<Response | undefined>;
802
- /** Resolves a request's identity by asking the object. `null` when anonymous, unreachable, or ungated. */
802
+ /**
803
+ * Resolves a request's identity by asking the object. `null` when anonymous,
804
+ * unreachable, or ungated.
805
+ *
806
+ * `expiresAtMs` (epoch ms) is the session's expiry, which the runtime forwards as
807
+ * the socket's credential expiry so the DO can drop a subscriber whose session has
808
+ * lapsed; `role` is better-auth's `admin()` column, which RLS role grants read.
809
+ * Both are absent when the session carries neither.
810
+ */
803
811
  resolveIdentity: (request: Request) => Promise<null | {
812
+ expiresAtMs?: number;
813
+ role?: string;
804
814
  userId: string;
805
815
  }>;
806
816
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { D as DoStorageLike } from "./packem_shared/adapter.d-2a61HAY2.js";
2
2
  export { l as lunoraAuthAdapter, a as lunoraD1Adapter, b as lunoraDoAdapter } from "./packem_shared/adapter.d-2a61HAY2.js";
3
3
  import { LunoraError } from '@lunora/errors';
4
- import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-De6IOirt.js";
5
- export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-De6IOirt.js";
4
+ import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-hkN1GE5-.js";
5
+ export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-hkN1GE5-.js";
6
6
  import { AppendAuthAuditEntry, AppendAuthAuditOptions, AuthAuditEvent, AuthAuditReader } from "./audit.js";
7
7
  export { AUTH_AUDIT_TABLE, type AuthAuditEntry, type AuthAuditOutcome, type ReadAuthAuditOptions, appendAuthAuditEntry, createAuthAuditReader, ensureAuthAuditTable, readAuthAuditLog } from "./audit.js";
8
8
  import { createAuthMiddleware } from 'better-auth/api';
@@ -799,8 +799,18 @@ interface DoAuthWiring {
799
799
  auditReader: AuthAuditReader;
800
800
  /** Forwards `/api/auth/*` to the object; `undefined` for anything else. */
801
801
  authHandler: (request: Request) => Promise<Response | undefined>;
802
- /** Resolves a request's identity by asking the object. `null` when anonymous, unreachable, or ungated. */
802
+ /**
803
+ * Resolves a request's identity by asking the object. `null` when anonymous,
804
+ * unreachable, or ungated.
805
+ *
806
+ * `expiresAtMs` (epoch ms) is the session's expiry, which the runtime forwards as
807
+ * the socket's credential expiry so the DO can drop a subscriber whose session has
808
+ * lapsed; `role` is better-auth's `admin()` column, which RLS role grants read.
809
+ * Both are absent when the session carries neither.
810
+ */
803
811
  resolveIdentity: (request: Request) => Promise<null | {
812
+ expiresAtMs?: number;
813
+ role?: string;
804
814
  userId: string;
805
815
  }>;
806
816
  }
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{l as r,a as o,b as a}from"./packem_shared/adapter-DA8DdALX.mjs";import{LunoraAuthAdminError as u,createAuthAdmin as i}from"./packem_shared/LunoraAuthAdminError-DtAKa22S.mjs";import{AUTH_AUDIT_TABLE as m,appendAuthAuditEntry as l,createAuthAuditReader as d,ensureAuthAuditTable as h,readAuthAuditLog as E}from"./audit.mjs";import{authAuditHook as p,buildAuditEntry as T,eventForPath as f,withAuthAudit as _}from"./packem_shared/authAuditHook-tG8gdR77.mjs";import{READ_AUDIT_PATH as D,INTERNAL_SECRET_HEADER as S,RESOLVE_SESSION_PATH as H,LunoraAuthDO as c}from"./packem_shared/AUTH_DO_AUDIT_PATH-D-RB2XYs.mjs";import{createAuth as L,resolveAuthOptions as P}from"./packem_shared/createAuth-DYFhkI1U.mjs";import{authDoColumnAdditions as I,authDoSchemaStatements as O}from"./packem_shared/authDoColumnAdditions-BCSs7qaN.mjs";import{createDoAuthWiring as N}from"./packem_shared/createDoAuthWiring-C8H_SGCq.mjs";import{emailGateDatabaseHooks as b,withEmailGate as g}from"./packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs";import{assertEmailAllowed as M,classifyEmail as q,emailGateMiddleware as C,loadEmailDomainLists as F}from"./email-guard.mjs";import{DEFAULT_AUTH_BASE_PATH as k,handleAuthRequest as B}from"./packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{LunoraAuthHeadersError as W,withAuthPlugins as Y}from"./middleware.mjs";import{compileMigrationsSql as z,ensureMigrated as J}from"./packem_shared/compileMigrationsSql-Y1YB3maO.mjs";import{default as Q}from"./schema.mjs";import{sessionPresets as Z,validateSessionPolicy as $}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{createSqlAuthStore as te,d1Executor as re}from"./sql-store.mjs";import{createMemoryAuthStore as ae,matchesWhere as Ae}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as ie,verifyTurnstile as se}from"./turnstile.mjs";import{verifyTurnstileMiddleware as le}from"./turnstile-middleware.mjs";export{m as AUTH_AUDIT_TABLE,D as AUTH_DO_AUDIT_PATH,S as AUTH_DO_SECRET_HEADER,H as AUTH_DO_SESSION_PATH,k as DEFAULT_AUTH_BASE_PATH,u as LunoraAuthAdminError,c as LunoraAuthDO,W as LunoraAuthHeadersError,ie as TURNSTILE_VERIFY_ENDPOINT,l as appendAuthAuditEntry,M as assertEmailAllowed,p as authAuditHook,I as authDoColumnAdditions,O as authDoSchemaStatements,Q as authTables,T as buildAuditEntry,q as classifyEmail,z as compileMigrationsSql,L as createAuth,i as createAuthAdmin,d as createAuthAuditReader,N as createDoAuthWiring,ae as createMemoryAuthStore,te as createSqlAuthStore,re as d1Executor,b as emailGateDatabaseHooks,C as emailGateMiddleware,h as ensureAuthAuditTable,J as ensureMigrated,f as eventForPath,B as handleAuthRequest,F as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,Ae as matchesWhere,E as readAuthAuditLog,P as resolveAuthOptions,Z as sessionPresets,$ as validateSessionPolicy,se as verifyTurnstile,le as verifyTurnstileMiddleware,_ as withAuthAudit,Y as withAuthPlugins,g as withEmailGate};
1
+ import{l as r,a as o,b as a}from"./packem_shared/adapter-DA8DdALX.mjs";import{LunoraAuthAdminError as u,createAuthAdmin as i}from"./packem_shared/LunoraAuthAdminError-DtAKa22S.mjs";import{AUTH_AUDIT_TABLE as m,appendAuthAuditEntry as l,createAuthAuditReader as d,ensureAuthAuditTable as h,readAuthAuditLog as E}from"./audit.mjs";import{authAuditHook as p,buildAuditEntry as T,eventForPath as f,withAuthAudit as _}from"./packem_shared/authAuditHook-tG8gdR77.mjs";import{READ_AUDIT_PATH as D,INTERNAL_SECRET_HEADER as S,RESOLVE_SESSION_PATH as H,LunoraAuthDO as c}from"./packem_shared/AUTH_DO_AUDIT_PATH-BdKhdNhp.mjs";import{createAuth as L,resolveAuthOptions as P}from"./packem_shared/createAuth-CtTKaLZN.mjs";import{authDoColumnAdditions as I,authDoSchemaStatements as O}from"./packem_shared/authDoColumnAdditions-BCSs7qaN.mjs";import{createDoAuthWiring as N}from"./packem_shared/createDoAuthWiring-BGL6oAV_.mjs";import{emailGateDatabaseHooks as b,withEmailGate as g}from"./packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs";import{assertEmailAllowed as M,classifyEmail as q,emailGateMiddleware as C,loadEmailDomainLists as F}from"./email-guard.mjs";import{DEFAULT_AUTH_BASE_PATH as k,handleAuthRequest as B}from"./packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{LunoraAuthHeadersError as W,withAuthPlugins as Y}from"./middleware.mjs";import{compileMigrationsSql as z,ensureMigrated as J}from"./packem_shared/compileMigrationsSql-TvwFrqmu.mjs";import{default as Q}from"./schema.mjs";import{sessionPresets as Z,validateSessionPolicy as $}from"./packem_shared/sessionPresets-C867Mlo4.mjs";import{createSqlAuthStore as te,d1Executor as re}from"./sql-store.mjs";import{createMemoryAuthStore as ae,matchesWhere as Ae}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as ie,verifyTurnstile as se}from"./turnstile.mjs";import{verifyTurnstileMiddleware as le}from"./turnstile-middleware.mjs";export{m as AUTH_AUDIT_TABLE,D as AUTH_DO_AUDIT_PATH,S as AUTH_DO_SECRET_HEADER,H as AUTH_DO_SESSION_PATH,k as DEFAULT_AUTH_BASE_PATH,u as LunoraAuthAdminError,c as LunoraAuthDO,W as LunoraAuthHeadersError,ie as TURNSTILE_VERIFY_ENDPOINT,l as appendAuthAuditEntry,M as assertEmailAllowed,p as authAuditHook,I as authDoColumnAdditions,O as authDoSchemaStatements,Q as authTables,T as buildAuditEntry,q as classifyEmail,z as compileMigrationsSql,L as createAuth,i as createAuthAdmin,d as createAuthAuditReader,N as createDoAuthWiring,ae as createMemoryAuthStore,te as createSqlAuthStore,re as d1Executor,b as emailGateDatabaseHooks,C as emailGateMiddleware,h as ensureAuthAuditTable,J as ensureMigrated,f as eventForPath,B as handleAuthRequest,F as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,Ae as matchesWhere,E as readAuthAuditLog,P as resolveAuthOptions,Z as sessionPresets,$ as validateSessionPolicy,se as verifyTurnstile,le as verifyTurnstileMiddleware,_ as withAuthAudit,Y as withAuthPlugins,g as withEmailGate};
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { L as LunoraAuth } from "./packem_shared/create-auth.d-De6IOirt.mjs";
2
+ import { L as LunoraAuth } from "./packem_shared/create-auth.d-hkN1GE5-.mjs";
3
3
  import 'better-auth';
4
4
  /**
5
5
  * Structural mirror of `@lunora/server`'s `MiddlewareNext` — the continuation
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { L as LunoraAuth } from "./packem_shared/create-auth.d-De6IOirt.js";
2
+ import { L as LunoraAuth } from "./packem_shared/create-auth.d-hkN1GE5-.js";
3
3
  import 'better-auth';
4
4
  /**
5
5
  * Structural mirror of `@lunora/server`'s `MiddlewareNext` — the continuation
@@ -0,0 +1 @@
1
+ import{b as a,d as u}from"./adapter-DA8DdALX.mjs";import{ensureAuthAuditTable as h,createAuthAuditReader as c}from"../audit.mjs";import{resolveAuthOptions as l,createAuth as d}from"./createAuth-CtTKaLZN.mjs";import{authDoSchemaStatements as p,authDoColumnAdditions as f}from"./authDoColumnAdditions-BCSs7qaN.mjs";import{handleAuthRequest as m}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const A=(o,e)=>{const s=Math.max(o.length,e.length);let t=o.length^e.length;for(let r=0;r<s;r+=1){const n=r<o.length?o.charCodeAt(r):0,i=r<e.length?e.charCodeAt(r):0;t|=n^i}return t===0},R="/__lunora/auth/session",g="/__lunora/auth/audit",y="x-lunora-auth-do-secret",S=o=>{if(o===null||typeof o!="object"||Array.isArray(o))return{error:"body must be a JSON object"};const e={};for(const[s,t]of Object.entries(o))if(s==="limit"||s==="sinceSeq"){if(typeof t!="number"||!Number.isFinite(t))return{error:`"${s}" must be a finite number`};e[s]=t}else if(s==="actorId"||s==="event"){if(typeof t!="string")return{error:`"${s}" must be a string`};e[s]=t}else return{error:`unknown audit read option "${s}"`};return{options:e}};class T{#s;#r;#t;#e;#o=!1;constructor(e,s,t={}){this.#t=e.storage,this.#r=s,this.#s=t}#n(){if(this.#e!==void 0)return this.#e;const e=this.#r();if(!this.#o){const s=l(e);for(const t of p(s))[...this.#t.sql.exec(t)];for(const t of f(s,r=>this.#a(r)))[...this.#t.sql.exec(t)];this.#o=!0}return this.#e=d({...e,database:a(this.#t)}),this.#e}#a(e){return[...this.#t.sql.exec("SELECT name FROM pragma_table_info(?)",e)].map(t=>String(t.name))}async#u(e){if(!this.#i(e))return Response.json({error:"unauthorized"},{status:401});let s;try{s=await e.json()}catch{return Response.json({error:"invalid body"},{status:400})}const t=S(s??{});if("error"in t)return Response.json({error:t.error},{status:400});const r=u(this.#t);await h(r);const n=await c(r).read(t.options);return Response.json({entries:n})}#i(e){const{internalSecret:s}=this.#s;if(s===void 0||s==="")return!1;const t=e.headers.get(y);return t!==null&&A(t,s)}async#h(e){if(!this.#i(e))return Response.json({error:"unauthorized"},{status:401});const t=await this.#n().api.getSession({headers:e.headers}),r=t?.user.id;if(r===void 0)return Response.json({});const n=t?.session.expiresAt,i=t?.user?.role;return Response.json({...n instanceof Date?{expiresAtMs:n.getTime()}:{},...typeof i=="string"&&i.length>0?{role:i}:{},userId:r})}async fetch(e){const s=new URL(e.url);if(s.pathname===R)return this.#h(e);if(s.pathname===g)return this.#u(e);const t=this.#n();return await m(t,e,this.#s.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{y as INTERNAL_SECRET_HEADER,T as LunoraAuthDO,g as READ_AUDIT_PATH,R as RESOLVE_SESSION_PATH};
@@ -1 +1 @@
1
- import{getMigrations as l}from"better-auth/db/migration";import{resolveAuthOptions as I}from"./createAuth-DYFhkI1U.mjs";const w=/pragma_index_list\s*\(/iu,b=/sqlite_master/iu,h=/^["`[]|["\]`]$/gu,y=/\s+/u,_=/create\s+unique\s+index/iu,x=/\bwhere\b/iu,M=t=>w.test(t)&&b.test(t),R=t=>(t.trim().split(y)[0]??"").replaceAll(h,"").trim(),D=t=>{const n=t.indexOf("(");if(n===-1)return{columns:[],tail:""};let s=0;for(let e=n;e<t.length;e+=1){const o=t[e];if(o==="(")s+=1;else if(o===")"&&(s-=1,s===0))return{columns:t.slice(n+1,e).split(",").map(i=>R(i)).filter(i=>i!==""),tail:t.slice(e+1)}}return{columns:[],tail:""}},T=async t=>{const n=t.prepare("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'"),{results:s}=await n.all(),e=[];for(const o of s??[]){const i=o.name,r=o.tbl_name,a=o.sql;if(typeof i!="string"||typeof r!="string")continue;if(typeof a!="string"){e.push({columnPosition:0,indexName:i,isPartial:0,isUnique:1,tableName:r});continue}const p=_.test(a)?1:0,{columns:m,tail:f}=D(a),d=x.test(f)?1:0;for(const[E,g]of m.entries())e.push({columnName:g,columnPosition:E,indexName:i,isPartial:d,isUnique:p,tableName:r})}return e},A=t=>{const n=async()=>{const e=await T(t);return{meta:{},results:e,success:!0}},s={all:n,bind:()=>s,run:n};return s},N=t=>new Proxy(t,{get(n,s,e){if(s==="prepare")return i=>M(i)?A(n):n.prepare(i);const o=Reflect.get(n,s,e);return typeof o=="function"?o.bind(n):o}}),P=t=>typeof t=="object"&&t!==null&&typeof t.prepare=="function"&&typeof t.batch=="function",u=t=>P(t.database)?{...t,database:N(t.database)}:t,c=new WeakMap,S=async t=>{const{options:n}=t,s=c.get(n);if(s){await s;return}const e=(async()=>{const{runMigrations:o}=await l(u(n));await o()})();c.set(n,e);try{await e}catch(o){throw c.delete(n),o}},U=async t=>{const{compileMigrations:n}=await l(u(I(t)));return n()};export{U as compileMigrationsSql,S as ensureMigrated};
1
+ import{getMigrations as l}from"better-auth/db/migration";import{resolveAuthOptions as I}from"./createAuth-CtTKaLZN.mjs";const w=/pragma_index_list\s*\(/iu,b=/sqlite_master/iu,h=/^["`[]|["\]`]$/gu,y=/\s+/u,_=/create\s+unique\s+index/iu,x=/\bwhere\b/iu,M=t=>w.test(t)&&b.test(t),R=t=>(t.trim().split(y)[0]??"").replaceAll(h,"").trim(),D=t=>{const n=t.indexOf("(");if(n===-1)return{columns:[],tail:""};let s=0;for(let e=n;e<t.length;e+=1){const o=t[e];if(o==="(")s+=1;else if(o===")"&&(s-=1,s===0))return{columns:t.slice(n+1,e).split(",").map(i=>R(i)).filter(i=>i!==""),tail:t.slice(e+1)}}return{columns:[],tail:""}},T=async t=>{const n=t.prepare("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'"),{results:s}=await n.all(),e=[];for(const o of s??[]){const i=o.name,r=o.tbl_name,a=o.sql;if(typeof i!="string"||typeof r!="string")continue;if(typeof a!="string"){e.push({columnPosition:0,indexName:i,isPartial:0,isUnique:1,tableName:r});continue}const p=_.test(a)?1:0,{columns:m,tail:f}=D(a),d=x.test(f)?1:0;for(const[E,g]of m.entries())e.push({columnName:g,columnPosition:E,indexName:i,isPartial:d,isUnique:p,tableName:r})}return e},A=t=>{const n=async()=>{const e=await T(t);return{meta:{},results:e,success:!0}},s={all:n,bind:()=>s,run:n};return s},N=t=>new Proxy(t,{get(n,s,e){if(s==="prepare")return i=>M(i)?A(n):n.prepare(i);const o=Reflect.get(n,s,e);return typeof o=="function"?o.bind(n):o}}),P=t=>typeof t=="object"&&t!==null&&typeof t.prepare=="function"&&typeof t.batch=="function",u=t=>P(t.database)?{...t,database:N(t.database)}:t,c=new WeakMap,S=async t=>{const{options:n}=t,s=c.get(n);if(s){await s;return}const e=(async()=>{const{runMigrations:o}=await l(u(n));await o()})();c.set(n,e);try{await e}catch(o){throw c.delete(n),o}},U=async t=>{const{compileMigrations:n}=await l(u(I(t)));return n()};export{U as compileMigrationsSql,S as ensureMigrated};
@@ -98,6 +98,16 @@ type LunoraAuth = ReturnType<typeof betterAuth>;
98
98
  * their own durable store can pass an explicit `rateLimit: { storage: … }`
99
99
  * (or `customStorage`), and any explicit value wins.
100
100
  *
101
+ * The limiter's *bucket key* is filled here too, and it is the half that makes
102
+ * the rest of the paragraph above true. A limit is only brute-force protection
103
+ * if the key identifies the client, so {@link defaultIpAddressHeaders} points
104
+ * better-auth at `cf-connecting-ip` instead of its own `x-forwarded-for`
105
+ * default, and {@link UNRESOLVED_IP_BUCKET_FACTOR} re-sizes the shared bucket
106
+ * for the deployments where no client IP can be resolved at all. Both are read
107
+ * off `advanced.ipAddress`, so a caller who configures that keeps their own
108
+ * policy end to end. The catch-all `customRules` entry is merged *after* the
109
+ * caller's own rules so theirs match first.
110
+ *
101
111
  * Session cookie cache is ON by default too.
102
112
  *
103
113
  * Every authenticated call resolves identity through better-auth's
@@ -113,8 +123,8 @@ type LunoraAuth = ReturnType<typeof betterAuth>;
113
123
  * is bounded by that TTL; callers who need immediate revocation opt out with
114
124
  * `session: { cookieCache: { enabled: false } }` (or the `strict` preset).
115
125
  *
116
- * Every explicit caller value is forwarded verbatim. The two `rateLimit` fills
117
- * merge into a single `rateLimit` object so neither clobbers the other.
126
+ * Every explicit caller value is forwarded verbatim. The `rateLimit` fills all
127
+ * merge into a single `rateLimit` object so none clobbers the others.
118
128
  */
119
129
  declare const resolveAuthOptions: (options: LunoraAuthOptions) => LunoraAuthOptions;
120
130
  /**
@@ -98,6 +98,16 @@ type LunoraAuth = ReturnType<typeof betterAuth>;
98
98
  * their own durable store can pass an explicit `rateLimit: { storage: … }`
99
99
  * (or `customStorage`), and any explicit value wins.
100
100
  *
101
+ * The limiter's *bucket key* is filled here too, and it is the half that makes
102
+ * the rest of the paragraph above true. A limit is only brute-force protection
103
+ * if the key identifies the client, so {@link defaultIpAddressHeaders} points
104
+ * better-auth at `cf-connecting-ip` instead of its own `x-forwarded-for`
105
+ * default, and {@link UNRESOLVED_IP_BUCKET_FACTOR} re-sizes the shared bucket
106
+ * for the deployments where no client IP can be resolved at all. Both are read
107
+ * off `advanced.ipAddress`, so a caller who configures that keeps their own
108
+ * policy end to end. The catch-all `customRules` entry is merged *after* the
109
+ * caller's own rules so theirs match first.
110
+ *
101
111
  * Session cookie cache is ON by default too.
102
112
  *
103
113
  * Every authenticated call resolves identity through better-auth's
@@ -113,8 +123,8 @@ type LunoraAuth = ReturnType<typeof betterAuth>;
113
123
  * is bounded by that TTL; callers who need immediate revocation opt out with
114
124
  * `session: { cookieCache: { enabled: false } }` (or the `strict` preset).
115
125
  *
116
- * Every explicit caller value is forwarded verbatim. The two `rateLimit` fills
117
- * merge into a single `rateLimit` object so neither clobbers the other.
126
+ * Every explicit caller value is forwarded verbatim. The `rateLimit` fills all
127
+ * merge into a single `rateLimit` object so none clobbers the others.
118
128
  */
119
129
  declare const resolveAuthOptions: (options: LunoraAuthOptions) => LunoraAuthOptions;
120
130
  /**
@@ -0,0 +1 @@
1
+ import{LunoraError as c}from"@lunora/errors";import{betterAuth as l}from"better-auth";import{getIP as u}from"better-auth/api";import{validateSessionPolicy as f}from"./sessionPresets-C867Mlo4.mjs";const n="cf-connecting-ip",h=()=>globalThis.navigator?.userAgent==="Cloudflare-Workers",m=e=>{const t=e!==void 0&&e.length>0;return h()?t?[n,"x-forwarded-for"]:[n]:t?["x-forwarded-for"]:[]},a="/**",p=100,A=e=>{const t=e.rateLimit?.customRules??{};return t[a]!==void 0?t:{...t,[a]:(o,r)=>u(o,e)===null?{max:r.max*p,window:r.window}:r}},d=32,E=e=>{const t=typeof e=="string"?e.trim().length:0;return t>0&&t<d},i=e=>typeof e=="string"?e.toLowerCase().startsWith("http://"):e&&typeof e=="object"?e.protocol==="http"?!0:e.protocol==="https"?!1:typeof e.fallback=="string"&&e.fallback.toLowerCase().startsWith("http://"):!1,g=new Set(["127.0.0.1","localhost"]),L=e=>{if(typeof e!="string")return;let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="http:"||!g.has(t.hostname)))return{allowedHosts:[t.hostname,`${t.hostname}:*`],fallback:e,protocol:"http"}},C=e=>{const t=L(e.baseURL)??e.baseURL;if(E(e.secret)){const s=`@lunora/auth: AUTH_SECRET is only ${String(e.secret?.trim().length)} characters. Use at least ${String(d)} for a brute-force-resistant secret — generate one with \`openssl rand -hex 32\`.`;if(!i(t))throw new c("INTERNAL",s);console.warn(s)}const o=e.advanced??{},r=o.ipAddress??{};return{...e,advanced:{...o,defaultCookieAttributes:o.defaultCookieAttributes??{httpOnly:!0,path:"/",sameSite:"lax"},ipAddress:{...r,...r.ipAddressHeaders===void 0?{ipAddressHeaders:m(r.trustedProxies)}:{}},...o.useSecureCookies===void 0?{useSecureCookies:!i(t)}:{}},baseURL:t}},v=e=>{const t=C(e),o=t.rateLimit?.enabled===void 0,r=t.rateLimit?.storage===void 0&&t.rateLimit?.enabled!==!1,s=t.rateLimit?.enabled===!1?{}:{rateLimit:{...t.rateLimit,...o?{enabled:!0}:{},...r?{storage:"database"}:{},customRules:A(t)}};return{...t,...s,...t.session?.cookieCache===void 0?{session:{...t.session,cookieCache:{enabled:!0,maxAge:60}}}:{}}},H=e=>{if(!e.secret||e.secret.trim()==="")throw new c("INTERNAL",'@lunora/auth: `secret` is required. Set AUTH_SECRET locally in .dev.vars (`lunora env set AUTH_SECRET "$(openssl rand -hex 32)"`), and in production with `wrangler secret put AUTH_SECRET`.');return e.session&&f(e.session),l(v(e))};export{H as createAuth,v as resolveAuthOptions};
@@ -0,0 +1 @@
1
+ import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as f,READ_AUDIT_PATH as h}from"./AUTH_DO_AUDIT_PATH-BdKhdNhp.mjs";import{DEFAULT_AUTH_BASE_PATH as A,isAuthRoutePath as y}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const w=c=>{const{basePath:d=A,internalSecret:o,namespace:a,objectName:l="auth"}=c,i=()=>{if(a)return a.get(a.idFromName(l))},p=async(t,r,n)=>{if(!o)return;const s=i();if(!s)return;const e=await s.fetch(new Request(new URL(t,r),{body:JSON.stringify(n),headers:{"content-type":"application/json",[u]:o},method:"POST"}));return e.ok?e:void 0};return{auditReader:{read:async t=>{const r=await p(h,"https://auth-do.invalid",t);return r?(await r.json())?.entries??[]:[]}},authHandler:async t=>{if(y(new URL(t.url).pathname,d))return i()?.fetch(t)},resolveIdentity:async t=>{if(!o)return null;const r=i();if(!r)return null;const n=new Headers(t.headers);n.set(u,o);const s=await r.fetch(new Request(new URL(f,t.url),{headers:n}));if(!s.ok)return null;const e=await s.json();return e?.userId?{...typeof e.expiresAtMs=="number"&&Number.isFinite(e.expiresAtMs)?{expiresAtMs:e.expiresAtMs}:{},...typeof e.role=="string"&&e.role.length>0?{role:e.role}:{},userId:e.userId}:null}}};export{w as createDoAuthWiring};
package/dist/schema.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TableDefinition } from '@lunora/server';
2
- import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-De6IOirt.mjs";
2
+ import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-hkN1GE5-.mjs";
3
3
  import 'better-auth';
4
4
  /**
5
5
  * Derive Lunora table definitions from a better-auth config — the bridge that
package/dist/schema.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TableDefinition } from '@lunora/server';
2
- import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-De6IOirt.js";
2
+ import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-hkN1GE5-.js";
3
3
  import 'better-auth';
4
4
  /**
5
5
  * Derive Lunora table definitions from a better-auth config — the bridge that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/auth",
3
- "version": "1.0.0-alpha.110",
3
+ "version": "1.0.0-alpha.112",
4
4
  "description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
5
5
  "keywords": [
6
6
  "auth",
@@ -103,8 +103,8 @@
103
103
  "@better-auth/oauth-provider": "1.7.1",
104
104
  "@better-auth/passkey": "1.7.1",
105
105
  "@better-auth/scim": "1.7.1",
106
- "@lunora/errors": "1.0.0-alpha.28",
107
- "@lunora/values": "1.0.0-alpha.36",
106
+ "@lunora/errors": "1.0.0-alpha.29",
107
+ "@lunora/values": "1.0.0-alpha.37",
108
108
  "@visulima/disposable-email-domains": "1.1.0",
109
109
  "@visulima/email-verifier": "1.0.2",
110
110
  "@visulima/free-email-domains": "1.0.1",
@@ -1 +0,0 @@
1
- import{b as i,d as u}from"./adapter-DA8DdALX.mjs";import{ensureAuthAuditTable as h,createAuthAuditReader as c}from"../audit.mjs";import{resolveAuthOptions as l,createAuth as d}from"./createAuth-DYFhkI1U.mjs";import{authDoSchemaStatements as f,authDoColumnAdditions as p}from"./authDoColumnAdditions-BCSs7qaN.mjs";import{handleAuthRequest as m}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const A=(o,t)=>{const e=Math.max(o.length,t.length);let s=o.length^t.length;for(let r=0;r<e;r+=1){const n=r<o.length?o.charCodeAt(r):0,a=r<t.length?t.charCodeAt(r):0;s|=n^a}return s===0},R="/__lunora/auth/session",y="/__lunora/auth/audit",g="x-lunora-auth-do-secret",S=o=>{if(o===null||typeof o!="object"||Array.isArray(o))return{error:"body must be a JSON object"};const t={};for(const[e,s]of Object.entries(o))if(e==="limit"||e==="sinceSeq"){if(typeof s!="number"||!Number.isFinite(s))return{error:`"${e}" must be a finite number`};t[e]=s}else if(e==="actorId"||e==="event"){if(typeof s!="string")return{error:`"${e}" must be a string`};t[e]=s}else return{error:`unknown audit read option "${e}"`};return{options:t}};class v{#s;#r;#t;#e;#o=!1;constructor(t,e,s={}){this.#t=t.storage,this.#r=e,this.#s=s}#n(){if(this.#e!==void 0)return this.#e;const t=this.#r();if(!this.#o){const e=l(t);for(const s of f(e))[...this.#t.sql.exec(s)];for(const s of p(e,r=>this.#i(r)))[...this.#t.sql.exec(s)];this.#o=!0}return this.#e=d({...t,database:i(this.#t)}),this.#e}#i(t){return[...this.#t.sql.exec("SELECT name FROM pragma_table_info(?)",t)].map(s=>String(s.name))}async#u(t){if(!this.#a(t))return Response.json({error:"unauthorized"},{status:401});let e;try{e=await t.json()}catch{return Response.json({error:"invalid body"},{status:400})}const s=S(e??{});if("error"in s)return Response.json({error:s.error},{status:400});const r=u(this.#t);await h(r);const n=await c(r).read(s.options);return Response.json({entries:n})}#a(t){const{internalSecret:e}=this.#s;if(e===void 0||e==="")return!1;const s=t.headers.get(g);return s!==null&&A(s,e)}async#h(t){if(!this.#a(t))return Response.json({error:"unauthorized"},{status:401});const r=(await this.#n().api.getSession({headers:t.headers}))?.user.id;return Response.json(r===void 0?{}:{userId:r})}async fetch(t){const e=new URL(t.url);if(e.pathname===R)return this.#h(t);if(e.pathname===y)return this.#u(t);const s=this.#n();return await m(s,t,this.#s.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{g as INTERNAL_SECRET_HEADER,v as LunoraAuthDO,y as READ_AUDIT_PATH,R as RESOLVE_SESSION_PATH};
@@ -1 +0,0 @@
1
- import{LunoraError as a}from"@lunora/errors";import{betterAuth as i}from"better-auth";import{validateSessionPolicy as c}from"./sessionPresets-C867Mlo4.mjs";const n=32,l=t=>{const e=typeof t=="string"?t.trim().length:0;return e>0&&e<n},s=t=>typeof t=="string"?t.toLowerCase().startsWith("http://"):t&&typeof t=="object"?t.protocol==="http"?!0:t.protocol==="https"?!1:typeof t.fallback=="string"&&t.fallback.toLowerCase().startsWith("http://"):!1,u=new Set(["127.0.0.1","localhost"]),d=t=>{if(typeof t!="string")return;let e;try{e=new URL(t)}catch{return}if(!(e.protocol!=="http:"||!u.has(e.hostname)))return{allowedHosts:[e.hostname,`${e.hostname}:*`],fallback:t,protocol:"http"}},h=t=>{const e=d(t.baseURL)??t.baseURL;if(l(t.secret)){const o=`@lunora/auth: AUTH_SECRET is only ${String(t.secret?.trim().length)} characters. Use at least ${String(n)} for a brute-force-resistant secret — generate one with \`openssl rand -hex 32\`.`;if(!s(e))throw new a("INTERNAL",o);console.warn(o)}const r=t.advanced??{};return{...t,advanced:{...r,defaultCookieAttributes:r.defaultCookieAttributes??{httpOnly:!0,path:"/",sameSite:"lax"},...r.useSecureCookies===void 0?{useSecureCookies:!s(e)}:{}},baseURL:e}},f=t=>{const e=h(t),r=e.rateLimit?.enabled===void 0,o=e.rateLimit?.storage===void 0&&e.rateLimit?.enabled!==!1;return{...e,...r||o?{rateLimit:{...e.rateLimit,...r?{enabled:!0}:{},...o?{storage:"database"}:{}}}:{},...e.session?.cookieCache===void 0?{session:{...e.session,cookieCache:{enabled:!0,maxAge:60}}}:{}}},g=t=>{if(!t.secret||t.secret.trim()==="")throw new a("INTERNAL",'@lunora/auth: `secret` is required. Set AUTH_SECRET locally in .dev.vars (`lunora env set AUTH_SECRET "$(openssl rand -hex 32)"`), and in production with `wrangler secret put AUTH_SECRET`.');return t.session&&c(t.session),i(f(t))};export{g as createAuth,f as resolveAuthOptions};
@@ -1 +0,0 @@
1
- import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as f,READ_AUDIT_PATH as p}from"./AUTH_DO_AUDIT_PATH-D-RB2XYs.mjs";import{DEFAULT_AUTH_BASE_PATH as A,isAuthRoutePath as R}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const E=c=>{const{basePath:d=A,internalSecret:o,namespace:s,objectName:l="auth"}=c,i=()=>{if(s)return s.get(s.idFromName(l))},h=async(e,t,n)=>{if(!o)return;const r=i();if(!r)return;const a=await r.fetch(new Request(new URL(e,t),{body:JSON.stringify(n),headers:{"content-type":"application/json",[u]:o},method:"POST"}));return a.ok?a:void 0};return{auditReader:{read:async e=>{const t=await h(p,"https://auth-do.invalid",e);return t?(await t.json())?.entries??[]:[]}},authHandler:async e=>{if(R(new URL(e.url).pathname,d))return i()?.fetch(e)},resolveIdentity:async e=>{if(!o)return null;const t=i();if(!t)return null;const n=new Headers(e.headers);n.set(u,o);const r=await t.fetch(new Request(new URL(f,e.url),{headers:n}));if(!r.ok)return null;const a=await r.json();return a?.userId?{userId:a.userId}:null}}};export{E as createDoAuthWiring};