@lunora/auth 1.0.0-alpha.25 → 1.0.0-alpha.250
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/LICENSE.md +32 -0
- package/README.md +102 -12
- package/dist/adapter.d.mts +4 -43
- package/dist/adapter.d.ts +4 -43
- package/dist/adapter.mjs +1 -48
- package/dist/audit.d.mts +142 -0
- package/dist/audit.d.ts +142 -0
- package/dist/audit.mjs +12 -0
- package/dist/cimd-node.d.mts +1 -0
- package/dist/cimd-node.d.ts +1 -0
- package/dist/cimd-node.mjs +1 -0
- package/dist/cimd-workers.d.mts +64 -0
- package/dist/cimd-workers.d.ts +64 -0
- package/dist/cimd-workers.mjs +1 -0
- package/dist/email-guard.d.mts +143 -0
- package/dist/email-guard.d.ts +143 -0
- package/dist/email-guard.mjs +1 -0
- package/dist/index.d.mts +415 -545
- package/dist/index.d.ts +415 -545
- package/dist/index.mjs +1 -12
- package/dist/middleware.d.mts +165 -155
- package/dist/middleware.d.ts +165 -155
- package/dist/middleware.mjs +1 -56
- package/dist/migrate.d.mts +47 -0
- package/dist/migrate.d.ts +47 -0
- package/dist/migrate.mjs +1 -0
- package/dist/packem_shared/AUTH_DO_AUDIT_PATH-CRtEqPlF.mjs +1 -0
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs +1 -0
- package/dist/packem_shared/LunoraAuthAdminError-uhKG1MgQ.mjs +1 -0
- package/dist/packem_shared/adapter-CBZ17jKi.mjs +2 -0
- package/dist/packem_shared/adapter.d-CElAPJ1U.d.ts +88 -0
- package/dist/packem_shared/adapter.d-CaPwF2v3.d.mts +88 -0
- package/dist/packem_shared/auth-do-Dkk1jOb6.mjs +1 -0
- package/dist/packem_shared/authAuditHook-DPTKdjBe.mjs +1 -0
- package/dist/packem_shared/authDiscoveryPaths-B14lM14u.mjs +1 -0
- package/dist/packem_shared/authDoColumnAdditions-UwF3Rl41.mjs +1 -0
- package/dist/packem_shared/create-auth.d-DNGo48je.d.mts +146 -0
- package/dist/packem_shared/create-auth.d-DNGo48je.d.ts +146 -0
- package/dist/packem_shared/createAuth-CLeBjzDR.mjs +1 -0
- package/dist/packem_shared/createDoAuthWiring-DIxLcjKQ.mjs +1 -0
- package/dist/packem_shared/createLunoraAuthClient-DRcFNwp9.mjs +1 -0
- package/dist/packem_shared/createSignUpInvitation-DHZL9fNl.mjs +1 -0
- package/dist/packem_shared/emailGateDatabaseHooks-BYLUpiN_.mjs +1 -0
- package/dist/packem_shared/invite-only.d-BAKOiLTW.d.ts +126 -0
- package/dist/packem_shared/invite-only.d-BDErbVxw.d.mts +126 -0
- package/dist/packem_shared/isDevEnvironment-yLbPngwn.mjs +1 -0
- package/dist/packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs +1 -0
- package/dist/packem_shared/mcp-CgX1hTOv.mjs +1 -0
- package/dist/packem_shared/on-cloudflare-edge-Dx6gFjBO.mjs +1 -0
- package/dist/packem_shared/quote-identifier-CGiYFBvY.mjs +1 -0
- package/dist/packem_shared/resolveSessionIdentity-ChaUVZSy.mjs +1 -0
- package/dist/packem_shared/sessionPresets-C867Mlo4.mjs +1 -0
- package/dist/packem_shared/sql-store-uMubPYI9.mjs +1 -0
- package/dist/packem_shared/uiConfig-B8pN6pks.mjs +1 -0
- package/dist/packem_shared/worker.d-BfQTtOho.d.ts +762 -0
- package/dist/packem_shared/worker.d-Dnw0s0_j.d.mts +762 -0
- package/dist/plugins-client.d.mts +250 -1
- package/dist/plugins-client.d.ts +250 -1
- package/dist/plugins-client.mjs +1 -2
- package/dist/plugins-enterprise-client.d.mts +13 -0
- package/dist/plugins-enterprise-client.d.ts +13 -0
- package/dist/plugins-enterprise-client.mjs +1 -0
- package/dist/plugins-enterprise.d.mts +1668 -0
- package/dist/plugins-enterprise.d.ts +1668 -0
- package/dist/plugins-enterprise.mjs +1 -0
- package/dist/plugins.d.mts +167 -2
- package/dist/plugins.d.ts +167 -2
- package/dist/plugins.mjs +1 -22
- package/dist/schema.d.mts +39 -39
- package/dist/schema.d.ts +39 -39
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.d.mts +43 -28
- package/dist/sql-store.d.ts +43 -28
- package/dist/sql-store.mjs +1 -184
- package/dist/store.d.mts +65 -47
- package/dist/store.d.ts +65 -47
- package/dist/store.mjs +1 -183
- package/dist/turnstile-middleware.d.mts +73 -55
- package/dist/turnstile-middleware.d.ts +73 -55
- package/dist/turnstile-middleware.mjs +1 -45
- package/dist/turnstile.d.mts +42 -59
- package/dist/turnstile.d.ts +42 -59
- package/dist/turnstile.mjs +1 -61
- package/dist/worker.d.mts +9 -0
- package/dist/worker.d.ts +9 -0
- package/dist/worker.mjs +1 -0
- package/package.json +57 -6
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-D4L7n6gN.mjs +0 -510
- package/dist/packem_shared/compileMigrationsSql-B9bj-mJv.mjs +0 -29
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.mts +0 -128
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.ts +0 -128
- package/dist/packem_shared/createAuth-BVMMllTm.mjs +0 -87
- package/dist/packem_shared/sessionPresets-Dwwd74_J.mjs +0 -38
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createAdapterFactory } from 'better-auth/adapters';
|
|
2
|
+
import { d1Executor } from "../sql-store.js";
|
|
3
|
+
import { AuthStore } from "../store.js";
|
|
4
|
+
/**
|
|
5
|
+
* The slice of `DurableObjectStorage` this module uses.
|
|
6
|
+
*
|
|
7
|
+
* Structural on purpose: it keeps `@lunora/auth` free of a `@lunora/do` dependency
|
|
8
|
+
* and lets tests supply a double whose `transaction` has the same semantics.
|
|
9
|
+
*/
|
|
10
|
+
interface DoStorageLike {
|
|
11
|
+
/** Synchronous SQL over the object's SQLite. */
|
|
12
|
+
sql: {
|
|
13
|
+
exec: (query: string, ...bindings: unknown[]) => Iterable<Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The platform's async transaction primitive. Everything the closure executes
|
|
17
|
+
* against `sql` joins the transaction — it is connection-scoped, so no handle is
|
|
18
|
+
* threaded through — and a throw rolls the whole thing back.
|
|
19
|
+
*/
|
|
20
|
+
transaction: <R>(closure: () => Promise<R>) => Promise<R>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `closure` atomically. Supplied by stores whose backend has real
|
|
24
|
+
* transactions — a Durable Object's storage does; D1 does not.
|
|
25
|
+
*/
|
|
26
|
+
type TransactionRunner = <R>(closure: () => Promise<R>) => Promise<R>;
|
|
27
|
+
/**
|
|
28
|
+
* A better-auth database adapter backed by an {@link AuthStore} — the bridge
|
|
29
|
+
* that routes better-auth's reads and writes through Lunora's data layer
|
|
30
|
+
* instead of better-auth's built-in D1/Kysely adapter. Pass the result as
|
|
31
|
+
* `createAuth({ database: lunoraAuthAdapter(store) })`; better-auth's
|
|
32
|
+
* `createAdapterFactory` handles id generation, default values, field-name
|
|
33
|
+
* mapping and output shaping, so this only translates the cleaned CRUD calls
|
|
34
|
+
* onto the store.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const auth = createAuth({
|
|
38
|
+
* secret: env.AUTH_SECRET,
|
|
39
|
+
* emailAndPassword: { enabled: true },
|
|
40
|
+
* database: lunoraAuthAdapter(lunoraStore), // lunoraStore writes via ctx.db
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Scope: the {@link AuthStore} interface is single-table CRUD. better-auth's
|
|
45
|
+
* relational `join` reads (an advanced opt-in) are not handled — pair the
|
|
46
|
+
* adapter with `disableJoins` or let better-auth fall back to per-table reads.
|
|
47
|
+
*
|
|
48
|
+
* No schema check either: an {@link AuthStore} is opaque CRUD with nothing to
|
|
49
|
+
* introspect, so better-auth logs that validation is unavailable for this adapter
|
|
50
|
+
* and proceeds. {@link lunoraD1Adapter} and {@link lunoraDoAdapter} know which
|
|
51
|
+
* SQLite backend they sit on and register one — see `schema-check.ts`.
|
|
52
|
+
*/
|
|
53
|
+
declare const lunoraAuthAdapter: (store: AuthStore, runInTransaction?: TransactionRunner) => ReturnType<typeof createAdapterFactory>;
|
|
54
|
+
/**
|
|
55
|
+
* One-liner for the common case: a better-auth `database` backed by a Cloudflare
|
|
56
|
+
* D1 binding, via Lunora's SQL store — equivalent to
|
|
57
|
+
* `lunoraAuthAdapter(createSqlAuthStore(d1Executor(d1)))`.
|
|
58
|
+
*
|
|
59
|
+
* Prefer this over passing the raw `env.DB` as `database`. With raw D1,
|
|
60
|
+
* better-auth resolves its Kysely adapter through a runtime `await import(...)`
|
|
61
|
+
* inside `auth.$context`, and that dynamic import never settles under
|
|
62
|
+
* `@cloudflare/vite-plugin`'s worker runner — so it hangs *every* auth request
|
|
63
|
+
* in `pnpm dev` (a standalone `wrangler dev` or a deployed worker bundle it
|
|
64
|
+
* up-front, so they're unaffected — which makes the hang baffling to debug).
|
|
65
|
+
* This explicit adapter skips that import entirely, so dev and prod behave the
|
|
66
|
+
* same. The migration instance is the one exception — it wants raw `env.DB` so
|
|
67
|
+
* `ensureMigrated`'s Kysely migrator can create the tables (its `$context` is
|
|
68
|
+
* never resolved, so the hang doesn't apply there).
|
|
69
|
+
*
|
|
70
|
+
* Carries a schema check: before the first write — better-auth awaits it in the
|
|
71
|
+
* router ahead of the rate limiter, which is the first thing that writes — the
|
|
72
|
+
* tables this configuration needs are compared against the ones D1 holds, and a
|
|
73
|
+
* mismatch fails with the table and column named instead of `no such table: …`
|
|
74
|
+
* inside an empty 500. See `schema-check.ts`.
|
|
75
|
+
*/
|
|
76
|
+
declare const lunoraD1Adapter: (d1: Parameters<typeof d1Executor>[0]) => ReturnType<typeof lunoraAuthAdapter>;
|
|
77
|
+
/**
|
|
78
|
+
* A better-auth `database` backed by a Durable Object's own SQLite —
|
|
79
|
+
* `lunoraAuthAdapter(createSqlAuthStore(doExecutor(storage)), doTransactionRunner(storage))`.
|
|
80
|
+
*
|
|
81
|
+
* Unlike `lunoraD1Adapter` this one exposes real transactions, so plugins that
|
|
82
|
+
* demand them (`@better-auth/scim`) accept it. Read the trade-offs in `do-store.ts`
|
|
83
|
+
* before reaching for it: the auth tables then live inside a single Durable Object.
|
|
84
|
+
*
|
|
85
|
+
* Schema-checked like {@link lunoraD1Adapter}, against the object's own SQLite.
|
|
86
|
+
*/
|
|
87
|
+
declare const lunoraDoAdapter: (storage: DoStorageLike) => ReturnType<typeof lunoraAuthAdapter>;
|
|
88
|
+
export { DoStorageLike as D, lunoraD1Adapter as a, lunoraDoAdapter as b, lunoraAuthAdapter as l };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createAdapterFactory } from 'better-auth/adapters';
|
|
2
|
+
import { d1Executor } from "../sql-store.mjs";
|
|
3
|
+
import { AuthStore } from "../store.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* The slice of `DurableObjectStorage` this module uses.
|
|
6
|
+
*
|
|
7
|
+
* Structural on purpose: it keeps `@lunora/auth` free of a `@lunora/do` dependency
|
|
8
|
+
* and lets tests supply a double whose `transaction` has the same semantics.
|
|
9
|
+
*/
|
|
10
|
+
interface DoStorageLike {
|
|
11
|
+
/** Synchronous SQL over the object's SQLite. */
|
|
12
|
+
sql: {
|
|
13
|
+
exec: (query: string, ...bindings: unknown[]) => Iterable<Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The platform's async transaction primitive. Everything the closure executes
|
|
17
|
+
* against `sql` joins the transaction — it is connection-scoped, so no handle is
|
|
18
|
+
* threaded through — and a throw rolls the whole thing back.
|
|
19
|
+
*/
|
|
20
|
+
transaction: <R>(closure: () => Promise<R>) => Promise<R>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `closure` atomically. Supplied by stores whose backend has real
|
|
24
|
+
* transactions — a Durable Object's storage does; D1 does not.
|
|
25
|
+
*/
|
|
26
|
+
type TransactionRunner = <R>(closure: () => Promise<R>) => Promise<R>;
|
|
27
|
+
/**
|
|
28
|
+
* A better-auth database adapter backed by an {@link AuthStore} — the bridge
|
|
29
|
+
* that routes better-auth's reads and writes through Lunora's data layer
|
|
30
|
+
* instead of better-auth's built-in D1/Kysely adapter. Pass the result as
|
|
31
|
+
* `createAuth({ database: lunoraAuthAdapter(store) })`; better-auth's
|
|
32
|
+
* `createAdapterFactory` handles id generation, default values, field-name
|
|
33
|
+
* mapping and output shaping, so this only translates the cleaned CRUD calls
|
|
34
|
+
* onto the store.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const auth = createAuth({
|
|
38
|
+
* secret: env.AUTH_SECRET,
|
|
39
|
+
* emailAndPassword: { enabled: true },
|
|
40
|
+
* database: lunoraAuthAdapter(lunoraStore), // lunoraStore writes via ctx.db
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Scope: the {@link AuthStore} interface is single-table CRUD. better-auth's
|
|
45
|
+
* relational `join` reads (an advanced opt-in) are not handled — pair the
|
|
46
|
+
* adapter with `disableJoins` or let better-auth fall back to per-table reads.
|
|
47
|
+
*
|
|
48
|
+
* No schema check either: an {@link AuthStore} is opaque CRUD with nothing to
|
|
49
|
+
* introspect, so better-auth logs that validation is unavailable for this adapter
|
|
50
|
+
* and proceeds. {@link lunoraD1Adapter} and {@link lunoraDoAdapter} know which
|
|
51
|
+
* SQLite backend they sit on and register one — see `schema-check.ts`.
|
|
52
|
+
*/
|
|
53
|
+
declare const lunoraAuthAdapter: (store: AuthStore, runInTransaction?: TransactionRunner) => ReturnType<typeof createAdapterFactory>;
|
|
54
|
+
/**
|
|
55
|
+
* One-liner for the common case: a better-auth `database` backed by a Cloudflare
|
|
56
|
+
* D1 binding, via Lunora's SQL store — equivalent to
|
|
57
|
+
* `lunoraAuthAdapter(createSqlAuthStore(d1Executor(d1)))`.
|
|
58
|
+
*
|
|
59
|
+
* Prefer this over passing the raw `env.DB` as `database`. With raw D1,
|
|
60
|
+
* better-auth resolves its Kysely adapter through a runtime `await import(...)`
|
|
61
|
+
* inside `auth.$context`, and that dynamic import never settles under
|
|
62
|
+
* `@cloudflare/vite-plugin`'s worker runner — so it hangs *every* auth request
|
|
63
|
+
* in `pnpm dev` (a standalone `wrangler dev` or a deployed worker bundle it
|
|
64
|
+
* up-front, so they're unaffected — which makes the hang baffling to debug).
|
|
65
|
+
* This explicit adapter skips that import entirely, so dev and prod behave the
|
|
66
|
+
* same. The migration instance is the one exception — it wants raw `env.DB` so
|
|
67
|
+
* `ensureMigrated`'s Kysely migrator can create the tables (its `$context` is
|
|
68
|
+
* never resolved, so the hang doesn't apply there).
|
|
69
|
+
*
|
|
70
|
+
* Carries a schema check: before the first write — better-auth awaits it in the
|
|
71
|
+
* router ahead of the rate limiter, which is the first thing that writes — the
|
|
72
|
+
* tables this configuration needs are compared against the ones D1 holds, and a
|
|
73
|
+
* mismatch fails with the table and column named instead of `no such table: …`
|
|
74
|
+
* inside an empty 500. See `schema-check.ts`.
|
|
75
|
+
*/
|
|
76
|
+
declare const lunoraD1Adapter: (d1: Parameters<typeof d1Executor>[0]) => ReturnType<typeof lunoraAuthAdapter>;
|
|
77
|
+
/**
|
|
78
|
+
* A better-auth `database` backed by a Durable Object's own SQLite —
|
|
79
|
+
* `lunoraAuthAdapter(createSqlAuthStore(doExecutor(storage)), doTransactionRunner(storage))`.
|
|
80
|
+
*
|
|
81
|
+
* Unlike `lunoraD1Adapter` this one exposes real transactions, so plugins that
|
|
82
|
+
* demand them (`@better-auth/scim`) accept it. Read the trade-offs in `do-store.ts`
|
|
83
|
+
* before reaching for it: the auth tables then live inside a single Durable Object.
|
|
84
|
+
*
|
|
85
|
+
* Schema-checked like {@link lunoraD1Adapter}, against the object's own SQLite.
|
|
86
|
+
*/
|
|
87
|
+
declare const lunoraDoAdapter: (storage: DoStorageLike) => ReturnType<typeof lunoraAuthAdapter>;
|
|
88
|
+
export { DoStorageLike as D, lunoraD1Adapter as a, lunoraDoAdapter as b, lunoraAuthAdapter as l };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getAuthTablesWithResolvedIndexes as U}from"@better-auth/core/db/internal";import{LunoraError as w,toErrorBody as Y}from"@lunora/errors";import{d as k,b as K}from"./adapter-CBZ17jKi.mjs";import{AUTH_AUDIT_TABLE as Q,ensureAuthAuditTable as z,createAuthAuditReader as Z}from"../audit.mjs";import{resolveAuthOptions as ee,createAuth as te}from"./createAuth-CLeBjzDR.mjs";import{handleAuthDiscoveryRequest as ne}from"./authDiscoveryPaths-B14lM14u.mjs";import{authDoSchemaStatements as re,authDoColumnAdditions as se}from"./authDoColumnAdditions-UwF3Rl41.mjs";import{handleAuthRequest as oe}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";import{decodeWire as I,encodeWire as v}from"@lunora/wire";import{q as a}from"./quote-identifier-CGiYFBvY.mjs";import{schemaDeclaresIssuer as ie,legacyIssuerCleanupStatements as ae,indexesReferencingIssuer as ce}from"./legacyIssuerCleanupStatements-D9Kf8OD1.mjs";import{resolveSessionIdentity as ue}from"./resolveSessionIdentity-ChaUVZSy.mjs";const le=(t,e)=>{const n=Math.max(t.length,e.length);let r=t.length^e.length;for(let s=0;s<n;s+=1){const o=s<t.length?t.charCodeAt(s):0,i=s<e.length?e.charCodeAt(s):0;r|=o^i}return r===0},j=2166136261,F=16777619,C=(t,e=j)=>{let n=e;for(let r=0;r<t.length;r+=1)n^=t.charCodeAt(r),n=Math.imul(n,F);return(n>>>0).toString(16).padStart(8,"0")},fe=t=>`${C(t,j)}${C(t,F)}`,Ee="/__lunora/auth/move",T="__lunora_auth_move__",R="__lunora_auth_move_rows__",y="__lunora_auth_purged__",x=100,de=16,me=100,pe=2n**64n,H=t=>t.startsWith("sqlite_")||t.startsWith("_cf_")||t===T||t===R||t===y,P=async(t,e)=>t.transaction(async()=>Promise.resolve().then(e)),E=(t,e,...n)=>[...t.sql.exec(e,...n)],u=(t,e,...n)=>{[...t.sql.exec(e,...n)]},_=t=>E(t,"SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").map(e=>String(e.name)).filter(e=>!H(e)),q=(t,e)=>E(t,"SELECT name, pk FROM pragma_table_info(?)",e).map(n=>({name:String(n.name),pk:Number(n.pk)})),g=(t,e)=>q(t,e).map(({name:n})=>n),W=(t,e)=>{const n=q(t,e),r=n.filter(({pk:s})=>s>0).toSorted((s,o)=>s.pk-o.pk).map(({name:s})=>s);return r.length>0?r:n.map(({name:s})=>s)},B=(t,e)=>Number(E(t,`SELECT count(*) AS n FROM ${a(e)}`)[0]?.n??0),V=t=>JSON.stringify(v(t)),b=(t,e)=>BigInt(`0x${fe(V(e.map(n=>t[n]??null)))}`),G=(t,e)=>{let n=0,r=0n;for(const s of t)n+=1,r=(r+e(s))%pe;return`${String(n)}:${r.toString(16)}`},X=(t,e)=>{const n=g(t,e).toSorted((r,s)=>r.localeCompare(s));return G(t.sql.exec(`SELECT * FROM ${a(e)}`),r=>b(r,n))},he=t=>_(t).map(e=>{const n=E(t,"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",e)[0]?.sql,r=E(t,"SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL",e).map(s=>({name:String(s.name),sql:String(s.sql)}));return{columns:g(t,e),indexes:r,name:e,rows:B(t,e),sql:typeof n=="string"?n:""}}),M=(t,e)=>{if(typeof e!="string"||!_(t).includes(e))throw new TypeError(`unknown table ${JSON.stringify(e)}`);return e},Te=(t,e,n)=>{const r=E(t,`SELECT rowid AS "__lunora_rowid__", * FROM ${a(e)} WHERE rowid > ? ORDER BY rowid LIMIT ?`,n,x),s=r.at(-1)?.__lunora_rowid__;return{last:typeof s=="number"?s:void 0,rows:r.map(({__lunora_rowid__:o,...i})=>i)}},Se=t=>g(t,T).length>0,O=t=>Se(t)?Object.fromEntries(E(t,`SELECT tbl, after, done, pass, cols, watermark FROM ${a(T)}`).map(e=>[String(e.tbl),{after:Number(e.after),columns:JSON.parse(String(e.cols)),done:e.done===1,pass:Number(e.pass),watermark:Number(e.watermark)}])):{},we=(t,e,n)=>G(t.sql.exec(`SELECT h FROM ${a(R)} WHERE tbl = ? AND pass = ?`,e,n),r=>BigInt(`0x${String(r.h)}`)),Re=t=>{u(t,`CREATE TABLE IF NOT EXISTS ${a(T)} (tbl TEXT NOT NULL PRIMARY KEY, after INTEGER NOT NULL, done INTEGER NOT NULL, pass INTEGER NOT NULL, cols TEXT NOT NULL, watermark INTEGER NOT NULL DEFAULT 0)`),u(t,`CREATE TABLE IF NOT EXISTS ${a(R)} (tbl TEXT NOT NULL, k TEXT NOT NULL, h TEXT NOT NULL, pass INTEGER NOT NULL, PRIMARY KEY (tbl, k))`)},Ae=(t,e,n)=>{if(!_(t).includes(e))return 0;const r=W(t,e).map(s=>`u.${a(s)}`).join(", ");return Number(E(t,`SELECT count(*) AS n FROM ${a(e)} u WHERE u.rowid > ? AND NOT EXISTS (SELECT 1 FROM ${a(R)} c WHERE c.tbl = ? AND c.k = json_array(${r}))`,n,e)[0]?.n??0)},Ne=/^CREATE TABLE /iu,_e=/^CREATE (?:UNIQUE )?INDEX /iu,Oe=(t,e,n)=>{if(H(e.name))throw new TypeError(`refusing reserved table ${JSON.stringify(e.name)}`);if(!Ne.test(e.sql)||e.indexes.some(({sql:s})=>!_e.test(s)))throw new TypeError(`refusing unexpected DDL for ${JSON.stringify(e.name)}`);if(n.has(e.name)){const s=new Set(g(t,e.name));for(const o of e.columns.filter(i=>!s.has(i)))u(t,`ALTER TABLE ${a(e.name)} ADD COLUMN ${a(o)}`)}else u(t,e.sql);const r=new Set(E(t,"SELECT name FROM sqlite_master WHERE type = 'index'").map(s=>String(s.name)));for(const s of e.indexes.filter(({name:o})=>!r.has(o)))u(t,s.sql)},ge=(t,e,n)=>{Re(t);const r=Ae(t,n.userTable,O(t)[n.userTable]?.watermark??0);if(r>0&&!n.force)throw new w("AUTH_MOVE_TARGET_NOT_EMPTY",`the pinned auth object has ${String(r)} user(s) the copy did not write; copying into it would merge two user bases — pass force to copy anyway`,{data:{users:r}});const s=new Set(_(t)),o=O(t);for(const c of e){Oe(t,c,s);const f=o[c.name],d=JSON.stringify(c.columns.toSorted((m,p)=>m.localeCompare(p)));f===void 0?u(t,`INSERT INTO ${a(T)} (tbl, after, done, pass, cols) VALUES (?, 0, 0, 1, ?)`,c.name,d):u(t,`UPDATE ${a(T)} SET cols = ? WHERE tbl = ?`,d,c.name)}u(t,`UPDATE ${a(T)} SET watermark = (SELECT coalesce(max(rowid), 0) FROM ${a(n.userTable)}) WHERE tbl = ?`,n.userTable);const i=O(t);return{cursors:Object.fromEntries(e.map(({name:c})=>[c,i[c]?.after??0])),done:Object.fromEntries(e.map(({name:c})=>[c,i[c]?.done??!1])),targetRows:Object.fromEntries(e.map(({name:c})=>[c,B(t,c)]))}},J=(t,e,n,r,s)=>E(t,`SELECT ${s.map(o=>a(o)).join(", ")} FROM ${a(e)} WHERE ${n.map(o=>`${a(o)} IS ?`).join(" AND ")} LIMIT 1`,...r)[0],$=(t,e)=>{if(!e)throw new w("AUTH_MOVE_CONFLICT",`a row copied into ${t} collides with a row the pinned auth object holds on its own — nothing from this page was written; pass force to keep the pinned object's rows`,{data:{table:t}});return"conflicts"},Le=t=>t instanceof Error&&t.message.includes("UNIQUE constraint failed"),ye=(t,e)=>{if(e.prior!==void 0)return $(e.table,e.force);try{u(t,`INSERT INTO ${a(e.table)} (${e.columns.map(n=>a(n)).join(", ")}) VALUES (${e.columns.map(()=>"?").join(", ")})`,...e.columns.map(n=>e.row[n]))}catch(n){if(!Le(n))throw n;return $(e.table,e.force)}return"copied"},$e=(t,e,n)=>{const r=b(n,e.columns).toString(16);return r===e.h?"unchanged":e.prior===void 0||r!==e.prior?$(e.table,e.force):(u(t,`UPDATE ${a(e.table)} SET ${e.columns.map(s=>`${a(s)} = ?`).join(", ")} WHERE ${e.keys.map(s=>`${a(s)} IS ?`).join(" AND ")}`,...e.columns.map(s=>e.row[s]),...e.key),"updated")},be=(t,e,n,r)=>{const{force:s,keys:o,pass:i}=r,c=Object.keys(n).toSorted((S,L)=>S.localeCompare(L));if(c.length+o.length>me)throw new TypeError(`${e} has too many columns to copy`);const f=o.map(S=>n[S]??null),d=V(f),m=b(n,c).toString(16),p=E(t,`SELECT h FROM ${a(R)} WHERE tbl = ? AND k = ?`,e,d)[0]?.h,l={columns:c,force:s,h:m,key:f,keys:o,prior:typeof p=="string"?p:void 0,row:n,table:e},h=l.prior===m?void 0:J(t,e,o,f,c);let A="unchanged";return l.prior!==m&&(A=h===void 0?ye(t,l):$e(t,l,h)),u(t,`INSERT INTO ${a(R)} (tbl, k, h, pass) VALUES (?, ?, ?, ?) ON CONFLICT (tbl, k) DO UPDATE SET h = excluded.h, pass = excluded.pass`,e,d,m,i),A},Ie=(t,e,n)=>{const{counts:r,force:s,keys:o,marker:i}=n,c=E(t,`SELECT k, h FROM ${a(R)} WHERE tbl = ? AND pass < ?`,e,i.pass);for(const f of c){const d=I(JSON.parse(String(f.k))),m=J(t,e,o,d,i.columns);m!==void 0&&(b(m,i.columns).toString(16)===String(f.h)?(u(t,`DELETE FROM ${a(e)} WHERE ${o.map(p=>`${a(p)} IS ?`).join(" AND ")}`,...d),r.deleted+=1):r[$(e,s)]+=1),u(t,`DELETE FROM ${a(R)} WHERE tbl = ? AND k = ?`,e,String(f.k))}},De=async(t,e,n)=>{const r=O(t)[e];if(r===void 0)throw new TypeError(`table ${JSON.stringify(e)} was not part of this move`);return P(t,()=>{const s={conflicts:0,copied:0,deleted:0,unchanged:0,updated:0},o=W(t,e);for(const i of n.rows)s[be(t,e,i,{force:n.force,keys:o,pass:r.pass})]+=1;return n.final&&Ie(t,e,{counts:s,force:n.force,keys:o,marker:r}),u(t,`UPDATE ${a(T)} SET after = ?, done = ? WHERE tbl = ?`,n.last,n.final?1:0,e),s})},Ue=async(t,e)=>{const n=_(t);return await P(t,()=>{const r=n.filter(o=>e[o]===void 0);if(r.length>0)throw new w("AUTH_MOVE_INCOMPLETE",`refusing to purge the un-pinned auth object: ${r.join(", ")} not fully copied — run the copy to done: true first`,{data:{unfinished:r}});const s=n.filter(o=>X(t,o)!==e[o]);if(s.length>0)throw new w("AUTH_MOVE_SOURCE_CHANGED",`refusing to purge the un-pinned auth object: ${s.join(", ")} changed since it was copied — run the copy again to carry the changes, then purge`,{data:{changed:s}});for(const o of n)u(t,`DROP TABLE ${a(o)}`);u(t,`CREATE TABLE IF NOT EXISTS ${a(y)} (at INTEGER NOT NULL)`),u(t,`INSERT INTO ${a(y)} (at) VALUES (?)`,Date.now())}),{dropped:n}},ke=t=>{if(g(t,y).length>0)throw new w("AUTH_MOVE_SOURCE_PURGED","the un-pinned auth object was purged after its tables were copied; anything in it now is new, and copying it would delete the pinned object's users")},Ce=new Set(["fingerprints","manifest","page","purge"]),Me=(t,e,n)=>{e.prepare();const r=O(t);return{copied:n?Object.fromEntries(Object.entries(r).map(([s,o])=>[s,we(t,s,o.pass)])):{},markers:r,order:e.order()}},ve=(t,e)=>{for(const n of e)u(t,`UPDATE ${a(T)} SET after = 0, done = 0, pass = pass + 1 WHERE tbl = ?`,n);return{}},je=[/(?:UNIQUE|NOT NULL|CHECK|FOREIGN KEY|PRIMARY KEY) constraint failed/u,/no such (?:table|column)/u,/datatype mismatch/u,/SQLITE_[A-Z_]+/u],Fe=t=>{const e=t instanceof Error?t.message:String(t);for(const n of je){const r=n.exec(e);if(r)return r[0]}return t instanceof Error?t.name:"unknown"},xe=async(t,e,n)=>{switch(Ce.has(String(e.op))&&ke(t),e.op){case"begin":return n.prepare(),ge(t,e.tables,{force:e.force===!0,userTable:n.userTable()});case"fingerprints":return{fingerprints:Object.fromEntries(_(t).map(r=>[r,X(t,r)]))};case"manifest":return{tables:he(t)};case"page":{const r=Te(t,M(t,e.table),Number(e.after??0));return{last:r.last,rows:v(r.rows)}}case"purge":{const r=await Ue(t,e.expected??{});return n.onPurge(),r}case"rescan":return ve(t,e.tables??[]);case"status":return Me(t,n,e.fingerprints===!0);case"write":return De(t,M(t,e.table),{final:e.final===!0,force:e.force===!0,last:Number(e.last),rows:I(e.rows)});default:throw new TypeError(`unknown move op ${JSON.stringify(e.op)}`)}},He=async(t,e,n)=>{try{return await xe(t,e,n)}catch(r){if(r instanceof w)throw r;console.error("@lunora/auth: auth move step failed",r);const s=String(e.op),o=typeof e.table=="string"?e.table:void 0,i=Fe(r);throw new w("AUTH_MOVE_FAILED",`auth move ${s}${o===void 0?"":` on ${o}`} failed: ${i}`,{data:{cause:i,op:s,...o===void 0?{}:{table:o}}})}},Pe=(t,e)=>{const n=r=>{const s=e.first.indexOf(r);if(s!==-1)return s;const o=e.last.indexOf(r);return o===-1?e.first.length:e.first.length+1+o};return t.toSorted((r,s)=>n(r.name)-n(s.name)||r.name.localeCompare(s.name))},qe=(t,e)=>({conflicts:0,copied:0,deleted:0,sourceRows:t.rows,table:t.name,targetRows:e,unchanged:0,updated:0}),ot=t=>{const e=async(n,r)=>{const s=await t(n,r);if(!s.ok){let o={};try{const i=await s.json();typeof i.error=="object"&&i.error!==null&&(o=i.error)}catch{}throw new w(o.code??"AUTH_MOVE_FAILED",o.message??`auth move ${String(r.op)} on the ${n} object failed (${String(s.status)})`,{data:o.data,status:s.status})}return s.json()};return{copy:async(n={})=>{const r=n.force===!0,s=await e("target",{op:"status"}),{tables:o}=await e("source",{op:"manifest"}),i=await e("target",{force:r,op:"begin",tables:o}),c=[];let f=0;for(const l of Pe(o,s.order)){const h=qe(l,i.targetRows[l.name]??0);let A=i.cursors[l.name]??0;for(c.push(h);;){if(f>=de)return{done:!1,tables:c};f+=1;const S=await e("source",{after:A,op:"page",table:l.name}),L=I(S.rows),D=L.length<x;if(L.length===0&&i.done[l.name]===!0)break;const N=await e("target",{final:D,force:r,last:S.last??A,op:"write",rows:S.rows,table:l.name});if(h.conflicts+=N.conflicts,h.copied+=N.copied,h.deleted+=N.deleted,h.unchanged+=N.unchanged,h.updated+=N.updated,h.targetRows+=N.copied-N.deleted,A=S.last??A,D)break}}const{fingerprints:d}=await e("source",{op:"fingerprints"}),m=await e("target",{fingerprints:!0,op:"status"}),p=Object.keys(d).filter(l=>m.copied[l]!==d[l]);return p.length>0&&await e("target",{op:"rescan",tables:p}),{done:p.length===0,tables:c}},purge:async()=>{const{copied:n,markers:r}=await e("target",{fingerprints:!0,op:"status"}),s=Object.fromEntries(Object.entries(r).filter(([,o])=>o.done).map(([o])=>[o,n[o]]));return e("source",{expected:s,op:"purge"})}}},We="/__lunora/auth/session",Be="/__lunora/auth/audit",Ve="x-lunora-auth-do-secret",Ge=t=>{if(t===null||typeof t!="object"||Array.isArray(t))return{error:"body must be a JSON object"};const e={};for(const[n,r]of Object.entries(t))if(n==="limit"||n==="sinceSeq"){if(typeof r!="number"||!Number.isFinite(r))return{error:`"${n}" must be a finite number`};e[n]=r}else if(n==="actorId"||n==="event"){if(typeof r!="string")return{error:`"${n}" must be a string`};e[n]=r}else return{error:`unknown audit read option "${n}"`};return{options:e}};class it{#n;#a;#c;#e;#t;#r=!1;#s={first:["user"],last:[]};constructor(e,n,r={}){this.#e=e.storage,this.#n=k(e.storage),this.#c=n,this.#a=r}#o(){if(this.#t!==void 0)return this.#t;const e=this.#c(),n=ee(e),{tables:r}=U(n);if(this.#s={first:[r.user?.modelName??"user",r.account?.modelName??"account",r.session?.modelName??"session"],last:[Q,r.rateLimit?.modelName??"rateLimit"]},!this.#r){for(const s of re(n))[...this.#e.sql.exec(s)];for(const s of se(n,o=>this.#u(o)))[...this.#e.sql.exec(s)];this.#l(n),this.#r=!0}return this.#t=te({...e,database:K(this.#e)}),this.#t}#u(e){return[...this.#e.sql.exec("SELECT name FROM pragma_table_info(?)",e)].map(r=>String(r.name))}#l(e){const{tables:n}=U(e),{account:r}=n;if(r===void 0)return;const s=Object.entries(r.fields).map(([o,i])=>i.fieldName??o);if(!(ie(s)||!this.#u(r.modelName).includes("issuer")))try{const o=[...this.#e.sql.exec("SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ?",r.modelName)].map(i=>({name:String(i.name),sql:typeof i.sql=="string"?i.sql:void 0}));for(const i of ae(r.modelName,ce(o)))[...this.#e.sql.exec(i)]}catch(o){console.error("@lunora/auth: could not drop the reverted `account.issuer` column; if it is still present, sign-ups will fail until it is removed.",o)}}async#f(e){if(!this.#i(e))return Response.json({error:"unauthorized"},{status:401});let n;try{n=await e.json()}catch{return Response.json({error:"invalid body"},{status:400})}const r=Ge(n??{});if("error"in r)return Response.json({error:r.error},{status:400});await z(this.#n);const s=await Z(this.#n).read(r.options);return Response.json({entries:s})}async#E(e){if(e.method!=="POST"||!this.#i(e))return Response.json({error:"unauthorized"},{status:401});try{const n=await e.json(),r=await He(this.#e,n,{onPurge:()=>{this.#r=!1,this.#t=void 0,this.#n=k(this.#e)},order:()=>this.#s,prepare:()=>{this.#o()},userTable:()=>this.#s.first[0]??"user"});return Response.json(r)}catch(n){const{body:r,redacted:s,status:o}=Y(n,{encodeData:i=>i,fallbackCode:"AUTH_MOVE_FAILED",redactedMessage:"auth move failed"});return s&&console.error("@lunora/auth: auth move failed",n),Response.json({error:r},{status:o})}}#i(e){const{internalSecret:n}=this.#a;if(n===void 0||n==="")return!1;const r=e.headers.get(Ve);return r!==null&&le(r,n)}async#d(e){return this.#i(e)?Response.json(await ue(this.#o(),e.headers)??{}):Response.json({error:"unauthorized"},{status:401})}async fetch(e){const n=new URL(e.url);if(n.pathname===We)return this.#d(e);if(n.pathname===Be)return this.#f(e);if(n.pathname===Ee)return this.#E(e);const r=this.#o();return await oe(r,e)??await ne(r,e)??Response.json({error:"not an auth route"},{status:404})}}export{Ve as I,it as L,Ee as M,Be as R,We as a,ot as c};
|
|
@@ -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{LunoraError as h}from"@lunora/errors";import{DEFAULT_AUTH_BASE_PATH as p}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const l=Symbol.for("lunora.auth.mcpResource"),i=/\/$/u,d="/.well-known/oauth-protected-resource",f="/.well-known/oauth-authorization-server",R=t=>`${d}${new URL(t).pathname.replace(i,"")}`,w=t=>`${f}${t.replace(i,"")}`,v=t=>{if(typeof t=="string")try{const e=new URL(t).pathname.replace(i,"");return e===""?void 0:e}catch{return}},m=(t,e,r)=>{const n=e.find(o=>o.id==="jwt")?.options,s=r?.disableJwtPlugin===!0?void 0:n?.jwt?.issuer;if(typeof s=="string")try{return new URL(s).pathname}catch{}return v(t.baseURL)??t.basePath??p},y=t=>(Array.isArray(t)?t:[]).map(e=>typeof e=="object"&&e!==null?e.identifier:e).filter(e=>typeof e=="string"),E=t=>{const e=t[l];if(typeof e=="string")return e;const r=t.options,n=[...new Set(y(r?.resources))];if(n.length===1)return n[0];if(n.length>1&&Array.isArray(r?.clientRegistrationDefaultResources))throw new h("AUTH_MCP_RESOURCE_AMBIGUOUS",`@lunora/auth cannot tell which of the provider's resources (${n.join(", ")}) is the MCP resource. Use \`mcp\` from "@lunora/auth/plugins", which records it, and pass the plugin object as is.`)},u=t=>{const e=t.plugins??[],r=e.filter(o=>o.id==="oauth-provider");if(r.length===0)return[];const n=r.map(o=>E(o)).filter(o=>o!==void 0).map(o=>R(o)),s=m(t,e,r[0]?.options);return[...new Set([...n,w(s)])]},a=new WeakMap,S=(t,e)=>{let r=a.get(t);return r===void 0&&(r=u(t.options(e)),a.set(t,r)),r},A=t=>t?.status===404?void 0:t,c=new WeakMap,P=(t,e)=>(t.method==="GET"||t.method==="HEAD")&&e.has(new URL(t.url).pathname),_=async(t,e)=>{let r=c.get(t);if(r===void 0&&(r=new Set(u(t.options)),c.set(t,r)),!!P(e,r))return A(await t.handler(e))};export{l as MCP_RESOURCE_KEY,u as authDiscoveryPaths,S as authDiscoveryPathsFor,w as authorizationServerPath,_ as handleAuthDiscoveryRequest,P as isDiscoveryRequest,R as protectedResourcePath,A as unlessNotFound};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getAuthTablesWithResolvedIndexes as l,getDatabaseFieldIndexName as N}from"@better-auth/core/db/internal";import{q as r}from"./quote-identifier-CGiYFBvY.mjs";const f=(n,e)=>{if(n==="id"||e.references?.field==="id")return"text";const{type:t}=e;if(Array.isArray(t))return"text";switch(t){case"boolean":return"integer";case"date":return"date";case"number":return"bigint"in e&&e.bigint===!0?"bigint":"integer";default:return"text"}},b=n=>{const{defaultValue:e,type:t,unique:o}=n;if(!(o===!0&&n.required===!1)&&!(e==null||typeof e=="function")){if(t==="boolean")return e===!0?"1":"0";if(t==="number"&&typeof e=="number"&&Number.isFinite(e))return String(e);if(t==="string"&&typeof e=="string")return`'${e.replaceAll("'","''")}'`}},p=(n,e)=>{const t=[];for(const[o,i]of Object.entries(e)){const s=i.unique===!0;if(!s&&i.index!==!0)continue;const c=i.fieldName??o,u=N(n,c,s);t.push(`CREATE ${s?"UNIQUE ":""}INDEX IF NOT EXISTS ${r(u)} ON ${r(n)} (${r(c)})`)}return t},T=(n,e)=>{const t=[r(n),f(n,e)],o=b(e);return e.required!==!1&&o!==void 0&&t.push("NOT NULL"),o!==void 0&&t.push(`DEFAULT ${o}`),t.join(" ")},E=(n,e)=>{const t=[r(n),f(n,e)];return e.required!==!1&&t.push("NOT NULL"),t.join(" ")},$=n=>{const{indexesByTable:e,tables:t}=l(n),o=[],i=[];for(const s of Object.values(t)){if(s.disableMigrations===!0)continue;const c=[`${r("id")} text NOT NULL PRIMARY KEY`,...Object.entries(s.fields).map(([u,a])=>E(a.fieldName??u,a))];o.push(`CREATE TABLE IF NOT EXISTS ${r(s.modelName)} (${c.join(", ")})`),i.push(...p(s.modelName,s.fields));for(const u of e.get(s.modelName)??[]){const a=u.unique===!0?"UNIQUE ":"",m=u.columns.map(d=>r(d)).join(", ");i.push(`CREATE ${a}INDEX IF NOT EXISTS ${r(u.name)} ON ${r(s.modelName)} (${m})`)}}return[...o,...i]},A=(n,e)=>{const{tables:t}=l(n),o=[];for(const i of Object.values(t)){if(i.disableMigrations===!0)continue;const s=new Set(e(i.modelName));if(s.size!==0)for(const[c,u]of Object.entries(i.fields)){const a=u.fieldName??c;s.has(a)||o.push(`ALTER TABLE ${r(i.modelName)} ADD COLUMN ${T(a,u)}`)}}return o};export{A as authDoColumnAdditions,$ as authDoSchemaStatements};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { betterAuth, BetterAuthOptions } from 'better-auth';
|
|
2
|
+
/**
|
|
3
|
+
* Lunora's options pass straight through to better-auth — the only thing we add
|
|
4
|
+
* is requiring `secret` up front so a misconfigured deployment fails loudly
|
|
5
|
+
* instead of at the first sign-in.
|
|
6
|
+
*
|
|
7
|
+
* For `database`, prefer `lunoraD1Adapter` (`database: lunoraD1Adapter(env.DB)`)
|
|
8
|
+
* over passing the raw `env.DB`. better-auth *does* accept a D1Database directly,
|
|
9
|
+
* but it then resolves its Kysely adapter via a runtime `await import(...)` inside
|
|
10
|
+
* `auth.$context` — and that import never settles under `@cloudflare/vite-plugin`'s
|
|
11
|
+
* worker runner, hanging every auth request in `pnpm dev`. The explicit adapter
|
|
12
|
+
* skips it, so dev and prod behave the same. (Raw `env.DB` is still correct for
|
|
13
|
+
* the migration-only instance — see `lunoraD1Adapter`'s note.)
|
|
14
|
+
*
|
|
15
|
+
* Session rotation / richer session policies are configured via the `session`
|
|
16
|
+
* field (a `SessionPolicy`); Lunora validates it for obviously-broken
|
|
17
|
+
* durations and forwards it verbatim to better-auth. See `sessionPresets`
|
|
18
|
+
* for ready-made rotation/expiry trade-offs.
|
|
19
|
+
*
|
|
20
|
+
* ## Serverless background tasks (Cloudflare Workers)
|
|
21
|
+
*
|
|
22
|
+
* better-auth runs some work *after* sending the response — most importantly the
|
|
23
|
+
* password-reset email, whose background send is what keeps reset responses
|
|
24
|
+
* constant-time (a timing-attack defence: the response doesn't reveal whether
|
|
25
|
+
* the account exists). On Cloudflare Workers a promise that isn't handed to
|
|
26
|
+
* `ctx.waitUntil` can be cancelled the moment the response returns, dropping
|
|
27
|
+
* that send and weakening the guarantee. Wire your request's `ctx.waitUntil`
|
|
28
|
+
* into better-auth's background handler so the work survives:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* // in your worker fetch handler, where `ctx: ExecutionContext` is in scope
|
|
32
|
+
* const auth = createAuth({
|
|
33
|
+
* secret: env.AUTH_SECRET,
|
|
34
|
+
* database: lunoraD1Adapter(env.DB),
|
|
35
|
+
* advanced: {
|
|
36
|
+
* backgroundTasks: { handler: (promise) => ctx.waitUntil(promise) },
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* (Lunora can't set this for you — `ctx.waitUntil` is per-request, but
|
|
42
|
+
* `createAuth` runs once at worker setup.)
|
|
43
|
+
*
|
|
44
|
+
* `databaseHooks` never receives a Lunora `MutationCtx` — the hook runs inside
|
|
45
|
+
* better-auth's own write path. A side effect on an app table is therefore a
|
|
46
|
+
* separate write and has to be idempotent; see `concepts/authentication`.
|
|
47
|
+
*/
|
|
48
|
+
type LunoraAuthOptions = BetterAuthOptions;
|
|
49
|
+
/**
|
|
50
|
+
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
51
|
+
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
52
|
+
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
53
|
+
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
54
|
+
*/
|
|
55
|
+
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the caller's options into the exact shape `createAuth` hands to
|
|
58
|
+
* `betterAuth` — the hardened, default-filled options the running worker uses.
|
|
59
|
+
* Exported (and pure) so the migration path can compile the schema from the
|
|
60
|
+
* same resolved options: `compileMigrationsSql` routes through here, so the
|
|
61
|
+
* `rateLimit` table the worker's durable limiter writes to is included in the
|
|
62
|
+
* migration rather than silently omitted (it would be, if migrations saw the
|
|
63
|
+
* raw options while the worker ran the resolved ones).
|
|
64
|
+
*
|
|
65
|
+
* ## What it fills (each gated independently on caller silence)
|
|
66
|
+
*
|
|
67
|
+
* Secure-by-default cookies + secret-strength warning via {@link hardenAuthOptions},
|
|
68
|
+
* applied first so all hardening composes onto one options object.
|
|
69
|
+
*
|
|
70
|
+
* Rate limiting is ON by default for `/api/auth/*`.
|
|
71
|
+
*
|
|
72
|
+
* better-auth's own default is `rateLimit.enabled ?? isProduction`, and its
|
|
73
|
+
* `isProduction` is `"production" === "production"` resolved at
|
|
74
|
+
* module-load time. On Cloudflare Workers that check is unreliable: the
|
|
75
|
+
* runtime has no Node `process.env` (absent entirely without
|
|
76
|
+
* `nodejs_compat`, and even with it `NODE_ENV` is rarely `"production"` at
|
|
77
|
+
* request time). So better-auth would silently leave auth endpoints
|
|
78
|
+
* _unthrottled_ on a real deployment — the surprise we refuse to ship.
|
|
79
|
+
*
|
|
80
|
+
* We therefore default `enabled: true` whenever the caller hasn't made an
|
|
81
|
+
* explicit choice. We only fill the `enabled` flag and otherwise forward
|
|
82
|
+
* the caller's `rateLimit` verbatim, so better-auth's `window` (10s) / `max`
|
|
83
|
+
* (100) defaults and any custom rules still apply. Callers who genuinely
|
|
84
|
+
* want it off can pass `rateLimit: { enabled: false }` (e.g. when fronting
|
|
85
|
+
* auth with their own limiter), and any explicit `enabled` value wins.
|
|
86
|
+
*
|
|
87
|
+
* We also default `storage: "database"` — but only when rate limiting is not
|
|
88
|
+
* explicitly disabled (`enabled !== false`). Filling storage under a disabled
|
|
89
|
+
* limiter is harmless at runtime but makes `getAuthTables` emit an unused
|
|
90
|
+
* `rateLimit` table, so we skip it there.
|
|
91
|
+
*
|
|
92
|
+
* better-auth's own default is `storage: "memory"` — a per-isolate,
|
|
93
|
+
* non-durable counter. On Cloudflare Workers that means each isolate keeps
|
|
94
|
+
* its own tally, counters vanish on isolate recycle, and traffic spread
|
|
95
|
+
* across isolates never sums to the configured `max` — a limiter that
|
|
96
|
+
* reports "enabled" while never enforcing a global limit (the exact
|
|
97
|
+
* brute-force / credential-stuffing protection on `/sign-in`, OTP, and
|
|
98
|
+
* password-reset it is meant to buy). `storage: "database"` rides the counter
|
|
99
|
+
* through the configured `database` adapter — Lunora's store over the D1 auth
|
|
100
|
+
* tables — so the limit is durable *and* atomic (the store's native
|
|
101
|
+
* `incrementOne` gives a one-winner guarantee across isolates). Callers with
|
|
102
|
+
* their own durable store can pass an explicit `rateLimit: { storage: … }`
|
|
103
|
+
* (or `customStorage`), and any explicit value wins.
|
|
104
|
+
*
|
|
105
|
+
* The limiter's *bucket key* is filled here too, and it is the half that makes
|
|
106
|
+
* the rest of the paragraph above true. A limit is only brute-force protection
|
|
107
|
+
* if the key identifies the client, so {@link defaultIpAddressHeaders} points
|
|
108
|
+
* better-auth at `cf-connecting-ip` instead of its own `x-forwarded-for`
|
|
109
|
+
* default, and {@link UNRESOLVED_IP_BUCKET_FACTOR} re-sizes the shared bucket
|
|
110
|
+
* for the deployments where no client IP can be resolved at all. Both are read
|
|
111
|
+
* off `advanced.ipAddress`, so a caller who configures that keeps their own
|
|
112
|
+
* policy end to end. The catch-all `customRules` entry is merged *after* the
|
|
113
|
+
* caller's own rules so theirs match first.
|
|
114
|
+
*
|
|
115
|
+
* Session cookie cache is ON by default too.
|
|
116
|
+
*
|
|
117
|
+
* Every authenticated call resolves identity through better-auth's
|
|
118
|
+
* `getSession`, which — without a cache — is a DB (D1) read on the hot path
|
|
119
|
+
* of every query/mutation/action that reads `ctx.auth` and of the WebSocket
|
|
120
|
+
* upgrade. better-auth's `session.cookieCache` carries the session payload in
|
|
121
|
+
* a short-lived signed cookie so `getSession` can answer without hitting the
|
|
122
|
+
* database until the cache window elapses. We default it on with a
|
|
123
|
+
* deliberately short 60s `maxAge` (better-auth's own default is 300s): long
|
|
124
|
+
* enough to erase the per-request read for a burst of calls, short enough
|
|
125
|
+
* that a revoked or role-changed session self-corrects within a minute.
|
|
126
|
+
* The one tradeoff — a revoked session stays valid until the cache expires —
|
|
127
|
+
* is bounded by that TTL; callers who need immediate revocation opt out with
|
|
128
|
+
* `session: { cookieCache: { enabled: false } }` (or the `strict` preset).
|
|
129
|
+
* With the organization plugin on, a small plugin re-issues that cache cookie
|
|
130
|
+
* whenever an `/organization/*` endpoint moved the caller's active organization
|
|
131
|
+
* or team (see `org-session-refresh.ts`), so a membership change is not hidden
|
|
132
|
+
* behind the cache window.
|
|
133
|
+
*
|
|
134
|
+
* Every explicit caller value is forwarded verbatim. The `rateLimit` fills all
|
|
135
|
+
* merge into a single `rateLimit` object so none clobbers the others.
|
|
136
|
+
*/
|
|
137
|
+
declare const resolveAuthOptions: (options: LunoraAuthOptions) => LunoraAuthOptions;
|
|
138
|
+
/**
|
|
139
|
+
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
140
|
+
* the `secret` requirement at construction time so misconfigured deployments
|
|
141
|
+
* fail loudly at the first fetch rather than the first sign-in attempt, then
|
|
142
|
+
* hands {@link resolveAuthOptions}'s hardened, default-filled options to
|
|
143
|
+
* better-auth.
|
|
144
|
+
*/
|
|
145
|
+
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
146
|
+
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c, resolveAuthOptions as r };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { betterAuth, BetterAuthOptions } from 'better-auth';
|
|
2
|
+
/**
|
|
3
|
+
* Lunora's options pass straight through to better-auth — the only thing we add
|
|
4
|
+
* is requiring `secret` up front so a misconfigured deployment fails loudly
|
|
5
|
+
* instead of at the first sign-in.
|
|
6
|
+
*
|
|
7
|
+
* For `database`, prefer `lunoraD1Adapter` (`database: lunoraD1Adapter(env.DB)`)
|
|
8
|
+
* over passing the raw `env.DB`. better-auth *does* accept a D1Database directly,
|
|
9
|
+
* but it then resolves its Kysely adapter via a runtime `await import(...)` inside
|
|
10
|
+
* `auth.$context` — and that import never settles under `@cloudflare/vite-plugin`'s
|
|
11
|
+
* worker runner, hanging every auth request in `pnpm dev`. The explicit adapter
|
|
12
|
+
* skips it, so dev and prod behave the same. (Raw `env.DB` is still correct for
|
|
13
|
+
* the migration-only instance — see `lunoraD1Adapter`'s note.)
|
|
14
|
+
*
|
|
15
|
+
* Session rotation / richer session policies are configured via the `session`
|
|
16
|
+
* field (a `SessionPolicy`); Lunora validates it for obviously-broken
|
|
17
|
+
* durations and forwards it verbatim to better-auth. See `sessionPresets`
|
|
18
|
+
* for ready-made rotation/expiry trade-offs.
|
|
19
|
+
*
|
|
20
|
+
* ## Serverless background tasks (Cloudflare Workers)
|
|
21
|
+
*
|
|
22
|
+
* better-auth runs some work *after* sending the response — most importantly the
|
|
23
|
+
* password-reset email, whose background send is what keeps reset responses
|
|
24
|
+
* constant-time (a timing-attack defence: the response doesn't reveal whether
|
|
25
|
+
* the account exists). On Cloudflare Workers a promise that isn't handed to
|
|
26
|
+
* `ctx.waitUntil` can be cancelled the moment the response returns, dropping
|
|
27
|
+
* that send and weakening the guarantee. Wire your request's `ctx.waitUntil`
|
|
28
|
+
* into better-auth's background handler so the work survives:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* // in your worker fetch handler, where `ctx: ExecutionContext` is in scope
|
|
32
|
+
* const auth = createAuth({
|
|
33
|
+
* secret: env.AUTH_SECRET,
|
|
34
|
+
* database: lunoraD1Adapter(env.DB),
|
|
35
|
+
* advanced: {
|
|
36
|
+
* backgroundTasks: { handler: (promise) => ctx.waitUntil(promise) },
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* (Lunora can't set this for you — `ctx.waitUntil` is per-request, but
|
|
42
|
+
* `createAuth` runs once at worker setup.)
|
|
43
|
+
*
|
|
44
|
+
* `databaseHooks` never receives a Lunora `MutationCtx` — the hook runs inside
|
|
45
|
+
* better-auth's own write path. A side effect on an app table is therefore a
|
|
46
|
+
* separate write and has to be idempotent; see `concepts/authentication`.
|
|
47
|
+
*/
|
|
48
|
+
type LunoraAuthOptions = BetterAuthOptions;
|
|
49
|
+
/**
|
|
50
|
+
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
51
|
+
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
52
|
+
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
53
|
+
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
54
|
+
*/
|
|
55
|
+
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the caller's options into the exact shape `createAuth` hands to
|
|
58
|
+
* `betterAuth` — the hardened, default-filled options the running worker uses.
|
|
59
|
+
* Exported (and pure) so the migration path can compile the schema from the
|
|
60
|
+
* same resolved options: `compileMigrationsSql` routes through here, so the
|
|
61
|
+
* `rateLimit` table the worker's durable limiter writes to is included in the
|
|
62
|
+
* migration rather than silently omitted (it would be, if migrations saw the
|
|
63
|
+
* raw options while the worker ran the resolved ones).
|
|
64
|
+
*
|
|
65
|
+
* ## What it fills (each gated independently on caller silence)
|
|
66
|
+
*
|
|
67
|
+
* Secure-by-default cookies + secret-strength warning via {@link hardenAuthOptions},
|
|
68
|
+
* applied first so all hardening composes onto one options object.
|
|
69
|
+
*
|
|
70
|
+
* Rate limiting is ON by default for `/api/auth/*`.
|
|
71
|
+
*
|
|
72
|
+
* better-auth's own default is `rateLimit.enabled ?? isProduction`, and its
|
|
73
|
+
* `isProduction` is `"production" === "production"` resolved at
|
|
74
|
+
* module-load time. On Cloudflare Workers that check is unreliable: the
|
|
75
|
+
* runtime has no Node `process.env` (absent entirely without
|
|
76
|
+
* `nodejs_compat`, and even with it `NODE_ENV` is rarely `"production"` at
|
|
77
|
+
* request time). So better-auth would silently leave auth endpoints
|
|
78
|
+
* _unthrottled_ on a real deployment — the surprise we refuse to ship.
|
|
79
|
+
*
|
|
80
|
+
* We therefore default `enabled: true` whenever the caller hasn't made an
|
|
81
|
+
* explicit choice. We only fill the `enabled` flag and otherwise forward
|
|
82
|
+
* the caller's `rateLimit` verbatim, so better-auth's `window` (10s) / `max`
|
|
83
|
+
* (100) defaults and any custom rules still apply. Callers who genuinely
|
|
84
|
+
* want it off can pass `rateLimit: { enabled: false }` (e.g. when fronting
|
|
85
|
+
* auth with their own limiter), and any explicit `enabled` value wins.
|
|
86
|
+
*
|
|
87
|
+
* We also default `storage: "database"` — but only when rate limiting is not
|
|
88
|
+
* explicitly disabled (`enabled !== false`). Filling storage under a disabled
|
|
89
|
+
* limiter is harmless at runtime but makes `getAuthTables` emit an unused
|
|
90
|
+
* `rateLimit` table, so we skip it there.
|
|
91
|
+
*
|
|
92
|
+
* better-auth's own default is `storage: "memory"` — a per-isolate,
|
|
93
|
+
* non-durable counter. On Cloudflare Workers that means each isolate keeps
|
|
94
|
+
* its own tally, counters vanish on isolate recycle, and traffic spread
|
|
95
|
+
* across isolates never sums to the configured `max` — a limiter that
|
|
96
|
+
* reports "enabled" while never enforcing a global limit (the exact
|
|
97
|
+
* brute-force / credential-stuffing protection on `/sign-in`, OTP, and
|
|
98
|
+
* password-reset it is meant to buy). `storage: "database"` rides the counter
|
|
99
|
+
* through the configured `database` adapter — Lunora's store over the D1 auth
|
|
100
|
+
* tables — so the limit is durable *and* atomic (the store's native
|
|
101
|
+
* `incrementOne` gives a one-winner guarantee across isolates). Callers with
|
|
102
|
+
* their own durable store can pass an explicit `rateLimit: { storage: … }`
|
|
103
|
+
* (or `customStorage`), and any explicit value wins.
|
|
104
|
+
*
|
|
105
|
+
* The limiter's *bucket key* is filled here too, and it is the half that makes
|
|
106
|
+
* the rest of the paragraph above true. A limit is only brute-force protection
|
|
107
|
+
* if the key identifies the client, so {@link defaultIpAddressHeaders} points
|
|
108
|
+
* better-auth at `cf-connecting-ip` instead of its own `x-forwarded-for`
|
|
109
|
+
* default, and {@link UNRESOLVED_IP_BUCKET_FACTOR} re-sizes the shared bucket
|
|
110
|
+
* for the deployments where no client IP can be resolved at all. Both are read
|
|
111
|
+
* off `advanced.ipAddress`, so a caller who configures that keeps their own
|
|
112
|
+
* policy end to end. The catch-all `customRules` entry is merged *after* the
|
|
113
|
+
* caller's own rules so theirs match first.
|
|
114
|
+
*
|
|
115
|
+
* Session cookie cache is ON by default too.
|
|
116
|
+
*
|
|
117
|
+
* Every authenticated call resolves identity through better-auth's
|
|
118
|
+
* `getSession`, which — without a cache — is a DB (D1) read on the hot path
|
|
119
|
+
* of every query/mutation/action that reads `ctx.auth` and of the WebSocket
|
|
120
|
+
* upgrade. better-auth's `session.cookieCache` carries the session payload in
|
|
121
|
+
* a short-lived signed cookie so `getSession` can answer without hitting the
|
|
122
|
+
* database until the cache window elapses. We default it on with a
|
|
123
|
+
* deliberately short 60s `maxAge` (better-auth's own default is 300s): long
|
|
124
|
+
* enough to erase the per-request read for a burst of calls, short enough
|
|
125
|
+
* that a revoked or role-changed session self-corrects within a minute.
|
|
126
|
+
* The one tradeoff — a revoked session stays valid until the cache expires —
|
|
127
|
+
* is bounded by that TTL; callers who need immediate revocation opt out with
|
|
128
|
+
* `session: { cookieCache: { enabled: false } }` (or the `strict` preset).
|
|
129
|
+
* With the organization plugin on, a small plugin re-issues that cache cookie
|
|
130
|
+
* whenever an `/organization/*` endpoint moved the caller's active organization
|
|
131
|
+
* or team (see `org-session-refresh.ts`), so a membership change is not hidden
|
|
132
|
+
* behind the cache window.
|
|
133
|
+
*
|
|
134
|
+
* Every explicit caller value is forwarded verbatim. The `rateLimit` fills all
|
|
135
|
+
* merge into a single `rateLimit` object so none clobbers the others.
|
|
136
|
+
*/
|
|
137
|
+
declare const resolveAuthOptions: (options: LunoraAuthOptions) => LunoraAuthOptions;
|
|
138
|
+
/**
|
|
139
|
+
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
140
|
+
* the `secret` requirement at construction time so misconfigured deployments
|
|
141
|
+
* fail loudly at the first fetch rather than the first sign-in attempt, then
|
|
142
|
+
* hands {@link resolveAuthOptions}'s hardened, default-filled options to
|
|
143
|
+
* better-auth.
|
|
144
|
+
*/
|
|
145
|
+
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
146
|
+
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c, resolveAuthOptions as r };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";import{isAPIError as u,createAuthMiddleware as h,getIP as f}from"better-auth/api";import{betterAuth as m}from"better-auth/minimal";import{o as g}from"./on-cloudflare-edge-Dx6gFjBO.mjs";import{authDiscoveryPaths as p}from"./authDiscoveryPaths-B14lM14u.mjs";import{setCookieCache as A}from"better-auth/cookies";import{validateSessionPolicy as E}from"./sessionPresets-C867Mlo4.mjs";const d="lunora-org-session-refresh",w=new Set(["/organization/accept-invitation","/organization/create","/organization/delete","/organization/leave","/organization/remove-member"]),S=new Set(["/organization/get-full-organization","/organization/get-organization"]),v=()=>({hooks:{after:[{handler:h(async e=>{const t=await e.getSignedCookie(e.context.authCookies.sessionToken.name,e.context.secret);if(!(typeof t!="string"||t===""))try{const r=await e.context.internalAdapter.findSession(t);if(!r||r.session.expiresAt.getTime()<=Date.now())return;const o=await e.getSignedCookie(e.context.authCookies.dontRememberToken.name,e.context.secret);await A(e,r,!!o)}catch(r){console.error("@lunora/auth: organization session cache refresh failed",r)}}),matcher:e=>{if(e.context.options.session?.cookieCache?.enabled!==!0||e.path===void 0)return!1;const t=u(e.context.returned);return w.has(e.path)&&!t||S.has(e.path)&&t}}]},id:d}),C=e=>{const t=e.plugins??[];return!t.some(r=>r.id==="organization")||t.some(r=>r.id===d)?e:{...e,plugins:[...t,v()]}},i="cf-connecting-ip",L=e=>{const t=e!==void 0&&e.length>0;return g()?t?[i,"x-forwarded-for"]:[i]:t?["x-forwarded-for"]:[]},s="/**",T=100,_=e=>{const t=e.rateLimit?.customRules??{};return t[s]!==void 0?t:{...t,[s]:(r,o)=>f(r,e)===null?{max:o.max*T,window:o.window}:o}},l=32,R=e=>{const t=typeof e=="string"?e.trim().length:0;return t>0&&t<l},c=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,k=new Set(["127.0.0.1","localhost"]),b=e=>{if(typeof e!="string")return;let t;try{t=new URL(e)}catch{return}if(!(t.protocol!=="http:"||!k.has(t.hostname)))return{allowedHosts:[t.hostname,`${t.hostname}:*`],fallback:e,protocol:"http"}},O=e=>{const t=b(e.baseURL)??e.baseURL;if(R(e.secret)){const n=`@lunora/auth: AUTH_SECRET is only ${String(e.secret?.trim().length)} characters. Use at least ${String(l)} for a brute-force-resistant secret — generate one with \`openssl rand -hex 32\`.`;if(!c(t))throw new a("INTERNAL",n);console.warn(n)}const r=e.advanced??{},o=r.ipAddress??{};return{...e,advanced:{...r,defaultCookieAttributes:r.defaultCookieAttributes??{httpOnly:!0,path:"/",sameSite:"lax"},ipAddress:{...o,...o.ipAddressHeaders===void 0?{ipAddressHeaders:L(o.trustedProxies)}:{}},...r.useSecureCookies===void 0?{useSecureCookies:!c(t)}:{}},baseURL:t}},y=e=>{const t=O(e),r=t.rateLimit?.enabled===void 0,o=t.rateLimit?.storage===void 0&&t.rateLimit?.enabled!==!1,n=t.rateLimit?.enabled===!1?{}:{rateLimit:{...t.rateLimit,...r?{enabled:!0}:{},...o?{storage:"database"}:{},customRules:_(t)}};return C({...t,...n,...t.session?.cookieCache===void 0?{session:{...t.session,cookieCache:{enabled:!0,maxAge:60}}}:{}})},I=e=>{if(!e.secret||e.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 e.session&&E(e.session),p(e),y(e)},B=e=>{const t=I(e);if(typeof t.database!="function")throw new a("INTERNAL","@lunora/auth: `createAuth` needs an adapter factory as `database` (`lunoraD1Adapter(env.DB)` or the Durable Object wiring). A raw D1 binding is for the migration-only instance: build it with `createMigrationAuth` from `@lunora/auth/migrate`.");return m(t)};export{B as createAuth,I as prepareAuthOptions,y as resolveAuthOptions};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as E}from"@lunora/errors";import{I as h,a as j,c as T,R as b,M as g}from"./auth-do-Dkk1jOb6.mjs";import{isDiscoveryRequest as S,unlessNotFound as _}from"./authDiscoveryPaths-B14lM14u.mjs";import{isAuthRoutePath as N,DEFAULT_AUTH_BASE_PATH as I}from"./DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs";const U=a=>{const{internalSecret:i,jurisdiction:c,objectName:p="auth"}=a,A=new Set(a.discoveryPaths),y=a.namespace;let{namespace:r}=a;if(r&&c!==void 0){if(typeof r.jurisdiction!="function")throw new TypeError(`@lunora/auth: Durable Object namespace does not support jurisdiction("${c}") — update @cloudflare/workers-types or remove the jurisdiction option`);r=r.jurisdiction(c)}const u=()=>{if(r)return r.get(r.idFromName(p))},R=async(e,t,o)=>{if(!i)return;const s=u();if(!s)return;const n=await s.fetch(new Request(new URL(e,t),{body:JSON.stringify(o),headers:{"content-type":"application/json",[h]:i},method:"POST"}));return n.ok?n:void 0},m=r,v=y&&m&&c!==void 0?T(async(e,t)=>{if(!i)throw new E("AUTH_MOVE_NOT_CONFIGURED","copying the auth object needs its internal secret; set the auth object's `internalSecret`");const o=e==="source"?y:m;return o.get(o.idFromName(p)).fetch(new Request(new URL(g,"https://auth-do.invalid"),{body:JSON.stringify(t),headers:{"content-type":"application/json",[h]:i},method:"POST"}))}):void 0;return{...v===void 0?{}:{jurisdictionMove:v},auditReader:{read:async e=>{const t=await R(b,"https://auth-do.invalid",e);return t?(await t.json())?.entries??[]:[]}},authHandler:async e=>{if(N(new URL(e.url).pathname,I))return u()?.fetch(e)},discoveryHandler:async e=>{if(S(e,A))return _(await u()?.fetch(e))},resolveIdentity:async e=>{if(!i)return null;const t=u();if(!t)return null;const o=new Headers(e.headers);o.set(h,i);const s=await t.fetch(new Request(new URL(j,e.url),{headers:o}));if(!s.ok)return null;const n=await s.json(),d=n?.userId;if(!n||typeof d!="string"||d.length===0)return null;const f={userId:d};for(const w of["email","name","orgId","role","teamId"]){const l=n[w];typeof l=="string"&&l.length>0&&(f[w]=l)}return typeof n.expiresAtMs=="number"&&Number.isFinite(n.expiresAtMs)&&(f.expiresAtMs=n.expiresAtMs),f}}};export{U as createDoAuthWiring};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{oauthProviderClient as d}from"@better-auth/oauth-provider/client";import{passkeyClient as g}from"@better-auth/passkey/client";import{oneTapClient as m,lastLoginMethodClient as p,deviceAuthorizationClient as C,anonymousClient as y,multiSessionClient as f,phoneNumberClient as S,usernameClient as b,adminClient as T,emailOTPClient as L,magicLinkClient as v,twoFactorClient as w,organizationClient as A}from"better-auth/client/plugins";const s=Symbol.for("lunora:session-change"),P="lunora:session-change",a=()=>{const n=globalThis;let o=n[s];return o===void 0&&(o={detach:void 0,listeners:new Set,tab:`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`},n[s]=o),o},k=()=>"document"in globalThis,E=async()=>{const n=[];for(const o of a().listeners)try{n.push(o())}catch{}await Promise.allSettled(n)},r=async()=>{if(k()&&typeof BroadcastChannel=="function")try{const n=new BroadcastChannel(P);n.postMessage({tab:a().tab,type:"lunora:session-change"}),n.close()}catch{}await E()},i={organization:A,twoFactor:w,passkey:g,magicLink:v,emailOtp:L,admin:T,username:b,phoneNumber:S,multiSession:f,anonymous:y,deviceAuthorization:C,lastLoginMethod:p,oauthProvider:d},N=(n={})=>{const o=[];for(const t of Object.keys(i))n[t]&&o.push(i[t]());return o},M=()=>globalThis.location?.origin,B=async()=>r(),O=()=>({fetchPlugins:[{hooks:{onSuccess:n=>{(n.request.method??"GET").toUpperCase()!=="GET"&&r().catch(()=>{})}},id:"lunora-session-sync",name:"lunora-session-sync"}],id:"lunora-session-sync"}),F=(n,o={})=>{const{baseURL:t,extraPlugins:c=[],oneTapClientId:e,plugins:l,...u}=o,h=e===void 0||e===""?[]:[m({clientId:e})];return n({...u,baseURL:t??M(),plugins:[O(),...N(l),...h,...c]})};export{F as createLunoraAuthClient,N as lunoraAuthPlugins,O as lunoraSessionSync,B as notifyLunoraSessionChange};
|
|
@@ -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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";import{APIError as l}from"better-auth/api";import{assertEmailAllowed as d}from"../email-guard.mjs";const u=e=>{switch(e){case 400:return"BAD_REQUEST";case 422:return"UNPROCESSABLE_ENTITY";case 429:return"TOO_MANY_REQUESTS";default:return"INTERNAL_SERVER_ERROR"}},c=e=>async(r,t)=>{const s=typeof r.email=="string"?r.email:void 0;if(s===void 0||s==="")return;let o;try{const a=e.onClassify!==void 0;o=await d(s,{...e,lists:{disposable:e.blockDisposable!==!1||a,free:a}})}catch(a){throw a instanceof n?new l(u(a.status),{code:a.code,message:a.message}):a}e.onClassify?.(o,r,t)},m=(e={})=>({user:{create:{before:c(e)}}}),R=(e,r={})=>{const t=c(r),s=e.databaseHooks?.user?.create?.before,o=s?async(a,i)=>(await t(a,i),s(a,i)):t;return{...e,databaseHooks:{...e.databaseHooks,user:{...e.databaseHooks?.user,create:{...e.databaseHooks?.user?.create,before:o}}}}};export{m as emailGateDatabaseHooks,R as withEmailGate};
|