@velajs/better-auth 0.6.0 → 1.0.0
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/CHANGELOG.md +12 -1
- package/README.md +19 -14
- package/dist/better-auth.service-BMkyFX-w.js +63 -0
- package/dist/better-auth.service-BMkyFX-w.js.map +1 -0
- package/dist/index.d.ts +280 -12
- package/dist/index.js +581 -15
- package/dist/index.js.map +1 -0
- package/dist/testing/index.d.ts +50 -2
- package/dist/testing/index.js +60 -3
- package/dist/testing/index.js.map +1 -0
- package/package.json +62 -42
- package/dist/better-auth.controller.d.ts +0 -21
- package/dist/better-auth.controller.js +0 -68
- package/dist/better-auth.module.d.ts +0 -52
- package/dist/better-auth.module.js +0 -138
- package/dist/better-auth.service.d.ts +0 -42
- package/dist/better-auth.service.js +0 -51
- package/dist/better-auth.tokens.d.ts +0 -5
- package/dist/better-auth.tokens.js +0 -4
- package/dist/better-auth.types.d.ts +0 -11
- package/dist/better-auth.types.js +0 -1
- package/dist/decorators/current-session.decorator.d.ts +0 -1
- package/dist/decorators/current-session.decorator.js +0 -7
- package/dist/decorators/current-user.decorator.d.ts +0 -1
- package/dist/decorators/current-user.decorator.js +0 -7
- package/dist/decorators/optional-auth.decorator.d.ts +0 -2
- package/dist/decorators/optional-auth.decorator.js +0 -5
- package/dist/decorators/public.decorator.d.ts +0 -2
- package/dist/decorators/public.decorator.js +0 -5
- package/dist/decorators/roles.decorator.d.ts +0 -2
- package/dist/decorators/roles.decorator.js +0 -5
- package/dist/guards/auth.guard.d.ts +0 -10
- package/dist/guards/auth.guard.js +0 -68
- package/dist/guards/roles.guard.d.ts +0 -5
- package/dist/guards/roles.guard.js +0 -36
- package/dist/testing/acting-as.d.ts +0 -46
- package/dist/testing/acting-as.js +0 -70
package/dist/index.js
CHANGED
|
@@ -1,15 +1,581 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-BMkyFX-w.js";
|
|
2
|
+
import { All, Controller, ForbiddenException, Inject, Injectable, InjectionToken, REQUEST_CONTEXT, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, defineModule, lazyProvider, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
|
|
3
|
+
import { AUTHZ } from "@velajs/authz/vela";
|
|
4
|
+
//#region src/decorators/public.decorator.ts
|
|
5
|
+
const Public = Reflector.createDecorator({ key: "vela.auth.public" });
|
|
6
|
+
const PUBLIC_KEY = Public.KEY;
|
|
7
|
+
/** Validate and canonicalize the route prefix used by the public auth controller. */
|
|
8
|
+
function normalizeBetterAuthBasePath(value) {
|
|
9
|
+
const basePath = value ?? "/api/auth";
|
|
10
|
+
if (basePath.length === 0 || basePath !== basePath.trim() || !basePath.startsWith("/") || basePath.startsWith("//") || basePath === "/" || basePath.endsWith("/") || /[\\?#*]/u.test(basePath) || /%(?:2e|2f|5c)/iu.test(basePath)) throw new Error("@velajs/better-auth: basePath must be a canonical absolute path such as \"/api/auth\"");
|
|
11
|
+
let decoded;
|
|
12
|
+
try {
|
|
13
|
+
decoded = decodeURIComponent(basePath);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error("@velajs/better-auth: basePath contains invalid percent encoding");
|
|
16
|
+
}
|
|
17
|
+
if (decoded.split("/").some((segment) => segment === "." || segment === "..")) throw new Error("@velajs/better-auth: basePath must not contain dot segments");
|
|
18
|
+
return basePath;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/better-auth.controller.ts
|
|
22
|
+
/**
|
|
23
|
+
* Build a catch-all controller that mounts better-auth's handler at `basePath`
|
|
24
|
+
* (default `/api/auth`). This is a factory because vela reads a controller's
|
|
25
|
+
* route off the class at decoration time, so a custom base path needs its own
|
|
26
|
+
* decorated class — the path can't be parametrized on a single shared class.
|
|
27
|
+
*
|
|
28
|
+
* Two base paths to keep consistent:
|
|
29
|
+
* - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);
|
|
30
|
+
* - better-auth routes against its OWN absolute `basePath` (the one you pass to
|
|
31
|
+
* `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.
|
|
32
|
+
*
|
|
33
|
+
* Both default to `/api/auth`, so the no-prefix / no-config case just works.
|
|
34
|
+
*/
|
|
35
|
+
function createBetterAuthCatchallController(basePath = "/api/auth") {
|
|
36
|
+
const normalizedBasePath = normalizeBetterAuthBasePath(basePath);
|
|
37
|
+
let BetterAuthCatchallController = class BetterAuthCatchallController {
|
|
38
|
+
auth;
|
|
39
|
+
constructor(auth) {
|
|
40
|
+
this.auth = auth;
|
|
41
|
+
}
|
|
42
|
+
async handle(c) {
|
|
43
|
+
return this.auth.handler(c.req.raw);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
__decorate([
|
|
47
|
+
All("/*"),
|
|
48
|
+
__decorateParam(0, Req()),
|
|
49
|
+
__decorateMetadata("design:type", Function),
|
|
50
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
51
|
+
__decorateMetadata("design:returntype", Promise)
|
|
52
|
+
], BetterAuthCatchallController.prototype, "handle", null);
|
|
53
|
+
BetterAuthCatchallController = __decorate([
|
|
54
|
+
Public(true),
|
|
55
|
+
Controller(normalizedBasePath),
|
|
56
|
+
Injectable(),
|
|
57
|
+
__decorateParam(0, Inject(BetterAuthService)),
|
|
58
|
+
__decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService])
|
|
59
|
+
], BetterAuthCatchallController);
|
|
60
|
+
return BetterAuthCatchallController;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Default-path (`/api/auth`) catch-all controller. Retained for back-compat;
|
|
64
|
+
* `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with
|
|
65
|
+
* the configured `basePath`. Prefer the factory for a custom base path.
|
|
66
|
+
*/
|
|
67
|
+
const BetterAuthCatchallController = createBetterAuthCatchallController();
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/better-auth.tokens.ts
|
|
70
|
+
const BETTER_AUTH_OPTIONS = new InjectionToken("vela.BetterAuthOptions");
|
|
71
|
+
const AUTH_USER_KEY = Symbol.for("vela.better-auth.user");
|
|
72
|
+
const AUTH_SESSION_KEY = Symbol.for("vela.better-auth.session");
|
|
73
|
+
const AUTH_ISSUER_KEY = Symbol.for("vela.better-auth.issuer");
|
|
74
|
+
const AUTH_PRINCIPAL_TYPE_KEY = Symbol.for("vela.better-auth.principal-type");
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/auth-request-state.ts
|
|
77
|
+
const ANONYMOUS = Object.freeze({ authenticated: false });
|
|
78
|
+
const stateByRequest = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
/** Clear any state before a guard evaluates a request. */
|
|
80
|
+
const beginAuthRequest = (context) => {
|
|
81
|
+
stateByRequest.set(context.getRequest(), ANONYMOUS);
|
|
82
|
+
};
|
|
83
|
+
/** Publish a fully verified session for downstream guards and parameters. */
|
|
84
|
+
const authenticateRequest = (context, state) => {
|
|
85
|
+
const authenticated = Object.freeze({
|
|
86
|
+
authenticated: true,
|
|
87
|
+
...state
|
|
88
|
+
});
|
|
89
|
+
stateByRequest.set(context.getRequest(), authenticated);
|
|
90
|
+
return authenticated;
|
|
91
|
+
};
|
|
92
|
+
/** Missing state is anonymous: no guard means no ambient identity. */
|
|
93
|
+
const getAuthRequestState = (context) => context.getType() === "http" ? stateByRequest.get(context.getRequest()) ?? ANONYMOUS : ANONYMOUS;
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/decorators/optional-auth.decorator.ts
|
|
96
|
+
const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
|
|
97
|
+
const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/guards/auth.guard.ts
|
|
100
|
+
let AuthGuard = class AuthGuard {
|
|
101
|
+
auth;
|
|
102
|
+
opts;
|
|
103
|
+
reflector = new Reflector();
|
|
104
|
+
constructor(auth, opts) {
|
|
105
|
+
this.auth = auth;
|
|
106
|
+
this.opts = opts;
|
|
107
|
+
}
|
|
108
|
+
async canActivate(context) {
|
|
109
|
+
if (context.getType() === "ws") {
|
|
110
|
+
if (hasValidWebSocketIdentity(context)) return true;
|
|
111
|
+
throw new AuthenticationRequiredException();
|
|
112
|
+
}
|
|
113
|
+
beginAuthRequest(context);
|
|
114
|
+
mirrorRequestContext(context, { authenticated: false });
|
|
115
|
+
if (this.reflector.getAllAndOverride(Public, context)) return true;
|
|
116
|
+
const request = context.getRequest();
|
|
117
|
+
const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));
|
|
118
|
+
if (data) {
|
|
119
|
+
mirrorRequestContext(context, authenticateRequest(context, {
|
|
120
|
+
user: data.user,
|
|
121
|
+
session: data.session,
|
|
122
|
+
issuer: this.opts.issuer ?? "better-auth",
|
|
123
|
+
principalType: "user"
|
|
124
|
+
}));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
|
|
128
|
+
throw new AuthenticationRequiredException();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
AuthGuard = __decorate([
|
|
132
|
+
Injectable(),
|
|
133
|
+
__decorateParam(0, Inject(BetterAuthService)),
|
|
134
|
+
__decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
|
|
135
|
+
__decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
|
|
136
|
+
], AuthGuard);
|
|
137
|
+
function hasValidWebSocketIdentity(context) {
|
|
138
|
+
try {
|
|
139
|
+
const data = context.switchToWs().getClient()?.data;
|
|
140
|
+
if (!data || typeof data !== "object") return false;
|
|
141
|
+
const record = data;
|
|
142
|
+
const principal = record.principal;
|
|
143
|
+
if (!principal || typeof principal !== "object") return false;
|
|
144
|
+
const fields = principal;
|
|
145
|
+
return typeof fields.issuer === "string" && fields.issuer.length > 0 && typeof fields.subject === "string" && fields.subject.length > 0 && (fields.principalType === "user" || fields.principalType === "service") && typeof record.tenantId === "string" && record.tenantId.length > 0 && typeof record.expiresAtMs === "number" && Number.isSafeInteger(record.expiresAtMs) && record.expiresAtMs > Date.now();
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Preserve the public REQUEST_CONTEXT symbols for applications that consume
|
|
152
|
+
* them directly. The private Request-keyed state above remains canonical: a
|
|
153
|
+
* missing/duplicated framework token must not prevent a verified guard from
|
|
154
|
+
* publishing identity to its own downstream decorators and guards.
|
|
155
|
+
*/
|
|
156
|
+
function mirrorRequestContext(context, state) {
|
|
157
|
+
const request = context.getRequest();
|
|
158
|
+
if (!state.authenticated) clearTrustedRequestIdentity(request);
|
|
159
|
+
else {
|
|
160
|
+
const tenantId = readActiveOrganizationId(state.session);
|
|
161
|
+
setTrustedRequestIdentity(request, {
|
|
162
|
+
principal: {
|
|
163
|
+
issuer: state.issuer,
|
|
164
|
+
subject: state.user.id,
|
|
165
|
+
principalType: state.principalType
|
|
166
|
+
},
|
|
167
|
+
...tenantId === void 0 ? {} : { tenantId }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const container = context.getContext().get("container");
|
|
171
|
+
if (!container) return;
|
|
172
|
+
let reqCtx;
|
|
173
|
+
try {
|
|
174
|
+
reqCtx = container.resolve(REQUEST_CONTEXT);
|
|
175
|
+
} catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!state.authenticated) {
|
|
179
|
+
reqCtx.set(AUTH_USER_KEY, void 0);
|
|
180
|
+
reqCtx.set(AUTH_SESSION_KEY, void 0);
|
|
181
|
+
reqCtx.set(AUTH_ISSUER_KEY, void 0);
|
|
182
|
+
reqCtx.set(AUTH_PRINCIPAL_TYPE_KEY, void 0);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
reqCtx.set(AUTH_USER_KEY, state.user);
|
|
186
|
+
reqCtx.set(AUTH_SESSION_KEY, state.session);
|
|
187
|
+
reqCtx.set(AUTH_ISSUER_KEY, state.issuer);
|
|
188
|
+
reqCtx.set(AUTH_PRINCIPAL_TYPE_KEY, state.principalType);
|
|
189
|
+
}
|
|
190
|
+
function readActiveOrganizationId(session) {
|
|
191
|
+
const descriptor = Object.getOwnPropertyDescriptor(session, "activeOrganizationId");
|
|
192
|
+
if (descriptor === void 0 || !("value" in descriptor)) return void 0;
|
|
193
|
+
const value = descriptor.value;
|
|
194
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
195
|
+
}
|
|
196
|
+
/** A test/provider override is trusted code, but its runtime result is not. */
|
|
197
|
+
function validateSessionData(value) {
|
|
198
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
199
|
+
try {
|
|
200
|
+
const userDescriptor = Object.getOwnPropertyDescriptor(value, "user");
|
|
201
|
+
const sessionDescriptor = Object.getOwnPropertyDescriptor(value, "session");
|
|
202
|
+
if (userDescriptor === void 0 || !("value" in userDescriptor) || sessionDescriptor === void 0 || !("value" in sessionDescriptor)) return;
|
|
203
|
+
const user = userDescriptor.value;
|
|
204
|
+
const session = sessionDescriptor.value;
|
|
205
|
+
if (user === null || typeof user !== "object" || session === null || typeof session !== "object") return;
|
|
206
|
+
const userIdDescriptor = Object.getOwnPropertyDescriptor(user, "id");
|
|
207
|
+
const sessionIdDescriptor = Object.getOwnPropertyDescriptor(session, "id");
|
|
208
|
+
const sessionUserIdDescriptor = Object.getOwnPropertyDescriptor(session, "userId");
|
|
209
|
+
const userId = userIdDescriptor !== void 0 && "value" in userIdDescriptor ? userIdDescriptor.value : void 0;
|
|
210
|
+
const sessionId = sessionIdDescriptor !== void 0 && "value" in sessionIdDescriptor ? sessionIdDescriptor.value : void 0;
|
|
211
|
+
const sessionUserId = sessionUserIdDescriptor !== void 0 && "value" in sessionUserIdDescriptor ? sessionUserIdDescriptor.value : void 0;
|
|
212
|
+
if (typeof userId !== "string" || userId.length === 0 || typeof sessionId !== "string" || sessionId.length === 0 || sessionUserId !== userId) return;
|
|
213
|
+
return {
|
|
214
|
+
user,
|
|
215
|
+
session
|
|
216
|
+
};
|
|
217
|
+
} catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `UnauthorizedException` preserves Nest-style direct behavior. The structural
|
|
223
|
+
* VelaError brand also survives test/runtime package duplication, so the
|
|
224
|
+
* central renderer still maps this to 401 rather than treating it as a foreign
|
|
225
|
+
* 500 error.
|
|
226
|
+
*/
|
|
227
|
+
var AuthenticationRequiredException = class extends UnauthorizedException {
|
|
228
|
+
type = "VelaError";
|
|
229
|
+
code = "unauthorized";
|
|
230
|
+
status = 401;
|
|
231
|
+
constructor() {
|
|
232
|
+
super("Authentication required");
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/decorators/roles.decorator.ts
|
|
237
|
+
const Roles = Reflector.createDecorator({ key: "vela.auth.roles" });
|
|
238
|
+
const ROLES_KEY = Roles.KEY;
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/guards/roles.guard.ts
|
|
241
|
+
const ACCESS_DENIED$1 = "Access denied";
|
|
242
|
+
let RolesGuard = class RolesGuard {
|
|
243
|
+
reflector = new Reflector();
|
|
244
|
+
canActivate(context) {
|
|
245
|
+
const required = this.reflector.getAllAndOverride(Roles, context);
|
|
246
|
+
if (!required || required.length === 0) return true;
|
|
247
|
+
const state = getAuthRequestState(context);
|
|
248
|
+
if (!state.authenticated) throw new ForbiddenException(ACCESS_DENIED$1);
|
|
249
|
+
const userRoles = normalizeRoles$1(state.user.role);
|
|
250
|
+
if (!required.some((r) => userRoles.includes(r))) throw new ForbiddenException(ACCESS_DENIED$1);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
RolesGuard = __decorate([Injectable()], RolesGuard);
|
|
255
|
+
function normalizeRoles$1(role) {
|
|
256
|
+
if (!role) return [];
|
|
257
|
+
if (Array.isArray(role)) return role;
|
|
258
|
+
return role.split(",").map((r) => r.trim()).filter(Boolean);
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/authz-bridge.ts
|
|
262
|
+
/** Stable issuer namespace used for better-auth session principals. */
|
|
263
|
+
const BETTER_AUTH_ISSUER = "better-auth";
|
|
264
|
+
const normalizeRoles = (role) => {
|
|
265
|
+
if (!role) return [];
|
|
266
|
+
if (Array.isArray(role)) return role.filter(Boolean);
|
|
267
|
+
return role.split(",").map((r) => r.trim()).filter(Boolean);
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Adapts a better-auth user into a stable `@velajs/authz` {@link Identity}.
|
|
271
|
+
* The issuer scopes `user.id` as both `subject` and the compatibility `userId`;
|
|
272
|
+
* the admin-plugin `role` field supplies local roles.
|
|
273
|
+
*
|
|
274
|
+
* Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
|
|
275
|
+
* request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
|
|
276
|
+
* `can()` checks grant nothing.
|
|
277
|
+
*/
|
|
278
|
+
const identityFromUser = (user, issuer = BETTER_AUTH_ISSUER, principalType = "user") => {
|
|
279
|
+
if (!user || typeof user.id !== "string" || user.id.length === 0) return { roles: [] };
|
|
280
|
+
if (issuer.length === 0) throw new Error("@velajs/better-auth: identity issuer must be non-empty");
|
|
281
|
+
return {
|
|
282
|
+
issuer,
|
|
283
|
+
subject: user.id,
|
|
284
|
+
principalType,
|
|
285
|
+
userId: user.id,
|
|
286
|
+
roles: normalizeRoles(user.role)
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Flattens a better-auth AC role's `statements` into `resource:action`
|
|
291
|
+
* permission strings — the granted-side format `@velajs/authz` matches
|
|
292
|
+
* (wildcards included).
|
|
293
|
+
*/
|
|
294
|
+
const permissionsFromAcRole = (role) => {
|
|
295
|
+
const permissions = [];
|
|
296
|
+
for (const [resource, actions] of Object.entries(role.statements ?? {})) for (const action of actions ?? []) permissions.push(`${resource}:${action}`);
|
|
297
|
+
return permissions;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
|
|
301
|
+
* better-auth access-control role table (`{ roleName: acRole }` — the same map
|
|
302
|
+
* shape passed to better-auth's admin/organization plugins). An identity's
|
|
303
|
+
* `roles` are unioned into their granted permission strings; unknown roles
|
|
304
|
+
* contribute nothing.
|
|
305
|
+
*
|
|
306
|
+
* ```ts
|
|
307
|
+
* const ac = createAccessControl({ posts: ['read', 'write'] });
|
|
308
|
+
* const authz = createAuthz({
|
|
309
|
+
* resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),
|
|
310
|
+
* });
|
|
311
|
+
* await authz.can(identityFromUser(user), 'posts:write');
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
const betterAuthAcResolver = (roles) => {
|
|
315
|
+
const grantsByRole = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const [name, role] of Object.entries(roles)) grantsByRole.set(name, permissionsFromAcRole(role));
|
|
317
|
+
return { grants(identity) {
|
|
318
|
+
const out = /* @__PURE__ */ new Set();
|
|
319
|
+
for (const name of identity.roles ?? []) for (const permission of grantsByRole.get(name) ?? []) out.add(permission);
|
|
320
|
+
return out;
|
|
321
|
+
} };
|
|
322
|
+
};
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/decorators/require-permission.decorator.ts
|
|
325
|
+
/**
|
|
326
|
+
* Declares the `@velajs/authz` permission(s) required to reach a controller or
|
|
327
|
+
* route handler. Read via `Reflector` in an authorization guard, then checked
|
|
328
|
+
* against the caller's `Identity` with `authz.can(...)`.
|
|
329
|
+
*
|
|
330
|
+
* ```ts
|
|
331
|
+
* @RequirePermission(['posts:write'])
|
|
332
|
+
* @Post()
|
|
333
|
+
* create() { ... }
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* The metadata is a plain `string[]` of permission strings in the granted-side
|
|
337
|
+
* format `@velajs/authz` matches (`resource:action`, or wildcards like
|
|
338
|
+
* `posts:*`). Handler-level metadata overrides class-level (standard
|
|
339
|
+
* `Reflector.getAllAndOverride` precedence).
|
|
340
|
+
*
|
|
341
|
+
* Semantics are **require-ALL** (AND): every listed permission must be granted
|
|
342
|
+
* for access — the `PermissionGuard` denies if any one is missing. This
|
|
343
|
+
* contrasts with `@Roles`, which is **OR** (any one of the listed roles
|
|
344
|
+
* suffices).
|
|
345
|
+
*/
|
|
346
|
+
const RequirePermission = Reflector.createDecorator({ key: "vela.authz.permissions" });
|
|
347
|
+
const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/guards/permission.guard.ts
|
|
350
|
+
const AUTHZ_TOKEN = AUTHZ;
|
|
351
|
+
const ACCESS_DENIED = "Access denied";
|
|
352
|
+
function resolveSingleAuthz(container, moduleId) {
|
|
353
|
+
try {
|
|
354
|
+
if (typeof container.resolveAll === "function") {
|
|
355
|
+
const candidates = container.resolveAll(AUTHZ_TOKEN, moduleId);
|
|
356
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
357
|
+
}
|
|
358
|
+
return container.resolve(AUTHZ_TOKEN, moduleId);
|
|
359
|
+
} catch {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
let PermissionGuard = class PermissionGuard {
|
|
364
|
+
reflector = new Reflector();
|
|
365
|
+
async canActivate(context) {
|
|
366
|
+
const required = this.reflector.getAllAndOverride(RequirePermission, context);
|
|
367
|
+
if (!required || required.length === 0) return true;
|
|
368
|
+
const container = resolveContextContainer(context);
|
|
369
|
+
const moduleId = context.getModuleId();
|
|
370
|
+
const authz = container === void 0 || moduleId === void 0 ? void 0 : resolveSingleAuthz(container, moduleId);
|
|
371
|
+
if (!authz) throw new ForbiddenException(ACCESS_DENIED);
|
|
372
|
+
const identity = resolveContextIdentity(context);
|
|
373
|
+
if (identity === void 0) throw new ForbiddenException(ACCESS_DENIED);
|
|
374
|
+
for (const permission of required) if (!await authz.can(identity, permission)) throw new ForbiddenException(ACCESS_DENIED);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
PermissionGuard = __decorate([Injectable()], PermissionGuard);
|
|
379
|
+
function resolveContextContainer(context) {
|
|
380
|
+
const direct = context.getContainer?.();
|
|
381
|
+
if (direct !== void 0) return direct;
|
|
382
|
+
if (context.getType() !== "http") return void 0;
|
|
383
|
+
try {
|
|
384
|
+
return context.getContext().get("container");
|
|
385
|
+
} catch {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function resolveContextIdentity(context) {
|
|
390
|
+
if (context.getType() === "ws") try {
|
|
391
|
+
const data = context.switchToWs().getClient()?.data;
|
|
392
|
+
if (!data || typeof data !== "object") return void 0;
|
|
393
|
+
const record = data;
|
|
394
|
+
const principal = record.principal;
|
|
395
|
+
if (!principal || typeof principal !== "object") return void 0;
|
|
396
|
+
const fields = principal;
|
|
397
|
+
if (typeof fields.issuer !== "string" || fields.issuer.length === 0 || typeof fields.subject !== "string" || fields.subject.length === 0 || fields.principalType !== "user" && fields.principalType !== "service" || typeof record.tenantId !== "string" || record.tenantId.length === 0 || typeof record.expiresAtMs !== "number" || !Number.isSafeInteger(record.expiresAtMs) || record.expiresAtMs <= Date.now()) return;
|
|
398
|
+
return {
|
|
399
|
+
issuer: fields.issuer,
|
|
400
|
+
subject: fields.subject,
|
|
401
|
+
principalType: fields.principalType,
|
|
402
|
+
userId: fields.subject,
|
|
403
|
+
roles: []
|
|
404
|
+
};
|
|
405
|
+
} catch {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const state = getAuthRequestState(context);
|
|
409
|
+
return state.authenticated ? identityFromUser(state.user, state.issuer, state.principalType) : void 0;
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/better-auth.module.ts
|
|
413
|
+
const referenceIds = /* @__PURE__ */ new WeakMap();
|
|
414
|
+
const explicitKeyClaims = /* @__PURE__ */ new Map();
|
|
415
|
+
let nextReferenceId = 1;
|
|
416
|
+
function referenceId(reference) {
|
|
417
|
+
const existing = referenceIds.get(reference);
|
|
418
|
+
if (existing !== void 0) return existing;
|
|
419
|
+
const id = nextReferenceId++;
|
|
420
|
+
referenceIds.set(reference, id);
|
|
421
|
+
return id;
|
|
422
|
+
}
|
|
423
|
+
function claimExplicitKey(key, kind, reference, shape) {
|
|
424
|
+
if (key.length === 0 || key !== key.trim()) throw new Error("@velajs/better-auth: an explicit module key must be a non-empty string");
|
|
425
|
+
const existing = explicitKeyClaims.get(key);
|
|
426
|
+
if (existing !== void 0 && (existing.kind !== kind || existing.reference !== reference || existing.shape !== shape)) throw new Error(`@velajs/better-auth: explicit module key "${key}" is already bound to a different auth registration`);
|
|
427
|
+
explicitKeyClaims.set(key, {
|
|
428
|
+
kind,
|
|
429
|
+
reference,
|
|
430
|
+
shape
|
|
431
|
+
});
|
|
432
|
+
return `explicit:${key}:ref:${referenceId(reference)}`;
|
|
433
|
+
}
|
|
434
|
+
function normalize(options) {
|
|
435
|
+
const basePath = normalizeBetterAuthBasePath(options.basePath);
|
|
436
|
+
const issuer = options.issuer ?? `better-auth:${basePath}`;
|
|
437
|
+
if (issuer.length === 0 || issuer !== issuer.trim()) throw new Error("@velajs/better-auth: issuer must be a non-empty stable namespace");
|
|
438
|
+
if (options.defaultPolicy !== void 0 && options.defaultPolicy !== "deny") throw new Error("@velajs/better-auth: defaultPolicy is deny-only; mark anonymous routes with @Public() or @OptionalAuth()");
|
|
439
|
+
return {
|
|
440
|
+
basePath,
|
|
441
|
+
issuer,
|
|
442
|
+
isGlobal: options.isGlobal ?? true,
|
|
443
|
+
defaultPolicy: "deny",
|
|
444
|
+
mountHandler: options.mountHandler ?? true
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/** Providers, controllers, and exports shared by both entry points. */
|
|
448
|
+
function commonContributions(n) {
|
|
449
|
+
return {
|
|
450
|
+
providers: [
|
|
451
|
+
BetterAuthService,
|
|
452
|
+
AuthGuard,
|
|
453
|
+
RolesGuard,
|
|
454
|
+
PermissionGuard
|
|
455
|
+
],
|
|
456
|
+
controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],
|
|
457
|
+
exports: [
|
|
458
|
+
BetterAuthService,
|
|
459
|
+
BETTER_AUTH_OPTIONS,
|
|
460
|
+
AuthGuard,
|
|
461
|
+
RolesGuard,
|
|
462
|
+
PermissionGuard
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The blessed engine generates `forRoot`. `setup` runs once per instance at
|
|
468
|
+
* call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,
|
|
469
|
+
* derives the auth builder from those options, mounts the catch-all controller,
|
|
470
|
+
* and — via the `global:` slot — registers the app-wide guard when `isGlobal`.
|
|
471
|
+
*
|
|
472
|
+
* `isGlobal` here means "apply AuthGuard app-wide", NOT "make this a global
|
|
473
|
+
* module", so the default `isGlobal → global: true` extras transform is
|
|
474
|
+
* replaced with identity; the flag reaches `setup` through the options bag.
|
|
475
|
+
*/
|
|
476
|
+
const authModuleHost = defineModule({
|
|
477
|
+
name: "BetterAuth",
|
|
478
|
+
optionsToken: BETTER_AUTH_OPTIONS,
|
|
479
|
+
transform: (definition) => definition,
|
|
480
|
+
key: (options) => stableHash(normalize(options)),
|
|
481
|
+
setup: ({ OPTIONS, options }) => {
|
|
482
|
+
const n = normalize(options);
|
|
483
|
+
const common = commonContributions(n);
|
|
484
|
+
const auth = options.auth;
|
|
485
|
+
return {
|
|
486
|
+
providers: [
|
|
487
|
+
{
|
|
488
|
+
provide: OPTIONS,
|
|
489
|
+
useValue: {
|
|
490
|
+
...n,
|
|
491
|
+
auth
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
lazyProvider({
|
|
495
|
+
provide: BETTER_AUTH_BUILDER,
|
|
496
|
+
inject: [OPTIONS],
|
|
497
|
+
useFactory: (o) => o.auth
|
|
498
|
+
}),
|
|
499
|
+
...common.providers
|
|
500
|
+
],
|
|
501
|
+
controllers: common.controllers,
|
|
502
|
+
exports: common.exports,
|
|
503
|
+
global: n.isGlobal ? { guards: [AuthGuard] } : void 0
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
var BetterAuthModule = class BetterAuthModule {
|
|
508
|
+
/**
|
|
509
|
+
* Synchronous registration. The auth instance is constructed by the consumer
|
|
510
|
+
* at module-load time and passed in directly. Use this when the inputs to
|
|
511
|
+
* `betterAuth({...})` are available at startup (Node apps with a static DB
|
|
512
|
+
* connection, in-memory adapters, etc.).
|
|
513
|
+
*/
|
|
514
|
+
static forRoot(options) {
|
|
515
|
+
const shape = stableHash(normalize(options));
|
|
516
|
+
const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : claimExplicitKey(options.key, "auth", options.auth, shape);
|
|
517
|
+
return {
|
|
518
|
+
...authModuleHost.ConfigurableModuleClass.forRoot({
|
|
519
|
+
...options,
|
|
520
|
+
key
|
|
521
|
+
}),
|
|
522
|
+
module: BetterAuthModule
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Deferred / DI-driven registration. The user factory runs **lazily**, on the
|
|
527
|
+
* first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
|
|
528
|
+
* In normal request handling that's `AuthGuard.canActivate` or the catch-all
|
|
529
|
+
* controller's `.handle`. At module load the factory does NOT run — it's only
|
|
530
|
+
* captured behind {@link lazyProvider}'s memoized thunk. This is what makes
|
|
531
|
+
* Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but
|
|
532
|
+
* it IS by the time a request flows through and the guard / catch-all reads
|
|
533
|
+
* the service. Inject deps resolve at module load (cheap BindingRef wrappers);
|
|
534
|
+
* their *values* are read at first auth use, inside your factory body.
|
|
535
|
+
*/
|
|
536
|
+
static forRootAsync(options) {
|
|
537
|
+
const n = normalize(options);
|
|
538
|
+
const common = commonContributions(n);
|
|
539
|
+
const shape = stableHash({
|
|
540
|
+
...n,
|
|
541
|
+
inject: options.inject
|
|
542
|
+
});
|
|
543
|
+
const key = options.key === void 0 ? `${shape}:factory:${referenceId(options.useFactory)}` : claimExplicitKey(options.key, "factory", options.useFactory, shape);
|
|
544
|
+
return {
|
|
545
|
+
module: BetterAuthModule,
|
|
546
|
+
key,
|
|
547
|
+
imports: options.imports ?? [],
|
|
548
|
+
providers: [
|
|
549
|
+
{
|
|
550
|
+
provide: BETTER_AUTH_OPTIONS,
|
|
551
|
+
useValue: n
|
|
552
|
+
},
|
|
553
|
+
lazyProvider({
|
|
554
|
+
provide: BETTER_AUTH_BUILDER,
|
|
555
|
+
inject: options.inject,
|
|
556
|
+
useFactory: options.useFactory
|
|
557
|
+
}),
|
|
558
|
+
...common.providers,
|
|
559
|
+
...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
|
|
560
|
+
],
|
|
561
|
+
controllers: common.controllers,
|
|
562
|
+
exports: common.exports
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/decorators/current-user.decorator.ts
|
|
568
|
+
const CurrentUser = createParamDecorator((_data, ctx) => {
|
|
569
|
+
const state = getAuthRequestState(ctx);
|
|
570
|
+
return state.authenticated ? state.user : void 0;
|
|
571
|
+
});
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/decorators/current-session.decorator.ts
|
|
574
|
+
const CurrentSession = createParamDecorator((_data, ctx) => {
|
|
575
|
+
const state = getAuthRequestState(ctx);
|
|
576
|
+
return state.authenticated ? state.session : void 0;
|
|
577
|
+
});
|
|
578
|
+
//#endregion
|
|
579
|
+
export { AUTH_ISSUER_KEY, AUTH_PRINCIPAL_TYPE_KEY, AUTH_SESSION_KEY, AUTH_USER_KEY, AuthGuard, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, BetterAuthCatchallController, BetterAuthModule, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, PermissionGuard, Public, REQUIRE_PERMISSION_KEY, ROLES_KEY, RequirePermission, Roles, RolesGuard, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
|
|
580
|
+
|
|
581
|
+
//# sourceMappingURL=index.js.map
|