@stacksjs/auth 0.70.88 → 0.70.91

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.
Files changed (47) hide show
  1. package/dist/authentication.d.ts +43 -0
  2. package/dist/authentication.js +413 -0
  3. package/dist/authenticator.d.ts +17 -0
  4. package/dist/authenticator.js +14 -0
  5. package/dist/authorizable.d.ts +77 -0
  6. package/dist/authorizable.js +28 -0
  7. package/dist/client.d.ts +22 -0
  8. package/dist/client.js +26 -0
  9. package/dist/email-verification.d.ts +29 -0
  10. package/dist/email-verification.js +111 -0
  11. package/dist/gate.d.ts +180 -0
  12. package/dist/gate.js +203 -0
  13. package/dist/index.d.ts +36 -0
  14. package/dist/index.js +26 -0
  15. package/dist/internal-constants.d.ts +19 -0
  16. package/dist/internal-constants.js +1 -0
  17. package/dist/middleware.d.ts +14 -0
  18. package/dist/middleware.js +28 -0
  19. package/dist/passkey.d.ts +97 -0
  20. package/dist/passkey.js +76 -0
  21. package/dist/password/reset.d.ts +10 -0
  22. package/dist/password/reset.js +156 -0
  23. package/dist/policy.d.ts +33 -0
  24. package/dist/policy.js +91 -0
  25. package/dist/rate-limiter.d.ts +44 -0
  26. package/dist/rate-limiter.js +91 -0
  27. package/dist/rbac-seed.d.ts +24 -0
  28. package/dist/rbac-seed.js +30 -0
  29. package/dist/rbac-store-bqb.d.ts +18 -0
  30. package/dist/rbac-store-bqb.js +180 -0
  31. package/dist/rbac.d.ts +265 -0
  32. package/dist/rbac.js +324 -0
  33. package/dist/register.d.ts +3 -0
  34. package/dist/register.js +43 -0
  35. package/dist/session-auth.d.ts +67 -0
  36. package/dist/session-auth.js +151 -0
  37. package/dist/team.d.ts +121 -0
  38. package/dist/team.js +88 -0
  39. package/dist/token.d.ts +16 -0
  40. package/dist/token.js +21 -0
  41. package/dist/tokens.d.ts +284 -0
  42. package/dist/tokens.js +489 -0
  43. package/dist/two-factor.d.ts +67 -0
  44. package/dist/two-factor.js +113 -0
  45. package/dist/user.d.ts +34 -0
  46. package/dist/user.js +41 -0
  47. package/package.json +3 -3
