@lunora/auth 1.0.0-alpha.4 → 1.0.0-alpha.41

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.
Files changed (51) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +55 -0
  3. package/dist/adapter.d.mts +35 -35
  4. package/dist/adapter.d.ts +35 -35
  5. package/dist/adapter.mjs +1 -47
  6. package/dist/audit.d.mts +114 -0
  7. package/dist/audit.d.ts +114 -0
  8. package/dist/audit.mjs +11 -0
  9. package/dist/email-guard.d.mts +122 -0
  10. package/dist/email-guard.d.ts +122 -0
  11. package/dist/email-guard.mjs +1 -0
  12. package/dist/index.d.mts +460 -146
  13. package/dist/index.d.ts +460 -146
  14. package/dist/index.mjs +1 -12
  15. package/dist/middleware.d.mts +156 -155
  16. package/dist/middleware.d.ts +156 -155
  17. package/dist/middleware.mjs +1 -53
  18. package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs +1 -0
  19. package/dist/packem_shared/LunoraAuthAdminError-BiNYZM9j.mjs +1 -0
  20. package/dist/packem_shared/authAuditHook-Dx3sqf3G.mjs +1 -0
  21. package/dist/packem_shared/compileMigrationsSql-ChiudSmt.mjs +1 -0
  22. package/dist/packem_shared/create-auth.d-De6IOirt.d.mts +128 -0
  23. package/dist/packem_shared/create-auth.d-De6IOirt.d.ts +128 -0
  24. package/dist/packem_shared/createAuth-DS6PL8Mb.mjs +1 -0
  25. package/dist/packem_shared/emailGateDatabaseHooks-DzBD1Qoq.mjs +1 -0
  26. package/dist/packem_shared/sessionPresets-DpEFjXKV.mjs +1 -0
  27. package/dist/plugins-client.mjs +1 -2
  28. package/dist/plugins.mjs +1 -22
  29. package/dist/schema.d.mts +39 -39
  30. package/dist/schema.d.ts +39 -39
  31. package/dist/schema.mjs +1 -62
  32. package/dist/sql-store.d.mts +28 -28
  33. package/dist/sql-store.d.ts +28 -28
  34. package/dist/sql-store.mjs +1 -162
  35. package/dist/store.d.mts +49 -31
  36. package/dist/store.d.ts +49 -31
  37. package/dist/store.mjs +1 -170
  38. package/dist/turnstile-middleware.d.mts +55 -55
  39. package/dist/turnstile-middleware.d.ts +55 -55
  40. package/dist/turnstile-middleware.mjs +1 -45
  41. package/dist/turnstile.d.mts +42 -59
  42. package/dist/turnstile.d.ts +42 -59
  43. package/dist/turnstile.mjs +1 -61
  44. package/package.json +19 -6
  45. package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
  46. package/dist/packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs +0 -249
  47. package/dist/packem_shared/compileMigrationsSql-wZH3oXDu.mjs +0 -28
  48. package/dist/packem_shared/create-auth.d-M36jwG_Y.d.mts +0 -58
  49. package/dist/packem_shared/create-auth.d-M36jwG_Y.d.ts +0 -58
  50. package/dist/packem_shared/createAuth-B-tvsvQU.mjs +0 -56
  51. package/dist/packem_shared/sessionPresets-B95rXrd8.mjs +0 -35
@@ -1,11 +1,12 @@
1
- import { L as LunoraAuth } from "./packem_shared/create-auth.d-M36jwG_Y.js";
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { L as LunoraAuth } from "./packem_shared/create-auth.d-De6IOirt.js";
2
3
  import 'better-auth';
