@velajs/better-auth 1.0.0 → 1.22.1
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 +34 -0
- package/README.md +24 -19
- package/dist/{better-auth.service-BMkyFX-w.js → better-auth.service-DurQ4JQf.js} +4 -4
- package/dist/better-auth.service-DurQ4JQf.js.map +1 -0
- package/dist/better-auth.service-NOlWXxd4.d.ts +68 -0
- package/dist/index.d.ts +29 -158
- package/dist/index.js +198 -390
- package/dist/index.js.map +1 -1
- package/dist/testing/index.d.ts +4 -3
- package/dist/testing/index.js +3 -2
- package/dist/testing/index.js.map +1 -1
- package/package.json +24 -25
- package/dist/better-auth.service-BMkyFX-w.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-
|
|
2
|
-
import { All, Controller,
|
|
3
|
-
import {
|
|
1
|
+
import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-DurQ4JQf.js";
|
|
2
|
+
import { All, Controller, Inject, Injectable, InjectionToken, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, defineProvider, getTrustedRequestIdentity, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
|
|
3
|
+
import { getContextIdentity } from "@velajs/authz/vela";
|
|
4
4
|
//#region src/decorators/public.decorator.ts
|
|
5
5
|
const Public = Reflector.createDecorator({ key: "vela.auth.public" });
|
|
6
6
|
const PUBLIC_KEY = Public.KEY;
|
|
@@ -59,38 +59,114 @@ function createBetterAuthCatchallController(basePath = "/api/auth") {
|
|
|
59
59
|
], BetterAuthCatchallController);
|
|
60
60
|
return BetterAuthCatchallController;
|
|
61
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
62
|
//#endregion
|
|
69
63
|
//#region src/better-auth.tokens.ts
|
|
70
64
|
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
65
|
//#endregion
|
|
76
66
|
//#region src/auth-request-state.ts
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
67
|
+
const sessions = /* @__PURE__ */ new WeakMap();
|
|
68
|
+
function beginAuthRequest(context) {
|
|
69
|
+
clearTrustedRequestIdentity(context.getRequest());
|
|
70
|
+
}
|
|
71
|
+
function authenticateRequest(context, data, issuer) {
|
|
72
|
+
const request = context.getRequest();
|
|
73
|
+
setTrustedRequestIdentity(request, {
|
|
74
|
+
principal: {
|
|
75
|
+
issuer,
|
|
76
|
+
subject: data.user.id,
|
|
77
|
+
principalType: "user"
|
|
78
|
+
},
|
|
79
|
+
expiresAtMs: data.session.expiresAt.getTime(),
|
|
80
|
+
roles: data.roles,
|
|
81
|
+
...data.tenantId === void 0 ? {} : { tenantId: data.tenantId }
|
|
88
82
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
const identity = getTrustedRequestIdentity(request);
|
|
84
|
+
if (identity) sessions.set(identity, data);
|
|
85
|
+
}
|
|
86
|
+
function getAuthRequestState(context) {
|
|
87
|
+
if (context.getType() !== "http") return void 0;
|
|
88
|
+
const identity = getTrustedRequestIdentity(context.getRequest());
|
|
89
|
+
return identity && sessions.get(identity);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/session-data.ts
|
|
93
|
+
function own(value, key) {
|
|
94
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
95
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
96
|
+
return descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
97
|
+
}
|
|
98
|
+
function requiredString(value) {
|
|
99
|
+
return typeof value === "string" && value.length > 0;
|
|
100
|
+
}
|
|
101
|
+
function date(value) {
|
|
102
|
+
return value instanceof Date && Number.isFinite(value.getTime()) ? new Date(value) : void 0;
|
|
103
|
+
}
|
|
104
|
+
function optionalString(value) {
|
|
105
|
+
return value === void 0 || value === null || typeof value === "string";
|
|
106
|
+
}
|
|
107
|
+
/** Keep plugin data opaque; base model fields below are validated individually. */
|
|
108
|
+
function dataProperties(value) {
|
|
109
|
+
return Object.fromEntries(Object.keys(value).map((key) => [key, own(value, key)]));
|
|
110
|
+
}
|
|
111
|
+
/** Validate the full Better Auth base models, not merely ids masquerading as them. */
|
|
112
|
+
function validateSessionData(value) {
|
|
113
|
+
try {
|
|
114
|
+
const user = own(value, "user");
|
|
115
|
+
const session = own(value, "session");
|
|
116
|
+
if (typeof user !== "object" || user === null || typeof session !== "object" || session === null) return;
|
|
117
|
+
const id = own(user, "id");
|
|
118
|
+
const email = own(user, "email");
|
|
119
|
+
const name = own(user, "name");
|
|
120
|
+
const emailVerified = own(user, "emailVerified");
|
|
121
|
+
const image = own(user, "image");
|
|
122
|
+
const createdAt = date(own(user, "createdAt"));
|
|
123
|
+
const updatedAt = date(own(user, "updatedAt"));
|
|
124
|
+
const sessionId = own(session, "id");
|
|
125
|
+
const userId = own(session, "userId");
|
|
126
|
+
const token = own(session, "token");
|
|
127
|
+
const sessionCreatedAt = date(own(session, "createdAt"));
|
|
128
|
+
const sessionUpdatedAt = date(own(session, "updatedAt"));
|
|
129
|
+
const expiresAt = date(own(session, "expiresAt"));
|
|
130
|
+
const ipAddress = own(session, "ipAddress");
|
|
131
|
+
const userAgent = own(session, "userAgent");
|
|
132
|
+
const organization = own(session, "activeOrganizationId");
|
|
133
|
+
const role = own(user, "role");
|
|
134
|
+
let roles = [];
|
|
135
|
+
if (role !== void 0 && role !== null) {
|
|
136
|
+
if (typeof role === "string") roles = role.split(",").map((part) => part.trim()).filter(Boolean);
|
|
137
|
+
else if (Array.isArray(role) && role.every((entry) => typeof entry === "string")) roles = role.map((entry) => entry.trim()).filter(Boolean);
|
|
138
|
+
else return void 0;
|
|
139
|
+
}
|
|
140
|
+
if (!requiredString(id) || !requiredString(email) || typeof name !== "string" || typeof emailVerified !== "boolean" || !optionalString(image) || !createdAt || !updatedAt || !requiredString(sessionId) || userId !== id || !requiredString(token) || !sessionCreatedAt || !sessionUpdatedAt || !expiresAt || expiresAt.getTime() <= Date.now() || !optionalString(ipAddress) || !optionalString(userAgent) || organization !== void 0 && organization !== null && !requiredString(organization)) return void 0;
|
|
141
|
+
return {
|
|
142
|
+
user: Object.freeze({
|
|
143
|
+
...dataProperties(user),
|
|
144
|
+
id,
|
|
145
|
+
email,
|
|
146
|
+
name,
|
|
147
|
+
emailVerified,
|
|
148
|
+
createdAt,
|
|
149
|
+
updatedAt,
|
|
150
|
+
image
|
|
151
|
+
}),
|
|
152
|
+
session: Object.freeze({
|
|
153
|
+
...dataProperties(session),
|
|
154
|
+
id: sessionId,
|
|
155
|
+
userId: id,
|
|
156
|
+
token,
|
|
157
|
+
createdAt: sessionCreatedAt,
|
|
158
|
+
updatedAt: sessionUpdatedAt,
|
|
159
|
+
expiresAt,
|
|
160
|
+
ipAddress,
|
|
161
|
+
userAgent
|
|
162
|
+
}),
|
|
163
|
+
roles: Object.freeze(roles),
|
|
164
|
+
...typeof organization === "string" ? { tenantId: organization } : {}
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
94
170
|
//#endregion
|
|
95
171
|
//#region src/decorators/optional-auth.decorator.ts
|
|
96
172
|
const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
|
|
@@ -107,25 +183,18 @@ let AuthGuard = class AuthGuard {
|
|
|
107
183
|
}
|
|
108
184
|
async canActivate(context) {
|
|
109
185
|
if (context.getType() === "ws") {
|
|
110
|
-
if (
|
|
111
|
-
throw new
|
|
186
|
+
if (getContextIdentity(context)) return true;
|
|
187
|
+
throw new UnauthorizedException("Authentication required");
|
|
112
188
|
}
|
|
113
189
|
beginAuthRequest(context);
|
|
114
|
-
mirrorRequestContext(context, { authenticated: false });
|
|
115
190
|
if (this.reflector.getAllAndOverride(Public, context)) return true;
|
|
116
|
-
const
|
|
117
|
-
const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));
|
|
191
|
+
const data = validateSessionData(await this.auth.api.getSession({ headers: context.getRequest().headers }));
|
|
118
192
|
if (data) {
|
|
119
|
-
|
|
120
|
-
user: data.user,
|
|
121
|
-
session: data.session,
|
|
122
|
-
issuer: this.opts.issuer ?? "better-auth",
|
|
123
|
-
principalType: "user"
|
|
124
|
-
}));
|
|
193
|
+
authenticateRequest(context, data, this.opts.issuer ?? "better-auth");
|
|
125
194
|
return true;
|
|
126
195
|
}
|
|
127
196
|
if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
|
|
128
|
-
throw new
|
|
197
|
+
throw new UnauthorizedException("Authentication required");
|
|
129
198
|
}
|
|
130
199
|
};
|
|
131
200
|
AuthGuard = __decorate([
|
|
@@ -134,280 +203,6 @@ AuthGuard = __decorate([
|
|
|
134
203
|
__decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
|
|
135
204
|
__decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
|
|
136
205
|
], 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
206
|
//#endregion
|
|
412
207
|
//#region src/better-auth.module.ts
|
|
413
208
|
const referenceIds = /* @__PURE__ */ new WeakMap();
|
|
@@ -435,75 +230,25 @@ function normalize(options) {
|
|
|
435
230
|
const basePath = normalizeBetterAuthBasePath(options.basePath);
|
|
436
231
|
const issuer = options.issuer ?? `better-auth:${basePath}`;
|
|
437
232
|
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
233
|
return {
|
|
440
234
|
basePath,
|
|
441
235
|
issuer,
|
|
442
236
|
isGlobal: options.isGlobal ?? true,
|
|
443
|
-
defaultPolicy: "deny",
|
|
444
237
|
mountHandler: options.mountHandler ?? true
|
|
445
238
|
};
|
|
446
239
|
}
|
|
447
240
|
/** Providers, controllers, and exports shared by both entry points. */
|
|
448
241
|
function commonContributions(n) {
|
|
449
242
|
return {
|
|
450
|
-
providers: [
|
|
451
|
-
BetterAuthService,
|
|
452
|
-
AuthGuard,
|
|
453
|
-
RolesGuard,
|
|
454
|
-
PermissionGuard
|
|
455
|
-
],
|
|
243
|
+
providers: [BetterAuthService, AuthGuard],
|
|
456
244
|
controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],
|
|
457
245
|
exports: [
|
|
458
246
|
BetterAuthService,
|
|
459
247
|
BETTER_AUTH_OPTIONS,
|
|
460
|
-
AuthGuard
|
|
461
|
-
RolesGuard,
|
|
462
|
-
PermissionGuard
|
|
248
|
+
AuthGuard
|
|
463
249
|
]
|
|
464
250
|
};
|
|
465
251
|
}
|
|
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
252
|
var BetterAuthModule = class BetterAuthModule {
|
|
508
253
|
/**
|
|
509
254
|
* Synchronous registration. The auth instance is constructed by the consumer
|
|
@@ -512,14 +257,21 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
512
257
|
* connection, in-memory adapters, etc.).
|
|
513
258
|
*/
|
|
514
259
|
static forRoot(options) {
|
|
515
|
-
const
|
|
260
|
+
const normalized = normalize(options);
|
|
261
|
+
const shape = stableHash(normalized);
|
|
516
262
|
const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : claimExplicitKey(options.key, "auth", options.auth, shape);
|
|
263
|
+
const common = commonContributions(normalized);
|
|
517
264
|
return {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
265
|
+
module: BetterAuthModule,
|
|
266
|
+
key,
|
|
267
|
+
providers: [
|
|
268
|
+
defineProvider(BETTER_AUTH_OPTIONS, { useValue: normalized }),
|
|
269
|
+
defineProvider(BETTER_AUTH_BUILDER, { useValue: () => options.auth }),
|
|
270
|
+
...common.providers,
|
|
271
|
+
...normalized.isGlobal ? provideGlobal("guard", AuthGuard) : []
|
|
272
|
+
],
|
|
273
|
+
controllers: common.controllers,
|
|
274
|
+
exports: common.exports
|
|
523
275
|
};
|
|
524
276
|
}
|
|
525
277
|
/**
|
|
@@ -527,11 +279,9 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
527
279
|
* first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
|
|
528
280
|
* In normal request handling that's `AuthGuard.canActivate` or the catch-all
|
|
529
281
|
* controller's `.handle`. At module load the factory does NOT run — it's only
|
|
530
|
-
* captured
|
|
531
|
-
*
|
|
532
|
-
*
|
|
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.
|
|
282
|
+
* captured in the checked builder provider. The Workers adapter supplies its
|
|
283
|
+
* native environment before DI; the lazily constructed auth instance belongs
|
|
284
|
+
* to that environment's application and never captures another app's bindings.
|
|
535
285
|
*/
|
|
536
286
|
static forRootAsync(options) {
|
|
537
287
|
const n = normalize(options);
|
|
@@ -546,14 +296,10 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
546
296
|
key,
|
|
547
297
|
imports: options.imports ?? [],
|
|
548
298
|
providers: [
|
|
549
|
-
{
|
|
550
|
-
|
|
551
|
-
useValue: n
|
|
552
|
-
},
|
|
553
|
-
lazyProvider({
|
|
554
|
-
provide: BETTER_AUTH_BUILDER,
|
|
299
|
+
defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),
|
|
300
|
+
defineProvider(BETTER_AUTH_BUILDER, {
|
|
555
301
|
inject: options.inject,
|
|
556
|
-
useFactory: options.useFactory
|
|
302
|
+
useFactory: (...deps) => () => options.useFactory(...deps)
|
|
557
303
|
}),
|
|
558
304
|
...common.providers,
|
|
559
305
|
...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
|
|
@@ -566,16 +312,78 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
566
312
|
//#endregion
|
|
567
313
|
//#region src/decorators/current-user.decorator.ts
|
|
568
314
|
const CurrentUser = createParamDecorator((_data, ctx) => {
|
|
569
|
-
|
|
570
|
-
return state.authenticated ? state.user : void 0;
|
|
315
|
+
return getAuthRequestState(ctx)?.user;
|
|
571
316
|
});
|
|
572
317
|
//#endregion
|
|
573
318
|
//#region src/decorators/current-session.decorator.ts
|
|
574
319
|
const CurrentSession = createParamDecorator((_data, ctx) => {
|
|
575
|
-
|
|
576
|
-
return state.authenticated ? state.session : void 0;
|
|
320
|
+
return getAuthRequestState(ctx)?.session;
|
|
577
321
|
});
|
|
578
322
|
//#endregion
|
|
579
|
-
|
|
323
|
+
//#region src/authz-bridge.ts
|
|
324
|
+
/** Stable issuer namespace used for better-auth session principals. */
|
|
325
|
+
const BETTER_AUTH_ISSUER = "better-auth";
|
|
326
|
+
const normalizeRoles = (role) => {
|
|
327
|
+
if (!role) return [];
|
|
328
|
+
if (Array.isArray(role)) return role.filter(Boolean);
|
|
329
|
+
return role.split(",").map((r) => r.trim()).filter(Boolean);
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Pure authorization projection; this does not authenticate or publish trusted state.
|
|
333
|
+
* Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.
|
|
334
|
+
* The issuer scopes `user.id` as both `subject` and the compatibility `userId`;
|
|
335
|
+
* the admin-plugin `role` field supplies local roles.
|
|
336
|
+
*
|
|
337
|
+
* Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
|
|
338
|
+
* request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
|
|
339
|
+
* `can()` checks grant nothing.
|
|
340
|
+
*/
|
|
341
|
+
const identityFromUser = (user, issuer = BETTER_AUTH_ISSUER, principalType = "user") => {
|
|
342
|
+
if (!user || typeof user.id !== "string" || user.id.length === 0) return { roles: [] };
|
|
343
|
+
if (issuer.length === 0) throw new Error("@velajs/better-auth: identity issuer must be non-empty");
|
|
344
|
+
return {
|
|
345
|
+
issuer,
|
|
346
|
+
subject: user.id,
|
|
347
|
+
principalType,
|
|
348
|
+
userId: user.id,
|
|
349
|
+
roles: normalizeRoles(user.role)
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Flattens a better-auth AC role's `statements` into `resource:action`
|
|
354
|
+
* permission strings — the granted-side format `@velajs/authz` matches
|
|
355
|
+
* (wildcards included).
|
|
356
|
+
*/
|
|
357
|
+
const permissionsFromAcRole = (role) => {
|
|
358
|
+
const permissions = [];
|
|
359
|
+
for (const [resource, actions] of Object.entries(role.statements ?? {})) for (const action of actions ?? []) permissions.push(`${resource}:${action}`);
|
|
360
|
+
return permissions;
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
|
|
364
|
+
* better-auth access-control role table (`{ roleName: acRole }` — the same map
|
|
365
|
+
* shape passed to better-auth's admin/organization plugins). An identity's
|
|
366
|
+
* `roles` are unioned into their granted permission strings; unknown roles
|
|
367
|
+
* contribute nothing.
|
|
368
|
+
*
|
|
369
|
+
* ```ts
|
|
370
|
+
* const ac = createAccessControl({ posts: ['read', 'write'] });
|
|
371
|
+
* const authz = createAuthz({
|
|
372
|
+
* resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),
|
|
373
|
+
* });
|
|
374
|
+
* await authz.can(identityFromUser(user), 'posts:write');
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
const betterAuthAcResolver = (roles) => {
|
|
378
|
+
const grantsByRole = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const [name, role] of Object.entries(roles)) grantsByRole.set(name, permissionsFromAcRole(role));
|
|
380
|
+
return { grants(identity) {
|
|
381
|
+
const out = /* @__PURE__ */ new Set();
|
|
382
|
+
for (const name of identity.roles ?? []) for (const permission of grantsByRole.get(name) ?? []) out.add(permission);
|
|
383
|
+
return out;
|
|
384
|
+
} };
|
|
385
|
+
};
|
|
386
|
+
//#endregion
|
|
387
|
+
export { AuthGuard, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, BetterAuthModule, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, Public, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
|
|
580
388
|
|
|
581
389
|
//# sourceMappingURL=index.js.map
|