@lunora/auth 1.0.0-alpha.10 → 1.0.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.mjs +1 -0
- package/dist/index.d.mts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.mjs +3 -3
- package/dist/middleware.d.mts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/packem_shared/{compileMigrationsSql-wZH3oXDu.mjs → compileMigrationsSql-Cz0omU09.mjs} +2 -1
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.mts +128 -0
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.ts +128 -0
- package/dist/packem_shared/{createAuth-B-tvsvQU.mjs → createAuth-CChKko05.mjs} +19 -5
- package/dist/packem_shared/{sessionPresets-B95rXrd8.mjs → sessionPresets-Dwwd74_J.mjs} +3 -0
- package/dist/schema.d.mts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/sql-store.mjs +22 -0
- package/dist/store.d.mts +18 -0
- package/dist/store.d.ts +18 -0
- package/dist/store.mjs +13 -0
- package/package.json +2 -2
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.mts +0 -58
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.ts +0 -58
package/dist/adapter.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const lunoraAuthAdapter = (store) => createAdapterFactory({
|
|
|
19
19
|
const [row] = await store.read(model, { limit: 1, where });
|
|
20
20
|
return asRowOrNull(row);
|
|
21
21
|
},
|
|
22
|
+
incrementOne: async ({ increment, model, set, where }) => asRowOrNull(await store.incrementOne(model, where, increment, set)),
|
|
22
23
|
update: async ({ model, update, where }) => {
|
|
23
24
|
const [row] = await store.update(model, where, update);
|
|
24
25
|
return asRowOrNull(row);
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { lunoraAuthAdapter, lunoraD1Adapter } from "./adapter.mjs";
|
|
2
|
-
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-
|
|
3
|
-
export { c as createAuth } from "./packem_shared/create-auth.d-
|
|
2
|
+
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.mjs";
|
|
3
|
+
export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.mjs";
|
|
4
4
|
export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.mjs";
|
|
5
5
|
export { default as authTables } from "./schema.mjs";
|
|
6
6
|
import { BetterAuthOptions } from 'better-auth';
|
|
@@ -333,6 +333,12 @@ declare const ensureMigrated: (auth: LunoraAuth | {
|
|
|
333
333
|
* Compile better-auth's migrations to a single SQL string. Useful for
|
|
334
334
|
* `wrangler d1 execute --file -` in CI so the deploy step applies the schema
|
|
335
335
|
* before the first user request.
|
|
336
|
+
*
|
|
337
|
+
* Compiles from the SAME resolved options `createAuth` runs with (via
|
|
338
|
+
* `resolveAuthOptions`), not the raw caller options — so the schema includes the
|
|
339
|
+
* `rateLimit` table the worker's default-on durable limiter writes to. Compiling
|
|
340
|
+
* from the raw options would omit it, and the running worker would then write to
|
|
341
|
+
* a table the migration never created.
|
|
336
342
|
*/
|
|
337
343
|
declare const compileMigrationsSql: (options: LunoraAuthOptions) => Promise<string>;
|
|
338
344
|
/**
|
|
@@ -378,9 +384,13 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
|
|
|
378
384
|
* });
|
|
379
385
|
* ```
|
|
380
386
|
*
|
|
381
|
-
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day
|
|
382
|
-
* -
|
|
383
|
-
*
|
|
387
|
+
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
|
|
388
|
+
* with a 60s signed-cookie session cache so bursts of authenticated calls
|
|
389
|
+
* skip the per-request DB session read.
|
|
390
|
+
* - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
|
|
391
|
+
* cookie cache **off** (fast revocation / short freshness is the whole point).
|
|
392
|
+
* - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
|
|
393
|
+
* with the same 60s cookie cache as `rolling`.
|
|
384
394
|
*/
|
|
385
395
|
declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
|
|
386
396
|
export { type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthCapabilities, type AuthInvitation, type AuthMember, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthTimestamp, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, type LunoraAuthOptions, type SessionPolicy, compileMigrationsSql, createAuthAdmin, ensureMigrated, handleAuthRequest, sessionPresets, validateSessionPolicy };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { lunoraAuthAdapter, lunoraD1Adapter } from "./adapter.js";
|
|
2
|
-
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-
|
|
3
|
-
export { c as createAuth } from "./packem_shared/create-auth.d-
|
|
2
|
+
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.js";
|
|
3
|
+
export { c as createAuth, r as resolveAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.js";
|
|
4
4
|
export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.js";
|
|
5
5
|
export { default as authTables } from "./schema.js";
|
|
6
6
|
import { BetterAuthOptions } from 'better-auth';
|
|
@@ -333,6 +333,12 @@ declare const ensureMigrated: (auth: LunoraAuth | {
|
|
|
333
333
|
* Compile better-auth's migrations to a single SQL string. Useful for
|
|
334
334
|
* `wrangler d1 execute --file -` in CI so the deploy step applies the schema
|
|
335
335
|
* before the first user request.
|
|
336
|
+
*
|
|
337
|
+
* Compiles from the SAME resolved options `createAuth` runs with (via
|
|
338
|
+
* `resolveAuthOptions`), not the raw caller options — so the schema includes the
|
|
339
|
+
* `rateLimit` table the worker's default-on durable limiter writes to. Compiling
|
|
340
|
+
* from the raw options would omit it, and the running worker would then write to
|
|
341
|
+
* a table the migration never created.
|
|
336
342
|
*/
|
|
337
343
|
declare const compileMigrationsSql: (options: LunoraAuthOptions) => Promise<string>;
|
|
338
344
|
/**
|
|
@@ -378,9 +384,13 @@ declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
|
|
|
378
384
|
* });
|
|
379
385
|
* ```
|
|
380
386
|
*
|
|
381
|
-
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day
|
|
382
|
-
* -
|
|
383
|
-
*
|
|
387
|
+
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day,
|
|
388
|
+
* with a 60s signed-cookie session cache so bursts of authenticated calls
|
|
389
|
+
* skip the per-request DB session read.
|
|
390
|
+
* - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation,
|
|
391
|
+
* cookie cache **off** (fast revocation / short freshness is the whole point).
|
|
392
|
+
* - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation,
|
|
393
|
+
* with the same 60s cookie cache as `rolling`.
|
|
384
394
|
*/
|
|
385
395
|
declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
|
|
386
396
|
export { type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthCapabilities, type AuthInvitation, type AuthMember, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthTimestamp, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, type LunoraAuthOptions, type SessionPolicy, compileMigrationsSql, createAuthAdmin, ensureMigrated, handleAuthRequest, sessionPresets, validateSessionPolicy };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export { lunoraAuthAdapter, lunoraD1Adapter } from './adapter.mjs';
|
|
2
2
|
export { LunoraAuthAdminError, createAuthAdmin } from './packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs';
|
|
3
|
-
export { createAuth } from './packem_shared/createAuth-
|
|
3
|
+
export { createAuth, resolveAuthOptions } from './packem_shared/createAuth-CChKko05.mjs';
|
|
4
4
|
export { DEFAULT_AUTH_BASE_PATH, handleAuthRequest } from './packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs';
|
|
5
5
|
export { LunoraAuthHeadersError, withAuthPlugins } from './middleware.mjs';
|
|
6
|
-
export { compileMigrationsSql, ensureMigrated } from './packem_shared/compileMigrationsSql-
|
|
6
|
+
export { compileMigrationsSql, ensureMigrated } from './packem_shared/compileMigrationsSql-Cz0omU09.mjs';
|
|
7
7
|
export { default as authTables } from './schema.mjs';
|
|
8
|
-
export { sessionPresets, validateSessionPolicy } from './packem_shared/sessionPresets-
|
|
8
|
+
export { sessionPresets, validateSessionPolicy } from './packem_shared/sessionPresets-Dwwd74_J.mjs';
|
|
9
9
|
export { createSqlAuthStore, d1Executor } from './sql-store.mjs';
|
|
10
10
|
export { createMemoryAuthStore, matchesWhere } from './store.mjs';
|
|
11
11
|
export { TURNSTILE_VERIFY_ENDPOINT, verifyTurnstile } from './turnstile.mjs';
|
package/dist/middleware.d.mts
CHANGED
package/dist/middleware.d.ts
CHANGED
package/dist/packem_shared/{compileMigrationsSql-wZH3oXDu.mjs → compileMigrationsSql-Cz0omU09.mjs}
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getMigrations } from 'better-auth/db/migration';
|
|
2
|
+
import { resolveAuthOptions } from './createAuth-CChKko05.mjs';
|
|
2
3
|
|
|
3
4
|
const migrating = /* @__PURE__ */ new WeakMap();
|
|
4
5
|
const ensureMigrated = async (auth) => {
|
|
@@ -21,7 +22,7 @@ const ensureMigrated = async (auth) => {
|
|
|
21
22
|
}
|
|
22
23
|
};
|
|
23
24
|
const compileMigrationsSql = async (options) => {
|
|
24
|
-
const { compileMigrations } = await getMigrations(options);
|
|
25
|
+
const { compileMigrations } = await getMigrations(resolveAuthOptions(options));
|
|
25
26
|
return compileMigrations();
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
type LunoraAuthOptions = BetterAuthOptions;
|
|
45
|
+
/**
|
|
46
|
+
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
47
|
+
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
48
|
+
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
49
|
+
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
50
|
+
*/
|
|
51
|
+
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the caller's options into the exact shape `createAuth` hands to
|
|
54
|
+
* `betterAuth` — the hardened, default-filled options the running worker uses.
|
|
55
|
+
* Exported (and pure) so the migration path can compile the schema from the
|
|
56
|
+
* same resolved options: `compileMigrationsSql` routes through here, so the
|
|
57
|
+
* `rateLimit` table the worker's durable limiter writes to is included in the
|
|
58
|
+
* migration rather than silently omitted (it would be, if migrations saw the
|
|
59
|
+
* raw options while the worker ran the resolved ones).
|
|
60
|
+
*
|
|
61
|
+
* ## What it fills (each gated independently on caller silence)
|
|
62
|
+
*
|
|
63
|
+
* Secure-by-default cookies + secret-strength warning via {@link hardenAuthOptions},
|
|
64
|
+
* applied first so all hardening composes onto one options object.
|
|
65
|
+
*
|
|
66
|
+
* Rate limiting is ON by default for `/api/auth/*`.
|
|
67
|
+
*
|
|
68
|
+
* better-auth's own default is `rateLimit.enabled ?? isProduction`, and its
|
|
69
|
+
* `isProduction` is `"development" === "production"` resolved at
|
|
70
|
+
* module-load time. On Cloudflare Workers that check is unreliable: the
|
|
71
|
+
* runtime has no Node `process.env` (absent entirely without
|
|
72
|
+
* `nodejs_compat`, and even with it `NODE_ENV` is rarely `"production"` at
|
|
73
|
+
* request time). So better-auth would silently leave auth endpoints
|
|
74
|
+
* _unthrottled_ on a real deployment — the surprise we refuse to ship.
|
|
75
|
+
*
|
|
76
|
+
* We therefore default `enabled: true` whenever the caller hasn't made an
|
|
77
|
+
* explicit choice. We only fill the `enabled` flag and otherwise forward
|
|
78
|
+
* the caller's `rateLimit` verbatim, so better-auth's `window` (10s) / `max`
|
|
79
|
+
* (100) defaults and any custom rules still apply. Callers who genuinely
|
|
80
|
+
* want it off can pass `rateLimit: { enabled: false }` (e.g. when fronting
|
|
81
|
+
* auth with their own limiter), and any explicit `enabled` value wins.
|
|
82
|
+
*
|
|
83
|
+
* We also default `storage: "database"` — but only when rate limiting is not
|
|
84
|
+
* explicitly disabled (`enabled !== false`). Filling storage under a disabled
|
|
85
|
+
* limiter is harmless at runtime but makes `getAuthTables` emit an unused
|
|
86
|
+
* `rateLimit` table, so we skip it there.
|
|
87
|
+
*
|
|
88
|
+
* better-auth's own default is `storage: "memory"` — a per-isolate,
|
|
89
|
+
* non-durable counter. On Cloudflare Workers that means each isolate keeps
|
|
90
|
+
* its own tally, counters vanish on isolate recycle, and traffic spread
|
|
91
|
+
* across isolates never sums to the configured `max` — a limiter that
|
|
92
|
+
* reports "enabled" while never enforcing a global limit (the exact
|
|
93
|
+
* brute-force / credential-stuffing protection on `/sign-in`, OTP, and
|
|
94
|
+
* password-reset it is meant to buy). `storage: "database"` rides the counter
|
|
95
|
+
* through the configured `database` adapter — Lunora's store over the D1 auth
|
|
96
|
+
* tables — so the limit is durable *and* atomic (the store's native
|
|
97
|
+
* `incrementOne` gives a one-winner guarantee across isolates). Callers with
|
|
98
|
+
* their own durable store can pass an explicit `rateLimit: { storage: … }`
|
|
99
|
+
* (or `customStorage`), and any explicit value wins.
|
|
100
|
+
*
|
|
101
|
+
* Session cookie cache is ON by default too.
|
|
102
|
+
*
|
|
103
|
+
* Every authenticated call resolves identity through better-auth's
|
|
104
|
+
* `getSession`, which — without a cache — is a DB (D1) read on the hot path
|
|
105
|
+
* of every query/mutation/action that reads `ctx.auth` and of the WebSocket
|
|
106
|
+
* upgrade. better-auth's `session.cookieCache` carries the session payload in
|
|
107
|
+
* a short-lived signed cookie so `getSession` can answer without hitting the
|
|
108
|
+
* database until the cache window elapses. We default it on with a
|
|
109
|
+
* deliberately short 60s `maxAge` (better-auth's own default is 300s): long
|
|
110
|
+
* enough to erase the per-request read for a burst of calls, short enough
|
|
111
|
+
* that a revoked or role-changed session self-corrects within a minute.
|
|
112
|
+
* The one tradeoff — a revoked session stays valid until the cache expires —
|
|
113
|
+
* is bounded by that TTL; callers who need immediate revocation opt out with
|
|
114
|
+
* `session: { cookieCache: { enabled: false } }` (or the `strict` preset).
|
|
115
|
+
*
|
|
116
|
+
* Every explicit caller value is forwarded verbatim. The two `rateLimit` fills
|
|
117
|
+
* merge into a single `rateLimit` object so neither clobbers the other.
|
|
118
|
+
*/
|
|
119
|
+
declare const resolveAuthOptions: (options: LunoraAuthOptions) => LunoraAuthOptions;
|
|
120
|
+
/**
|
|
121
|
+
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
122
|
+
* the `secret` requirement at construction time so misconfigured deployments
|
|
123
|
+
* fail loudly at the first fetch rather than the first sign-in attempt, then
|
|
124
|
+
* hands {@link resolveAuthOptions}'s hardened, default-filled options to
|
|
125
|
+
* better-auth.
|
|
126
|
+
*/
|
|
127
|
+
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
128
|
+
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c, resolveAuthOptions as r };
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
type LunoraAuthOptions = BetterAuthOptions;
|
|
45
|
+
/**
|
|
46
|
+
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
47
|
+
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
48
|
+
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
49
|
+
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
50
|
+
*/
|
|
51
|
+
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the caller's options into the exact shape `createAuth` hands to
|
|
54
|
+
* `betterAuth` — the hardened, default-filled options the running worker uses.
|
|
55
|
+
* Exported (and pure) so the migration path can compile the schema from the
|
|
56
|
+
* same resolved options: `compileMigrationsSql` routes through here, so the
|
|
57
|
+
* `rateLimit` table the worker's durable limiter writes to is included in the
|
|
58
|
+
* migration rather than silently omitted (it would be, if migrations saw the
|
|
59
|
+
* raw options while the worker ran the resolved ones).
|
|
60
|
+
*
|
|
61
|
+
* ## What it fills (each gated independently on caller silence)
|
|
62
|
+
*
|
|
63
|
+
* Secure-by-default cookies + secret-strength warning via {@link hardenAuthOptions},
|
|
64
|
+
* applied first so all hardening composes onto one options object.
|
|
65
|
+
*
|
|
66
|
+
* Rate limiting is ON by default for `/api/auth/*`.
|
|
67
|
+
*
|
|
68
|
+
* better-auth's own default is `rateLimit.enabled ?? isProduction`, and its
|
|
69
|
+
* `isProduction` is `"development" === "production"` resolved at
|
|
70
|
+
* module-load time. On Cloudflare Workers that check is unreliable: the
|
|
71
|
+
* runtime has no Node `process.env` (absent entirely without
|
|
72
|
+
* `nodejs_compat`, and even with it `NODE_ENV` is rarely `"production"` at
|
|
73
|
+
* request time). So better-auth would silently leave auth endpoints
|
|
74
|
+
* _unthrottled_ on a real deployment — the surprise we refuse to ship.
|
|
75
|
+
*
|
|
76
|
+
* We therefore default `enabled: true` whenever the caller hasn't made an
|
|
77
|
+
* explicit choice. We only fill the `enabled` flag and otherwise forward
|
|
78
|
+
* the caller's `rateLimit` verbatim, so better-auth's `window` (10s) / `max`
|
|
79
|
+
* (100) defaults and any custom rules still apply. Callers who genuinely
|
|
80
|
+
* want it off can pass `rateLimit: { enabled: false }` (e.g. when fronting
|
|
81
|
+
* auth with their own limiter), and any explicit `enabled` value wins.
|
|
82
|
+
*
|
|
83
|
+
* We also default `storage: "database"` — but only when rate limiting is not
|
|
84
|
+
* explicitly disabled (`enabled !== false`). Filling storage under a disabled
|
|
85
|
+
* limiter is harmless at runtime but makes `getAuthTables` emit an unused
|
|
86
|
+
* `rateLimit` table, so we skip it there.
|
|
87
|
+
*
|
|
88
|
+
* better-auth's own default is `storage: "memory"` — a per-isolate,
|
|
89
|
+
* non-durable counter. On Cloudflare Workers that means each isolate keeps
|
|
90
|
+
* its own tally, counters vanish on isolate recycle, and traffic spread
|
|
91
|
+
* across isolates never sums to the configured `max` — a limiter that
|
|
92
|
+
* reports "enabled" while never enforcing a global limit (the exact
|
|
93
|
+
* brute-force / credential-stuffing protection on `/sign-in`, OTP, and
|
|
94
|
+
* password-reset it is meant to buy). `storage: "database"` rides the counter
|
|
95
|
+
* through the configured `database` adapter — Lunora's store over the D1 auth
|
|
96
|
+
* tables — so the limit is durable *and* atomic (the store's native
|
|
97
|
+
* `incrementOne` gives a one-winner guarantee across isolates). Callers with
|
|
98
|
+
* their own durable store can pass an explicit `rateLimit: { storage: … }`
|
|
99
|
+
* (or `customStorage`), and any explicit value wins.
|
|
100
|
+
*
|
|
101
|
+
* Session cookie cache is ON by default too.
|
|
102
|
+
*
|
|
103
|
+
* Every authenticated call resolves identity through better-auth's
|
|
104
|
+
* `getSession`, which — without a cache — is a DB (D1) read on the hot path
|
|
105
|
+
* of every query/mutation/action that reads `ctx.auth` and of the WebSocket
|
|
106
|
+
* upgrade. better-auth's `session.cookieCache` carries the session payload in
|
|
107
|
+
* a short-lived signed cookie so `getSession` can answer without hitting the
|
|
108
|
+
* database until the cache window elapses. We default it on with a
|
|
109
|
+
* deliberately short 60s `maxAge` (better-auth's own default is 300s): long
|
|
110
|
+
* enough to erase the per-request read for a burst of calls, short enough
|
|
111
|
+
* that a revoked or role-changed session self-corrects within a minute.
|
|
112
|
+
* The one tradeoff — a revoked session stays valid until the cache expires —
|
|
113
|
+
* is bounded by that TTL; callers who need immediate revocation opt out with
|
|
114
|
+
* `session: { cookieCache: { enabled: false } }` (or the `strict` preset).
|
|
115
|
+
*
|
|
116
|
+
* Every explicit caller value is forwarded verbatim. The two `rateLimit` fills
|
|
117
|
+
* merge into a single `rateLimit` object so neither clobbers the other.
|
|
118
|
+
*/
|
|
119
|
+
declare const resolveAuthOptions: (options: LunoraAuthOptions) => LunoraAuthOptions;
|
|
120
|
+
/**
|
|
121
|
+
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
122
|
+
* the `secret` requirement at construction time so misconfigured deployments
|
|
123
|
+
* fail loudly at the first fetch rather than the first sign-in attempt, then
|
|
124
|
+
* hands {@link resolveAuthOptions}'s hardened, default-filled options to
|
|
125
|
+
* better-auth.
|
|
126
|
+
*/
|
|
127
|
+
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
128
|
+
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c, resolveAuthOptions as r };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { betterAuth } from 'better-auth';
|
|
2
|
-
import { validateSessionPolicy } from './sessionPresets-
|
|
2
|
+
import { validateSessionPolicy } from './sessionPresets-Dwwd74_J.mjs';
|
|
3
3
|
|
|
4
4
|
const MIN_SECRET_LENGTH = 32;
|
|
5
5
|
const isWeakSecret = (secret) => {
|
|
@@ -39,6 +39,22 @@ const hardenAuthOptions = (options) => {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
const resolveAuthOptions = (options) => {
|
|
43
|
+
const hardened = hardenAuthOptions(options);
|
|
44
|
+
const needsRateLimitEnabled = hardened.rateLimit?.enabled === void 0;
|
|
45
|
+
const needsRateLimitStorage = hardened.rateLimit?.storage === void 0 && hardened.rateLimit?.enabled !== false;
|
|
46
|
+
return {
|
|
47
|
+
...hardened,
|
|
48
|
+
...needsRateLimitEnabled || needsRateLimitStorage ? {
|
|
49
|
+
rateLimit: {
|
|
50
|
+
...hardened.rateLimit,
|
|
51
|
+
...needsRateLimitEnabled ? { enabled: true } : {},
|
|
52
|
+
...needsRateLimitStorage ? { storage: "database" } : {}
|
|
53
|
+
}
|
|
54
|
+
} : {},
|
|
55
|
+
...hardened.session?.cookieCache === void 0 ? { session: { ...hardened.session, cookieCache: { enabled: true, maxAge: 60 } } } : {}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
42
58
|
const createAuth = (options) => {
|
|
43
59
|
if (!options.secret || options.secret.trim() === "") {
|
|
44
60
|
throw new Error(
|
|
@@ -48,9 +64,7 @@ const createAuth = (options) => {
|
|
|
48
64
|
if (options.session) {
|
|
49
65
|
validateSessionPolicy(options.session);
|
|
50
66
|
}
|
|
51
|
-
|
|
52
|
-
const resolvedOptions = hardened.rateLimit?.enabled === void 0 ? { ...hardened, rateLimit: { ...hardened.rateLimit, enabled: true } } : hardened;
|
|
53
|
-
return betterAuth(resolvedOptions);
|
|
67
|
+
return betterAuth(resolveAuthOptions(options));
|
|
54
68
|
};
|
|
55
69
|
|
|
56
|
-
export { createAuth };
|
|
70
|
+
export { createAuth, resolveAuthOptions };
|
|
@@ -16,16 +16,19 @@ const validateSessionPolicy = (policy) => {
|
|
|
16
16
|
};
|
|
17
17
|
const sessionPresets = {
|
|
18
18
|
longLived: {
|
|
19
|
+
cookieCache: { enabled: true, maxAge: 60 },
|
|
19
20
|
expiresIn: 30 * DAY,
|
|
20
21
|
freshAge: DAY,
|
|
21
22
|
updateAge: DAY
|
|
22
23
|
},
|
|
23
24
|
rolling: {
|
|
25
|
+
cookieCache: { enabled: true, maxAge: 60 },
|
|
24
26
|
expiresIn: 7 * DAY,
|
|
25
27
|
freshAge: DAY,
|
|
26
28
|
updateAge: DAY
|
|
27
29
|
},
|
|
28
30
|
strict: {
|
|
31
|
+
cookieCache: { enabled: false },
|
|
29
32
|
expiresIn: HOUR,
|
|
30
33
|
freshAge: 5 * MINUTE,
|
|
31
34
|
updateAge: 15 * MINUTE
|
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-
|
|
2
|
+
import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.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-
|
|
2
|
+
import { a as LunoraAuthOptions } from "./packem_shared/create-auth.d-Mwhb4gSc.js";
|
|
3
3
|
import 'better-auth';
|
|
4
4
|
/**
|
|
5
5
|
* Derive Lunora table definitions from a better-auth config — the bridge that
|
package/dist/sql-store.mjs
CHANGED
|
@@ -111,6 +111,28 @@ const createSqlAuthStore = (executor) => {
|
|
|
111
111
|
);
|
|
112
112
|
return { ...data };
|
|
113
113
|
},
|
|
114
|
+
incrementOne: async (model, where, increment, set) => {
|
|
115
|
+
const table = quoteId(model);
|
|
116
|
+
const incrementColumns = Object.keys(increment);
|
|
117
|
+
const setColumns = set ? Object.keys(set) : [];
|
|
118
|
+
const fragment = compileWhere(where);
|
|
119
|
+
if (incrementColumns.length === 0 && setColumns.length === 0) {
|
|
120
|
+
const [row2] = await executor.all(`SELECT * FROM ${table}${whereSuffix(fragment)} LIMIT 1`, fragment.params);
|
|
121
|
+
return row2;
|
|
122
|
+
}
|
|
123
|
+
const assignments = [
|
|
124
|
+
// COALESCE(col, 0) so a NULL counter advances from 0 rather than staying
|
|
125
|
+
// NULL (`NULL + ? = NULL` in SQLite/D1) — matches the memory store, which
|
|
126
|
+
// treats a non-numeric/absent counter as 0.
|
|
127
|
+
...incrementColumns.map((column) => `${quoteId(column)} = COALESCE(${quoteId(column)}, 0) + ?`),
|
|
128
|
+
...setColumns.map((column) => `${quoteId(column)} = ?`)
|
|
129
|
+
].join(", ");
|
|
130
|
+
const [row] = await executor.all(
|
|
131
|
+
`UPDATE ${table} SET ${assignments} WHERE rowid IN (SELECT rowid FROM ${table}${whereSuffix(fragment)} LIMIT 1) RETURNING *`,
|
|
132
|
+
[...incrementColumns.map((column) => increment[column]), ...setColumns.map((column) => set[column]), ...fragment.params]
|
|
133
|
+
);
|
|
134
|
+
return row;
|
|
135
|
+
},
|
|
114
136
|
read: async (model, query) => {
|
|
115
137
|
const fragment = compileWhere(query.where);
|
|
116
138
|
const parameters = [...fragment.params];
|
package/dist/store.d.mts
CHANGED
|
@@ -43,6 +43,24 @@ interface AuthStore {
|
|
|
43
43
|
count: (model: string, where: ReadonlyArray<AuthWhereClause>) => Promise<number>;
|
|
44
44
|
/** Insert `data` into `model`; return the stored row (the adapter pre-fills `id`). */
|
|
45
45
|
create: (model: string, data: AuthRow) => Promise<AuthRow>;
|
|
46
|
+
/**
|
|
47
|
+
* Atomically apply signed numeric deltas to **at most one** row in `model`
|
|
48
|
+
* matching `where`, then return the updated row (or `undefined` if the guard
|
|
49
|
+
* matched none). For each `increment` entry it applies `field = field + delta`
|
|
50
|
+
* (a negative delta decrements); the optional `set` map assigns absolute
|
|
51
|
+
* values in the same step. The `where` clause is both selector **and** guard
|
|
52
|
+
* — comparison operators are honoured, so a guard like
|
|
53
|
+
* `{ field: "count", operator: "lt", value: max }` only mutates the row while
|
|
54
|
+
* it still satisfies the predicate.
|
|
55
|
+
*
|
|
56
|
+
* Backs better-auth's durable (`storage: "database"`) rate limiter, whose
|
|
57
|
+
* counter rides these tables. Implementing it natively — one statement that
|
|
58
|
+
* guards, increments, and returns — gives the **one-winner-across-isolates**
|
|
59
|
+
* guarantee the read-then-update fallback cannot: on Workers two concurrent
|
|
60
|
+
* requests would otherwise both read `count=4` and both write `5`, letting a
|
|
61
|
+
* `max` of 5 pass 6+. Same race-closing rationale as {@link AuthStore.consumeOne}.
|
|
62
|
+
*/
|
|
63
|
+
incrementOne: (model: string, where: ReadonlyArray<AuthWhereClause>, increment: Record<string, number>, set?: AuthRow) => Promise<AuthRow | undefined>;
|
|
46
64
|
/** Read rows from `model` honouring the filter/sort/window in `query`. */
|
|
47
65
|
read: (model: string, query: AuthQuery) => Promise<AuthRow[]>;
|
|
48
66
|
/** Delete rows in `model` matching `where`; return how many were removed. */
|
package/dist/store.d.ts
CHANGED
|
@@ -43,6 +43,24 @@ interface AuthStore {
|
|
|
43
43
|
count: (model: string, where: ReadonlyArray<AuthWhereClause>) => Promise<number>;
|
|
44
44
|
/** Insert `data` into `model`; return the stored row (the adapter pre-fills `id`). */
|
|
45
45
|
create: (model: string, data: AuthRow) => Promise<AuthRow>;
|
|
46
|
+
/**
|
|
47
|
+
* Atomically apply signed numeric deltas to **at most one** row in `model`
|
|
48
|
+
* matching `where`, then return the updated row (or `undefined` if the guard
|
|
49
|
+
* matched none). For each `increment` entry it applies `field = field + delta`
|
|
50
|
+
* (a negative delta decrements); the optional `set` map assigns absolute
|
|
51
|
+
* values in the same step. The `where` clause is both selector **and** guard
|
|
52
|
+
* — comparison operators are honoured, so a guard like
|
|
53
|
+
* `{ field: "count", operator: "lt", value: max }` only mutates the row while
|
|
54
|
+
* it still satisfies the predicate.
|
|
55
|
+
*
|
|
56
|
+
* Backs better-auth's durable (`storage: "database"`) rate limiter, whose
|
|
57
|
+
* counter rides these tables. Implementing it natively — one statement that
|
|
58
|
+
* guards, increments, and returns — gives the **one-winner-across-isolates**
|
|
59
|
+
* guarantee the read-then-update fallback cannot: on Workers two concurrent
|
|
60
|
+
* requests would otherwise both read `count=4` and both write `5`, letting a
|
|
61
|
+
* `max` of 5 pass 6+. Same race-closing rationale as {@link AuthStore.consumeOne}.
|
|
62
|
+
*/
|
|
63
|
+
incrementOne: (model: string, where: ReadonlyArray<AuthWhereClause>, increment: Record<string, number>, set?: AuthRow) => Promise<AuthRow | undefined>;
|
|
46
64
|
/** Read rows from `model` honouring the filter/sort/window in `query`. */
|
|
47
65
|
read: (model: string, query: AuthQuery) => Promise<AuthRow[]>;
|
|
48
66
|
/** Delete rows in `model` matching `where`; return how many were removed. */
|
package/dist/store.mjs
CHANGED
|
@@ -128,6 +128,19 @@ const createMemoryAuthStore = () => {
|
|
|
128
128
|
tableOf(model).push(row);
|
|
129
129
|
return Promise.resolve({ ...row });
|
|
130
130
|
},
|
|
131
|
+
incrementOne: (model, where, increment, set) => {
|
|
132
|
+
const row = tableOf(model).find((candidate) => matchesWhere(candidate, where));
|
|
133
|
+
if (!row) {
|
|
134
|
+
return Promise.resolve(void 0);
|
|
135
|
+
}
|
|
136
|
+
for (const [field, delta] of Object.entries(increment)) {
|
|
137
|
+
row[field] = (typeof row[field] === "number" ? row[field] : 0) + delta;
|
|
138
|
+
}
|
|
139
|
+
if (set) {
|
|
140
|
+
Object.assign(row, set);
|
|
141
|
+
}
|
|
142
|
+
return Promise.resolve({ ...row });
|
|
143
|
+
},
|
|
131
144
|
read: (model, query) => {
|
|
132
145
|
let rows = tableOf(model).filter((row) => matchesWhere(row, query.where));
|
|
133
146
|
if (query.sortBy) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/auth",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.12",
|
|
4
4
|
"description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"@better-auth/passkey": "^1.6.22",
|
|
86
|
-
"@lunora/server": "1.0.0-alpha.
|
|
86
|
+
"@lunora/server": "1.0.0-alpha.11",
|
|
87
87
|
"@lunora/values": "1.0.0-alpha.3",
|
|
88
88
|
"better-auth": "^1.6.22"
|
|
89
89
|
},
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
type LunoraAuthOptions = BetterAuthOptions;
|
|
45
|
-
/**
|
|
46
|
-
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
47
|
-
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
48
|
-
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
49
|
-
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
50
|
-
*/
|
|
51
|
-
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
52
|
-
/**
|
|
53
|
-
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
54
|
-
* the `secret` requirement at construction time so misconfigured deployments
|
|
55
|
-
* fail loudly at the first fetch rather than the first sign-in attempt.
|
|
56
|
-
*/
|
|
57
|
-
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
58
|
-
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c };
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
type LunoraAuthOptions = BetterAuthOptions;
|
|
45
|
-
/**
|
|
46
|
-
* The full better-auth instance: `auth.handler` accepts a `Request` and
|
|
47
|
-
* returns a `Response` (used by `handleAuthRequest`); `auth.api`
|
|
48
|
-
* exposes the typed endpoint surface for server-side calls (e.g.
|
|
49
|
-
* `auth.api.getSession({ headers })` inside a query/mutation).
|
|
50
|
-
*/
|
|
51
|
-
type LunoraAuth = ReturnType<typeof betterAuth>;
|
|
52
|
-
/**
|
|
53
|
-
* Create the auth instance. Thin wrapper around `betterAuth` that enforces
|
|
54
|
-
* the `secret` requirement at construction time so misconfigured deployments
|
|
55
|
-
* fail loudly at the first fetch rather than the first sign-in attempt.
|
|
56
|
-
*/
|
|
57
|
-
declare const createAuth: (options: LunoraAuthOptions) => LunoraAuth;
|
|
58
|
-
export { LunoraAuth as L, LunoraAuthOptions as a, createAuth as c };
|