3
4
  /**
4
- * Structural mirror of `@lunora/server`'s `MiddlewareNext` — the continuation
5
- * callback handed to a middleware. Repeated here so this package does not take
6
- * a runtime dependency on `@lunora/server`; the types are assignable from the
7
- * fully-typed builder.
8
- */
5
+ * Structural mirror of `@lunora/server`'s `MiddlewareNext` — the continuation
6
+ * callback handed to a middleware. Repeated here so this package does not take
7
+ * a runtime dependency on `@lunora/server`; the types are assignable from the
8
+ * fully-typed builder.
9
+ */
9
10
  interface MiddlewareNext<ContextIn> {
10
11
  (): Promise<ContextIn>;
11
12
  <Extension extends Record<string, unknown>>(options: {
@@ -13,174 +14,174 @@ interface MiddlewareNext<ContextIn> {
13
14
  }): Promise<ContextIn & Extension>;
14
15
  }
15
16
  /**
16
- * Thrown by the runtime header guard when a privileged `ctx.authApi.*` endpoint
17
- * is invoked without a `headers` property on its argument object. Carries the
18
- * offending `method` name so callers can pinpoint the bad call site, and points
19
- * at the explicit escape hatches.
20
- *
21
- * This is the runtime sibling of the static `auth_api_call_without_headers`
22
- * advisor lint — both treat a header-less `ctx.authApi.*` call as an
23
- * authorization bypass, so a call that trips the lint also trips this guard.
24
- */
25
- declare class LunoraAuthHeadersError extends Error {
17
+ * Thrown by the runtime header guard when a privileged `ctx.authApi.*` endpoint
18
+ * is invoked without a `headers` property on its argument object. Carries the
19
+ * offending `method` name so callers can pinpoint the bad call site, and points
20
+ * at the explicit escape hatches.
21
+ *
22
+ * This is the runtime sibling of the static `auth_api_call_without_headers`
23
+ * advisor lint — both treat a header-less `ctx.authApi.*` call as an
24
+ * authorization bypass, so a call that trips the lint also trips this guard.
25
+ */
26
+ declare class LunoraAuthHeadersError extends LunoraError {
26
27
  /** The `ctx.authApi.&lt;method>` that was called without `headers`. */
27
28
  readonly method: string;
28
29
  constructor(method: string);
29
30
  }
30
31
  /**
31
- * Options for {@link withAuthPlugins}.
32
- */
32
+ * Options for {@link withAuthPlugins}.
33
+ */
33
34
  interface WithAuthPluginsOptions {
34
35
  /**
35
- * Whether to install the runtime header guard around `ctx.authApi`.
36
- *
37
- * **Defaults to `true` — the safe default.** When enabled, every
38
- * `ctx.authApi.&lt;method>(…)` call that omits `headers` throws
39
- * {@link LunoraAuthHeadersError} instead of silently running with full
40
- * server-to-server privileges. For a deliberate, per-call unauthenticated
41
- * invocation, use the explicit `ctx.authApi.withoutHeaders()` escape hatch
42
- * rather than disabling the guard wholesale.
43
- *
44
- * Set to `false` only when you have audited every `ctx.authApi.*` call site
45
- * and accept responsibility for passing headers yourself. This is the loud,
46
- * all-or-nothing opt-out; prefer `withoutHeaders()` for one-offs.
47
- */
36
+ * Whether to install the runtime header guard around `ctx.authApi`.
37
+ *
38
+ * **Defaults to `true` — the safe default.** When enabled, every
39
+ * `ctx.authApi.&lt;method>(…)` call that omits `headers` throws
40
+ * {@link LunoraAuthHeadersError} instead of silently running with full
41
+ * server-to-server privileges. For a deliberate, per-call unauthenticated
42
+ * invocation, use the explicit `ctx.authApi.withoutHeaders()` escape hatch
43
+ * rather than disabling the guard wholesale.
44
+ *
45
+ * Set to `false` only when you have audited every `ctx.authApi.*` call site
46
+ * and accept responsibility for passing headers yourself. This is the loud,
47
+ * all-or-nothing opt-out; prefer `withoutHeaders()` for one-offs.
48
+ */
48
49
  enforceHeaders?: boolean;
49
50
  }
50
51
  /**
51
- * The Lunora context extension this middleware installs. It does *not* replace
52
- * `ctx.auth` (the identity-only surface populated by the runtime — `userId` and
53
- * `getIdentity()`); instead it adds a sibling `ctx.authApi` that points at the
54
- * full better-auth plugin API surface.
55
- *
56
- * The shape of `authApi` is the shape of the better-auth instance's `api` —
57
- * a flat record of endpoint functions like `createOrganization`, `banUser`,
58
- * `listMembers`, … contributed by whichever plugins are configured on the auth
59
- * instance. Because the type is `Auth["api"]` and `LunoraAuth` is generic over
60
- * the auth instance, callers get end-to-end inference: the endpoints they see
61
- * are exactly the ones their auth instance loaded.
62
- */
52
+ * The Lunora context extension this middleware installs. It does *not* replace
53
+ * `ctx.auth` (the identity-only surface populated by the runtime — `userId` and
54
+ * `getIdentity()`); instead it adds a sibling `ctx.authApi` that points at the
55
+ * full better-auth plugin API surface.
56
+ *
57
+ * The shape of `authApi` is the shape of the better-auth instance's `api` —
58
+ * a flat record of endpoint functions like `createOrganization`, `banUser`,
59
+ * `listMembers`, … contributed by whichever plugins are configured on the auth
60
+ * instance. Because the type is `Auth["api"]` and `LunoraAuth` is generic over
61
+ * the auth instance, callers get end-to-end inference: the endpoints they see
62
+ * are exactly the ones their auth instance loaded.
63
+ */
63
64
  interface LunoraAuthApiContext<Auth extends LunoraAuth> {
64
65
  /**
65
- * The better-auth endpoint surface — every endpoint contributed by every
66
- * plugin configured on the auth instance, ready to call directly:
67
- *
68
- * ```ts
69
- * await ctx.authApi.createOrganization({ body: { name: "Acme" }, headers });
70
- * await ctx.authApi.banUser({ body: { userId: "u_1" }, headers });
71
- * ```
72
- *
73
- * # ⚠️ SECURITY: privileged surface — you MUST pass `headers`
74
- *
75
- * This is the **full, privileged** better-auth API (`auth.api`) — it
76
- * includes admin/management endpoints such as `banUser`, `setRole`,
77
- * impersonation, `createOrganization`, `removeMember`, … better-auth
78
- * authorizes these calls from the caller's session in the `headers` you
79
- * pass. Invoked **without** `headers`, better-auth treats the call as a
80
- * trusted server-side invocation and **bypasses session authorization
81
- * entirely** — any procedure that can reach `ctx.authApi` could then ban a
82
- * user, escalate a role, or read another tenant's data.
83
- *
84
- * To stop that bypass at runtime, `withAuthPlugins` installs a guard around
85
- * `ctx.authApi` by default: a header-less call to any endpoint throws
86
- * {@link LunoraAuthHeadersError} rather than running with full privileges.
87
- * The same `auth_api_call_without_headers` advisor lint catches it
88
- * statically; the guard is the runtime backstop for the cases the lint
89
- * can't see (dynamic method names, indirected calls). For the rare,
90
- * deliberate unauthenticated server-to-server call, opt out explicitly with
91
- * `ctx.authApi.withoutHeaders().&lt;method>(…)`.
92
- *
93
- * Lunora's procedure context carries only the resolved identity, not the
94
- * raw inbound `Headers`, so this middleware CANNOT pre-bind them for you.
95
- * Therefore: **thread the inbound `Headers` into every `ctx.authApi.*`
96
- * call** (typically from an HTTP action — see {@link withAuthPlugins}). A
97
- * header-less call is an authorization bypass, not a convenience.
98
- */
66
+ * The better-auth endpoint surface — every endpoint contributed by every
67
+ * plugin configured on the auth instance, ready to call directly:
68
+ *
69
+ * ```ts
70
+ * await ctx.authApi.createOrganization({ body: { name: "Acme" }, headers });
71
+ * await ctx.authApi.banUser({ body: { userId: "u_1" }, headers });
72
+ * ```
73
+ *
74
+ * # ⚠️ SECURITY: privileged surface — you MUST pass `headers`
75
+ *
76
+ * This is the **full, privileged** better-auth API (`auth.api`) — it
77
+ * includes admin/management endpoints such as `banUser`, `setRole`,
78
+ * impersonation, `createOrganization`, `removeMember`, … better-auth
79
+ * authorizes these calls from the caller's session in the `headers` you
80
+ * pass. Invoked **without** `headers`, better-auth treats the call as a
81
+ * trusted server-side invocation and **bypasses session authorization
82
+ * entirely** — any procedure that can reach `ctx.authApi` could then ban a
83
+ * user, escalate a role, or read another tenant's data.
84
+ *
85
+ * To stop that bypass at runtime, `withAuthPlugins` installs a guard around
86
+ * `ctx.authApi` by default: a header-less call to any endpoint throws
87
+ * {@link LunoraAuthHeadersError} rather than running with full privileges.
88
+ * The same `auth_api_call_without_headers` advisor lint catches it
89
+ * statically; the guard is the runtime backstop for the cases the lint
90
+ * can't see (dynamic method names, indirected calls). For the rare,
91
+ * deliberate unauthenticated server-to-server call, opt out explicitly with
92
+ * `ctx.authApi.withoutHeaders().&lt;method>(…)`.
93
+ *
94
+ * Lunora's procedure context carries only the resolved identity, not the
95
+ * raw inbound `Headers`, so this middleware CANNOT pre-bind them for you.
96
+ * Therefore: **thread the inbound `Headers` into every `ctx.authApi.*`
97
+ * call** (typically from an HTTP action — see {@link withAuthPlugins}). A
98
+ * header-less call is an authorization bypass, not a convenience.
99
+ */
99
100
  readonly authApi: {
100
101
  /**
101
- * Explicit, loud escape hatch from the runtime header guard. Returns
102
- * the raw, **unguarded** `auth.api` surface — every endpoint reached
103
- * through it runs as a trusted server-to-server call with session
104
- * authorization skipped.
105
- *
106
- * ```ts
107
- * // A scheduled job with no inbound request that must create the
108
- * // system org. Audited and intentional:
109
- * await ctx.authApi.withoutHeaders().createOrganization({ body: { name } });
110
- * ```
111
- *
112
- * Use only for deliberate, audited unauthenticated calls. For ordinary
113
- * request-driven calls, pass `headers` so authorization is enforced.
114
- */
102
+ * Explicit, loud escape hatch from the runtime header guard. Returns
103
+ * the raw, **unguarded** `auth.api` surface — every endpoint reached
104
+ * through it runs as a trusted server-to-server call with session
105
+ * authorization skipped.
106
+ *
107
+ * ```ts
108
+ * // A scheduled job with no inbound request that must create the
109
+ * // system org. Audited and intentional:
110
+ * await ctx.authApi.withoutHeaders().createOrganization({ body: { name } });
111
+ * ```
112
+ *
113
+ * Use only for deliberate, audited unauthenticated calls. For ordinary
114
+ * request-driven calls, pass `headers` so authorization is enforced.
115
+ */
115
116
  withoutHeaders: () => Auth["api"];
116
117
  } & Auth["api"];
117
118
  }
118
119
  /**
119
- * Build a Lunora middleware that mounts a better-auth instance's plugin API
120
- * onto `ctx.authApi`. Compose it with `.use(...)` once per builder and every
121
- * downstream handler gets typed access to the plugin endpoints — no more
122
- * importing the auth instance directly from every query/mutation file.
123
- *
124
- * # ⚠️ SECURITY: headers are load-bearing for authorization
125
- *
126
- * `ctx.authApi` is the **full privileged** better-auth surface (`auth.api`):
127
- * `banUser`, `setRole`, impersonation, `createOrganization`, `removeMember`,
128
- * and so on. better-auth authorizes these from the caller's session carried in
129
- * the `headers` you pass. **Called without `headers`, better-auth treats the
130
- * invocation as a trusted server-side call and skips session authorization
131
- * altogether** — so a header-less `ctx.authApi.banUser(...)` from any procedure
132
- * runs with full privileges regardless of who the caller is. This is an
133
- * authorization bypass, not just a missing convenience.
134
- *
135
- * To make that bypass fail loudly instead of silently, this middleware wraps
136
- * `ctx.authApi` in a **runtime header guard by default**: any endpoint called
137
- * without a `headers` property throws {@link LunoraAuthHeadersError}. The guard
138
- * mirrors the static `auth_api_call_without_headers` advisor lint exactly, so
139
- * the two agree on what counts as a header-bearing call.
140
- *
141
- * - **Default (safe):** `withAuthPlugins(auth)` — header-less calls throw.
142
- * - **Per-call opt-out (preferred):** `ctx.authApi.withoutHeaders().banUser(…)`
143
- * for a deliberate, audited unauthenticated server-to-server call.
144
- * - **Whole-middleware opt-out (loud):** `withAuthPlugins(auth, { enforceHeaders: false })`
145
- * disables the guard entirely; only do this once every call site is audited.
146
- *
147
- * Lunora's procedure context does not currently carry the raw request headers
148
- * (only the resolved identity — see `AuthState` in `@lunora/server`), so
149
- * this middleware **cannot** pre-bind headers for you and does **not** do so.
150
- * You MUST pass the inbound `Headers` explicitly into **every** `ctx.authApi.*`
151
- * call, from a transport that has them — typically an HTTP action:
152
- *
153
- * ```ts
154
- * // lunora/orgs.ts
155
- * import { httpAction } from "@lunora/server";
156
- * import { withAuthPlugins } from "@lunora/auth/middleware";
157
- * import { auth } from "./auth.js";
158
- *
159
- * export const createOrg = httpAction(async (ctx, request) => {
160
- * const { name } = await request.json();
161
- *
162
- * // ctx.authApi is installed by withAuthPlugins(auth) on the builder.
163
- * const org = await ctx.authApi.createOrganization({
164
- * body: { name },
165
- * headers: request.headers,
166
- * });
167
- *
168
- * return Response.json(org);
169
- * });
170
- * ```
171
- *
172
- * For internal server-to-server calls where there is no inbound request
173
- * (e.g. a scheduled job that creates the system org), opt out explicitly with
174
- * `ctx.authApi.withoutHeaders()` and authenticate with whatever bearer token
175
- * your auth instance is configured to honour.
176
- */
120
+ * Build a Lunora middleware that mounts a better-auth instance's plugin API
121
+ * onto `ctx.authApi`. Compose it with `.use(...)` once per builder and every
122
+ * downstream handler gets typed access to the plugin endpoints — no more
123
+ * importing the auth instance directly from every query/mutation file.
124
+ *
125
+ * # ⚠️ SECURITY: headers are load-bearing for authorization
126
+ *
127
+ * `ctx.authApi` is the **full privileged** better-auth surface (`auth.api`):
128
+ * `banUser`, `setRole`, impersonation, `createOrganization`, `removeMember`,
129
+ * and so on. better-auth authorizes these from the caller's session carried in
130
+ * the `headers` you pass. **Called without `headers`, better-auth treats the
131
+ * invocation as a trusted server-side call and skips session authorization
132
+ * altogether** — so a header-less `ctx.authApi.banUser(...)` from any procedure
133
+ * runs with full privileges regardless of who the caller is. This is an
134
+ * authorization bypass, not just a missing convenience.
135
+ *
136
+ * To make that bypass fail loudly instead of silently, this middleware wraps
137
+ * `ctx.authApi` in a **runtime header guard by default**: any endpoint called
138
+ * without a `headers` property throws {@link LunoraAuthHeadersError}. The guard
139
+ * mirrors the static `auth_api_call_without_headers` advisor lint exactly, so
140
+ * the two agree on what counts as a header-bearing call.
141
+ *
142
+ * - **Default (safe):** `withAuthPlugins(auth)` — header-less calls throw.
143
+ * - **Per-call opt-out (preferred):** `ctx.authApi.withoutHeaders().banUser(…)`
144
+ * for a deliberate, audited unauthenticated server-to-server call.
145
+ * - **Whole-middleware opt-out (loud):** `withAuthPlugins(auth, { enforceHeaders: false })`
146
+ * disables the guard entirely; only do this once every call site is audited.
147
+ *
148
+ * Lunora's procedure context does not currently carry the raw request headers
149
+ * (only the resolved identity — see `AuthState` in `@lunora/server`), so
150
+ * this middleware **cannot** pre-bind headers for you and does **not** do so.
151
+ * You MUST pass the inbound `Headers` explicitly into **every** `ctx.authApi.*`
152
+ * call, from a transport that has them — typically an HTTP action:
153
+ *
154
+ * ```ts
155
+ * // lunora/orgs.ts
156
+ * import { httpAction } from "@lunora/server";
157
+ * import { withAuthPlugins } from "@lunora/auth/middleware";
158
+ * import { auth } from "./auth.js";
159
+ *
160
+ * export const createOrg = httpAction(async (ctx, request) => {
161
+ * const { name } = await request.json();
162
+ *
163
+ * // ctx.authApi is installed by withAuthPlugins(auth) on the builder.
164
+ * const org = await ctx.authApi.createOrganization({
165
+ * body: { name },
166
+ * headers: request.headers,
167
+ * });
168
+ *
169
+ * return Response.json(org);
170
+ * });
171
+ * ```
172
+ *
173
+ * For internal server-to-server calls where there is no inbound request
174
+ * (e.g. a scheduled job that creates the system org), opt out explicitly with
175
+ * `ctx.authApi.withoutHeaders()` and authenticate with whatever bearer token
176
+ * your auth instance is configured to honour.
177
+ */
177
178
  /**
178
- * Shape of the middleware {@link withAuthPlugins} returns: a callable generic
179
- * over the incoming ctx so chaining `.use(...)` preserves whatever ctx fields
180
- * the upstream middleware already installed. Lives as its own interface
181
- * because TypeScript doesn't allow declaring `const fn: &lt;CtxIn>() => ...` —
182
- * the generic must live on a callable type alias or interface.
183
- */
179
+ * Shape of the middleware {@link withAuthPlugins} returns: a callable generic
180
+ * over the incoming ctx so chaining `.use(...)` preserves whatever ctx fields
181
+ * the upstream middleware already installed. Lives as its own interface
182
+ * because TypeScript doesn't allow declaring `const fn: &lt;CtxIn>() => ...` —
183
+ * the generic must live on a callable type alias or interface.
184
+ */
184
185
  type WithAuthPluginsMiddleware<Auth extends LunoraAuth> = <ContextIn>(options: {
185
186
  ctx: ContextIn;
186
187
  next: MiddlewareNext<ContextIn>;
@@ -1,53 +1 @@
1
- const callHasHeaders = (argument) => {
2
- if (argument === void 0) {
3
- return false;
4
- }
5
- if (typeof argument !== "object" || argument === null) {
6
- return true;
7
- }
8
- const { headers } = argument;
9
- return headers !== void 0 && headers !== null;
10
- };
11
- const guardAuthApi = (api) => {
12
- const withoutHeaders = () => api;
13
- return /* @__PURE__ */ new Proxy(api, {
14
- // eslint-disable-next-line sonarjs/function-return-type -- a Proxy `get` trap is intrinsically polymorphic: it returns the synthetic `withoutHeaders`, the guarded endpoint wrapper, or any passthrough property value
15
- get(target, property, receiver) {
16
- if (property === "withoutHeaders" && !(property in target)) {
17
- return withoutHeaders;
18
- }
19
- const value = Reflect.get(target, property, receiver);
20
- if (typeof value !== "function" || typeof property !== "string") {
21
- return value;
22
- }
23
- const method = property;
24
- return (...arguments_) => {
25
- if (!callHasHeaders(arguments_[0])) {
26
- return Promise.reject(new LunoraAuthHeadersError(method));
27
- }
28
- return Reflect.apply(value, target, arguments_);
29
- };
30
- }
31
- });
32
- };
33
- class LunoraAuthHeadersError extends Error {
34
- /** The `ctx.authApi.&lt;method>` that was called without `headers`. */
35
- method;
36
- constructor(method) {
37
- super(
38
- `@lunora/auth: ctx.authApi.${method}(…) was called without \`headers\`. better-auth treats a header-less call as a trusted server-to-server invocation and skips session authorization entirely — an authorization bypass. Pass the inbound request headers: ctx.authApi.${method}({ body, headers: request.headers }). If you genuinely intend an unauthenticated server-to-server call, opt out explicitly via ctx.authApi.withoutHeaders().<method>(…), or disable the guard for the whole middleware with withAuthPlugins(auth, { enforceHeaders: false }).`
39
- );
40
- this.name = "LunoraAuthHeadersError";
41
- this.method = method;
42
- }
43
- }
44
- const withAuthPlugins = (auth, options = {}) => {
45
- const enforceHeaders = options.enforceHeaders ?? true;
46
- const authApi = enforceHeaders ? guardAuthApi(auth.api) : auth.api;
47
- return async ({ next }) => {
48
- const extended = await next({ ctx: { authApi } });
49
- return extended;
50
- };
51
- };
52
-
53
- export { LunoraAuthHeadersError, withAuthPlugins };
1
+ import{LunoraError as i}from"@lunora/errors";const h=e=>{if(e===void 0)return!1;if(typeof e!="object"||e===null)return!0;const{headers:t}=e;return t!=null},d=e=>{const t=()=>e;return new Proxy(e,{get(a,r,o){if(r==="withoutHeaders"&&!(r in a))return t;const s=Reflect.get(a,r,o);if(typeof s!="function"||typeof r!="string")return s;const u=r;return(...n)=>h(n[0])?Reflect.apply(s,a,n):Promise.reject(new c(u))}})};class c extends i{method;constructor(t){super("AUTH_HEADERS_MISSING",`@lunora/auth: ctx.authApi.${t}(…) was called without \`headers\`. better-auth treats a header-less call as a trusted server-to-server invocation and skips session authorization entirely — an authorization bypass. Pass the inbound request headers: ctx.authApi.${t}({ body, headers: request.headers }). If you genuinely intend an unauthenticated server-to-server call, opt out explicitly via ctx.authApi.withoutHeaders().<method>(…), or disable the guard for the whole middleware with withAuthPlugins(auth, { enforceHeaders: false }).`,{name:"LunoraAuthHeadersError"}),this.method=t}}const p=(e,t={})=>{const a=t.enforceHeaders??!0?d(e.api):e.api;return async({next:r})=>await r({ctx:{authApi:a}})};export{c as LunoraAuthHeadersError,p as withAuthPlugins};
@@ -0,0 +1 @@
1
+ const s="/api/auth",r=async(h,t,n=s)=>{const a=new URL(t.url),e=n.endsWith("/")?n.slice(0,-1):n;if(!(a.pathname!==e&&!a.pathname.startsWith(`${e}/`)))return h.handler(t)};export{s as DEFAULT_AUTH_BASE_PATH,r as handleAuthRequest};
@@ -0,0 +1 @@
1
+ import{LunoraError as S}from"@lunora/errors";import{getAuthTables as A}from"better-auth/db";class p extends S{constructor(u,w){super(w,u,{name:"LunoraAuthAdminError"})}}const O=50,M=500,z=3600,R=z*24,D=100*365*24*60*60,U=2880*60*1e3,N=new Set(["accessToken","backupCodes","idToken","password","publicKey","refreshToken","secret","token"]),k=s=>Math.min(Math.max(Math.trunc(s??O),1),M),T=s=>Math.max(0,Math.trunc(s??0)),c=s=>{const u={};for(const[w,m]of Object.entries(s))N.has(w)||(u[w]=m instanceof Date?m.getTime():m);return u},I=s=>Array.isArray(s)?s.join(","):s,b=s=>s.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),x=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),E={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},_=s=>s==="boolean"?"boolean":s==="date"?"date":s==="number"?"number":"string",L=s=>{const u=[];for(const[w,m]of Object.entries(s))m.input===!1||m.references!==void 0||x.has(w)||u.push({name:w,plugin:E[w],required:m.required===!0,type:_(m.type),unique:m.unique===!0});return u},P=s=>{if(s instanceof p)return s;const u=s,w=u?.body?.code??u?.code??"AUTH_ADMIN_ERROR",m=u?.body?.message??u?.message??"auth admin operation failed";return new p(m,w)},F=(s,u={})=>{const w=s.$context,m=u.features??{},y=e=>{const a=new Set((e.plugins??[]).map(i=>i.id)),t=i=>a.has(i);return{accounts:m.accounts??!0,admin:m.admin??t("admin"),organization:m.organization??t("organization"),passkey:m.passkey??t("passkey"),twoFactor:m.twoFactor??t("two-factor")}},r=async e=>{try{return await e(await w)}catch(a){throw P(a)}},h=e=>c(e),g=async(e,a,t)=>{const i=t.where&&t.where.length>0?t.where:void 0,[n,o]=await Promise.all([e.adapter.findMany({limit:k(t.limit),model:a,offset:T(t.offset),sortBy:t.sortBy,where:i}),e.adapter.count({model:a,where:i})]);return{rows:n.map(d=>c(d)),total:o}};return{banUser:({expiresInSeconds:e,reason:a,userId:t})=>r(async i=>{let n=null;if(e!==void 0){if(!Number.isInteger(e)||e<=0)throw new p("expiresInSeconds must be a positive finite integer","INVALID_BAN_SECONDS");const d=Math.min(e,D);n=new Date(Date.now()+d*1e3)}const o=await i.internalAdapter.updateUser(t,{banExpires:n,banned:!0,banReason:a??"No reason"});return await i.internalAdapter.deleteUserSessions(t),h(o)}),cancelInvitation:({invitationId:e})=>r(async a=>{await a.adapter.delete({model:"invitation",where:[{field:"id",value:e}]})}),capabilities:()=>r(e=>Promise.resolve(y(e.options))),addMember:({organizationId:e,role:a,userId:t})=>r(async i=>{const n=await i.adapter.create({data:{createdAt:new Date,organizationId:e,role:a===void 0||a===""?"member":a,userId:t},model:"member"});return c(n)}),addTeamMember:({teamId:e,userId:a})=>r(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,teamId:e,userId:a},model:"teamMember"});return c(i)}),config:()=>r(e=>{const a=e.options,t=y(a),i=new Set((a.plugins??[]).map(l=>l.id)),n=A(a),o=a.session??{},d=a.rateLimit??{};return Promise.resolve({capabilities:t,emailAndPassword:a.emailAndPassword?.enabled??!1,organization:{enabled:t.organization,roles:!!n.organizationRole,teams:!!n.team},plugins:[...i].toSorted((l,f)=>l.localeCompare(f)),rateLimit:{enabled:d.enabled??!1,max:d.max,window:d.window},session:{cookieCache:o.cookieCache?.enabled,expiresIn:o.expiresIn,freshAge:o.freshAge,updateAge:o.updateAge},socialProviders:Object.keys(a.socialProviders??{}).toSorted((l,f)=>l.localeCompare(f)),userFields:L(n.user?.fields??{})})}),createOrganization:({logo:e,metadata:a,name:t,ownerId:i,slug:n})=>r(async o=>{const d=b(n!==void 0&&n!==""?n:t);if(d==="")throw new p("could not derive a slug from the organization name","ORG_SLUG_INVALID");if(await o.adapter.findOne({model:"organization",where:[{field:"slug",value:d}]}))throw new p("an organization with this slug already exists","ORG_SLUG_TAKEN");const l=await o.adapter.create({data:{createdAt:new Date,logo:e===void 0||e===""?void 0:e,metadata:a===void 0?void 0:JSON.stringify(a),name:t,slug:d},model:"organization"});return i!==void 0&&i!==""&&await o.adapter.create({data:{createdAt:new Date,organizationId:l.id,role:"owner",userId:i},model:"member"}),c(l)}),createOrgRole:({organizationId:e,permission:a,role:t})=>r(async i=>{const n=await i.adapter.create({data:{createdAt:new Date,organizationId:e,permission:JSON.stringify(a),role:t},model:"organizationRole"});return c(n)}),createTeam:({name:e,organizationId:a})=>r(async t=>{const i=await t.adapter.create({data:{createdAt:new Date,name:e,organizationId:a},model:"team"});return c(i)}),deleteOrganization:({organizationId:e})=>r(async a=>{const t=A(a.options);if(await a.adapter.deleteMany({model:"member",where:[{field:"organizationId",value:e}]}),await a.adapter.deleteMany({model:"invitation",where:[{field:"organizationId",value:e}]}),t.team){const i=await a.adapter.findMany({model:"team",where:[{field:"organizationId",value:e}]});for(const n of i)await a.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:n.id}]});await a.adapter.deleteMany({model:"team",where:[{field:"organizationId",value:e}]})}t.organizationRole&&await a.adapter.deleteMany({model:"organizationRole",where:[{field:"organizationId",value:e}]}),await a.adapter.delete({model:"organization",where:[{field:"id",value:e}]})}),deleteOrgRole:({roleId:e})=>r(async a=>{await a.adapter.delete({model:"organizationRole",where:[{field:"id",value:e}]})}),inviteMember:({email:e,inviterId:a,organizationId:t,role:i})=>r(async n=>{let o=a;if(o===void 0||o===""){const l=await n.adapter.findMany({model:"member",where:[{field:"organizationId",value:t}]});o=(l.find(f=>typeof f.role=="string"&&f.role.includes("owner"))??l[0])?.userId}if(o===void 0||o==="")throw new p("provide an inviter — the organization has no members to attribute the invitation to","INVITER_REQUIRED");const d=await n.adapter.create({data:{createdAt:new Date,email:e.toLowerCase(),expiresAt:new Date(Date.now()+U),inviterId:o,organizationId:t,role:i===void 0||i===""?"member":i,status:"pending"},model:"invitation"});return c(d)}),listOrgRoles:({limit:e,offset:a,organizationId:t})=>r(i=>g(i,"organizationRole",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listTeamMembers:({limit:e,offset:a,teamId:t})=>r(i=>g(i,"teamMember",{limit:e,offset:a,where:[{field:"teamId",value:t}]})),listTeams:({limit:e,offset:a,organizationId:t})=>r(i=>g(i,"team",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),removeTeam:({teamId:e})=>r(async a=>{await a.adapter.deleteMany({model:"teamMember",where:[{field:"teamId",value:e}]}),await a.adapter.delete({model:"team",where:[{field:"id",value:e}]})}),removeTeamMember:({teamMemberId:e})=>r(async a=>{await a.adapter.delete({model:"teamMember",where:[{field:"id",value:e}]})}),updateMemberRole:({memberId:e,role:a})=>r(async t=>{const i=await t.adapter.update({model:"member",update:{role:I(a)},where:[{field:"id",value:e}]});return c(i??{id:e,role:I(a)})}),updateOrganization:({logo:e,metadata:a,name:t,organizationId:i,slug:n})=>r(async o=>{const d={};if(t!==void 0&&(d.name=t),n!==void 0&&n!==""&&(d.slug=b(n)),e!==void 0&&(d.logo=e===""?void 0:e),a!==void 0&&(d.metadata=JSON.stringify(a)),Object.keys(d).length===0)return c({id:i});const l=await o.adapter.update({model:"organization",update:d,where:[{field:"id",value:i}]});return c(l??{id:i})}),updateOrgRole:({permission:e,roleId:a})=>r(async t=>{const i=await t.adapter.update({model:"organizationRole",update:{permission:JSON.stringify(e),updatedAt:new Date},where:[{field:"id",value:a}]});return c(i??{id:a,permission:JSON.stringify(e)})}),updateTeam:({name:e,teamId:a})=>r(async t=>{const i=await t.adapter.update({model:"team",update:{name:e,updatedAt:new Date},where:[{field:"id",value:a}]});return c(i??{id:a,name:e})}),createUser:({data:e,email:a,name:t,password:i,role:n})=>r(async o=>{const d=a.toLowerCase();if(await o.internalAdapter.findUserByEmail(d))throw new p("a user with this email already exists","USER_ALREADY_EXISTS");const l=await o.internalAdapter.createUser({email:d,name:t,role:n===void 0?void 0:I(n),...e});if(i!==void 0&&i!==""){const f=await o.password.hash(i);await o.internalAdapter.linkAccount({accountId:l.id,password:f,providerId:"credential",userId:l.id})}return h(l)}),deletePasskey:({passkeyId:e})=>r(async a=>{await a.adapter.delete({model:"passkey",where:[{field:"id",value:e}]})}),disableTwoFactor:({userId:e})=>r(async a=>{await a.adapter.deleteMany({model:"twoFactor",where:[{field:"userId",value:e}]}),await a.internalAdapter.updateUser(e,{twoFactorEnabled:!1})}),impersonateUser:({userId:e})=>r(async a=>{const t=await a.internalAdapter.findUserById(e);if(!t)throw new p("user not found","USER_NOT_FOUND");const i=u.impersonationSeconds;let n=z;if(i!==void 0){if(!Number.isInteger(i)||!Number.isFinite(i)||i<=0)throw new p("impersonationSeconds must be a positive finite integer","INVALID_IMPERSONATION_SECONDS");n=Math.min(i,R)}const o=new Date(Date.now()+n*1e3),d=await a.internalAdapter.createSession(e,!0,{expiresAt:o,impersonatedBy:u.impersonatedBy??e},!0);return{expiresAt:d.expiresAt instanceof Date?d.expiresAt.getTime():o.getTime(),token:d.token,user:h(t)}}),listAccounts:({userId:e})=>r(async a=>(await a.adapter.findMany({model:"account",where:[{field:"userId",value:e}]})).map(t=>c(t))),listInvitations:({limit:e,offset:a,organizationId:t})=>r(i=>g(i,"invitation",{limit:e,offset:a,where:[{field:"organizationId",value:t}]})),listMembers:({limit:e,offset:a,organizationId:t})=>r(i=>g(i,"member",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:[{field:"organizationId",value:t}]})),listOrganizations:({limit:e,offset:a})=>r(t=>g(t,"organization",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"}})),listPasskeys:({userId:e})=>r(async a=>(await a.adapter.findMany({model:"passkey",where:[{field:"userId",value:e}]})).map(t=>c(t))),listSessions:({limit:e,offset:a,userId:t})=>r(i=>g(i,"session",{limit:e,offset:a,sortBy:{direction:"desc",field:"createdAt"},where:t===void 0||t===""?void 0:[{field:"userId",value:t}]})),listUsers:({filterField:e,filterValue:a,limit:t,offset:i,search:n,searchField:o,sortBy:d,sortDirection:l})=>r(f=>{const v=[];return n!==void 0&&n!==""&&v.push({field:o??"email",operator:"contains",value:n}),a!==void 0&&v.push({field:e??"email",operator:"eq",value:a}),g(f,"user",{limit:t,offset:i,sortBy:{direction:l??"desc",field:d??"createdAt"},where:v})}),removeMember:({memberId:e})=>r(async a=>{await a.adapter.delete({model:"member",where:[{field:"id",value:e}]})}),removeUser:({userId:e})=>r(async a=>{await a.internalAdapter.deleteUserSessions(e),await a.internalAdapter.deleteUser(e)}),revokeUserSession:({sessionId:e})=>r(async a=>{const t=await a.adapter.findOne({model:"session",where:[{field:"id",value:e}]});t?.token&&await a.internalAdapter.deleteSession(t.token)}),revokeUserSessions:({userId:e})=>r(async a=>{await a.internalAdapter.deleteUserSessions(e)}),setRole:({role:e,userId:a})=>r(async t=>{const i=await t.internalAdapter.updateUser(a,{role:I(e)});return h(i)}),setUserPassword:({newPassword:e,userId:a})=>r(async t=>{const i=t.password.config.minPasswordLength,n=t.password.config.maxPasswordLength;if(e.length<i)throw new p(`password must be at least ${i.toString()} characters`,"PASSWORD_TOO_SHORT");if(e.length>n)throw new p(`password must be at most ${n.toString()} characters`,"PASSWORD_TOO_LONG");const o=await t.password.hash(e);await t.internalAdapter.updatePassword(a,o)}),unbanUser:({userId:e})=>r(async a=>{const t=await a.internalAdapter.updateUser(e,{banExpires:null,banned:!1,banReason:null});return h(t)}),unlinkAccount:({accountId:e,userId:a})=>r(async t=>{await t.adapter.delete({model:"account",where:[{field:"id",value:e},{connector:"AND",field:"userId",value:a}]})}),updateUser:({data:e,userId:a})=>r(async t=>{const i=await t.internalAdapter.updateUser(a,e);return h(i)})}};export{p as LunoraAuthAdminError,F as createAuthAdmin};
@@ -0,0 +1 @@
1
+ import{createAuthMiddleware as a}from"better-auth/api";import{appendAuthAuditEntry as u}from"../audit.mjs";const c=r=>{const t=r.toLowerCase(),e=n=>t===n||t.endsWith(n);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(t.includes("/sign-in/"))return"sign-in";if(e("/sign-out"))return"sign-out";if(e("/change-password")||e("/set-password"))return"password-change";if(e("/reset-password")||e("/request-password-reset")||e("/forget-password"))return"password-reset";if(e("/verify-email"))return"email-verification";if(t.includes("/two-factor/enable")||t.includes("/totp/enable"))return"mfa-enable";if(t.includes("/two-factor/disable")||t.includes("/totp/disable"))return"mfa-disable";if(e("/refresh-token")||e("/token"))return"token-refresh";if(e("/revoke-session")||e("/revoke-sessions")||e("/revoke-other-sessions"))return"session-revoke";if(e("/link-social"))return"account-link";if(e("/unlink-account"))return"account-unlink"},s=(r,t)=>r.headers?.get(t)??r.request?.headers.get(t)??void 0,d=r=>{const t=s(r,"x-forwarded-for");return s(r,"cf-connecting-ip")??(t===void 0?void 0:t.split(",")[0]?.trim())??s(r,"x-real-ip")},f=r=>{const t=r.context?.newSession??r.context?.session,e=t?.user?.id??t?.session?.userId,n=t?.user?.email;return{...e===void 0?{}:{actorId:e},...n===void 0?{}:{actorEmail:n}}},l=r=>{const t=r.context?.returned;if(t instanceof Error)return"failure";if(typeof t=="object"&&t!==null&&"status"in t){const e=Number(t.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},p=(r,t=Date.now())=>{const e=r.path===void 0?void 0:c(r.path);if(e===void 0)return;const n=d(r),o=s(r,"user-agent");return{...f(r),event:e,outcome:l(r),ts:t,...n===void 0?{}:{ip:n},...o===void 0?{}:{userAgent:o},detail:{path:r.path}}},h=r=>a(async t=>{try{const e=p(t);if(e!==void 0){const n=await u(r.executor,e,{redactDetail:r.redactDetail,retention:r.retention});r.onRecord!==void 0&&await r.onRecord(n)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}return{}}),k=(r,t)=>{const e=h(t),n=r.hooks?.after,o=n?async i=>(await n(i),e(i)):e;return{...r,hooks:{...r.hooks,after:o}}};export{h as authAuditHook,p as buildAuditEntry,c as eventForPath,k as withAuthAudit};
@@ -0,0 +1 @@
1
+ import{getMigrations as e}from"better-auth/db/migration";import{resolveAuthOptions as s}from"./createAuth-DS6PL8Mb.mjs";const o=new WeakMap,g=async i=>{const{options:t}=i,n=o.get(t);if(n){await n;return}const r=(async()=>{const{runMigrations:a}=await e(t);await a()})();o.set(t,r);try{await r}catch(a){throw o.delete(t),a}},w=async i=>{const{compileMigrations:t}=await e(s(i));return t()};export{w as compileMigrationsSql,g as ensureMigrated};
@@ -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 `"production" === "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 };