@velajs/better-auth 0.6.1 → 2.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 -0
- package/README.md +42 -32
- 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 +33 -152
- package/dist/index.js +255 -230
- 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 +25 -23
- package/dist/better-auth.service-BMkyFX-w.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
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;
|
|
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
|
+
}
|
|
7
20
|
//#endregion
|
|
8
21
|
//#region src/better-auth.controller.ts
|
|
9
22
|
/**
|
|
@@ -20,6 +33,7 @@ const PUBLIC_KEY = Public.KEY;
|
|
|
20
33
|
* Both default to `/api/auth`, so the no-prefix / no-config case just works.
|
|
21
34
|
*/
|
|
22
35
|
function createBetterAuthCatchallController(basePath = "/api/auth") {
|
|
36
|
+
const normalizedBasePath = normalizeBetterAuthBasePath(basePath);
|
|
23
37
|
let BetterAuthCatchallController = class BetterAuthCatchallController {
|
|
24
38
|
auth;
|
|
25
39
|
constructor(auth) {
|
|
@@ -38,24 +52,121 @@ function createBetterAuthCatchallController(basePath = "/api/auth") {
|
|
|
38
52
|
], BetterAuthCatchallController.prototype, "handle", null);
|
|
39
53
|
BetterAuthCatchallController = __decorate([
|
|
40
54
|
Public(true),
|
|
41
|
-
Controller(
|
|
55
|
+
Controller(normalizedBasePath),
|
|
42
56
|
Injectable(),
|
|
43
57
|
__decorateParam(0, Inject(BetterAuthService)),
|
|
44
58
|
__decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService])
|
|
45
59
|
], BetterAuthCatchallController);
|
|
46
60
|
return BetterAuthCatchallController;
|
|
47
61
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Default-path (`/api/auth`) catch-all controller. Retained for back-compat;
|
|
50
|
-
* `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with
|
|
51
|
-
* the configured `basePath`. Prefer the factory for a custom base path.
|
|
52
|
-
*/
|
|
53
|
-
const BetterAuthCatchallController = createBetterAuthCatchallController();
|
|
54
62
|
//#endregion
|
|
55
63
|
//#region src/better-auth.tokens.ts
|
|
56
64
|
const BETTER_AUTH_OPTIONS = new InjectionToken("vela.BetterAuthOptions");
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/auth-request-state.ts
|
|
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 }
|
|
82
|
+
});
|
|
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
|
+
}
|
|
59
170
|
//#endregion
|
|
60
171
|
//#region src/decorators/optional-auth.decorator.ts
|
|
61
172
|
const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
|
|
@@ -71,19 +182,18 @@ let AuthGuard = class AuthGuard {
|
|
|
71
182
|
this.opts = opts;
|
|
72
183
|
}
|
|
73
184
|
async canActivate(context) {
|
|
185
|
+
if (context.getType() === "ws") {
|
|
186
|
+
if (getContextIdentity(context)) return true;
|
|
187
|
+
throw new UnauthorizedException("Authentication required");
|
|
188
|
+
}
|
|
189
|
+
beginAuthRequest(context);
|
|
74
190
|
if (this.reflector.getAllAndOverride(Public, context)) return true;
|
|
75
|
-
const
|
|
76
|
-
const path = new URL(request.url).pathname;
|
|
77
|
-
const basePath = this.opts.basePath ?? "/api/auth";
|
|
78
|
-
if (path === basePath || path.startsWith(`${basePath}/`)) return true;
|
|
79
|
-
const data = await this.auth.api.getSession({ headers: request.headers });
|
|
191
|
+
const data = validateSessionData(await this.auth.api.getSession({ headers: context.getRequest().headers }));
|
|
80
192
|
if (data) {
|
|
81
|
-
|
|
82
|
-
reqCtx.set(AUTH_USER_KEY, data.user);
|
|
83
|
-
reqCtx.set(AUTH_SESSION_KEY, data.session);
|
|
193
|
+
authenticateRequest(context, data, this.opts.issuer ?? "better-auth");
|
|
84
194
|
return true;
|
|
85
195
|
}
|
|
86
|
-
if (this.
|
|
196
|
+
if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
|
|
87
197
|
throw new UnauthorizedException("Authentication required");
|
|
88
198
|
}
|
|
89
199
|
};
|
|
@@ -93,209 +203,52 @@ AuthGuard = __decorate([
|
|
|
93
203
|
__decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
|
|
94
204
|
__decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
|
|
95
205
|
], AuthGuard);
|
|
96
|
-
function resolveRequestContext(context) {
|
|
97
|
-
return context.getContext().get("container").resolve(REQUEST_CONTEXT);
|
|
98
|
-
}
|
|
99
|
-
//#endregion
|
|
100
|
-
//#region src/decorators/roles.decorator.ts
|
|
101
|
-
const Roles = Reflector.createDecorator({ key: "vela.auth.roles" });
|
|
102
|
-
const ROLES_KEY = Roles.KEY;
|
|
103
|
-
//#endregion
|
|
104
|
-
//#region src/guards/roles.guard.ts
|
|
105
|
-
let RolesGuard = class RolesGuard {
|
|
106
|
-
reflector = new Reflector();
|
|
107
|
-
canActivate(context) {
|
|
108
|
-
const required = this.reflector.getAllAndOverride(Roles, context);
|
|
109
|
-
if (!required || required.length === 0) return true;
|
|
110
|
-
const user = context.getContext().get("container").resolve(REQUEST_CONTEXT).get(AUTH_USER_KEY);
|
|
111
|
-
if (!user) throw new ForbiddenException("Role check requires authentication");
|
|
112
|
-
const userRoles = normalizeRoles$1(user.role);
|
|
113
|
-
if (!required.some((r) => userRoles.includes(r))) throw new ForbiddenException(`Insufficient role; one of [${required.join(", ")}] required`);
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
RolesGuard = __decorate([Injectable()], RolesGuard);
|
|
118
|
-
function normalizeRoles$1(role) {
|
|
119
|
-
if (!role) return [];
|
|
120
|
-
if (Array.isArray(role)) return role;
|
|
121
|
-
return role.split(",").map((r) => r.trim()).filter(Boolean);
|
|
122
|
-
}
|
|
123
|
-
//#endregion
|
|
124
|
-
//#region src/authz-bridge.ts
|
|
125
|
-
const normalizeRoles = (role) => {
|
|
126
|
-
if (!role) return [];
|
|
127
|
-
if (Array.isArray(role)) return role.filter(Boolean);
|
|
128
|
-
return role.split(",").map((r) => r.trim()).filter(Boolean);
|
|
129
|
-
};
|
|
130
|
-
/**
|
|
131
|
-
* Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps
|
|
132
|
-
* `user.id` → `userId` and the admin-plugin `role` field → `roles`.
|
|
133
|
-
*
|
|
134
|
-
* Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
|
|
135
|
-
* request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
|
|
136
|
-
* `can()` checks grant nothing.
|
|
137
|
-
*/
|
|
138
|
-
const identityFromUser = (user) => {
|
|
139
|
-
if (!user) return { roles: [] };
|
|
140
|
-
return {
|
|
141
|
-
userId: user.id,
|
|
142
|
-
roles: normalizeRoles(user.role)
|
|
143
|
-
};
|
|
144
|
-
};
|
|
145
|
-
/**
|
|
146
|
-
* Flattens a better-auth AC role's `statements` into `resource:action`
|
|
147
|
-
* permission strings — the granted-side format `@velajs/authz` matches
|
|
148
|
-
* (wildcards included).
|
|
149
|
-
*/
|
|
150
|
-
const permissionsFromAcRole = (role) => {
|
|
151
|
-
const permissions = [];
|
|
152
|
-
for (const [resource, actions] of Object.entries(role.statements ?? {})) for (const action of actions ?? []) permissions.push(`${resource}:${action}`);
|
|
153
|
-
return permissions;
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
|
|
157
|
-
* better-auth access-control role table (`{ roleName: acRole }` — the same map
|
|
158
|
-
* shape passed to better-auth's admin/organization plugins). An identity's
|
|
159
|
-
* `roles` are unioned into their granted permission strings; unknown roles
|
|
160
|
-
* contribute nothing.
|
|
161
|
-
*
|
|
162
|
-
* ```ts
|
|
163
|
-
* const ac = createAccessControl({ posts: ['read', 'write'] });
|
|
164
|
-
* const authz = createAuthz({
|
|
165
|
-
* resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),
|
|
166
|
-
* });
|
|
167
|
-
* await authz.can(identityFromUser(user), 'posts:write');
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
const betterAuthAcResolver = (roles) => {
|
|
171
|
-
const grantsByRole = /* @__PURE__ */ new Map();
|
|
172
|
-
for (const [name, role] of Object.entries(roles)) grantsByRole.set(name, permissionsFromAcRole(role));
|
|
173
|
-
return { grants(identity) {
|
|
174
|
-
const out = /* @__PURE__ */ new Set();
|
|
175
|
-
for (const name of identity.roles ?? []) for (const permission of grantsByRole.get(name) ?? []) out.add(permission);
|
|
176
|
-
return out;
|
|
177
|
-
} };
|
|
178
|
-
};
|
|
179
|
-
//#endregion
|
|
180
|
-
//#region src/decorators/require-permission.decorator.ts
|
|
181
|
-
/**
|
|
182
|
-
* Declares the `@velajs/authz` permission(s) required to reach a controller or
|
|
183
|
-
* route handler. Read via `Reflector` in an authorization guard, then checked
|
|
184
|
-
* against the caller's `Identity` with `authz.can(...)`.
|
|
185
|
-
*
|
|
186
|
-
* ```ts
|
|
187
|
-
* @RequirePermission(['posts:write'])
|
|
188
|
-
* @Post()
|
|
189
|
-
* create() { ... }
|
|
190
|
-
* ```
|
|
191
|
-
*
|
|
192
|
-
* The metadata is a plain `string[]` of permission strings in the granted-side
|
|
193
|
-
* format `@velajs/authz` matches (`resource:action`, or wildcards like
|
|
194
|
-
* `posts:*`). Handler-level metadata overrides class-level (standard
|
|
195
|
-
* `Reflector.getAllAndOverride` precedence).
|
|
196
|
-
*
|
|
197
|
-
* Semantics are **require-ALL** (AND): every listed permission must be granted
|
|
198
|
-
* for access — the `PermissionGuard` denies if any one is missing. This
|
|
199
|
-
* contrasts with `@Roles`, which is **OR** (any one of the listed roles
|
|
200
|
-
* suffices).
|
|
201
|
-
*/
|
|
202
|
-
const RequirePermission = Reflector.createDecorator({ key: "vela.authz.permissions" });
|
|
203
|
-
const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;
|
|
204
|
-
//#endregion
|
|
205
|
-
//#region src/guards/permission.guard.ts
|
|
206
|
-
const AUTHZ_TOKEN = AUTHZ;
|
|
207
|
-
let PermissionGuard = class PermissionGuard {
|
|
208
|
-
reflector = new Reflector();
|
|
209
|
-
async canActivate(context) {
|
|
210
|
-
const required = this.reflector.getAllAndOverride(RequirePermission, context);
|
|
211
|
-
if (!required || required.length === 0) return true;
|
|
212
|
-
const container = context.getContext().get("container");
|
|
213
|
-
let authz;
|
|
214
|
-
try {
|
|
215
|
-
authz = container.resolve(AUTHZ_TOKEN);
|
|
216
|
-
} catch {
|
|
217
|
-
authz = void 0;
|
|
218
|
-
}
|
|
219
|
-
if (!authz) throw new ForbiddenException("Authorization is not configured");
|
|
220
|
-
const user = container.resolve(REQUEST_CONTEXT).get(AUTH_USER_KEY);
|
|
221
|
-
if (!user) throw new ForbiddenException("Permission check requires authentication");
|
|
222
|
-
const identity = identityFromUser(user);
|
|
223
|
-
for (const permission of required) if (!await authz.can(identity, permission)) throw new ForbiddenException(`Missing permission: ${permission}`);
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
PermissionGuard = __decorate([Injectable()], PermissionGuard);
|
|
228
206
|
//#endregion
|
|
229
207
|
//#region src/better-auth.module.ts
|
|
230
|
-
const
|
|
208
|
+
const referenceIds = /* @__PURE__ */ new WeakMap();
|
|
209
|
+
const explicitKeyClaims = /* @__PURE__ */ new Map();
|
|
210
|
+
let nextReferenceId = 1;
|
|
211
|
+
function referenceId(reference) {
|
|
212
|
+
const existing = referenceIds.get(reference);
|
|
213
|
+
if (existing !== void 0) return existing;
|
|
214
|
+
const id = nextReferenceId++;
|
|
215
|
+
referenceIds.set(reference, id);
|
|
216
|
+
return id;
|
|
217
|
+
}
|
|
218
|
+
function claimExplicitKey(key, kind, reference, shape) {
|
|
219
|
+
if (key.length === 0 || key !== key.trim()) throw new Error("@velajs/better-auth: an explicit module key must be a non-empty string");
|
|
220
|
+
const existing = explicitKeyClaims.get(key);
|
|
221
|
+
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`);
|
|
222
|
+
explicitKeyClaims.set(key, {
|
|
223
|
+
kind,
|
|
224
|
+
reference,
|
|
225
|
+
shape
|
|
226
|
+
});
|
|
227
|
+
return `explicit:${key}:ref:${referenceId(reference)}`;
|
|
228
|
+
}
|
|
231
229
|
function normalize(options) {
|
|
230
|
+
const basePath = normalizeBetterAuthBasePath(options.basePath);
|
|
231
|
+
const issuer = options.issuer ?? `better-auth:${basePath}`;
|
|
232
|
+
if (issuer.length === 0 || issuer !== issuer.trim()) throw new Error("@velajs/better-auth: issuer must be a non-empty stable namespace");
|
|
232
233
|
return {
|
|
233
|
-
basePath
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
basePath,
|
|
235
|
+
issuer,
|
|
236
|
+
isGlobal: options.isGlobal ?? true,
|
|
236
237
|
mountHandler: options.mountHandler ?? true
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
/** Providers, controllers, and exports shared by both entry points. */
|
|
240
241
|
function commonContributions(n) {
|
|
241
242
|
return {
|
|
242
|
-
providers: [
|
|
243
|
-
BetterAuthService,
|
|
244
|
-
AuthGuard,
|
|
245
|
-
RolesGuard,
|
|
246
|
-
PermissionGuard
|
|
247
|
-
],
|
|
243
|
+
providers: [BetterAuthService, AuthGuard],
|
|
248
244
|
controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],
|
|
249
245
|
exports: [
|
|
250
246
|
BetterAuthService,
|
|
251
247
|
BETTER_AUTH_OPTIONS,
|
|
252
|
-
AuthGuard
|
|
253
|
-
RolesGuard,
|
|
254
|
-
PermissionGuard
|
|
248
|
+
AuthGuard
|
|
255
249
|
]
|
|
256
250
|
};
|
|
257
251
|
}
|
|
258
|
-
/**
|
|
259
|
-
* The blessed engine generates `forRoot`. `setup` runs once per instance at
|
|
260
|
-
* call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,
|
|
261
|
-
* derives the auth builder from those options, mounts the catch-all controller,
|
|
262
|
-
* and — via the `global:` slot — registers the app-wide guard when `isGlobal`.
|
|
263
|
-
*
|
|
264
|
-
* `isGlobal` here means "apply AuthGuard app-wide", NOT "make this a global
|
|
265
|
-
* module", so the default `isGlobal → global: true` extras transform is
|
|
266
|
-
* replaced with identity; the flag reaches `setup` through the options bag.
|
|
267
|
-
*/
|
|
268
|
-
const authModuleHost = defineModule({
|
|
269
|
-
name: "BetterAuth",
|
|
270
|
-
optionsToken: BETTER_AUTH_OPTIONS,
|
|
271
|
-
transform: (definition) => definition,
|
|
272
|
-
key: (options) => stableHash(normalize(options)),
|
|
273
|
-
setup: ({ OPTIONS, options }) => {
|
|
274
|
-
const n = normalize(options);
|
|
275
|
-
const common = commonContributions(n);
|
|
276
|
-
const auth = options.auth;
|
|
277
|
-
return {
|
|
278
|
-
providers: [
|
|
279
|
-
{
|
|
280
|
-
provide: OPTIONS,
|
|
281
|
-
useValue: {
|
|
282
|
-
...n,
|
|
283
|
-
auth
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
lazyProvider({
|
|
287
|
-
provide: BETTER_AUTH_BUILDER,
|
|
288
|
-
inject: [OPTIONS],
|
|
289
|
-
useFactory: (o) => o.auth
|
|
290
|
-
}),
|
|
291
|
-
...common.providers
|
|
292
|
-
],
|
|
293
|
-
controllers: common.controllers,
|
|
294
|
-
exports: common.exports,
|
|
295
|
-
global: n.isGlobal ? { guards: [AuthGuard] } : void 0
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
252
|
var BetterAuthModule = class BetterAuthModule {
|
|
300
253
|
/**
|
|
301
254
|
* Synchronous registration. The auth instance is constructed by the consumer
|
|
@@ -304,9 +257,21 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
304
257
|
* connection, in-memory adapters, etc.).
|
|
305
258
|
*/
|
|
306
259
|
static forRoot(options) {
|
|
260
|
+
const normalized = normalize(options);
|
|
261
|
+
const shape = stableHash(normalized);
|
|
262
|
+
const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : claimExplicitKey(options.key, "auth", options.auth, shape);
|
|
263
|
+
const common = commonContributions(normalized);
|
|
307
264
|
return {
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
310
275
|
};
|
|
311
276
|
}
|
|
312
277
|
/**
|
|
@@ -314,31 +279,27 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
314
279
|
* first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
|
|
315
280
|
* In normal request handling that's `AuthGuard.canActivate` or the catch-all
|
|
316
281
|
* controller's `.handle`. At module load the factory does NOT run — it's only
|
|
317
|
-
* captured
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* the service. Inject deps resolve at module load (cheap BindingRef wrappers);
|
|
321
|
-
* 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.
|
|
322
285
|
*/
|
|
323
286
|
static forRootAsync(options) {
|
|
324
287
|
const n = normalize(options);
|
|
325
288
|
const common = commonContributions(n);
|
|
289
|
+
const shape = stableHash({
|
|
290
|
+
...n,
|
|
291
|
+
inject: options.inject
|
|
292
|
+
});
|
|
293
|
+
const key = options.key === void 0 ? `${shape}:factory:${referenceId(options.useFactory)}` : claimExplicitKey(options.key, "factory", options.useFactory, shape);
|
|
326
294
|
return {
|
|
327
295
|
module: BetterAuthModule,
|
|
328
|
-
key
|
|
329
|
-
...n,
|
|
330
|
-
inject: options.inject
|
|
331
|
-
}),
|
|
296
|
+
key,
|
|
332
297
|
imports: options.imports ?? [],
|
|
333
298
|
providers: [
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
useValue: n
|
|
337
|
-
},
|
|
338
|
-
lazyProvider({
|
|
339
|
-
provide: BETTER_AUTH_BUILDER,
|
|
299
|
+
defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),
|
|
300
|
+
defineProvider(BETTER_AUTH_BUILDER, {
|
|
340
301
|
inject: options.inject,
|
|
341
|
-
useFactory: options.useFactory
|
|
302
|
+
useFactory: (...deps) => () => options.useFactory(...deps)
|
|
342
303
|
}),
|
|
343
304
|
...common.providers,
|
|
344
305
|
...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
|
|
@@ -350,15 +311,79 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
350
311
|
};
|
|
351
312
|
//#endregion
|
|
352
313
|
//#region src/decorators/current-user.decorator.ts
|
|
353
|
-
const CurrentUser =
|
|
354
|
-
return ctx
|
|
314
|
+
const CurrentUser = createParamDecorator((_data, ctx) => {
|
|
315
|
+
return getAuthRequestState(ctx)?.user;
|
|
355
316
|
});
|
|
356
317
|
//#endregion
|
|
357
318
|
//#region src/decorators/current-session.decorator.ts
|
|
358
|
-
const CurrentSession =
|
|
359
|
-
return ctx
|
|
319
|
+
const CurrentSession = createParamDecorator((_data, ctx) => {
|
|
320
|
+
return getAuthRequestState(ctx)?.session;
|
|
360
321
|
});
|
|
361
322
|
//#endregion
|
|
362
|
-
|
|
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 };
|
|
363
388
|
|
|
364
389
|
//# sourceMappingURL=index.js.map
|