@lunora/auth 0.0.0 → 1.0.0-alpha.2
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 +105 -0
- package/README.md +125 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/adapter.d.mts +43 -0
- package/dist/adapter.d.ts +43 -0
- package/dist/adapter.mjs +47 -0
- package/dist/index.d.mts +386 -0
- package/dist/index.d.ts +386 -0
- package/dist/index.mjs +12 -0
- package/dist/middleware.d.mts +189 -0
- package/dist/middleware.d.ts +189 -0
- package/dist/middleware.mjs +53 -0
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +11 -0
- package/dist/packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs +249 -0
- package/dist/packem_shared/compileMigrationsSql-wZH3oXDu.mjs +28 -0
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.mts +58 -0
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.ts +58 -0
- package/dist/packem_shared/createAuth-B-tvsvQU.mjs +56 -0
- package/dist/packem_shared/sessionPresets-B95rXrd8.mjs +35 -0
- package/dist/plugins-client.d.mts +2 -0
- package/dist/plugins-client.d.ts +2 -0
- package/dist/plugins-client.mjs +2 -0
- package/dist/plugins.d.mts +22 -0
- package/dist/plugins.d.ts +22 -0
- package/dist/plugins.mjs +22 -0
- package/dist/schema.d.mts +44 -0
- package/dist/schema.d.ts +44 -0
- package/dist/schema.mjs +62 -0
- package/dist/sql-store.d.mts +51 -0
- package/dist/sql-store.d.ts +51 -0
- package/dist/sql-store.mjs +162 -0
- package/dist/store.d.mts +66 -0
- package/dist/store.d.ts +66 -0
- package/dist/store.mjs +170 -0
- package/dist/turnstile-middleware.d.mts +80 -0
- package/dist/turnstile-middleware.d.ts +80 -0
- package/dist/turnstile-middleware.mjs +45 -0
- package/dist/turnstile.d.mts +98 -0
- package/dist/turnstile.d.ts +98 -0
- package/dist/turnstile.mjs +61 -0
- package/package.json +80 -18
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
export { lunoraAuthAdapter, lunoraD1Adapter } from "./adapter.js";
|
|
2
|
+
import { L as LunoraAuth, a as LunoraAuthOptions } from "./packem_shared/create-auth.d-M36jwG_Y.js";
|
|
3
|
+
export { c as createAuth } from "./packem_shared/create-auth.d-M36jwG_Y.js";
|
|
4
|
+
export { type LunoraAuthApiContext, LunoraAuthHeadersError, type WithAuthPluginsMiddleware, type WithAuthPluginsOptions, withAuthPlugins } from "./middleware.js";
|
|
5
|
+
export { default as authTables } from "./schema.js";
|
|
6
|
+
import { BetterAuthOptions } from 'better-auth';
|
|
7
|
+
export { type SqlExecutor, createSqlAuthStore, d1Executor } from "./sql-store.js";
|
|
8
|
+
export { type AuthQuery, type AuthRow, type AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere } from "./store.js";
|
|
9
|
+
export { type FetchLike, TURNSTILE_VERIFY_ENDPOINT, type TurnstileVerifyResult, type VerifyTurnstileOptions, verifyTurnstile } from "./turnstile.js";
|
|
10
|
+
export { type VerifyTurnstileMiddlewareOptions, verifyTurnstileMiddleware } from "./turnstile-middleware.js";
|
|
11
|
+
import 'better-auth/adapters';
|
|
12
|
+
import '@lunora/server';
|
|
13
|
+
/**
|
|
14
|
+
* A timestamp as it leaves the admin API: epoch-ms (better-auth stores `Date`s,
|
|
15
|
+
* which we normalize on output) or `null` when the column is unset.
|
|
16
|
+
*/
|
|
17
|
+
type AuthTimestamp = null | number;
|
|
18
|
+
/**
|
|
19
|
+
* One user row as the admin API surfaces it. The fixed keys mirror better-auth's
|
|
20
|
+
* core `user` table plus the `admin()` plugin columns (`role`/`banned`/…); the
|
|
21
|
+
* index signature carries any app-defined `user.additionalFields` so callers
|
|
22
|
+
* (the studio) can render them generically. Password material never lives on
|
|
23
|
+
* this row (it's in the `account` table) and is never returned.
|
|
24
|
+
*/
|
|
25
|
+
interface AuthAdminUser {
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
banExpires?: AuthTimestamp;
|
|
28
|
+
banned?: boolean | null;
|
|
29
|
+
banReason?: null | string;
|
|
30
|
+
createdAt?: AuthTimestamp;
|
|
31
|
+
email?: null | string;
|
|
32
|
+
emailVerified?: boolean | null;
|
|
33
|
+
id: string;
|
|
34
|
+
image?: null | string;
|
|
35
|
+
name?: null | string;
|
|
36
|
+
role?: null | string;
|
|
37
|
+
updatedAt?: AuthTimestamp;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One session row as the admin API surfaces it. Mirrors better-auth's `session`
|
|
41
|
+
* table; `impersonatedBy` is set when the session was minted by
|
|
42
|
+
* {@link AuthAdmin.impersonateUser}. The signing `token` is stripped — it's a
|
|
43
|
+
* bearer credential, and the only place we hand one back is the explicit
|
|
44
|
+
* impersonation flow.
|
|
45
|
+
*/
|
|
46
|
+
interface AuthAdminSession {
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
createdAt?: AuthTimestamp;
|
|
49
|
+
expiresAt?: AuthTimestamp;
|
|
50
|
+
id: string;
|
|
51
|
+
impersonatedBy?: null | string;
|
|
52
|
+
ipAddress?: null | string;
|
|
53
|
+
userAgent?: null | string;
|
|
54
|
+
userId: string;
|
|
55
|
+
}
|
|
56
|
+
/** One linked account (a credential or OAuth provider). All token material is stripped. */
|
|
57
|
+
interface AuthAccount {
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
accountId?: null | string;
|
|
60
|
+
createdAt?: AuthTimestamp;
|
|
61
|
+
id: string;
|
|
62
|
+
providerId?: null | string;
|
|
63
|
+
scope?: null | string;
|
|
64
|
+
userId: string;
|
|
65
|
+
}
|
|
66
|
+
/** One organization row (from the `organization` plugin). */
|
|
67
|
+
interface AuthOrganization {
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
createdAt?: AuthTimestamp;
|
|
70
|
+
id: string;
|
|
71
|
+
name?: null | string;
|
|
72
|
+
slug?: null | string;
|
|
73
|
+
}
|
|
74
|
+
/** One organization membership row. */
|
|
75
|
+
interface AuthMember {
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
createdAt?: AuthTimestamp;
|
|
78
|
+
id: string;
|
|
79
|
+
organizationId: string;
|
|
80
|
+
role?: null | string;
|
|
81
|
+
userId: string;
|
|
82
|
+
}
|
|
83
|
+
/** One pending organization invitation. */
|
|
84
|
+
interface AuthInvitation {
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
email?: null | string;
|
|
87
|
+
expiresAt?: AuthTimestamp;
|
|
88
|
+
id: string;
|
|
89
|
+
organizationId: string;
|
|
90
|
+
role?: null | string;
|
|
91
|
+
status?: null | string;
|
|
92
|
+
}
|
|
93
|
+
/** One registered passkey. Credential secrets (`publicKey`) are stripped. */
|
|
94
|
+
interface AuthPasskey {
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
createdAt?: AuthTimestamp;
|
|
97
|
+
deviceType?: null | string;
|
|
98
|
+
id: string;
|
|
99
|
+
name?: null | string;
|
|
100
|
+
userId: string;
|
|
101
|
+
}
|
|
102
|
+
/** A page of rows plus the unpaginated total. */
|
|
103
|
+
interface AuthPage<T> {
|
|
104
|
+
rows: T[];
|
|
105
|
+
total: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Which admin surfaces a given auth instance supports, derived from the enabled
|
|
109
|
+
* better-auth plugins (and any {@link CreateAuthAdminOptions.features} overrides).
|
|
110
|
+
* The studio calls {@link AuthAdmin.capabilities} once and renders only the
|
|
111
|
+
* panels whose capability is `true` — so a deployment that doesn't enable, say,
|
|
112
|
+
* the `organization` plugin never shows an Organizations section.
|
|
113
|
+
*/
|
|
114
|
+
interface AuthCapabilities {
|
|
115
|
+
/** Linked-account browsing/unlinking — core (the `account` table always exists). */
|
|
116
|
+
accounts: boolean;
|
|
117
|
+
/** The `admin()` plugin: ban/role/impersonate/create/delete/set-password. */
|
|
118
|
+
admin: boolean;
|
|
119
|
+
/** The `organization` plugin: orgs, members, invitations. */
|
|
120
|
+
organization: boolean;
|
|
121
|
+
/** The `@better-auth/passkey` plugin: per-user passkeys. */
|
|
122
|
+
passkey: boolean;
|
|
123
|
+
/** The `two-factor` plugin: per-user 2FA status / disable. */
|
|
124
|
+
twoFactor: boolean;
|
|
125
|
+
}
|
|
126
|
+
/** A scalar value usable in an adapter `where` clause / filter. */
|
|
127
|
+
type WhereValue = boolean | number | string;
|
|
128
|
+
/** Filtering / paging options for {@link AuthAdmin.listUsers}. */
|
|
129
|
+
interface ListUsersOptions {
|
|
130
|
+
/** Column to filter on with `=` (defaults to `email` when `filterValue` is set). */
|
|
131
|
+
filterField?: string;
|
|
132
|
+
/** Exact-match filter value. */
|
|
133
|
+
filterValue?: WhereValue;
|
|
134
|
+
limit?: number;
|
|
135
|
+
offset?: number;
|
|
136
|
+
/** Substring search value (matched with `contains`). */
|
|
137
|
+
search?: string;
|
|
138
|
+
/** Column the `search` substring matches against (default `email`). */
|
|
139
|
+
searchField?: string;
|
|
140
|
+
/** Column to order by (default `createdAt`). */
|
|
141
|
+
sortBy?: string;
|
|
142
|
+
sortDirection?: "asc" | "desc";
|
|
143
|
+
}
|
|
144
|
+
/** The result of {@link AuthAdmin.impersonateUser}: a fresh session token to act as the target user. */
|
|
145
|
+
interface ImpersonationResult {
|
|
146
|
+
expiresAt: AuthTimestamp;
|
|
147
|
+
/** Bearer session token for the impersonated user. The caller is responsible for using it (e.g. setting the cookie). */
|
|
148
|
+
token: string;
|
|
149
|
+
user: AuthAdminUser;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The full read + write surface the studio's auth dashboard drives, backed by
|
|
153
|
+
* better-auth's tables. Returned by {@link createAuthAdmin}. The runtime accepts
|
|
154
|
+
* a structurally-compatible object as its `authAdmin` option and exposes each
|
|
155
|
+
* method behind an admin-token-gated endpoint. Methods whose backing plugin is
|
|
156
|
+
* absent still exist (they're not conditionally omitted) but the studio gates
|
|
157
|
+
* them on {@link AuthAdmin.capabilities}; calling one for an unconfigured plugin
|
|
158
|
+
* surfaces the underlying adapter error.
|
|
159
|
+
*/
|
|
160
|
+
interface AuthAdmin {
|
|
161
|
+
banUser: (input: {
|
|
162
|
+
expiresInSeconds?: number;
|
|
163
|
+
reason?: string;
|
|
164
|
+
userId: string;
|
|
165
|
+
}) => Promise<AuthAdminUser>;
|
|
166
|
+
cancelInvitation: (input: {
|
|
167
|
+
invitationId: string;
|
|
168
|
+
}) => Promise<void>;
|
|
169
|
+
capabilities: () => Promise<AuthCapabilities>;
|
|
170
|
+
createUser: (input: {
|
|
171
|
+
data?: Record<string, unknown>;
|
|
172
|
+
email: string;
|
|
173
|
+
name: string;
|
|
174
|
+
password?: string;
|
|
175
|
+
role?: string | string[];
|
|
176
|
+
}) => Promise<AuthAdminUser>;
|
|
177
|
+
deletePasskey: (input: {
|
|
178
|
+
passkeyId: string;
|
|
179
|
+
}) => Promise<void>;
|
|
180
|
+
disableTwoFactor: (input: {
|
|
181
|
+
userId: string;
|
|
182
|
+
}) => Promise<void>;
|
|
183
|
+
impersonateUser: (input: {
|
|
184
|
+
userId: string;
|
|
185
|
+
}) => Promise<ImpersonationResult>;
|
|
186
|
+
listAccounts: (input: {
|
|
187
|
+
userId: string;
|
|
188
|
+
}) => Promise<AuthAccount[]>;
|
|
189
|
+
listInvitations: (options: {
|
|
190
|
+
limit?: number;
|
|
191
|
+
offset?: number;
|
|
192
|
+
organizationId: string;
|
|
193
|
+
}) => Promise<AuthPage<AuthInvitation>>;
|
|
194
|
+
listMembers: (options: {
|
|
195
|
+
limit?: number;
|
|
196
|
+
offset?: number;
|
|
197
|
+
organizationId: string;
|
|
198
|
+
}) => Promise<AuthPage<AuthMember>>;
|
|
199
|
+
listOrganizations: (options: {
|
|
200
|
+
limit?: number;
|
|
201
|
+
offset?: number;
|
|
202
|
+
}) => Promise<AuthPage<AuthOrganization>>;
|
|
203
|
+
listPasskeys: (input: {
|
|
204
|
+
userId: string;
|
|
205
|
+
}) => Promise<AuthPasskey[]>;
|
|
206
|
+
listSessions: (options: {
|
|
207
|
+
limit?: number;
|
|
208
|
+
offset?: number;
|
|
209
|
+
userId?: string;
|
|
210
|
+
}) => Promise<AuthPage<AuthAdminSession>>;
|
|
211
|
+
listUsers: (options: ListUsersOptions) => Promise<AuthPage<AuthAdminUser>>;
|
|
212
|
+
removeMember: (input: {
|
|
213
|
+
memberId: string;
|
|
214
|
+
}) => Promise<void>;
|
|
215
|
+
removeUser: (input: {
|
|
216
|
+
userId: string;
|
|
217
|
+
}) => Promise<void>;
|
|
218
|
+
revokeUserSession: (input: {
|
|
219
|
+
sessionId: string;
|
|
220
|
+
}) => Promise<void>;
|
|
221
|
+
revokeUserSessions: (input: {
|
|
222
|
+
userId: string;
|
|
223
|
+
}) => Promise<void>;
|
|
224
|
+
setRole: (input: {
|
|
225
|
+
role: string | string[];
|
|
226
|
+
userId: string;
|
|
227
|
+
}) => Promise<AuthAdminUser>;
|
|
228
|
+
setUserPassword: (input: {
|
|
229
|
+
newPassword: string;
|
|
230
|
+
userId: string;
|
|
231
|
+
}) => Promise<void>;
|
|
232
|
+
unbanUser: (input: {
|
|
233
|
+
userId: string;
|
|
234
|
+
}) => Promise<AuthAdminUser>;
|
|
235
|
+
unlinkAccount: (input: {
|
|
236
|
+
accountId: string;
|
|
237
|
+
userId: string;
|
|
238
|
+
}) => Promise<void>;
|
|
239
|
+
updateUser: (input: {
|
|
240
|
+
data: Record<string, unknown>;
|
|
241
|
+
userId: string;
|
|
242
|
+
}) => Promise<AuthAdminUser>;
|
|
243
|
+
}
|
|
244
|
+
/** Options for {@link createAuthAdmin}. */
|
|
245
|
+
interface CreateAuthAdminOptions {
|
|
246
|
+
/**
|
|
247
|
+
* Force individual {@link AuthCapabilities} on or off regardless of which
|
|
248
|
+
* plugins are detected — e.g. `{ impersonate: false }`-style opt-outs by
|
|
249
|
+
* setting `admin: false`, or hiding linked accounts with `accounts: false`.
|
|
250
|
+
* A capability is reported only when both its plugin is enabled *and* its
|
|
251
|
+
* override isn't `false`.
|
|
252
|
+
*/
|
|
253
|
+
features?: Partial<AuthCapabilities>;
|
|
254
|
+
/**
|
|
255
|
+
* User id recorded as the impersonator on sessions minted by
|
|
256
|
+
* {@link AuthAdmin.impersonateUser}. Defaults to the impersonated user's own
|
|
257
|
+
* id (a self-reference) since the trusted admin plane has no acting user.
|
|
258
|
+
*/
|
|
259
|
+
impersonatedBy?: string;
|
|
260
|
+
/**
|
|
261
|
+
* How long (in seconds) an impersonation session lives. Must be a positive
|
|
262
|
+
* finite integer. Capped at 24 × {@link DEFAULT_IMPERSONATION_SECONDS}
|
|
263
|
+
* (86 400 s / 24 h). Defaults to {@link DEFAULT_IMPERSONATION_SECONDS}
|
|
264
|
+
* (3 600 s / 1 h).
|
|
265
|
+
*/
|
|
266
|
+
impersonationSeconds?: number;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* A normalized failure from an admin operation. better-auth throws `APIError`s
|
|
270
|
+
* carrying a `body.code` (e.g. `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`); we
|
|
271
|
+
* surface that `code` so the runtime can map it onto an HTTP status and the
|
|
272
|
+
* studio can show a meaningful message instead of a generic 500.
|
|
273
|
+
*/
|
|
274
|
+
declare class LunoraAuthAdminError extends Error {
|
|
275
|
+
readonly code: string;
|
|
276
|
+
constructor(message: string, code: string);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Build the studio's auth user-management plane on top of better-auth.
|
|
280
|
+
*
|
|
281
|
+
* Pass the result as the runtime's `authAdmin` option; the runtime exposes each
|
|
282
|
+
* method behind an admin-token-gated `/_lunora/admin/auth/*` endpoint. The set
|
|
283
|
+
* of usable surfaces is reported by {@link AuthAdmin.capabilities} — derived
|
|
284
|
+
* from the enabled better-auth plugins, so enabling `admin()`, `organization()`,
|
|
285
|
+
* `twoFactor()`, or the passkey plugin in the auth config is what lights up the
|
|
286
|
+
* matching dashboard panels.
|
|
287
|
+
*
|
|
288
|
+
* **Trust model — important.** These operations talk to better-auth's
|
|
289
|
+
* `internalAdapter` (and `adapter`/password hasher) **directly**, deliberately
|
|
290
|
+
* bypassing the plugins' own endpoints, which require the caller to hold an
|
|
291
|
+
* admin-role session. That session check is the wrong gate here: the runtime
|
|
292
|
+
* already authorizes every call with `LUNORA_ADMIN_TOKEN`, so this helper acts
|
|
293
|
+
* as a trusted server-side operator. It is therefore not an end-user-callable
|
|
294
|
+
* API — never expose it on a path that isn't admin-token gated.
|
|
295
|
+
*
|
|
296
|
+
* `auth.$context` is a promise (better-auth resolves the adapter, password
|
|
297
|
+
* config, etc. lazily); we memoize it so the first call pays the cost once.
|
|
298
|
+
*/
|
|
299
|
+
declare const createAuthAdmin: (auth: LunoraAuth, options?: CreateAuthAdminOptions) => AuthAdmin;
|
|
300
|
+
/**
|
|
301
|
+
* Default basePath used by better-auth's client + handler. Override via the
|
|
302
|
+
* second argument if you mount the auth routes somewhere else.
|
|
303
|
+
*/
|
|
304
|
+
declare const DEFAULT_AUTH_BASE_PATH: string;
|
|
305
|
+
/**
|
|
306
|
+
* Route an inbound `Request` to better-auth if the path falls under
|
|
307
|
+
* `basePath`; otherwise return `null` so the caller can continue dispatching.
|
|
308
|
+
*
|
|
309
|
+
* Better-auth handles arbitrarily nested paths (`/api/auth/sign-in/email`,
|
|
310
|
+
* `/api/auth/callback/github`, …), so we use prefix matching instead of the
|
|
311
|
+
* exact-path map `createWorker` consumes for top-level routes.
|
|
312
|
+
*/
|
|
313
|
+
declare const handleAuthRequest: (auth: LunoraAuth, request: Request, basePath?: string) => Promise<Response | undefined>;
|
|
314
|
+
/**
|
|
315
|
+
* Apply better-auth's required schema (`user`, `session`, `account`,
|
|
316
|
+
* `verification`) to the configured database. Idempotent — better-auth
|
|
317
|
+
* diffs the existing schema and only runs the missing DDL.
|
|
318
|
+
*
|
|
319
|
+
* Cached per `options` reference so the diff cost (one PRAGMA-style sweep
|
|
320
|
+
* per table) doesn't fire on every request. In Cloudflare Workers the same
|
|
321
|
+
* `env.DB` binding is reused across invocations within a single isolate, so
|
|
322
|
+
* caching against the options object — which captures the binding — is
|
|
323
|
+
* sufficient.
|
|
324
|
+
*
|
|
325
|
+
* For production you should prefer pre-applying the schema at deploy time
|
|
326
|
+
* via `compileMigrationsSql` + `wrangler d1 execute`; this helper exists for
|
|
327
|
+
* dev/playground and small deployments.
|
|
328
|
+
*/
|
|
329
|
+
declare const ensureMigrated: (auth: LunoraAuth | {
|
|
330
|
+
options: LunoraAuthOptions;
|
|
331
|
+
}) => Promise<void>;
|
|
332
|
+
/**
|
|
333
|
+
* Compile better-auth's migrations to a single SQL string. Useful for
|
|
334
|
+
* `wrangler d1 execute --file -` in CI so the deploy step applies the schema
|
|
335
|
+
* before the first user request.
|
|
336
|
+
*/
|
|
337
|
+
declare const compileMigrationsSql: (options: LunoraAuthOptions) => Promise<string>;
|
|
338
|
+
/**
|
|
339
|
+
* Lunora-friendly view over better-auth's `session` option.
|
|
340
|
+
*
|
|
341
|
+
* This is a typed alias for better-auth's own `session` shape — Lunora stays a
|
|
342
|
+
* thin wrapper, so we don't reimplement the fields, we just give them a named,
|
|
343
|
+
* documented home so callers get autocomplete without reaching into
|
|
344
|
+
* `BetterAuthOptions`. The most relevant fields for session rotation / richer
|
|
345
|
+
* policies are `expiresIn` (absolute session lifetime, in seconds),
|
|
346
|
+
* `updateAge` (rolling-rotation interval, in seconds — how often an active
|
|
347
|
+
* session's expiry is pushed forward; `0` rotates on every use),
|
|
348
|
+
* `disableSessionRefresh` (turn rolling rotation off entirely so sessions
|
|
349
|
+
* expire at the absolute `expiresIn` regardless of activity), `freshAge`
|
|
350
|
+
* (freshness window, in seconds, for sensitive operations like account
|
|
351
|
+
* deletion; `0` treats every session as fresh — not recommended), and
|
|
352
|
+
* `cookieCache` (opt-in signed-cookie session cache to skip DB reads).
|
|
353
|
+
*
|
|
354
|
+
* See better-auth's `session` option for the full field list.
|
|
355
|
+
*/
|
|
356
|
+
type SessionPolicy = NonNullable<BetterAuthOptions["session"]>;
|
|
357
|
+
/**
|
|
358
|
+
* Validate a {@link SessionPolicy} and return it unchanged (pass-through).
|
|
359
|
+
*
|
|
360
|
+
* better-auth happily accepts the `session` block verbatim, so the only job
|
|
361
|
+
* here is to catch obviously-broken numeric inputs at construction time —
|
|
362
|
+
* negative or non-finite durations — rather than letting them produce
|
|
363
|
+
* surprising cookie expiries at runtime. Field names mirror better-auth's
|
|
364
|
+
* `session` option exactly so the validated object forwards 1:1.
|
|
365
|
+
*/
|
|
366
|
+
declare const validateSessionPolicy: (policy: SessionPolicy) => SessionPolicy;
|
|
367
|
+
/**
|
|
368
|
+
* Ready-made {@link SessionPolicy} presets covering the common rotation /
|
|
369
|
+
* expiry trade-offs. Spread or override fields as needed:
|
|
370
|
+
*
|
|
371
|
+
* ```ts
|
|
372
|
+
* import { createAuth, sessionPresets } from "@lunora/auth";
|
|
373
|
+
*
|
|
374
|
+
* export const auth = createAuth({
|
|
375
|
+
* secret: env.AUTH_SECRET,
|
|
376
|
+
* database: env.DB,
|
|
377
|
+
* session: { ...sessionPresets.rolling, freshAge: 60 * 5 },
|
|
378
|
+
* });
|
|
379
|
+
* ```
|
|
380
|
+
*
|
|
381
|
+
* - `rolling` — balanced default: 7-day absolute expiry, rotated once per day.
|
|
382
|
+
* - `strict` — short, security-sensitive: 1-hour expiry, 15-minute rotation.
|
|
383
|
+
* - `longLived` — low-friction consumer apps: 30-day expiry, daily rotation.
|
|
384
|
+
*/
|
|
385
|
+
declare const sessionPresets: Record<"longLived" | "rolling" | "strict", SessionPolicy>;
|
|
386
|
+
export { type AuthAccount, type AuthAdmin, type AuthAdminSession, type AuthAdminUser, type AuthCapabilities, type AuthInvitation, type AuthMember, type AuthOrganization, type AuthPage, type AuthPasskey, type AuthTimestamp, type CreateAuthAdminOptions, DEFAULT_AUTH_BASE_PATH, type ImpersonationResult, type ListUsersOptions, type LunoraAuth, LunoraAuthAdminError, type LunoraAuthOptions, type SessionPolicy, compileMigrationsSql, createAuthAdmin, ensureMigrated, handleAuthRequest, sessionPresets, validateSessionPolicy };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { lunoraAuthAdapter, lunoraD1Adapter } from './adapter.mjs';
|
|
2
|
+
export { LunoraAuthAdminError, createAuthAdmin } from './packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs';
|
|
3
|
+
export { createAuth } from './packem_shared/createAuth-B-tvsvQU.mjs';
|
|
4
|
+
export { DEFAULT_AUTH_BASE_PATH, handleAuthRequest } from './packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs';
|
|
5
|
+
export { LunoraAuthHeadersError, withAuthPlugins } from './middleware.mjs';
|
|
6
|
+
export { compileMigrationsSql, ensureMigrated } from './packem_shared/compileMigrationsSql-wZH3oXDu.mjs';
|
|
7
|
+
export { default as authTables } from './schema.mjs';
|
|
8
|
+
export { sessionPresets, validateSessionPolicy } from './packem_shared/sessionPresets-B95rXrd8.mjs';
|
|
9
|
+
export { createSqlAuthStore, d1Executor } from './sql-store.mjs';
|
|
10
|
+
export { createMemoryAuthStore, matchesWhere } from './store.mjs';
|
|
11
|
+
export { TURNSTILE_VERIFY_ENDPOINT, verifyTurnstile } from './turnstile.mjs';
|
|
12
|
+
export { verifyTurnstileMiddleware } from './turnstile-middleware.mjs';
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { L as LunoraAuth } from "./packem_shared/create-auth.d-M36jwG_Y.mjs";
|
|
2
|
+
import 'better-auth';
|
|
3
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
interface MiddlewareNext<ContextIn> {
|
|
10
|
+
(): Promise<ContextIn>;
|
|
11
|
+
<Extension extends Record<string, unknown>>(options: {
|
|
12
|
+
ctx: Extension;
|
|
13
|
+
}): Promise<ContextIn & Extension>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
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 {
|
|
26
|
+
/** The `ctx.authApi.<method>` that was called without `headers`. */
|
|
27
|
+
readonly method: string;
|
|
28
|
+
constructor(method: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Options for {@link withAuthPlugins}.
|
|
32
|
+
*/
|
|
33
|
+
interface WithAuthPluginsOptions {
|
|
34
|
+
/**
|
|
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.<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
|
+
*/
|
|
48
|
+
enforceHeaders?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
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
|
+
*/
|
|
63
|
+
interface LunoraAuthApiContext<Auth extends LunoraAuth> {
|
|
64
|
+
/**
|
|
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().<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
|
+
*/
|
|
99
|
+
readonly authApi: {
|
|
100
|
+
/**
|
|
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
|
+
*/
|
|
115
|
+
withoutHeaders: () => Auth["api"];
|
|
116
|
+
} & Auth["api"];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
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
|
+
*/
|
|
177
|
+
/**
|
|
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: <CtxIn>() => ...` —
|
|
182
|
+
* the generic must live on a callable type alias or interface.
|
|
183
|
+
*/
|
|
184
|
+
type WithAuthPluginsMiddleware<Auth extends LunoraAuth> = <ContextIn>(options: {
|
|
185
|
+
ctx: ContextIn;
|
|
186
|
+
next: MiddlewareNext<ContextIn>;
|
|
187
|
+
}) => Promise<ContextIn & LunoraAuthApiContext<Auth>>;
|
|
188
|
+
declare const withAuthPlugins: <Auth extends LunoraAuth>(auth: Auth, options?: WithAuthPluginsOptions) => WithAuthPluginsMiddleware<Auth>;
|
|
189
|
+
export { LunoraAuthApiContext, LunoraAuthHeadersError, WithAuthPluginsMiddleware, WithAuthPluginsOptions, withAuthPlugins };
|