@fluixi/auth 1.0.0-alpha.100
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 +21 -0
- package/README.md +72 -0
- package/dist/client.cjs +219 -0
- package/dist/client.d.ts +177 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +291 -0
- package/dist/client.mjs +199 -0
- package/dist/cookies.cjs +43 -0
- package/dist/cookies.d.ts +7 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +19 -0
- package/dist/cookies.mjs +22 -0
- package/dist/remote.cjs +52 -0
- package/dist/remote.d.ts +20 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +30 -0
- package/dist/remote.mjs +31 -0
- package/dist/server.cjs +203 -0
- package/dist/server.d.ts +131 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +167 -0
- package/dist/server.mjs +183 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +26 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.mjs +5 -0
- package/package.json +76 -0
package/dist/remote.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function remoteAuth(options) {
|
|
2
|
+
const base = new URL((options.basePath ?? '/api/auth').replace(/\/$/, '') + '/', options.url);
|
|
3
|
+
const call = (input, init) => (options.fetch ?? globalThis.fetch)(input, init);
|
|
4
|
+
return {
|
|
5
|
+
async handler(request) {
|
|
6
|
+
const url = new URL(request.url);
|
|
7
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
8
|
+
return call(new URL(url.pathname + url.search, base), {
|
|
9
|
+
method: request.method,
|
|
10
|
+
headers: request.headers,
|
|
11
|
+
body: hasBody ? request.body : undefined,
|
|
12
|
+
redirect: 'manual',
|
|
13
|
+
...(hasBody ? { duplex: 'half' } : {}),
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
api: {
|
|
17
|
+
async getSession({ headers }) {
|
|
18
|
+
const cookie = headers.get('cookie');
|
|
19
|
+
// No cookie, no session: spare the API a request from every visitor.
|
|
20
|
+
if (!cookie)
|
|
21
|
+
return null;
|
|
22
|
+
const response = await call(new URL('get-session', base), { headers: { cookie, accept: 'application/json' } });
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
return null;
|
|
25
|
+
const body = (await response.json().catch(() => null));
|
|
26
|
+
return body && body.user ? { user: body.user, session: body.session ?? null } : null;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
package/dist/remote.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/remote.ts
|
|
2
|
+
function remoteAuth(options) {
|
|
3
|
+
const base = new URL((options.basePath ?? "/api/auth").replace(/\/$/, "") + "/", options.url);
|
|
4
|
+
const call = (input, init) => (options.fetch ?? globalThis.fetch)(input, init);
|
|
5
|
+
return {
|
|
6
|
+
async handler(request) {
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
9
|
+
return call(new URL(url.pathname + url.search, base), {
|
|
10
|
+
method: request.method,
|
|
11
|
+
headers: request.headers,
|
|
12
|
+
body: hasBody ? request.body : void 0,
|
|
13
|
+
redirect: "manual",
|
|
14
|
+
...hasBody ? { duplex: "half" } : {}
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
api: {
|
|
18
|
+
async getSession({ headers }) {
|
|
19
|
+
const cookie = headers.get("cookie");
|
|
20
|
+
if (!cookie) return null;
|
|
21
|
+
const response = await call(new URL("get-session", base), { headers: { cookie, accept: "application/json" } });
|
|
22
|
+
if (!response.ok) return null;
|
|
23
|
+
const body = await response.json().catch(() => null);
|
|
24
|
+
return body && body.user ? { user: body.user, session: body.session ?? null } : null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
remoteAuth
|
|
31
|
+
};
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*! @fluixi/auth v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/server.ts
|
|
22
|
+
var server_exports = {};
|
|
23
|
+
__export(server_exports, {
|
|
24
|
+
AuthError: () => AuthError,
|
|
25
|
+
authMiddleware: () => authMiddleware,
|
|
26
|
+
defineAuth: () => defineAuth,
|
|
27
|
+
deleteCookie: () => import_cookie2.deleteCookie,
|
|
28
|
+
getAuth: () => getAuth,
|
|
29
|
+
getCookie: () => getCookie,
|
|
30
|
+
getCookies: () => getCookies,
|
|
31
|
+
member: () => member,
|
|
32
|
+
mountAuthRoutes: () => mountAuthRoutes,
|
|
33
|
+
optionalUser: () => optionalUser,
|
|
34
|
+
parseCookies: () => import_cookie2.parseCookies,
|
|
35
|
+
remoteAuth: () => remoteAuth,
|
|
36
|
+
requirePermission: () => requirePermission,
|
|
37
|
+
requireRole: () => requireRole,
|
|
38
|
+
requireUser: () => requireUser,
|
|
39
|
+
serializeCookie: () => import_cookie2.serializeCookie,
|
|
40
|
+
signedIn: () => signedIn
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(server_exports);
|
|
43
|
+
var import_server2 = require("@fluixi/server");
|
|
44
|
+
|
|
45
|
+
// src/types.ts
|
|
46
|
+
var AUTH_LOCALS_KEY = "auth";
|
|
47
|
+
|
|
48
|
+
// src/cookies.ts
|
|
49
|
+
var import_server = require("@fluixi/server");
|
|
50
|
+
var import_cookie = require("@fluixi/utils/cookie");
|
|
51
|
+
var import_cookie2 = require("@fluixi/utils/cookie");
|
|
52
|
+
function requestHeaders() {
|
|
53
|
+
const req = (0, import_server.getRequestEvent)()?.request;
|
|
54
|
+
return req?.headers ?? null;
|
|
55
|
+
}
|
|
56
|
+
function getCookie(name) {
|
|
57
|
+
return (0, import_cookie.parseCookies)(requestHeaders()?.get("cookie"))[name] ?? null;
|
|
58
|
+
}
|
|
59
|
+
function getCookies() {
|
|
60
|
+
return (0, import_cookie.parseCookies)(requestHeaders()?.get("cookie"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/remote.ts
|
|
64
|
+
function remoteAuth(options) {
|
|
65
|
+
const base = new URL((options.basePath ?? "/api/auth").replace(/\/$/, "") + "/", options.url);
|
|
66
|
+
const call = (input, init) => (options.fetch ?? globalThis.fetch)(input, init);
|
|
67
|
+
return {
|
|
68
|
+
async handler(request) {
|
|
69
|
+
const url = new URL(request.url);
|
|
70
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
71
|
+
return call(new URL(url.pathname + url.search, base), {
|
|
72
|
+
method: request.method,
|
|
73
|
+
headers: request.headers,
|
|
74
|
+
body: hasBody ? request.body : void 0,
|
|
75
|
+
redirect: "manual",
|
|
76
|
+
...hasBody ? { duplex: "half" } : {}
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
api: {
|
|
80
|
+
async getSession({ headers }) {
|
|
81
|
+
const cookie = headers.get("cookie");
|
|
82
|
+
if (!cookie) return null;
|
|
83
|
+
const response = await call(new URL("get-session", base), { headers: { cookie, accept: "application/json" } });
|
|
84
|
+
if (!response.ok) return null;
|
|
85
|
+
const body = await response.json().catch(() => null);
|
|
86
|
+
return body && body.user ? { user: body.user, session: body.session ?? null } : null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/server.ts
|
|
93
|
+
var _auth = null;
|
|
94
|
+
function defineAuth(instance) {
|
|
95
|
+
const auth = {
|
|
96
|
+
raw: instance,
|
|
97
|
+
handler: (request) => instance.handler(request),
|
|
98
|
+
getSession: async (request) => {
|
|
99
|
+
const res = await instance.api.getSession({ headers: request.headers });
|
|
100
|
+
return { user: res?.user ?? null, session: res?.session ?? null };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
_auth = auth;
|
|
104
|
+
return auth;
|
|
105
|
+
}
|
|
106
|
+
function getAuth() {
|
|
107
|
+
if (!_auth) {
|
|
108
|
+
throw new Error("[@fluixi/auth] defineAuth() was not called — no auth instance registered.");
|
|
109
|
+
}
|
|
110
|
+
return _auth;
|
|
111
|
+
}
|
|
112
|
+
function mountAuthRoutes() {
|
|
113
|
+
return (request) => getAuth().handler(request);
|
|
114
|
+
}
|
|
115
|
+
function authMiddleware() {
|
|
116
|
+
return async (request, next) => {
|
|
117
|
+
const session = await getAuth().getSession(request);
|
|
118
|
+
(0, import_server2.getRequestLocals)(request)[AUTH_LOCALS_KEY] = session;
|
|
119
|
+
return next();
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
var AuthError = class extends Error {
|
|
123
|
+
constructor(status, redirectTo, message, code) {
|
|
124
|
+
super(message ?? (status === 401 ? "Unauthorized" : "Forbidden"));
|
|
125
|
+
this.status = status;
|
|
126
|
+
this.redirectTo = redirectTo;
|
|
127
|
+
this.code = code;
|
|
128
|
+
this.name = "AuthError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function current() {
|
|
132
|
+
return (0, import_server2.getLocals)()[AUTH_LOCALS_KEY] ?? { user: null, session: null };
|
|
133
|
+
}
|
|
134
|
+
async function optionalUser() {
|
|
135
|
+
return current().user;
|
|
136
|
+
}
|
|
137
|
+
async function requireUser(opts) {
|
|
138
|
+
const user = current().user;
|
|
139
|
+
if (!user) throw new AuthError(401, opts?.redirectTo ?? "/login");
|
|
140
|
+
return user;
|
|
141
|
+
}
|
|
142
|
+
async function requireRole(role, opts) {
|
|
143
|
+
const user = await requireUser(opts);
|
|
144
|
+
if (user.role !== role) throw new AuthError(403, opts?.redirectTo);
|
|
145
|
+
return user;
|
|
146
|
+
}
|
|
147
|
+
async function requirePermission(check, opts) {
|
|
148
|
+
await requireUser(opts);
|
|
149
|
+
const ok = typeof check === "function" ? check() : check;
|
|
150
|
+
if (!ok) throw new AuthError(403, opts?.redirectTo);
|
|
151
|
+
}
|
|
152
|
+
var SESSION_COOKIE = {
|
|
153
|
+
type: "apiKey",
|
|
154
|
+
in: "cookie",
|
|
155
|
+
name: "better-auth.session_token",
|
|
156
|
+
description: "Set by sign-in. Named `__Secure-better-auth.session_token` over HTTPS."
|
|
157
|
+
};
|
|
158
|
+
async function sessionFor(request) {
|
|
159
|
+
const resolved = (0, import_server2.getLocals)()[AUTH_LOCALS_KEY];
|
|
160
|
+
return resolved ?? getAuth().getSession(request);
|
|
161
|
+
}
|
|
162
|
+
function signedIn(options = {}) {
|
|
163
|
+
return {
|
|
164
|
+
async check(request) {
|
|
165
|
+
const { user, session } = await sessionFor(request);
|
|
166
|
+
if (!user || !session) throw new AuthError(401);
|
|
167
|
+
return { user, session };
|
|
168
|
+
},
|
|
169
|
+
security: { name: "session", scheme: options.scheme ?? SESSION_COOKIE },
|
|
170
|
+
refuses: [401]
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function member(options = {}) {
|
|
174
|
+
const signed = signedIn(options);
|
|
175
|
+
return {
|
|
176
|
+
async check(request) {
|
|
177
|
+
const auth = await signed.check(request);
|
|
178
|
+
if (!auth.session.activeOrganizationId) {
|
|
179
|
+
throw new AuthError(403, void 0, "Choose an organisation first.", "no_active_organization");
|
|
180
|
+
}
|
|
181
|
+
const api = getAuth().raw.api;
|
|
182
|
+
if (!api.getActiveMember || !api.hasPermission) {
|
|
183
|
+
throw new Error("[@fluixi/auth] member() needs Better Auth's organization() plugin.");
|
|
184
|
+
}
|
|
185
|
+
const membership = await api.getActiveMember({ headers: request.headers });
|
|
186
|
+
if (!membership) {
|
|
187
|
+
throw new AuthError(403, void 0, "You are not a member of this organisation.", "not_a_member");
|
|
188
|
+
}
|
|
189
|
+
if (options.roles) {
|
|
190
|
+
const held = membership.role.split(",").map((role2) => role2.trim());
|
|
191
|
+
if (!options.roles.some((role2) => held.includes(role2))) throw new AuthError(403);
|
|
192
|
+
}
|
|
193
|
+
if (options.permissions) {
|
|
194
|
+
const { success } = await api.hasPermission({ headers: request.headers, body: { permissions: options.permissions } });
|
|
195
|
+
if (!success) throw new AuthError(403);
|
|
196
|
+
}
|
|
197
|
+
const { id, organizationId, userId, role } = membership;
|
|
198
|
+
return { ...auth, member: { id, organizationId, userId, role } };
|
|
199
|
+
},
|
|
200
|
+
security: signed.security,
|
|
201
|
+
refuses: [401, 403]
|
|
202
|
+
};
|
|
203
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fluixi/auth/server: the server-side bindings.
|
|
3
|
+
*
|
|
4
|
+
* `defineAuth(betterAuth(...))` registers the engine; `authMiddleware()` resolves the
|
|
5
|
+
* session once per request and writes it to `locals.auth` (via the @fluixi/start locals
|
|
6
|
+
* bridge), so the server helpers: `requireUser`, `requireRole`, `requirePermission` ,
|
|
7
|
+
* are pure reads of request-scoped state. No manual token parsing; works inside loaders
|
|
8
|
+
* AND server functions (same request context).
|
|
9
|
+
*/
|
|
10
|
+
import type { Middleware } from '@fluixi/start';
|
|
11
|
+
import { type AuthSession, type AuthUser, type BetterAuthServer, type SessionData } from './types.js';
|
|
12
|
+
export interface FluixiAuth<U = AuthUser> {
|
|
13
|
+
/** The underlying Better Auth instance. */
|
|
14
|
+
raw: BetterAuthServer;
|
|
15
|
+
/** Mount at `/api/auth/*`. */
|
|
16
|
+
handler: (request: Request) => Promise<Response>;
|
|
17
|
+
/** Resolve the session for a request (used by the middleware). */
|
|
18
|
+
getSession: (request: Request) => Promise<SessionData<U>>;
|
|
19
|
+
}
|
|
20
|
+
export declare function defineAuth<U = AuthUser>(instance: BetterAuthServer): FluixiAuth<U>;
|
|
21
|
+
export declare function getAuth<U = AuthUser>(): FluixiAuth<U>;
|
|
22
|
+
/** Catch-all route handler for `/api/auth/*` (sugar over `auth.handler`). */
|
|
23
|
+
export declare function mountAuthRoutes(): (request: Request) => Promise<Response>;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the session ONCE per request and stash it on `locals.auth`. Runs before the
|
|
26
|
+
* render (and before server functions), so every helper below is a pure locals read.
|
|
27
|
+
* Place it in `src/middleware.ts`: `export default defineMiddleware([ authMiddleware() ])`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function authMiddleware(): Middleware;
|
|
30
|
+
/**
|
|
31
|
+
* Thrown by the require* helpers and the guards; carries the HTTP status, an optional redirect
|
|
32
|
+
* target, and a `code` when the refusal is more precise than 401 or 403 alone.
|
|
33
|
+
*/
|
|
34
|
+
export declare class AuthError extends Error {
|
|
35
|
+
status: 401 | 403;
|
|
36
|
+
redirectTo?: string | undefined;
|
|
37
|
+
code?: string | undefined;
|
|
38
|
+
constructor(status: 401 | 403, redirectTo?: string | undefined, message?: string, code?: string | undefined);
|
|
39
|
+
}
|
|
40
|
+
/** The current user or null: never throws. */
|
|
41
|
+
export declare function optionalUser<U = AuthUser>(): Promise<U | null>;
|
|
42
|
+
/** The current user, or throw `AuthError(401, redirectTo)`. */
|
|
43
|
+
export declare function requireUser<U = AuthUser>(opts?: {
|
|
44
|
+
redirectTo?: string;
|
|
45
|
+
}): Promise<U>;
|
|
46
|
+
/** Require a specific role, or throw `AuthError(403)`. */
|
|
47
|
+
export declare function requireRole<U extends {
|
|
48
|
+
role?: string;
|
|
49
|
+
} = AuthUser>(role: string, opts?: {
|
|
50
|
+
redirectTo?: string;
|
|
51
|
+
}): Promise<U>;
|
|
52
|
+
/**
|
|
53
|
+
* Require a permission. Pass a boolean or a predicate (e.g. your RBAC/ABAC `can(user, …)`):
|
|
54
|
+
* `await requirePermission(() => can(user, 'edit', post))`. Throws `AuthError(403)` if false.
|
|
55
|
+
*/
|
|
56
|
+
export declare function requirePermission(check: boolean | (() => boolean), opts?: {
|
|
57
|
+
redirectTo?: string;
|
|
58
|
+
}): Promise<void>;
|
|
59
|
+
export interface GuardOptions {
|
|
60
|
+
/** The security scheme the API description gives, when the cookie is renamed or a token is used. */
|
|
61
|
+
scheme?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
/** A signed-in caller's session, as a guard hands it to the handler. */
|
|
64
|
+
export interface SignedIn<U = AuthUser> {
|
|
65
|
+
user: U;
|
|
66
|
+
session: AuthSession;
|
|
67
|
+
}
|
|
68
|
+
/** The caller's membership of the organisation their session has active. */
|
|
69
|
+
export interface ActiveMember {
|
|
70
|
+
id: string;
|
|
71
|
+
organizationId: string;
|
|
72
|
+
userId: string;
|
|
73
|
+
/** One role, or several comma-separated, as Better Auth stores them. */
|
|
74
|
+
role: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* An `@fluixi/api` guard: the caller must be signed in. The handler gets `auth.user` and
|
|
78
|
+
* `auth.session`. Refuses with 401.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* export const GET = route({ guard: signedIn(), handler: ({ auth }) => auth.user });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function signedIn<U = AuthUser>(options?: GuardOptions): {
|
|
85
|
+
check(request: Request): Promise<SignedIn<U>>;
|
|
86
|
+
security: {
|
|
87
|
+
name: string;
|
|
88
|
+
scheme: {
|
|
89
|
+
type: string;
|
|
90
|
+
in: string;
|
|
91
|
+
name: string;
|
|
92
|
+
description: string;
|
|
93
|
+
} | Record<string, unknown>;
|
|
94
|
+
};
|
|
95
|
+
refuses: readonly [401];
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* An `@fluixi/api` guard: the caller must be a member of the organisation their session has
|
|
99
|
+
* active, with one of `roles` if given, and every one of `permissions` if given (checked by
|
|
100
|
+
* Better Auth's access control). The handler gets `auth.member.organizationId`, which is the
|
|
101
|
+
* tenant every query of that route is scoped to.
|
|
102
|
+
*
|
|
103
|
+
* Refuses with 401 when signed out, else 403: `no_active_organization` when none is active,
|
|
104
|
+
* `not_a_member` when the membership is gone, `forbidden` for a role or permission.
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* export const DELETE = route({ guard: member({ roles: ['owner', 'admin'] }), handler });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function member<U = AuthUser>(options?: GuardOptions & {
|
|
111
|
+
roles?: readonly string[];
|
|
112
|
+
permissions?: Record<string, string[]>;
|
|
113
|
+
}): {
|
|
114
|
+
check(request: Request): Promise<SignedIn<U> & {
|
|
115
|
+
member: ActiveMember;
|
|
116
|
+
}>;
|
|
117
|
+
security: {
|
|
118
|
+
name: string;
|
|
119
|
+
scheme: {
|
|
120
|
+
type: string;
|
|
121
|
+
in: string;
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
124
|
+
} | Record<string, unknown>;
|
|
125
|
+
};
|
|
126
|
+
refuses: readonly [401, 403];
|
|
127
|
+
};
|
|
128
|
+
export { parseCookies, serializeCookie, deleteCookie, getCookie, getCookies, type CookieOptions, } from './cookies.js';
|
|
129
|
+
export { remoteAuth, type RemoteAuthOptions } from './remote.js';
|
|
130
|
+
export type { SessionData, AuthSession, AuthUser, BetterAuthServer } from './types.js';
|
|
131
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { getLocals, getRequestLocals } from '@fluixi/server';
|
|
2
|
+
import { AUTH_LOCALS_KEY, } from './types.js';
|
|
3
|
+
// Registered instance: so the helpers/middleware find "the" auth without the app
|
|
4
|
+
// threading it through every call. defineAuth sets it; getAuth reads it.
|
|
5
|
+
let _auth = null;
|
|
6
|
+
export function defineAuth(instance) {
|
|
7
|
+
const auth = {
|
|
8
|
+
raw: instance,
|
|
9
|
+
handler: (request) => instance.handler(request),
|
|
10
|
+
getSession: async (request) => {
|
|
11
|
+
const res = await instance.api.getSession({ headers: request.headers });
|
|
12
|
+
return { user: res?.user ?? null, session: res?.session ?? null };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
_auth = auth;
|
|
16
|
+
return auth;
|
|
17
|
+
}
|
|
18
|
+
export function getAuth() {
|
|
19
|
+
if (!_auth) {
|
|
20
|
+
throw new Error('[@fluixi/auth] defineAuth() was not called — no auth instance registered.');
|
|
21
|
+
}
|
|
22
|
+
return _auth;
|
|
23
|
+
}
|
|
24
|
+
/** Catch-all route handler for `/api/auth/*` (sugar over `auth.handler`). */
|
|
25
|
+
export function mountAuthRoutes() {
|
|
26
|
+
return (request) => getAuth().handler(request);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the session ONCE per request and stash it on `locals.auth`. Runs before the
|
|
30
|
+
* render (and before server functions), so every helper below is a pure locals read.
|
|
31
|
+
* Place it in `src/middleware.ts`: `export default defineMiddleware([ authMiddleware() ])`.
|
|
32
|
+
*/
|
|
33
|
+
export function authMiddleware() {
|
|
34
|
+
return async (request, next) => {
|
|
35
|
+
const session = await getAuth().getSession(request);
|
|
36
|
+
getRequestLocals(request)[AUTH_LOCALS_KEY] = session;
|
|
37
|
+
return next();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Thrown by the require* helpers and the guards; carries the HTTP status, an optional redirect
|
|
42
|
+
* target, and a `code` when the refusal is more precise than 401 or 403 alone.
|
|
43
|
+
*/
|
|
44
|
+
export class AuthError extends Error {
|
|
45
|
+
status;
|
|
46
|
+
redirectTo;
|
|
47
|
+
code;
|
|
48
|
+
constructor(status, redirectTo, message, code) {
|
|
49
|
+
super(message ?? (status === 401 ? 'Unauthorized' : 'Forbidden'));
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.redirectTo = redirectTo;
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.name = 'AuthError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function current() {
|
|
57
|
+
return getLocals()[AUTH_LOCALS_KEY] ?? { user: null, session: null };
|
|
58
|
+
}
|
|
59
|
+
/** The current user or null: never throws. */
|
|
60
|
+
export async function optionalUser() {
|
|
61
|
+
return current().user;
|
|
62
|
+
}
|
|
63
|
+
/** The current user, or throw `AuthError(401, redirectTo)`. */
|
|
64
|
+
export async function requireUser(opts) {
|
|
65
|
+
const user = current().user;
|
|
66
|
+
if (!user)
|
|
67
|
+
throw new AuthError(401, opts?.redirectTo ?? '/login');
|
|
68
|
+
return user;
|
|
69
|
+
}
|
|
70
|
+
/** Require a specific role, or throw `AuthError(403)`. */
|
|
71
|
+
export async function requireRole(role, opts) {
|
|
72
|
+
const user = await requireUser(opts);
|
|
73
|
+
if (user.role !== role)
|
|
74
|
+
throw new AuthError(403, opts?.redirectTo);
|
|
75
|
+
return user;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Require a permission. Pass a boolean or a predicate (e.g. your RBAC/ABAC `can(user, …)`):
|
|
79
|
+
* `await requirePermission(() => can(user, 'edit', post))`. Throws `AuthError(403)` if false.
|
|
80
|
+
*/
|
|
81
|
+
export async function requirePermission(check, opts) {
|
|
82
|
+
await requireUser(opts);
|
|
83
|
+
const ok = typeof check === 'function' ? check() : check;
|
|
84
|
+
if (!ok)
|
|
85
|
+
throw new AuthError(403, opts?.redirectTo);
|
|
86
|
+
}
|
|
87
|
+
/** Better Auth's session cookie, as an API description names it. */
|
|
88
|
+
const SESSION_COOKIE = {
|
|
89
|
+
type: 'apiKey',
|
|
90
|
+
in: 'cookie',
|
|
91
|
+
name: 'better-auth.session_token',
|
|
92
|
+
description: 'Set by sign-in. Named `__Secure-better-auth.session_token` over HTTPS.',
|
|
93
|
+
};
|
|
94
|
+
/** The session for this request: from `authMiddleware()` when it ran, else resolved here. */
|
|
95
|
+
async function sessionFor(request) {
|
|
96
|
+
const resolved = getLocals()[AUTH_LOCALS_KEY];
|
|
97
|
+
return resolved ?? getAuth().getSession(request);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* An `@fluixi/api` guard: the caller must be signed in. The handler gets `auth.user` and
|
|
101
|
+
* `auth.session`. Refuses with 401.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* export const GET = route({ guard: signedIn(), handler: ({ auth }) => auth.user });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function signedIn(options = {}) {
|
|
108
|
+
return {
|
|
109
|
+
async check(request) {
|
|
110
|
+
const { user, session } = await sessionFor(request);
|
|
111
|
+
if (!user || !session)
|
|
112
|
+
throw new AuthError(401);
|
|
113
|
+
return { user, session };
|
|
114
|
+
},
|
|
115
|
+
security: { name: 'session', scheme: options.scheme ?? SESSION_COOKIE },
|
|
116
|
+
refuses: [401],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* An `@fluixi/api` guard: the caller must be a member of the organisation their session has
|
|
121
|
+
* active, with one of `roles` if given, and every one of `permissions` if given (checked by
|
|
122
|
+
* Better Auth's access control). The handler gets `auth.member.organizationId`, which is the
|
|
123
|
+
* tenant every query of that route is scoped to.
|
|
124
|
+
*
|
|
125
|
+
* Refuses with 401 when signed out, else 403: `no_active_organization` when none is active,
|
|
126
|
+
* `not_a_member` when the membership is gone, `forbidden` for a role or permission.
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* export const DELETE = route({ guard: member({ roles: ['owner', 'admin'] }), handler });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function member(options = {}) {
|
|
133
|
+
const signed = signedIn(options);
|
|
134
|
+
return {
|
|
135
|
+
async check(request) {
|
|
136
|
+
const auth = await signed.check(request);
|
|
137
|
+
if (!auth.session.activeOrganizationId) {
|
|
138
|
+
throw new AuthError(403, undefined, 'Choose an organisation first.', 'no_active_organization');
|
|
139
|
+
}
|
|
140
|
+
const api = getAuth().raw.api;
|
|
141
|
+
if (!api.getActiveMember || !api.hasPermission) {
|
|
142
|
+
throw new Error("[@fluixi/auth] member() needs Better Auth's organization() plugin.");
|
|
143
|
+
}
|
|
144
|
+
const membership = await api.getActiveMember({ headers: request.headers });
|
|
145
|
+
if (!membership) {
|
|
146
|
+
throw new AuthError(403, undefined, 'You are not a member of this organisation.', 'not_a_member');
|
|
147
|
+
}
|
|
148
|
+
if (options.roles) {
|
|
149
|
+
const held = membership.role.split(',').map((role) => role.trim());
|
|
150
|
+
if (!options.roles.some((role) => held.includes(role)))
|
|
151
|
+
throw new AuthError(403);
|
|
152
|
+
}
|
|
153
|
+
if (options.permissions) {
|
|
154
|
+
const { success } = await api.hasPermission({ headers: request.headers, body: { permissions: options.permissions } });
|
|
155
|
+
if (!success)
|
|
156
|
+
throw new AuthError(403);
|
|
157
|
+
}
|
|
158
|
+
const { id, organizationId, userId, role } = membership;
|
|
159
|
+
return { ...auth, member: { id, organizationId, userId, role } };
|
|
160
|
+
},
|
|
161
|
+
security: signed.security,
|
|
162
|
+
refuses: [401, 403],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Cookie API: proper, secure-by-default cookie management (parse/serialize/request reads).
|
|
166
|
+
export { parseCookies, serializeCookie, deleteCookie, getCookie, getCookies, } from './cookies.js';
|
|
167
|
+
export { remoteAuth } from './remote.js';
|