@lunora/auth 1.0.0-alpha.7 → 1.0.0-alpha.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +38 -0
- package/README.md +56 -1
- package/dist/adapter.d.mts +4 -43
- package/dist/adapter.d.ts +4 -43
- package/dist/adapter.mjs +1 -47
- package/dist/audit.d.mts +146 -0
- package/dist/audit.d.ts +146 -0
- package/dist/audit.mjs +12 -0
- package/dist/email-guard.d.mts +122 -0
- package/dist/email-guard.d.ts +122 -0
- package/dist/email-guard.mjs +1 -0
- package/dist/index.d.mts +698 -147
- package/dist/index.d.ts +698 -147
- package/dist/index.mjs +1 -12
- package/dist/middleware.d.mts +157 -156
- package/dist/middleware.d.ts +157 -156
- package/dist/middleware.mjs +1 -53
- package/dist/packem_shared/AUTH_DO_AUDIT_PATH-C4897amZ.mjs +1 -0
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs +1 -0
- package/dist/packem_shared/LunoraAuthAdminError-CiHsF1qZ.mjs +1 -0
- package/dist/packem_shared/adapter-RvDcm0Zy.mjs +1 -0
- package/dist/packem_shared/adapter.d-2a61HAY2.d.ts +76 -0
- package/dist/packem_shared/adapter.d-DrD3bb1u.d.mts +76 -0
- package/dist/packem_shared/authAuditHook-DG_ZNO53.mjs +1 -0
- package/dist/packem_shared/authDoColumnAdditions-B8BRbdzn.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-BcvcHAqo.mjs +1 -0
- package/dist/packem_shared/create-auth.d-De6IOirt.d.mts +128 -0
- package/dist/packem_shared/create-auth.d-De6IOirt.d.ts +128 -0
- package/dist/packem_shared/createAuth-DRtd4q6u.mjs +1 -0
- package/dist/packem_shared/createDoAuthWiring-acnXUGZr.mjs +1 -0
- package/dist/packem_shared/createLunoraAuthClient-CedinxXU.mjs +1 -0
- package/dist/packem_shared/emailGateDatabaseHooks-DzBD1Qoq.mjs +1 -0
- package/dist/packem_shared/sessionPresets-DpEFjXKV.mjs +1 -0
- package/dist/packem_shared/uiConfig-BrYEFK3O.mjs +1 -0
- package/dist/plugins-client.d.mts +68 -1
- package/dist/plugins-client.d.ts +68 -1
- package/dist/plugins-client.mjs +1 -2
- package/dist/plugins-enterprise-client.d.mts +9 -0
- package/dist/plugins-enterprise-client.d.ts +9 -0
- package/dist/plugins-enterprise-client.mjs +1 -0
- package/dist/plugins-enterprise.d.mts +1576 -0
- package/dist/plugins-enterprise.d.ts +1576 -0
- package/dist/plugins-enterprise.mjs +1 -0
- package/dist/plugins.d.mts +83 -2
- package/dist/plugins.d.ts +83 -2
- package/dist/plugins.mjs +1 -22
- package/dist/schema.d.mts +39 -39
- package/dist/schema.d.ts +39 -39
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.d.mts +28 -28
- package/dist/sql-store.d.ts +28 -28
- package/dist/sql-store.mjs +1 -162
- package/dist/store.d.mts +49 -31
- package/dist/store.d.ts +49 -31
- package/dist/store.mjs +1 -170
- package/dist/turnstile-middleware.d.mts +55 -55
- package/dist/turnstile-middleware.d.ts +55 -55
- package/dist/turnstile-middleware.mjs +1 -45
- package/dist/turnstile.d.mts +42 -59
- package/dist/turnstile.d.ts +42 -59
- package/dist/turnstile.mjs +1 -61
- package/package.json +38 -5
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs +0 -249
- package/dist/packem_shared/compileMigrationsSql-wZH3oXDu.mjs +0 -28
- 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/packem_shared/createAuth-B-tvsvQU.mjs +0 -56
- package/dist/packem_shared/sessionPresets-B95rXrd8.mjs +0 -35
package/dist/middleware.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
|
26
|
-
/** The `ctx.authApi
|
|
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 {
|
|
27
|
+
/** The `ctx.authApi.<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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.<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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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().<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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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:
|
|
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: <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>;
|
package/dist/middleware.mjs
CHANGED
|
@@ -1,53 +1 @@
|
|
|
1
|
-
const
|
|
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.<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
|
+
import{w as i,d as u}from"./adapter-RvDcm0Zy.mjs";import{ensureAuthAuditTable as h,createAuthAuditReader as c}from"../audit.mjs";import{resolveAuthOptions as l,createAuth as f}from"./createAuth-DRtd4q6u.mjs";import{authDoSchemaStatements as d,authDoColumnAdditions as m}from"./authDoColumnAdditions-B8BRbdzn.mjs";import{handleAuthRequest as p}from"./DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs";const A=(o,e)=>{const t=Math.max(o.length,e.length);let r=o.length^e.length;for(let s=0;s<t;s+=1){const n=s<o.length?o.charCodeAt(s):0,a=s<e.length?e.charCodeAt(s):0;r|=n^a}return r===0},b="/__lunora/auth/session",R="/__lunora/auth/audit",S="x-lunora-auth-do-secret",g=o=>{if(o===null||typeof o!="object"||Array.isArray(o))return{error:"body must be a JSON object"};const e={};for(const[t,r]of Object.entries(o))if(t==="limit"||t==="sinceSeq"){if(typeof r!="number"||!Number.isFinite(r))return{error:`"${t}" must be a finite number`};e[t]=r}else if(t==="actorId"||t==="event"){if(typeof r!="string")return{error:`"${t}" must be a string`};e[t]=r}else return{error:`unknown audit read option "${t}"`};return{options:e}};class T{#r;#s;#t;#e;#o=!1;constructor(e,t,r={}){this.#t=e.storage,this.#s=t,this.#r=r}#n(){if(this.#e!==void 0)return this.#e;const e=this.#s();if(!this.#o){const t=l(e);for(const r of d(t))[...this.#t.sql.exec(r)];for(const r of m(t,s=>this.#i(s)))[...this.#t.sql.exec(r)];this.#o=!0}return this.#e=f({...e,database:i(this.#t)}),this.#e}#i(e){return[...this.#t.sql.exec("SELECT name FROM pragma_table_info(?)",e)].map(t=>String(t.name))}async#u(e){if(!this.#a(e))return Response.json({error:"unauthorized"},{status:401});let t;try{t=await e.json()}catch{return Response.json({error:"invalid body"},{status:400})}const r=g(t??{});if("error"in r)return Response.json({error:r.error},{status:400});const s=u(this.#t);await h(s);const n=await c(s).read(r.options);return Response.json({entries:n})}#a(e){const{internalSecret:t}=this.#r;if(t===void 0||t==="")return!1;const r=e.headers.get(S);return r!==null&&A(r,t)}async#h(e){if(!this.#a(e))return Response.json({error:"unauthorized"},{status:401});const t=(await this.#n().api.getSession({headers:e.headers}))?.user.id;return Response.json(t===void 0?{}:{userId:t})}async fetch(e){const t=new URL(e.url);if(t.pathname===b)return this.#h(e);if(t.pathname===R)return this.#u(e);const r=this.#n();return await p(r,e,this.#r.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{S as INTERNAL_SECRET_HEADER,T as LunoraAuthDO,R as READ_AUDIT_PATH,b as RESOLVE_SESSION_PATH};
|
|
@@ -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{createLocalAccountIssuer as S}from"@better-auth/core/db";import{LunoraError as O}from"@lunora/errors";import{getAuthTables as A}from"better-auth/db";class p extends O{constructor(c,w){super(w,c,{name:"LunoraAuthAdminError"})}}const M=50,D=500,z=3600,R=z*24,U=100*365*24*60*60,N=2880*60*1e3,k=new Set(["accessToken","backupCodes","idToken","password","publicKey","refreshToken","secret","token"]),T=s=>Math.min(Math.max(Math.trunc(s??M),1),D),x=s=>Math.max(0,Math.trunc(s??0)),u=s=>{const c={};for(const[w,m]of Object.entries(s))k.has(w)||(c[w]=m instanceof Date?m.getTime():m);return c},I=s=>Array.isArray(s)?s.join(","):s,b=s=>s.toLowerCase().replaceAll(/[^\da-z]+/g,"-").replaceAll(/^-|-$/g,""),E=new Set(["banExpires","banned","banReason","createdAt","email","emailVerified","id","name","role","updatedAt"]),L={displayUsername:"username",phoneNumber:"phone-number",phoneNumberVerified:"phone-number",username:"username"},_=s=>s==="boolean"?"boolean":s==="date"?"date":s==="number"?"number":"string",P=s=>{const c=[];for(const[w,m]of Object.entries(s))m.input===!1||m.references!==void 0||E.has(w)||c.push({name:w,plugin:L[w],required:m.required===!0,type:_(m.type),unique:m.unique===!0});return c},B=s=>{if(s instanceof p)return s;const c=s,w=c?.body?.code??c?.code??"AUTH_ADMIN_ERROR",m=c?.body?.message??c?.message??"auth admin operation failed";return new p(m,w)},j=(s,c={})=>{const w=s.$context,m=c.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 B(a)}},h=e=>u(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:T(t.limit),model:a,offset:x(t.offset),sortBy:t.sortBy,where:i}),e.adapter.count({model:a,where:i})]);return{rows:n.map(d=>u(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,U);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 u(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 u(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:P(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"}),u(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 u(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 u(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()+N),inviterId:o,organizationId:t,role:i===void 0||i===""?"member":i,status:"pending"},model:"invitation"});return u(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 u(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 u({id:i});const l=await o.adapter.update({model:"organization",update:d,where:[{field:"id",value:i}]});return u(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 u(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 u(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},{method:"admin"});if(i!==void 0&&i!==""){const f=await o.password.hash(i);await o.internalAdapter.linkAccount({issuer:S("credential"),password:f,providerAccountId:l.id,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=c.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:c.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=>u(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=>u(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,j as createAuthAdmin};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createAdapterFactory as l}from"better-auth/adapters";import{createSqlAuthStore as c,d1Executor as m}from"../sql-store.mjs";const i=e=>({all:(a,r)=>Promise.resolve([...e.sql.exec(a,...r)]),run:(a,r)=>([...e.sql.exec(a,...r)],Promise.resolve())}),u=e=>async a=>e.transaction(a),p=e=>e,o=e=>e??null,w=e=>e,y=e=>({consumeOne:async({model:a,where:r})=>o(await e.consumeOne(a,r)),count:async({model:a,where:r})=>e.count(a,r??[]),create:async({data:a,model:r})=>p(await e.create(r,a)),delete:async({model:a,where:r})=>{await e.remove(a,r)},deleteMany:async({model:a,where:r})=>e.remove(a,r),findMany:async({limit:a,model:r,offset:n,sortBy:t,where:s})=>w(await e.read(r,{limit:a,offset:n,sortBy:t,where:s??[]})),findOne:async({model:a,where:r})=>{const[n]=await e.read(a,{limit:1,where:r});return o(n)},incrementOne:async({increment:a,model:r,set:n,where:t})=>o(await e.incrementOne(r,t,a,n)),update:async({model:a,update:r,where:n})=>{const[t]=await e.update(a,n,r);return o(t)},updateMany:async({model:a,update:r,where:n})=>(await e.update(a,n,r)).length}),d=(e,a)=>{const r=t=>s=>l({adapter:()=>y(e),config:{adapterId:"lunora",adapterName:"Lunora Adapter",transaction:t(s),supportsBooleans:!1,supportsDates:!1,supportsJSON:!1,supportsNumericIds:!1}})(s),n=()=>!1;return r(a?t=>async s=>a(async()=>s(r(n)(t))):n)},O=e=>d(c(m(e))),x=e=>d(c(i(e)),u(e));export{i as d,O as l,d as s,x as w};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createAdapterFactory } from 'better-auth/adapters';
|
|
2
|
+
import { d1Executor } from "../sql-store.js";
|
|
3
|
+
import { AuthStore } from "../store.js";
|
|
4
|
+
/**
|
|
5
|
+
* The slice of `DurableObjectStorage` this module uses.
|
|
6
|
+
*
|
|
7
|
+
* Structural on purpose: it keeps `@lunora/auth` free of a `@lunora/do` dependency
|
|
8
|
+
* and lets tests supply a double whose `transaction` has the same semantics.
|
|
9
|
+
*/
|
|
10
|
+
interface DoStorageLike {
|
|
11
|
+
/** Synchronous SQL over the object's SQLite. */
|
|
12
|
+
sql: {
|
|
13
|
+
exec: (query: string, ...bindings: unknown[]) => Iterable<Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The platform's async transaction primitive. Everything the closure executes
|
|
17
|
+
* against `sql` joins the transaction — it is connection-scoped, so no handle is
|
|
18
|
+
* threaded through — and a throw rolls the whole thing back.
|
|
19
|
+
*/
|
|
20
|
+
transaction: <R>(closure: () => Promise<R>) => Promise<R>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `closure` atomically. Supplied by stores whose backend has real
|
|
24
|
+
* transactions — a Durable Object's storage does; D1 does not.
|
|
25
|
+
*/
|
|
26
|
+
type TransactionRunner = <R>(closure: () => Promise<R>) => Promise<R>;
|
|
27
|
+
/**
|
|
28
|
+
* A better-auth database adapter backed by an {@link AuthStore} — the bridge
|
|
29
|
+
* that routes better-auth's reads and writes through Lunora's data layer
|
|
30
|
+
* instead of better-auth's built-in D1/Kysely adapter. Pass the result as
|
|
31
|
+
* `createAuth({ database: lunoraAuthAdapter(store) })`; better-auth's
|
|
32
|
+
* `createAdapterFactory` handles id generation, default values, field-name
|
|
33
|
+
* mapping and output shaping, so this only translates the cleaned CRUD calls
|
|
34
|
+
* onto the store.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const auth = createAuth({
|
|
38
|
+
* secret: env.AUTH_SECRET,
|
|
39
|
+
* emailAndPassword: { enabled: true },
|
|
40
|
+
* database: lunoraAuthAdapter(lunoraStore), // lunoraStore writes via ctx.db
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Scope: the {@link AuthStore} interface is single-table CRUD. better-auth's
|
|
45
|
+
* relational `join` reads (an advanced opt-in) are not handled — pair the
|
|
46
|
+
* adapter with `disableJoins` or let better-auth fall back to per-table reads.
|
|
47
|
+
*/
|
|
48
|
+
declare const lunoraAuthAdapter: (store: AuthStore, runInTransaction?: TransactionRunner) => ReturnType<typeof createAdapterFactory>;
|
|
49
|
+
/**
|
|
50
|
+
* One-liner for the common case: a better-auth `database` backed by a Cloudflare
|
|
51
|
+
* D1 binding, via Lunora's SQL store — equivalent to
|
|
52
|
+
* `lunoraAuthAdapter(createSqlAuthStore(d1Executor(d1)))`.
|
|
53
|
+
*
|
|
54
|
+
* Prefer this over passing the raw `env.DB` as `database`. With raw D1,
|
|
55
|
+
* better-auth resolves its Kysely adapter through a runtime `await import(...)`
|
|
56
|
+
* inside `auth.$context`, and that dynamic import never settles under
|
|
57
|
+
* `@cloudflare/vite-plugin`'s worker runner — so it hangs *every* auth request
|
|
58
|
+
* in `pnpm dev` (a standalone `wrangler dev` or a deployed worker bundle it
|
|
59
|
+
* up-front, so they're unaffected — which makes the hang baffling to debug).
|
|
60
|
+
* This explicit adapter skips that import entirely, so dev and prod behave the
|
|
61
|
+
* same. The migration instance is the one exception — it wants raw `env.DB` so
|
|
62
|
+
* `ensureMigrated`'s Kysely migrator can create the tables (its `$context` is
|
|
63
|
+
* never resolved, so the hang doesn't apply there).
|
|
64
|
+
*/
|
|
65
|
+
declare const lunoraD1Adapter: (d1: Parameters<typeof d1Executor>[0]) => ReturnType<typeof lunoraAuthAdapter>;
|
|
66
|
+
/**
|
|
67
|
+
* A better-auth `database` backed by a Durable Object's own SQLite —
|
|
68
|
+
* `lunoraAuthAdapter(createSqlAuthStore(doExecutor(storage)), doTransactionRunner(storage))`.
|
|
69
|
+
*
|
|
70
|
+
* Unlike `lunoraD1Adapter` this one exposes real transactions, so plugins that
|
|
71
|
+
* demand them (`@better-auth/scim`) accept it. Read the trade-offs in `do-store.ts`
|
|
72
|
+
* before reaching for it: the auth tables then live inside a single Durable Object.
|
|
73
|
+
* @experimental
|
|
74
|
+
*/
|
|
75
|
+
declare const lunoraDoAdapter: (storage: DoStorageLike) => ReturnType<typeof lunoraAuthAdapter>;
|
|
76
|
+
export { DoStorageLike as D, lunoraD1Adapter as a, lunoraDoAdapter as b, lunoraAuthAdapter as l };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createAdapterFactory } from 'better-auth/adapters';
|
|
2
|
+
import { d1Executor } from "../sql-store.mjs";
|
|
3
|
+
import { AuthStore } from "../store.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* The slice of `DurableObjectStorage` this module uses.
|
|
6
|
+
*
|
|
7
|
+
* Structural on purpose: it keeps `@lunora/auth` free of a `@lunora/do` dependency
|
|
8
|
+
* and lets tests supply a double whose `transaction` has the same semantics.
|
|
9
|
+
*/
|
|
10
|
+
interface DoStorageLike {
|
|
11
|
+
/** Synchronous SQL over the object's SQLite. */
|
|
12
|
+
sql: {
|
|
13
|
+
exec: (query: string, ...bindings: unknown[]) => Iterable<Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The platform's async transaction primitive. Everything the closure executes
|
|
17
|
+
* against `sql` joins the transaction — it is connection-scoped, so no handle is
|
|
18
|
+
* threaded through — and a throw rolls the whole thing back.
|
|
19
|
+
*/
|
|
20
|
+
transaction: <R>(closure: () => Promise<R>) => Promise<R>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `closure` atomically. Supplied by stores whose backend has real
|
|
24
|
+
* transactions — a Durable Object's storage does; D1 does not.
|
|
25
|
+
*/
|
|
26
|
+
type TransactionRunner = <R>(closure: () => Promise<R>) => Promise<R>;
|
|
27
|
+
/**
|
|
28
|
+
* A better-auth database adapter backed by an {@link AuthStore} — the bridge
|
|
29
|
+
* that routes better-auth's reads and writes through Lunora's data layer
|
|
30
|
+
* instead of better-auth's built-in D1/Kysely adapter. Pass the result as
|
|
31
|
+
* `createAuth({ database: lunoraAuthAdapter(store) })`; better-auth's
|
|
32
|
+
* `createAdapterFactory` handles id generation, default values, field-name
|
|
33
|
+
* mapping and output shaping, so this only translates the cleaned CRUD calls
|
|
34
|
+
* onto the store.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const auth = createAuth({
|
|
38
|
+
* secret: env.AUTH_SECRET,
|
|
39
|
+
* emailAndPassword: { enabled: true },
|
|
40
|
+
* database: lunoraAuthAdapter(lunoraStore), // lunoraStore writes via ctx.db
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Scope: the {@link AuthStore} interface is single-table CRUD. better-auth's
|
|
45
|
+
* relational `join` reads (an advanced opt-in) are not handled — pair the
|
|
46
|
+
* adapter with `disableJoins` or let better-auth fall back to per-table reads.
|
|
47
|
+
*/
|
|
48
|
+
declare const lunoraAuthAdapter: (store: AuthStore, runInTransaction?: TransactionRunner) => ReturnType<typeof createAdapterFactory>;
|
|
49
|
+
/**
|
|
50
|
+
* One-liner for the common case: a better-auth `database` backed by a Cloudflare
|
|
51
|
+
* D1 binding, via Lunora's SQL store — equivalent to
|
|
52
|
+
* `lunoraAuthAdapter(createSqlAuthStore(d1Executor(d1)))`.
|
|
53
|
+
*
|
|
54
|
+
* Prefer this over passing the raw `env.DB` as `database`. With raw D1,
|
|
55
|
+
* better-auth resolves its Kysely adapter through a runtime `await import(...)`
|
|
56
|
+
* inside `auth.$context`, and that dynamic import never settles under
|
|
57
|
+
* `@cloudflare/vite-plugin`'s worker runner — so it hangs *every* auth request
|
|
58
|
+
* in `pnpm dev` (a standalone `wrangler dev` or a deployed worker bundle it
|
|
59
|
+
* up-front, so they're unaffected — which makes the hang baffling to debug).
|
|
60
|
+
* This explicit adapter skips that import entirely, so dev and prod behave the
|
|
61
|
+
* same. The migration instance is the one exception — it wants raw `env.DB` so
|
|
62
|
+
* `ensureMigrated`'s Kysely migrator can create the tables (its `$context` is
|
|
63
|
+
* never resolved, so the hang doesn't apply there).
|
|
64
|
+
*/
|
|
65
|
+
declare const lunoraD1Adapter: (d1: Parameters<typeof d1Executor>[0]) => ReturnType<typeof lunoraAuthAdapter>;
|
|
66
|
+
/**
|
|
67
|
+
* A better-auth `database` backed by a Durable Object's own SQLite —
|
|
68
|
+
* `lunoraAuthAdapter(createSqlAuthStore(doExecutor(storage)), doTransactionRunner(storage))`.
|
|
69
|
+
*
|
|
70
|
+
* Unlike `lunoraD1Adapter` this one exposes real transactions, so plugins that
|
|
71
|
+
* demand them (`@better-auth/scim`) accept it. Read the trade-offs in `do-store.ts`
|
|
72
|
+
* before reaching for it: the auth tables then live inside a single Durable Object.
|
|
73
|
+
* @experimental
|
|
74
|
+
*/
|
|
75
|
+
declare const lunoraDoAdapter: (storage: DoStorageLike) => ReturnType<typeof lunoraAuthAdapter>;
|
|
76
|
+
export { DoStorageLike as D, lunoraD1Adapter as a, lunoraDoAdapter as b, lunoraAuthAdapter as l };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createAuthMiddleware as u}from"better-auth/api";import{appendAuthAuditEntry as c}from"../audit.mjs";const d=t=>{const i=t.toLowerCase(),e=r=>i===r||i.endsWith(r);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(e("/sign-in/social")||e("/sign-in/magic-link"))return"sign-in-initiated";if(i.includes("/sign-in/")||i.includes("/callback/")||e("/magic-link/verify")||i.includes("/two-factor/verify-"))return"sign-in";if(e("/sign-out"))return"sign-out";if(e("/change-password")||e("/set-password"))return"password-change";if(e("/reset-password")||e("/request-password-reset")||e("/forget-password"))return"password-reset";if(e("/verify-email"))return"email-verification";if(i.includes("/two-factor/enable")||i.includes("/totp/enable"))return"mfa-enable";if(i.includes("/two-factor/disable")||i.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"},n=(t,i)=>t.headers?.get(i)??t.request?.headers.get(i)??void 0,l=t=>{const i=n(t,"x-forwarded-for");return n(t,"cf-connecting-ip")??(i===void 0?void 0:i.split(",")[0]?.trim())??n(t,"x-real-ip")},f=t=>{const i=t.context?.newSession??t.context?.session,e=i?.user?.id??i?.session?.userId,r=i?.user?.email;return{...e===void 0?{}:{actorId:e},...r===void 0?{}:{actorEmail:r}}},p=t=>{const i=t.context?.returned;if(i instanceof Error)return"failure";if(typeof i=="object"&&i!==null&&"status"in i){const e=Number(i.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},a=320,g=(t,i)=>{if(i!=="sign-in"&&i!=="sign-in-initiated")return;const e=t.body?.email??t.body?.username;if(!(typeof e!="string"||e.length===0))return e.length>a?e.slice(0,a):e},h=(t,i=Date.now())=>{const e=t.path===void 0?void 0:d(t.path);if(e===void 0)return;const r=l(t),s=n(t,"user-agent"),o=g(t,e);return{...f(t),event:e,outcome:p(t),ts:i,...r===void 0?{}:{ip:r},...o===void 0?{}:{targetEmail:o},...s===void 0?{}:{userAgent:s},detail:{path:t.path}}},v=t=>u(async i=>{try{const e=h(i);if(e!==void 0){const r=await c(t.executor,e,{redactDetail:t.redactDetail,retention:t.retention});t.onRecord!==void 0&&await t.onRecord(r)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}}),w=(t,i)=>{const e=v(i),r=t.hooks?.after,s=r?async o=>(await r(o),e(o)):e;return{...t,hooks:{...t.hooks,after:s}}};export{v as authAuditHook,h as buildAuditEntry,d as eventForPath,w as withAuthAudit};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getAuthTablesWithResolvedIndexes as f,getDatabaseFieldIndexName as N}from"@better-auth/core/db/internal";const s=n=>`"${n.replaceAll('"','""')}"`,c=(n,e)=>{if(n==="id"||e.references?.field==="id")return"text";const{type:t}=e;if(Array.isArray(t))return"text";switch(t){case"boolean":return"integer";case"date":return"date";case"number":return"bigint"in e&&e.bigint===!0?"bigint":"integer";default:return"text"}},b=n=>{const{defaultValue:e,type:t,unique:r}=n;if(!(r===!0&&n.required===!1)&&!(e==null||typeof e=="function")){if(t==="boolean")return e===!0?"1":"0";if(t==="number"&&typeof e=="number"&&Number.isFinite(e))return String(e);if(t==="string"&&typeof e=="string")return`'${e.replaceAll("'","''")}'`}},p=(n,e)=>{const t=[];for(const[r,o]of Object.entries(e)){const i=o.unique===!0;if(!i&&o.index!==!0)continue;const l=o.fieldName??r,u=N(n,l,i);t.push(`CREATE ${i?"UNIQUE ":""}INDEX IF NOT EXISTS ${s(u)} ON ${s(n)} (${s(l)})`)}return t},E=(n,e)=>{const t=[s(n),c(n,e)],r=b(e);return e.required!==!1&&r!==void 0&&t.push("NOT NULL"),r!==void 0&&t.push(`DEFAULT ${r}`),t.join(" ")},T=(n,e)=>{const t=[s(n),c(n,e)];return e.required!==!1&&t.push("NOT NULL"),t.join(" ")},A=n=>{const{indexesByTable:e,tables:t}=f(n),r=[],o=[];for(const i of Object.values(t)){if(i.disableMigrations===!0)continue;const l=[`${s("id")} text NOT NULL PRIMARY KEY`,...Object.entries(i.fields).map(([u,a])=>T(a.fieldName??u,a))];r.push(`CREATE TABLE IF NOT EXISTS ${s(i.modelName)} (${l.join(", ")})`),o.push(...p(i.modelName,i.fields));for(const u of e.get(i.modelName)??[]){const a=u.unique===!0?"UNIQUE ":"",d=u.columns.map(m=>s(m)).join(", ");o.push(`CREATE ${a}INDEX IF NOT EXISTS ${s(u.name)} ON ${s(i.modelName)} (${d})`)}}return[...r,...o]},$=(n,e)=>{const{tables:t}=f(n),r=[];for(const o of Object.values(t)){if(o.disableMigrations===!0)continue;const i=new Set(e(o.modelName));if(i.size!==0)for(const[l,u]of Object.entries(o.fields)){const a=u.fieldName??l;i.has(a)||r.push(`ALTER TABLE ${s(o.modelName)} ADD COLUMN ${E(a,u)}`)}}return r};export{$ as authDoColumnAdditions,A as authDoSchemaStatements};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getMigrations as l}from"better-auth/db/migration";import{resolveAuthOptions as d}from"./createAuth-DRtd4q6u.mjs";const w=/pragma_index_list\s*\(/iu,x=/sqlite_master/iu,h=/^["`[]|["\]`]$/gu,q=/\s+/u,M=/create\s+unique\s+index/iu,_=/\bwhere\b/iu,E=t=>w.test(t)&&x.test(t),N=t=>(t.trim().split(q)[0]??"").replaceAll(h,"").trim(),P=t=>{const e=t.indexOf("(");if(e===-1)return{columns:[],tail:""};let i=0;for(let s=e;s<t.length;s+=1){const n=t[s];if(n==="(")i+=1;else if(n===")"&&(i-=1,i===0))return{columns:t.slice(e+1,s).split(",").map(a=>N(a)).filter(a=>a!==""),tail:t.slice(s+1)}}return{columns:[],tail:""}},R=async t=>{const e=t.prepare("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'"),{results:i}=await e.all(),s=[];for(const n of i??[]){const a=n.name,o=n.tbl_name,r=n.sql;if(typeof a!="string"||typeof o!="string")continue;if(typeof r!="string"){s.push({columnPosition:0,indexName:a,isPartial:0,isUnique:1,tableName:o});continue}const p=M.test(r)?1:0,{columns:m,tail:f}=P(r),g=_.test(f)?1:0;for(const[y,b]of m.entries())s.push({columnName:b,columnPosition:y,indexName:a,isPartial:g,isUnique:p,tableName:o})}return s},D=t=>{const e=async()=>{const s=await R(t);return{meta:{},results:s,success:!0}},i={all:e,bind:()=>i,run:e};return i},I=t=>new Proxy(t,{get(e,i,s){if(i==="prepare")return a=>E(a)?D(e):e.prepare(a);const n=Reflect.get(e,i,s);return typeof n=="function"?n.bind(e):n}}),O=t=>typeof t=="object"&&t!==null&&typeof t.prepare=="function"&&typeof t.batch=="function",u=t=>O(t.database)?{...t,database:I(t.database)}:t,c=new WeakMap,S=async t=>{const{options:e}=t,i=c.get(e);if(i){await i;return}const s=(async()=>{const{runMigrations:n}=await l(u(e));await n()})();c.set(e,s);try{await s}catch(n){throw c.delete(e),n}},U=async t=>{const{compileMigrations:e}=await l(u(d(t)));return e()};export{U as compileMigrationsSql,S as ensureMigrated};
|