@@ -0,0 +1,43 @@
1
+ import { User } from '@stacksjs/orm';
2
+ import type { AuthCredentials, AuthToken, NewAccessToken, PersonalAccessToken, TokenCreateOptions } from '@stacksjs/types';
3
+ declare type UserModel = NonNullable<Awaited<ReturnType<typeof User.find>>>;
4
+ export declare class Auth {
5
+ static attempt(credentials: AuthCredentials): Promise<boolean>;
6
+ static validate(credentials: AuthCredentials): Promise<boolean>;
7
+ static login(credentials: AuthCredentials, options?: TokenCreateOptions): Promise<
8
+ { user: UserModel, token: AuthToken, refreshToken?: string, expiresIn?: number } | null
9
+ >;
10
+ static loginUsingId(userId: number, options?: TokenCreateOptions): Promise<
11
+ { user: UserModel, token: AuthToken, refreshToken?: string, expiresIn?: number } | null
12
+ >;
13
+ static logout(): Promise<void>;
14
+ static user(): Promise<UserModel | undefined>;
15
+ static check(): Promise<boolean>;
16
+ static guest(): Promise<boolean>;
17
+ static id(): Promise<number | undefined>;
18
+ static setUser(user: UserModel): void;
19
+ static createTokenForUser(user: UserModel, options?: TokenCreateOptions): Promise<NewAccessToken>;
20
+ static createToken(user: UserModel, name?: string, abilities?: string[]): Promise<AuthToken>;
21
+ static requestToken(credentials: AuthCredentials, clientId: number, clientSecret: string): Promise<{ token: AuthToken } | null>;
22
+ static validateToken(token: string): Promise<boolean>;
23
+ static getUserFromToken(token: string): Promise<UserModel | undefined>;
24
+ static currentAccessToken(): Promise<PersonalAccessToken | undefined>;
25
+ static tokenCan(ability: string): Promise<boolean>;
26
+ static tokenCant(ability: string): Promise<boolean>;
27
+ static tokenAbilities(): Promise<string[]>;
28
+ static tokenCanAll(abilities: string[]): Promise<boolean>;
29
+ static tokenCanAny(abilities: string[]): Promise<boolean>;
30
+ static tokens(userId?: number): Promise<PersonalAccessToken[]>;
31
+ static revokeToken(token: string): Promise<void>;
32
+ static revokeTokenById(tokenId: number): Promise<void>;
33
+ static revokeAllTokens(userId?: number): Promise<void>;
34
+ static revokeOtherTokens(userId?: number): Promise<void>;
35
+ static pruneExpiredTokens(): Promise<number>;
36
+ static pruneRevokedTokens(): Promise<number>;
37
+ static rotateToken(oldToken: string): Promise<AuthToken | null>;
38
+ static findToken(tokenId: number): Promise<PersonalAccessToken | null>;
39
+ static once(credentials: AuthCredentials): Promise<boolean>;
40
+ static guard(_name?: string): typeof Auth;
41
+ static viaRemember(): boolean;
42
+ static clearState(): void;
43
+ }
@@ -0,0 +1,413 @@
1
+ import { config } from "@stacksjs/config";
2
+ import { db } from "@stacksjs/database";
3
+ import { HttpError } from "@stacksjs/error-handling";
4
+ import { formatDate, User } from "@stacksjs/orm";
5
+ import { getCurrentRequest, request } from "@stacksjs/router";
6
+ import { Buffer } from "node:buffer";
7
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
8
+ import { decrypt, encrypt, verifyHash } from "@stacksjs/security";
9
+ import { log } from "@stacksjs/logging";
10
+ import { DUMMY_BCRYPT_HASH } from "./internal-constants";
11
+ import { RateLimiter } from "./rate-limiter";
12
+ const REQUEST_AUTH_STATE_KEY = Symbol.for("stacks.requestAuthState");
13
+ function authStateOrNull() {
14
+ const req = getCurrentRequest();
15
+ if (!req)
16
+ return null;
17
+ let state = req[REQUEST_AUTH_STATE_KEY];
18
+ if (!state) {
19
+ state = {};
20
+ req[REQUEST_AUTH_STATE_KEY] = state;
21
+ }
22
+ return state;
23
+ }
24
+ function hashToken(token) {
25
+ return createHash("sha256").update(token).digest("hex");
26
+ }
27
+ import { createToken as createRawToken, getPasswordChangedAt, isIssuedBeforePasswordChange, parseScopes } from "./tokens";
28
+
29
+ export class Auth {
30
+ static getBearerToken() {
31
+ let bearerToken = request.bearerToken?.();
32
+ if (!bearerToken) {
33
+ const authHeader = request.headers?.get?.("authorization") || request.headers?.get?.("Authorization");
34
+ if (authHeader && authHeader.startsWith("Bearer "))
35
+ bearerToken = authHeader.substring(7);
36
+ }
37
+ return bearerToken || null;
38
+ }
39
+ static parseToken(token) {
40
+ const firstColonIndex = token.indexOf(":");
41
+ if (firstColonIndex === -1)
42
+ return null;
43
+ const plainToken = token.substring(0, firstColonIndex), encryptedId = token.substring(firstColonIndex + 1);
44
+ if (!plainToken || !encryptedId)
45
+ return null;
46
+ return { plainToken, encryptedId };
47
+ }
48
+ static async getClientSecret() {
49
+ const state = authStateOrNull();
50
+ if (state?.clientSecret)
51
+ return state.clientSecret;
52
+ const client = await this.getPersonalAccessClient();
53
+ if (state)
54
+ state.clientSecret = client.secret;
55
+ return client.secret;
56
+ }
57
+ static async encryptTokenId(id) {
58
+ return await encrypt(String(id));
59
+ }
60
+ static async decryptTokenId(encryptedId) {
61
+ try {
62
+ return await decrypt(encryptedId);
63
+ } catch {
64
+ try {
65
+ const clientSecret = await this.getClientSecret();
66
+ return await decrypt(encryptedId, clientSecret);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ }
72
+ static async getPersonalAccessClient() {
73
+ try {
74
+ const client = await db.selectFrom("oauth_clients").where("personal_access_client", "=", !0).where("revoked", "=", !1).selectAll().executeTakeFirst();
75
+ if (!client)
76
+ throw new HttpError(500, "No personal access client found. Please run `./buddy auth:setup` first.");
77
+ return client;
78
+ } catch (error) {
79
+ if (error instanceof Error && error.message.includes("does not exist"))
80
+ throw new HttpError(500, "OAuth tables not found. Please run `./buddy auth:setup` first.");
81
+ throw error;
82
+ }
83
+ }
84
+ static async validateClient(clientId, clientSecret) {
85
+ const client = await db.selectFrom("oauth_clients").where("id", "=", clientId).where("revoked", "=", !1).selectAll().executeTakeFirst(), provided = Buffer.from(clientSecret);
86
+ if (!client?.secret) {
87
+ const dummy = Buffer.alloc(Math.max(provided.length, 1)), padded = provided.length > 0 ? provided : Buffer.alloc(1);
88
+ timingSafeEqual(dummy, padded);
89
+ return !1;
90
+ }
91
+ const stored = String(client.secret);
92
+ if (stored.startsWith("$2"))
93
+ return await verifyHash(clientSecret, stored);
94
+ const storedBuf = Buffer.from(stored);
95
+ if (storedBuf.length !== provided.length) {
96
+ timingSafeEqual(storedBuf, storedBuf);
97
+ return !1;
98
+ }
99
+ return timingSafeEqual(storedBuf, provided);
100
+ }
101
+ static async getTokenFromId(tokenId) {
102
+ const result = await db.selectFrom("oauth_access_tokens").where("id", "=", tokenId).selectAll().executeTakeFirst();
103
+ if (!result)
104
+ return null;
105
+ const token = result;
106
+ return {
107
+ id: token.id,
108
+ userId: token.user_id,
109
+ clientId: token.oauth_client_id,
110
+ name: token.name || "auth-token",
111
+ scopes: parseScopes(token.scopes),
112
+ abilities: parseScopes(token.scopes),
113
+ expiresAt: token.expires_at ? new Date(String(token.expires_at)) : null,
114
+ createdAt: token.created_at ? new Date(String(token.created_at)) : new Date,
115
+ updatedAt: token.updated_at ? new Date(String(token.updated_at)) : new Date,
116
+ revoked: !!token.revoked
117
+ };
118
+ }
119
+ static async attempt(credentials) {
120
+ const username = config.auth.username || "email", password = config.auth.password || "password", email = credentials[username];
121
+ if (!email)
122
+ return !1;
123
+ const isRateLimited = await RateLimiter.isRateLimited(email), user = await User.where("email", "=", email).first(), authPass = credentials[password] || "", hashToVerify = user?.password || DUMMY_BCRYPT_HASH, hashCheck = await verifyHash(authPass, hashToVerify);
124
+ if (isRateLimited)
125
+ return !1;
126
+ if (hashCheck && user) {
127
+ await RateLimiter.resetAttempts(email);
128
+ const state = authStateOrNull();
129
+ if (state)
130
+ state.authUser = user;
131
+ return !0;
132
+ }
133
+ await RateLimiter.recordFailedAttempt(email);
134
+ return !1;
135
+ }
136
+ static async validate(credentials) {
137
+ const username = config.auth.username || "email", password = config.auth.password || "password", email = credentials[username];
138
+ if (!email)
139
+ return !1;
140
+ const user = await User.where("email", "=", email).first(), authPass = credentials[password] || "", hashToVerify = user?.password || DUMMY_BCRYPT_HASH;
141
+ return await verifyHash(authPass, hashToVerify) && !!user;
142
+ }
143
+ static async login(credentials, options) {
144
+ const isValid = await this.attempt(credentials), authedUser = authStateOrNull()?.authUser;
145
+ if (!isValid || !authedUser)
146
+ return null;
147
+ const { plainTextToken, refreshToken, expiresIn } = await this.createTokenForUser(authedUser, options);
148
+ return { user: authedUser, token: plainTextToken, refreshToken, expiresIn };
149
+ }
150
+ static async loginUsingId(userId, options) {
151
+ const user = await User.find(userId);
152
+ if (!user)
153
+ return null;
154
+ const state = authStateOrNull();
155
+ if (state)
156
+ state.authUser = user;
157
+ const { plainTextToken, refreshToken, expiresIn } = await this.createTokenForUser(user, options);
158
+ return { user, token: plainTextToken, refreshToken, expiresIn };
159
+ }
160
+ static async logout() {
161
+ const bearerToken = this.getBearerToken();
162
+ if (bearerToken) {
163
+ const accessToken = await db.selectFrom("oauth_access_tokens").where("token", "=", hashToken(bearerToken)).select(["id"]).executeTakeFirst();
164
+ if (accessToken)
165
+ await db.updateTable("oauth_refresh_tokens").set({ revoked: !0 }).where("access_token_id", "=", Number(accessToken.id)).execute();
166
+ await this.revokeToken(bearerToken);
167
+ }
168
+ const state = authStateOrNull();
169
+ if (state) {
170
+ state.authUser = void 0;
171
+ state.currentToken = void 0;
172
+ }
173
+ }
174
+ static async user() {
175
+ const state = authStateOrNull();
176
+ if (state?.authUser)
177
+ return state.authUser;
178
+ const bearerToken = this.getBearerToken();
179
+ if (!bearerToken)
180
+ return;
181
+ const user = await this.getUserFromToken(bearerToken);
182
+ if (user && state)
183
+ state.authUser = user;
184
+ return user;
185
+ }
186
+ static async check() {
187
+ return await this.user() !== void 0;
188
+ }
189
+ static async guest() {
190
+ return !await this.check();
191
+ }
192
+ static async id() {
193
+ return (await this.user())?.id;
194
+ }
195
+ static setUser(user) {
196
+ const state = authStateOrNull();
197
+ if (state)
198
+ state.authUser = user;
199
+ }
200
+ static async createTokenForUser(user, options) {
201
+ const name = options?.name ?? config.auth.defaultTokenName ?? "auth-token", abilities = options?.abilities ?? options?.scopes ?? config.auth.defaultAbilities ?? ["*"], accessTtlMs = options?.expiresInMinutes !== void 0 ? options.expiresInMinutes * 60 * 1000 : config.auth.tokenExpiry ?? 3600000, expiresAt = options?.expiresAt ?? new Date(Date.now() + accessTtlMs), expiresInMinutes = Math.max(1, Math.floor((expiresAt.getTime() - Date.now()) / 60000)), refreshExpiresInDays = options?.refreshExpiresInDays ?? Math.max(1, Math.round((config.auth.refreshTokenExpiry ?? 2592000000) / 86400000));
202
+ log.debug(`[auth] Creating token for user#${user.id}: ${name}`);
203
+ const result = await createRawToken(user.id, name, abilities, {
204
+ expiresInMinutes,
205
+ withRefreshToken: options?.withRefreshToken !== !1,
206
+ refreshExpiresInDays
207
+ }), plainTextToken = result.plainTextToken;
208
+ return {
209
+ accessToken: {
210
+ id: result.accessToken.id,
211
+ userId: result.accessToken.userId,
212
+ clientId: result.accessToken.clientId,
213
+ name: result.accessToken.name,
214
+ scopes: result.accessToken.scopes,
215
+ abilities,
216
+ expiresAt: result.accessToken.expiresAt ?? expiresAt,
217
+ createdAt: result.accessToken.createdAt,
218
+ updatedAt: result.accessToken.updatedAt,
219
+ revoked: result.accessToken.revoked,
220
+ plainTextToken
221
+ },
222
+ plainTextToken,
223
+ refreshToken: result.refreshToken,
224
+ expiresIn: result.expiresIn
225
+ };
226
+ }
227
+ static async createToken(user, name = config.auth.defaultTokenName || "auth-token", abilities = config.auth.defaultAbilities || ["*"]) {
228
+ const { plainTextToken } = await this.createTokenForUser(user, { name, abilities });
229
+ return plainTextToken;
230
+ }
231
+ static async requestToken(credentials, clientId, clientSecret) {
232
+ if (!await this.validateClient(clientId, clientSecret))
233
+ throw new HttpError(401, "Invalid client credentials");
234
+ const isValid = await this.attempt(credentials), authedUser = authStateOrNull()?.authUser;
235
+ if (!isValid || !authedUser)
236
+ return null;
237
+ return { token: await this.createToken(authedUser, "user-auth-token") };
238
+ }
239
+ static async validateToken(token) {
240
+ const hashedPlainToken = hashToken(token), accessToken = await db.selectFrom("oauth_access_tokens").where("token", "=", hashedPlainToken).selectAll().executeTakeFirst();
241
+ if (!accessToken)
242
+ return !1;
243
+ log.debug(`[auth] Token validated for token#${accessToken.id}`);
244
+ if (accessToken.expires_at && new Date(String(accessToken.expires_at)) < new Date) {
245
+ await db.deleteFrom("oauth_access_tokens").where("id", "=", accessToken.id).execute();
246
+ return !1;
247
+ }
248
+ if (accessToken.revoked)
249
+ return !1;
250
+ if (isIssuedBeforePasswordChange(accessToken.created_at, await getPasswordChangedAt(accessToken.user_id)))
251
+ return !1;
252
+ await db.updateTable("oauth_access_tokens").set({ updated_at: formatDate(new Date) }).where("id", "=", accessToken.id).execute();
253
+ return !0;
254
+ }
255
+ static async getUserFromToken(token) {
256
+ const hashedPlainToken = hashToken(token), accessToken = await db.selectFrom("oauth_access_tokens").where("token", "=", hashedPlainToken).selectAll().executeTakeFirst();
257
+ if (!accessToken)
258
+ return;
259
+ if (accessToken.expires_at && new Date(String(accessToken.expires_at)) < new Date) {
260
+ await db.deleteFrom("oauth_access_tokens").where("id", "=", accessToken.id).execute();
261
+ return;
262
+ }
263
+ if (accessToken.revoked)
264
+ return;
265
+ const stateForToken = authStateOrNull();
266
+ if (stateForToken)
267
+ stateForToken.currentToken = await this.getTokenFromId(accessToken.id) ?? void 0;
268
+ await db.updateTable("oauth_access_tokens").set({ updated_at: formatDate(new Date) }).where("id", "=", accessToken.id).execute();
269
+ if (!accessToken?.user_id)
270
+ return;
271
+ const user = await User.find(accessToken.user_id);
272
+ if (isIssuedBeforePasswordChange(accessToken.created_at, await getPasswordChangedAt(accessToken.user_id)))
273
+ return;
274
+ return user;
275
+ }
276
+ static async currentAccessToken() {
277
+ const state = authStateOrNull();
278
+ if (state?.currentToken)
279
+ return state.currentToken;
280
+ const bearerToken = this.getBearerToken();
281
+ if (!bearerToken)
282
+ return;
283
+ const accessToken = await db.selectFrom("oauth_access_tokens").where("token", "=", hashToken(bearerToken)).select(["id"]).executeTakeFirst();
284
+ if (!accessToken)
285
+ return;
286
+ const token = await this.getTokenFromId(Number(accessToken.id));
287
+ if (token && state)
288
+ state.currentToken = token;
289
+ return token ?? void 0;
290
+ }
291
+ static async tokenCan(ability) {
292
+ const token = await this.currentAccessToken();
293
+ if (!token)
294
+ return !1;
295
+ if (token.abilities.includes("*"))
296
+ return !0;
297
+ return token.abilities.includes(ability);
298
+ }
299
+ static async tokenCant(ability) {
300
+ return !await this.tokenCan(ability);
301
+ }
302
+ static async tokenAbilities() {
303
+ return (await this.currentAccessToken())?.abilities ?? [];
304
+ }
305
+ static async tokenCanAll(abilities) {
306
+ const token = await this.currentAccessToken();
307
+ if (!token)
308
+ return !1;
309
+ if (token.abilities.includes("*"))
310
+ return !0;
311
+ return abilities.every((a) => token.abilities.includes(a));
312
+ }
313
+ static async tokenCanAny(abilities) {
314
+ const token = await this.currentAccessToken();
315
+ if (!token)
316
+ return !1;
317
+ if (token.abilities.includes("*"))
318
+ return !0;
319
+ return abilities.some((a) => token.abilities.includes(a));
320
+ }
321
+ static async tokens(userId) {
322
+ const uid = userId ?? await this.id();
323
+ if (!uid)
324
+ return [];
325
+ return (await db.selectFrom("oauth_access_tokens").where("user_id", "=", uid).where("revoked", "=", !1).selectAll().execute()).map((token) => ({
326
+ id: Number(token.id),
327
+ userId: Number(token.user_id),
328
+ clientId: Number(token.oauth_client_id),
329
+ name: String(token.name || "auth-token"),
330
+ scopes: parseScopes(String(token.scopes ?? "")),
331
+ abilities: parseScopes(String(token.scopes ?? "")),
332
+ expiresAt: token.expires_at ? new Date(String(token.expires_at)) : null,
333
+ createdAt: token.created_at ? new Date(String(token.created_at)) : new Date,
334
+ updatedAt: token.updated_at ? new Date(String(token.updated_at)) : new Date,
335
+ revoked: !!token.revoked
336
+ }));
337
+ }
338
+ static async revokeToken(token) {
339
+ await db.updateTable("oauth_access_tokens").set({ revoked: !0, updated_at: formatDate(new Date) }).where("token", "=", hashToken(token)).execute();
340
+ }
341
+ static async revokeTokenById(tokenId) {
342
+ await db.updateTable("oauth_access_tokens").set({ revoked: !0, updated_at: formatDate(new Date) }).where("id", "=", tokenId).execute();
343
+ }
344
+ static async revokeAllTokens(userId) {
345
+ const uid = userId ?? await this.id();
346
+ if (!uid)
347
+ return;
348
+ await db.updateTable("oauth_access_tokens").set({ revoked: !0, updated_at: formatDate(new Date) }).where("user_id", "=", uid).execute();
349
+ }
350
+ static async revokeOtherTokens(userId) {
351
+ const uid = userId ?? await this.id();
352
+ if (!uid)
353
+ return;
354
+ const currentToken = await this.currentAccessToken();
355
+ if (!currentToken)
356
+ return;
357
+ await db.updateTable("oauth_access_tokens").set({ revoked: !0, updated_at: formatDate(new Date) }).where("user_id", "=", uid).where("id", "!=", currentToken.id).execute();
358
+ }
359
+ static async pruneExpiredTokens() {
360
+ const result = await db.deleteFrom("oauth_access_tokens").where("expires_at", "<", formatDate(new Date)).executeTakeFirst();
361
+ return Number(result?.numDeletedRows) || 0;
362
+ }
363
+ static async pruneRevokedTokens() {
364
+ const result = await db.deleteFrom("oauth_access_tokens").where("revoked", "=", !0).executeTakeFirst();
365
+ return Number(result?.numDeletedRows) || 0;
366
+ }
367
+ static async rotateToken(oldToken) {
368
+ const { findToken: findRawToken, revokeToken: revokeRawToken } = await import("./tokens"), existing = await findRawToken(oldToken);
369
+ if (!existing)
370
+ return null;
371
+ const remainingMs = existing.expiresAt ? existing.expiresAt.getTime() - Date.now() : config.auth.tokenExpiry ?? 3600000, expiresInMinutes = Math.max(1, Math.floor(remainingMs / 60000));
372
+ await revokeRawToken(oldToken);
373
+ const user = await User.find(existing.userId);
374
+ if (!user)
375
+ return null;
376
+ return (await this.createTokenForUser(user, {
377
+ name: existing.name,
378
+ abilities: existing.scopes ?? ["*"],
379
+ expiresInMinutes,
380
+ withRefreshToken: !1
381
+ })).plainTextToken;
382
+ }
383
+ static async findToken(tokenId) {
384
+ return this.getTokenFromId(tokenId);
385
+ }
386
+ static async once(credentials) {
387
+ const username = config.auth.username || "email", password = config.auth.password || "password", email = credentials[username];
388
+ if (!email)
389
+ return !1;
390
+ const user = await User.where("email", "=", email).first(), authPass = credentials[password] || "", hashToVerify = user?.password || DUMMY_BCRYPT_HASH;
391
+ if (await verifyHash(authPass, hashToVerify) && user) {
392
+ const state = authStateOrNull();
393
+ if (state)
394
+ state.authUser = user;
395
+ return !0;
396
+ }
397
+ return !1;
398
+ }
399
+ static guard(_name) {
400
+ return this;
401
+ }
402
+ static viaRemember() {
403
+ return !1;
404
+ }
405
+ static clearState() {
406
+ const state = authStateOrNull();
407
+ if (state) {
408
+ state.authUser = void 0;
409
+ state.currentToken = void 0;
410
+ state.clientSecret = void 0;
411
+ }
412
+ }
413
+ }
@@ -0,0 +1,17 @@
1
+ export declare function generateTwoFactorSecret(): string;
2
+ export declare function generateTwoFactorToken(secret: Secret): Promise<Token>;
3
+ export declare function verifyTwoFactorCode(token: Token, secret: Secret): Promise<boolean>;
4
+ /**
5
+ * Generate an otpauth:// URI for two-factor authentication
6
+ *
7
+ * This URI can be used with any QR code library to generate a scannable
8
+ * QR code for authenticator apps.
9
+ *
10
+ * @param user - User identifier (email or username)
11
+ * @param service - Service name (e.g., 'StacksJS 2FA')
12
+ * @param secret - Optional secret (will be generated if not provided)
13
+ * @returns The otpauth:// URI string
14
+ */
15
+ export declare function generateTwoFactorUri(user?: string, service?: string, secret?: Secret): string;
16
+ export type Token = string;
17
+ export type Secret = string;
@@ -0,0 +1,14 @@
1
+ import { generateTOTP, generateTOTPSecret, totpKeyUri, verifyTOTP } from "@stacksjs/ts-auth";
2
+ export function generateTwoFactorSecret() {
3
+ return generateTOTPSecret();
4
+ }
5
+ export async function generateTwoFactorToken(secret) {
6
+ return generateTOTP({ secret });
7
+ }
8
+ export async function verifyTwoFactorCode(token, secret) {
9
+ return verifyTOTP(token, { secret });
10
+ }
11
+ export function generateTwoFactorUri(user, service, secret) {
12
+ const userIdentifier = user || "johndoe@example.com", serviceName = service || "StacksJS 2fa", otpSecret = secret || generateTwoFactorSecret();
13
+ return totpKeyUri(userIdentifier, serviceName, otpSecret);
14
+ }
@@ -0,0 +1,77 @@
1
+ import { any, can, cannot } from './gate';
2
+ import type { AuthorizationResponse } from './gate';
3
+ import type { UserModel as OrmUserModel } from '@stacksjs/orm';
4
+ /**
5
+ * Check if a user can perform an ability
6
+ *
7
+ * @example
8
+ * if (await userCan(user, 'edit-settings')) { ... }
9
+ * if (await userCan(user, 'update', post)) { ... }
10
+ */
11
+ export declare function userCan(user: UserModel | null, ability: string, ...args: any[]): Promise<boolean>;
12
+ /**
13
+ * Check if a user cannot perform an ability
14
+ *
15
+ * @example
16
+ * if (await userCannot(user, 'delete', post)) { ... }
17
+ */
18
+ export declare function userCannot(user: UserModel | null, ability: string, ...args: any[]): Promise<boolean>;
19
+ /**
20
+ * Check if a user can perform any of the given abilities
21
+ *
22
+ * @example
23
+ * if (await userCanAny(user, ['update', 'delete'], post)) { ... }
24
+ */
25
+ export declare function userCanAny(user: UserModel | null, abilities: string[], ...args: any[]): Promise<boolean>;
26
+ /**
27
+ * Check if a user can perform all of the given abilities
28
+ *
29
+ * @example
30
+ * if (await userCanAll(user, ['view', 'update'], post)) { ... }
31
+ */
32
+ export declare function userCanAll(user: UserModel | null, abilities: string[], ...args: any[]): Promise<boolean>;
33
+ /**
34
+ * Authorize a user or throw an exception
35
+ *
36
+ * @example
37
+ * await authorizeUser(user, 'update', post) // Throws if not allowed
38
+ */
39
+ export declare function authorizeUser(user: UserModel | null, ability: string, ...args: any[]): Promise<AuthorizationResponse>;
40
+ /**
41
+ * Get detailed authorization result
42
+ *
43
+ * @example
44
+ * const result = await inspectUser(user, 'update', post)
45
+ * if (result.denied()) {
46
+ * console.log(result.message)
47
+ * }
48
+ */
49
+ export declare function inspectUser(user: UserModel | null, ability: string, ...args: any[]): Promise<AuthorizationResponse>;
50
+ /**
51
+ * Authorizable trait for user models
52
+ *
53
+ * Add authorization methods to a user object
54
+ *
55
+ * @example
56
+ * const authorizedUser = withAuthorization(user)
57
+ * if (await authorizedUser.can('update', post)) { ... }
58
+ */
59
+ export declare function withAuthorization<T extends UserModel>(user: T): T & AuthorizableMethods;
60
+ /**
61
+ * Authorization methods interface
62
+ */
63
+ export declare interface AuthorizableMethods {
64
+ can(ability: string, ...args: any[]): Promise<boolean>
65
+ cannot(ability: string, ...args: any[]): Promise<boolean>
66
+ canAny(abilities: string[], ...args: any[]): Promise<boolean>
67
+ canAll(abilities: string[], ...args: any[]): Promise<boolean>
68
+ authorize(ability: string, ...args: any[]): Promise<AuthorizationResponse>
69
+ }
70
+ // Alias the ORM-derived UserModel under the name the rest of this module
71
+ // expects. Using the row/instance shape (rather than `typeof User`) lets
72
+ // callers pass plain authenticated user objects to the gate helpers.
73
+ declare type UserModel = OrmUserModel;
74
+ /**
75
+ * Type for a user with authorization methods
76
+ */
77
+ export type AuthorizableUser<T extends UserModel> = T & AuthorizableMethods;
@@ -0,0 +1,28 @@
1
+ import { all, any, authorize as gateAuthorize, can, cannot, inspect } from "./gate";
2
+ export async function userCan(user, ability, ...args) {
3
+ return can(ability, user, ...args);
4
+ }
5
+ export async function userCannot(user, ability, ...args) {
6
+ return cannot(ability, user, ...args);
7
+ }
8
+ export async function userCanAny(user, abilities, ...args) {
9
+ return any(abilities, user, ...args);
10
+ }
11
+ export async function userCanAll(user, abilities, ...args) {
12
+ return all(abilities, user, ...args);
13
+ }
14
+ export async function authorizeUser(user, ability, ...args) {
15
+ return gateAuthorize(ability, user, ...args);
16
+ }
17
+ export async function inspectUser(user, ability, ...args) {
18
+ return inspect(ability, user, ...args);
19
+ }
20
+ export function withAuthorization(user) {
21
+ return Object.assign(user, {
22
+ can: (ability, ...args) => userCan(user, ability, ...args),
23
+ cannot: (ability, ...args) => userCannot(user, ability, ...args),
24
+ canAny: (abilities, ...args) => userCanAny(user, abilities, ...args),
25
+ canAll: (abilities, ...args) => userCanAll(user, abilities, ...args),
26
+ authorize: (ability, ...args) => authorizeUser(user, ability, ...args)
27
+ });
28
+ }
@@ -0,0 +1,22 @@
1
+ import type { Result } from '@stacksjs/error-handling';
2
+ /**
3
+ * Create a personal access OAuth client.
4
+ *
5
+ * Idempotent: returns an `err({ code: 'already-exists' })` when a
6
+ * non-revoked personal access client already exists in the
7
+ * `oauth_clients` table, rather than inserting a duplicate. The
8
+ * previous behaviour silently created a second row, which made
9
+ * `getPersonalAccessClient()` (which `LIMIT 1`s with no `ORDER BY`)
10
+ * return whichever the DB happened to surface first — racy with
11
+ * subsequent token mints. See stacksjs/stacks#1860 M-7.
12
+ *
13
+ * The plaintext secret is what callers (e.g., `./buddy auth:token`)
14
+ * surface to the operator — they store it themselves. The DB holds
15
+ * only the bcrypt hash so a DB compromise doesn't leak usable client
16
+ * credentials (stacksjs/stacks#1861 M-1).
17
+ */
18
+ export declare function createPersonalAccessClient(): Promise<Result<string, CreatePersonalAccessClientError>>;
19
+ export declare interface CreatePersonalAccessClientError {
20
+ code: 'already-exists'
21
+ message: string
22
+ }
package/dist/client.js ADDED
@@ -0,0 +1,26 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { db } from "@stacksjs/database";
3
+ import { formatDate } from "@stacksjs/orm";
4
+ import { err, HttpError, ok } from "@stacksjs/error-handling";
5
+ import { makeHash } from "@stacksjs/security";
6
+ export async function createPersonalAccessClient() {
7
+ if ((await db.selectFrom("oauth_clients").where("personal_access_client", "=", !0).where("revoked", "=", !1).select(["id"]).executeTakeFirst())?.id)
8
+ return err({
9
+ code: "already-exists",
10
+ message: "A personal access client already exists. Revoke the existing client (`./buddy auth:revoke-client`) before creating a new one."
11
+ });
12
+ const secret = randomBytes(40).toString("hex"), hashedSecret = await makeHash(secret, { algorithm: "bcrypt" });
13
+ await db.insertInto("oauth_clients").values({
14
+ name: "Personal Access Client",
15
+ secret: hashedSecret,
16
+ provider: "local",
17
+ redirect: "http://localhost",
18
+ personal_access_client: !0,
19
+ password_client: !1,
20
+ revoked: !1,
21
+ created_at: formatDate(new Date)
22
+ }).execute();
23
+ if (!(await db.selectFrom("oauth_clients").where("secret", "=", hashedSecret).select(["id"]).executeTakeFirst())?.id)
24
+ throw new HttpError(500, "Failed to create personal access client");
25
+ return ok(secret);
26
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Check if a user's email is verified
3
+ */
4
+ export declare function isEmailVerified(user: { email_verified_at?: string | Date | null }): boolean;
5
+ /**
6
+ * Send a verification email to the user
7
+ */
8
+ export declare function sendVerificationEmail(user: { id: number, email: string, name?: string }): Promise<void>;
9
+ /**
10
+ * Verify a user's email with the provided token
11
+ */
12
+ export declare function verifyEmail(userId: number, token: string): Promise<EmailVerificationResult>;
13
+ /**
14
+ * Resend verification email with rate limiting
15
+ */
16
+ export declare function resendVerificationEmail(user: { id: number, email: string, name?: string, email_verified_at?: string | Date | null }): Promise<EmailVerificationResult>;
17
+ /**
18
+ * Email verification facade
19
+ */
20
+ export declare const EmailVerification: {
21
+ isVerified: unknown;
22
+ send: unknown;
23
+ verify: unknown;
24
+ resend: unknown
25
+ };
26
+ export declare interface EmailVerificationResult {
27
+ success: boolean
28
+ message?: string
29
+ }