@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.
- package/dist/authentication.d.ts +43 -0
- package/dist/authentication.js +413 -0
- package/dist/authenticator.d.ts +17 -0
- package/dist/authenticator.js +14 -0
- package/dist/authorizable.d.ts +77 -0
- package/dist/authorizable.js +28 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +26 -0
- package/dist/email-verification.d.ts +29 -0
- package/dist/email-verification.js +111 -0
- package/dist/gate.d.ts +180 -0
- package/dist/gate.js +203 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +26 -0
- package/dist/internal-constants.d.ts +19 -0
- package/dist/internal-constants.js +1 -0
- package/dist/middleware.d.ts +14 -0
- package/dist/middleware.js +28 -0
- package/dist/passkey.d.ts +97 -0
- package/dist/passkey.js +76 -0
- package/dist/password/reset.d.ts +10 -0
- package/dist/password/reset.js +156 -0
- package/dist/policy.d.ts +33 -0
- package/dist/policy.js +91 -0
- package/dist/rate-limiter.d.ts +44 -0
- package/dist/rate-limiter.js +91 -0
- package/dist/rbac-seed.d.ts +24 -0
- package/dist/rbac-seed.js +30 -0
- package/dist/rbac-store-bqb.d.ts +18 -0
- package/dist/rbac-store-bqb.js +180 -0
- package/dist/rbac.d.ts +265 -0
- package/dist/rbac.js +324 -0
- package/dist/register.d.ts +3 -0
- package/dist/register.js +43 -0
- package/dist/session-auth.d.ts +67 -0
- package/dist/session-auth.js +151 -0
- package/dist/team.d.ts +121 -0
- package/dist/team.js +88 -0
- package/dist/token.d.ts +16 -0
- package/dist/token.js +21 -0
- package/dist/tokens.d.ts +284 -0
- package/dist/tokens.js +489 -0
- package/dist/two-factor.d.ts +67 -0
- package/dist/two-factor.js +113 -0
- package/dist/user.d.ts +34 -0
- package/dist/user.js +41 -0
- package/package.json +3 -3
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { config } from "@stacksjs/config";
|
|
4
|
+
import { db } from "@stacksjs/database";
|
|
5
|
+
import { mail, template } from "@stacksjs/email";
|
|
6
|
+
import { log } from "@stacksjs/logging";
|
|
7
|
+
function getVerificationKey() {
|
|
8
|
+
const appKey = config.app.key;
|
|
9
|
+
if (typeof appKey !== "string" || appKey.length === 0)
|
|
10
|
+
throw Error("[auth] config.app.key is not set \u2014 email-verification HMAC requires a real APP_KEY. " + "Run `./buddy key:generate` to provision one, or set the APP_KEY env var before booting the app.");
|
|
11
|
+
return appKey;
|
|
12
|
+
}
|
|
13
|
+
function generateVerificationToken(userId) {
|
|
14
|
+
const nonce = randomBytes(32).toString("hex"), payload = `${userId}:${nonce}`, hash = createHmac("sha256", getVerificationKey()).update(payload).digest("hex");
|
|
15
|
+
return { token: nonce, hash };
|
|
16
|
+
}
|
|
17
|
+
function verifyToken(userId, token, storedHash) {
|
|
18
|
+
const payload = `${userId}:${token}`, hash = createHmac("sha256", getVerificationKey()).update(payload).digest("hex"), a = Buffer.from(hash), b = Buffer.from(storedHash);
|
|
19
|
+
if (a.length !== b.length)
|
|
20
|
+
return !1;
|
|
21
|
+
return timingSafeEqual(a, b);
|
|
22
|
+
}
|
|
23
|
+
function getExpiryMinutes() {
|
|
24
|
+
const emailVerification = (config.auth ?? {}).emailVerification;
|
|
25
|
+
if (emailVerification != null && typeof emailVerification === "object") {
|
|
26
|
+
const ev = emailVerification;
|
|
27
|
+
if (typeof ev.expire === "number")
|
|
28
|
+
return ev.expire;
|
|
29
|
+
}
|
|
30
|
+
return 60;
|
|
31
|
+
}
|
|
32
|
+
function getVerificationUrl(userId, token) {
|
|
33
|
+
const base = config.app.url ? `https://${config.app.url}` : `http://localhost:${process.env.PORT || "3000"}`, filled = (config.auth.emailVerification?.url ?? "/verify-email/{id}/{token}").replace("{id}", String(userId)).replace("{token}", token);
|
|
34
|
+
return /^https?:\/\//.test(filled) ? filled : `${base}${filled.startsWith("/") ? "" : "/"}${filled}`;
|
|
35
|
+
}
|
|
36
|
+
export function isEmailVerified(user) {
|
|
37
|
+
return user.email_verified_at != null;
|
|
38
|
+
}
|
|
39
|
+
export async function sendVerificationEmail(user) {
|
|
40
|
+
const { token, hash } = generateVerificationToken(user.id), expiryMinutes = getExpiryMinutes(), expiresAt = new Date(Date.now() + expiryMinutes * 60 * 1000);
|
|
41
|
+
await db.deleteFrom("email_verifications").where("user_id", "=", user.id).execute();
|
|
42
|
+
await db.insertInto("email_verifications").values({
|
|
43
|
+
user_id: user.id,
|
|
44
|
+
token: hash,
|
|
45
|
+
expires_at: expiresAt.toISOString()
|
|
46
|
+
}).executeTakeFirst();
|
|
47
|
+
const verificationUrl = getVerificationUrl(user.id, token), appName = config.app.name || "Stacks";
|
|
48
|
+
try {
|
|
49
|
+
const { html, text } = await template("email-verification", {
|
|
50
|
+
subject: `Verify Your ${appName} Email Address`,
|
|
51
|
+
variables: {
|
|
52
|
+
verificationUrl,
|
|
53
|
+
expiryMinutes,
|
|
54
|
+
userName: user.name || user.email
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!html && !text)
|
|
58
|
+
throw Error("email-verification template missing or rendered empty");
|
|
59
|
+
await mail.send({
|
|
60
|
+
to: user.email,
|
|
61
|
+
subject: `Verify Your ${appName} Email Address`,
|
|
62
|
+
text,
|
|
63
|
+
html
|
|
64
|
+
});
|
|
65
|
+
} catch (templateError) {
|
|
66
|
+
const errorMessage = templateError instanceof Error ? templateError.message : String(templateError);
|
|
67
|
+
log.warn(`[email] Email verification template failed, using plain text fallback: ${errorMessage}`);
|
|
68
|
+
await mail.send({
|
|
69
|
+
to: user.email,
|
|
70
|
+
subject: `Verify Your ${appName} Email Address`,
|
|
71
|
+
text: `Please verify your email address by visiting: ${verificationUrl}
|
|
72
|
+
|
|
73
|
+
This link expires in ${expiryMinutes} minutes.`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function verifyEmail(userId, token) {
|
|
78
|
+
const record = await db.selectFrom("email_verifications").where("user_id", "=", userId).selectAll().executeTakeFirst();
|
|
79
|
+
if (!record)
|
|
80
|
+
return { success: !1, message: "No verification request found. Please request a new verification email." };
|
|
81
|
+
const expiresAt = new Date(record.expires_at);
|
|
82
|
+
if (Number.isNaN(expiresAt.getTime()) || new Date > expiresAt) {
|
|
83
|
+
await db.deleteFrom("email_verifications").where("user_id", "=", userId).execute();
|
|
84
|
+
return { success: !1, message: "Verification link has expired. Please request a new one." };
|
|
85
|
+
}
|
|
86
|
+
if (!verifyToken(userId, token, record.token))
|
|
87
|
+
return { success: !1, message: "Invalid verification link." };
|
|
88
|
+
await db.updateTable("users").set({ email_verified_at: new Date().toISOString() }).where("id", "=", userId).executeTakeFirst();
|
|
89
|
+
await db.deleteFrom("email_verifications").where("user_id", "=", userId).execute();
|
|
90
|
+
return { success: !0, message: "Email verified successfully." };
|
|
91
|
+
}
|
|
92
|
+
export async function resendVerificationEmail(user) {
|
|
93
|
+
if (isEmailVerified(user))
|
|
94
|
+
return { success: !1, message: "Email is already verified." };
|
|
95
|
+
const existing = await db.selectFrom("email_verifications").where("user_id", "=", user.id).selectAll().executeTakeFirst();
|
|
96
|
+
if (existing) {
|
|
97
|
+
const createdAt = new Date(existing.created_at), secondsSince = (Date.now() - createdAt.getTime()) / 1000;
|
|
98
|
+
if (Number.isNaN(secondsSince))
|
|
99
|
+
return { success: !1, message: "Please wait a moment before requesting another verification email." };
|
|
100
|
+
if (secondsSince < 60)
|
|
101
|
+
return { success: !1, message: `Please wait ${Math.ceil(60 - secondsSince)} seconds before requesting another verification email.` };
|
|
102
|
+
}
|
|
103
|
+
await sendVerificationEmail(user);
|
|
104
|
+
return { success: !0, message: "Verification email sent." };
|
|
105
|
+
}
|
|
106
|
+
export const EmailVerification = {
|
|
107
|
+
isVerified: isEmailVerified,
|
|
108
|
+
send: sendVerificationEmail,
|
|
109
|
+
verify: verifyEmail,
|
|
110
|
+
resend: resendVerificationEmail
|
|
111
|
+
};
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { UserModel as OrmUserModel } from '@stacksjs/orm';
|
|
2
|
+
/**
|
|
3
|
+
* Define a new authorization gate
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* define('edit-settings', (user) => user?.isAdmin)
|
|
7
|
+
* define('update-post', (user, post) => user?.id === post.userId)
|
|
8
|
+
*/
|
|
9
|
+
export declare function define<T = any>(ability: string, callback: GateCallback<T>): void;
|
|
10
|
+
/**
|
|
11
|
+
* Register a policy for a model
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* policy('Post', PostPolicy)
|
|
15
|
+
* policy(Post, PostPolicy)
|
|
16
|
+
*/
|
|
17
|
+
export declare function policy(model: string | { name: string }, policyClass: new () => Policy): void;
|
|
18
|
+
/**
|
|
19
|
+
* Register a callback to run before all gate checks
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* before((user, _ability) => {
|
|
23
|
+
* if (user?.isSuperAdmin) return true // Super admins can do anything
|
|
24
|
+
* return null // Continue to normal checks
|
|
25
|
+
* })
|
|
26
|
+
*/
|
|
27
|
+
export declare function before(callback: (user: UserModel | null, ability: string, args: any[]) => boolean | null | Promise<boolean | null>): void;
|
|
28
|
+
/**
|
|
29
|
+
* Register a callback to run after all gate checks
|
|
30
|
+
*/
|
|
31
|
+
export declare function after(callback: (user: UserModel | null, ability: string, result: boolean, args: any[]) => boolean | void | Promise<boolean | void>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Check if the user is allowed to perform an ability
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* if (await allows('edit-settings', user)) { ... }
|
|
37
|
+
* if (await allows('update', user, post)) { ... }
|
|
38
|
+
*/
|
|
39
|
+
export declare function allows(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if the user is denied from performing an ability
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* if (await denies('delete', user, post)) { ... }
|
|
45
|
+
*/
|
|
46
|
+
export declare function denies(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Check if the user can perform an ability (alias for allows)
|
|
49
|
+
*/
|
|
50
|
+
export declare function can(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Check if the user cannot perform an ability (alias for denies)
|
|
53
|
+
*/
|
|
54
|
+
export declare function cannot(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* Check if the user can perform any of the given abilities
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* if (await any(['update', 'delete'], user, post)) { ... }
|
|
60
|
+
*/
|
|
61
|
+
export declare function any(abilities: string[], user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Check if the user can perform all of the given abilities
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* if (await all(['view', 'update'], user, post)) { ... }
|
|
67
|
+
*/
|
|
68
|
+
export declare function all(abilities: string[], user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if the user can perform none of the given abilities
|
|
71
|
+
*/
|
|
72
|
+
export declare function none(abilities: string[], user: UserModel | null, ...args: any[]): Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Authorize an ability or throw an exception
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* await authorize('update', user, post) // Throws if not allowed
|
|
78
|
+
*/
|
|
79
|
+
export declare function authorize(ability: string, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
|
|
80
|
+
/**
|
|
81
|
+
* Get detailed inspection result for an ability check
|
|
82
|
+
*/
|
|
83
|
+
export declare function inspect(ability: string, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
|
|
84
|
+
/**
|
|
85
|
+
* Get a policy instance for a model
|
|
86
|
+
*/
|
|
87
|
+
export declare function getPolicyFor<T = any>(model: T): Policy<T> | null;
|
|
88
|
+
/**
|
|
89
|
+
* Check if a gate is defined
|
|
90
|
+
*/
|
|
91
|
+
export declare function has(ability: string): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Check if a policy is registered for a model
|
|
94
|
+
*/
|
|
95
|
+
export declare function hasPolicy(model: string | { name: string }): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Get all defined gate names
|
|
98
|
+
*/
|
|
99
|
+
export declare function abilities(): string[];
|
|
100
|
+
/**
|
|
101
|
+
* Clear all gates and policies (useful for testing)
|
|
102
|
+
*/
|
|
103
|
+
export declare function flush(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Gate facade for convenient access
|
|
106
|
+
*/
|
|
107
|
+
export declare const Gate: {
|
|
108
|
+
define: typeof define;
|
|
109
|
+
policy: typeof policy;
|
|
110
|
+
before: typeof before;
|
|
111
|
+
after: typeof after;
|
|
112
|
+
allows: typeof allows;
|
|
113
|
+
denies: typeof denies;
|
|
114
|
+
can: typeof can;
|
|
115
|
+
cannot: typeof cannot;
|
|
116
|
+
any: typeof any;
|
|
117
|
+
all: typeof all;
|
|
118
|
+
none: typeof none;
|
|
119
|
+
authorize: typeof authorize;
|
|
120
|
+
inspect: typeof inspect;
|
|
121
|
+
has: typeof has;
|
|
122
|
+
hasPolicy: typeof hasPolicy;
|
|
123
|
+
abilities: typeof abilities;
|
|
124
|
+
getPolicyFor: typeof getPolicyFor;
|
|
125
|
+
flush: typeof flush;
|
|
126
|
+
AuthorizationResponse: typeof AuthorizationResponse;
|
|
127
|
+
AuthorizationException: typeof AuthorizationException
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Policy class interface
|
|
131
|
+
*/
|
|
132
|
+
export declare interface Policy<T = any> {
|
|
133
|
+
before?(user: UserModel | null, ability: string): boolean | null | Promise<boolean | null>
|
|
134
|
+
viewAny?(user: UserModel | null): boolean | Promise<boolean> | AuthorizationResponse
|
|
135
|
+
view?(user: UserModel | null, model: T): boolean | Promise<boolean> | AuthorizationResponse
|
|
136
|
+
create?(user: UserModel | null): boolean | Promise<boolean> | AuthorizationResponse
|
|
137
|
+
update?(user: UserModel | null, model: T): boolean | Promise<boolean> | AuthorizationResponse
|
|
138
|
+
delete?(user: UserModel | null, model: T): boolean | Promise<boolean> | AuthorizationResponse
|
|
139
|
+
restore?(user: UserModel | null, model: T): boolean | Promise<boolean> | AuthorizationResponse
|
|
140
|
+
forceDelete?(user: UserModel | null, model: T): boolean | Promise<boolean> | AuthorizationResponse
|
|
141
|
+
[key: string]: PolicyMethod | undefined
|
|
142
|
+
}
|
|
143
|
+
// Alias the ORM-derived UserModel under the name this module uses internally.
|
|
144
|
+
// The gate API receives authenticated user objects (rows / instances),
|
|
145
|
+
// not the User class constructor.
|
|
146
|
+
declare type UserModel = OrmUserModel;
|
|
147
|
+
/**
|
|
148
|
+
* Gate callback function type
|
|
149
|
+
*/
|
|
150
|
+
export type GateCallback<T = any> = (_user: UserModel | null, ..._args: T[]) => boolean | Promise<boolean> | AuthorizationResponse;
|
|
151
|
+
/**
|
|
152
|
+
* Policy method type. The return type intentionally allows `null` so that
|
|
153
|
+
* a policy's `before()` hook (which returns `null` to delegate to the
|
|
154
|
+
* underlying ability check) is index-compatible with the catch-all
|
|
155
|
+
* `[key: string]: PolicyMethod | undefined` signature on `Policy`.
|
|
156
|
+
*/
|
|
157
|
+
export type PolicyMethod<T = any> = (_user: UserModel | null, _model?: T, ..._args: any[]) => boolean | null | Promise<boolean | null> | AuthorizationResponse;
|
|
158
|
+
/**
|
|
159
|
+
* Authorization response for detailed allow/deny
|
|
160
|
+
*/
|
|
161
|
+
export declare class AuthorizationResponse {
|
|
162
|
+
readonly isAllowed: boolean;
|
|
163
|
+
readonly message?: string;
|
|
164
|
+
readonly code?: string;
|
|
165
|
+
constructor(allowed: boolean, message?: string, code?: string);
|
|
166
|
+
static allow(message?: string): AuthorizationResponse;
|
|
167
|
+
static deny(message?: string, code?: string): AuthorizationResponse;
|
|
168
|
+
allowed(): boolean;
|
|
169
|
+
denied(): boolean;
|
|
170
|
+
authorize(): void;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Authorization exception
|
|
174
|
+
*/
|
|
175
|
+
export declare class AuthorizationException extends Error {
|
|
176
|
+
public readonly code?: string;
|
|
177
|
+
public readonly status?: number;
|
|
178
|
+
constructor(message?: string, code?: string, status?: number);
|
|
179
|
+
}
|
|
180
|
+
export default Gate;
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const RESERVED_POLICY_MEMBERS = new Set([
|
|
2
|
+
"before",
|
|
3
|
+
"allow",
|
|
4
|
+
"deny",
|
|
5
|
+
"denyIf",
|
|
6
|
+
"denyUnless",
|
|
7
|
+
"allowIf",
|
|
8
|
+
"constructor"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export class AuthorizationResponse {
|
|
12
|
+
isAllowed;
|
|
13
|
+
message;
|
|
14
|
+
code;
|
|
15
|
+
constructor(allowed, message, code) {
|
|
16
|
+
this.isAllowed = allowed;
|
|
17
|
+
this.message = message;
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
static allow(message) {
|
|
21
|
+
return new AuthorizationResponse(!0, message);
|
|
22
|
+
}
|
|
23
|
+
static deny(message, code) {
|
|
24
|
+
return new AuthorizationResponse(!1, message || "This action is unauthorized.", code);
|
|
25
|
+
}
|
|
26
|
+
allowed() {
|
|
27
|
+
return this.isAllowed;
|
|
28
|
+
}
|
|
29
|
+
denied() {
|
|
30
|
+
return !this.isAllowed;
|
|
31
|
+
}
|
|
32
|
+
authorize() {
|
|
33
|
+
if (!this.isAllowed)
|
|
34
|
+
throw new AuthorizationException(this.message || "This action is unauthorized.", this.code);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AuthorizationException extends Error {
|
|
39
|
+
code;
|
|
40
|
+
status;
|
|
41
|
+
constructor(message = "This action is unauthorized.", code, status = 403) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.name = "AuthorizationException";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const state = {
|
|
49
|
+
gates: new Map,
|
|
50
|
+
policies: new Map,
|
|
51
|
+
beforeCallbacks: [],
|
|
52
|
+
afterCallbacks: []
|
|
53
|
+
};
|
|
54
|
+
export function define(ability, callback) {
|
|
55
|
+
state.gates.set(ability, callback);
|
|
56
|
+
}
|
|
57
|
+
export function policy(model, policyClass) {
|
|
58
|
+
const modelName = typeof model === "string" ? model : model.name;
|
|
59
|
+
state.policies.set(modelName, policyClass);
|
|
60
|
+
}
|
|
61
|
+
export function before(callback) {
|
|
62
|
+
state.beforeCallbacks.push(callback);
|
|
63
|
+
}
|
|
64
|
+
export function after(callback) {
|
|
65
|
+
state.afterCallbacks.push(callback);
|
|
66
|
+
}
|
|
67
|
+
export async function allows(ability, user, ...args) {
|
|
68
|
+
return check(ability, user, ...args);
|
|
69
|
+
}
|
|
70
|
+
export async function denies(ability, user, ...args) {
|
|
71
|
+
return !await check(ability, user, ...args);
|
|
72
|
+
}
|
|
73
|
+
export async function can(ability, user, ...args) {
|
|
74
|
+
return check(ability, user, ...args);
|
|
75
|
+
}
|
|
76
|
+
export async function cannot(ability, user, ...args) {
|
|
77
|
+
return !await check(ability, user, ...args);
|
|
78
|
+
}
|
|
79
|
+
export async function any(abilities, user, ...args) {
|
|
80
|
+
for (const ability of abilities)
|
|
81
|
+
if (await check(ability, user, ...args))
|
|
82
|
+
return !0;
|
|
83
|
+
return !1;
|
|
84
|
+
}
|
|
85
|
+
export async function all(abilities, user, ...args) {
|
|
86
|
+
for (const ability of abilities)
|
|
87
|
+
if (!await check(ability, user, ...args))
|
|
88
|
+
return !1;
|
|
89
|
+
return !0;
|
|
90
|
+
}
|
|
91
|
+
export async function none(abilities, user, ...args) {
|
|
92
|
+
return !await any(abilities, user, ...args);
|
|
93
|
+
}
|
|
94
|
+
export async function authorize(ability, user, ...args) {
|
|
95
|
+
const result = await inspect(ability, user, ...args);
|
|
96
|
+
if (!result.isAllowed)
|
|
97
|
+
throw new AuthorizationException(result.message, result.code);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
export async function inspect(ability, user, ...args) {
|
|
101
|
+
let response = null;
|
|
102
|
+
for (const callback of state.beforeCallbacks) {
|
|
103
|
+
const beforeResult = await callback(user, ability, args);
|
|
104
|
+
if (beforeResult === !0) {
|
|
105
|
+
response = AuthorizationResponse.allow();
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (beforeResult === !1) {
|
|
109
|
+
response = AuthorizationResponse.deny();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!response)
|
|
114
|
+
response = await resolveAbility(ability, user, args);
|
|
115
|
+
for (const callback of state.afterCallbacks) {
|
|
116
|
+
const afterResult = await callback(user, ability, response.isAllowed, args);
|
|
117
|
+
if (typeof afterResult === "boolean")
|
|
118
|
+
return afterResult ? AuthorizationResponse.allow() : AuthorizationResponse.deny();
|
|
119
|
+
}
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
async function resolveAbility(ability, user, args) {
|
|
123
|
+
const model = args[0];
|
|
124
|
+
if (model && typeof model === "object") {
|
|
125
|
+
const modelName = model.constructor?.name, policyClass = state.policies.get(modelName);
|
|
126
|
+
if (policyClass) {
|
|
127
|
+
const policyInstance = new policyClass;
|
|
128
|
+
if (policyInstance.before) {
|
|
129
|
+
const beforeResult = await policyInstance.before(user, ability);
|
|
130
|
+
if (beforeResult === !0)
|
|
131
|
+
return AuthorizationResponse.allow();
|
|
132
|
+
if (beforeResult === !1)
|
|
133
|
+
return AuthorizationResponse.deny();
|
|
134
|
+
}
|
|
135
|
+
const method = RESERVED_POLICY_MEMBERS.has(ability) ? void 0 : policyInstance[ability];
|
|
136
|
+
if (typeof method === "function") {
|
|
137
|
+
const result = await method.call(policyInstance, user, ...args);
|
|
138
|
+
return normalizeResponse(result ?? !1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const gate = state.gates.get(ability);
|
|
143
|
+
if (gate)
|
|
144
|
+
return normalizeResponse(await gate(user, ...args));
|
|
145
|
+
return AuthorizationResponse.deny(`No gate or policy defined for ability: ${ability}`);
|
|
146
|
+
}
|
|
147
|
+
async function check(ability, user, ...args) {
|
|
148
|
+
return (await inspect(ability, user, ...args)).isAllowed;
|
|
149
|
+
}
|
|
150
|
+
function normalizeResponse(result) {
|
|
151
|
+
if (result instanceof AuthorizationResponse)
|
|
152
|
+
return result;
|
|
153
|
+
if (typeof result !== "boolean")
|
|
154
|
+
throw TypeError(`[gate] Policy must return boolean or AuthorizationResponse; got ${typeof result}. If you returned a model/value by mistake, return \`true\`/\`false\` instead.`);
|
|
155
|
+
return result ? AuthorizationResponse.allow() : AuthorizationResponse.deny();
|
|
156
|
+
}
|
|
157
|
+
export function getPolicyFor(model) {
|
|
158
|
+
if (!model || typeof model !== "object")
|
|
159
|
+
return null;
|
|
160
|
+
const modelName = model.constructor?.name, policyClass = state.policies.get(modelName);
|
|
161
|
+
if (policyClass)
|
|
162
|
+
return new policyClass;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
export function has(ability) {
|
|
166
|
+
return state.gates.has(ability);
|
|
167
|
+
}
|
|
168
|
+
export function hasPolicy(model) {
|
|
169
|
+
const modelName = typeof model === "string" ? model : model.name;
|
|
170
|
+
return state.policies.has(modelName);
|
|
171
|
+
}
|
|
172
|
+
export function abilities() {
|
|
173
|
+
return Array.from(state.gates.keys());
|
|
174
|
+
}
|
|
175
|
+
export function flush() {
|
|
176
|
+
state.gates.clear();
|
|
177
|
+
state.policies.clear();
|
|
178
|
+
state.beforeCallbacks = [];
|
|
179
|
+
state.afterCallbacks = [];
|
|
180
|
+
}
|
|
181
|
+
export const Gate = {
|
|
182
|
+
define,
|
|
183
|
+
policy,
|
|
184
|
+
before,
|
|
185
|
+
after,
|
|
186
|
+
allows,
|
|
187
|
+
denies,
|
|
188
|
+
can,
|
|
189
|
+
cannot,
|
|
190
|
+
any,
|
|
191
|
+
all,
|
|
192
|
+
none,
|
|
193
|
+
authorize,
|
|
194
|
+
inspect,
|
|
195
|
+
has,
|
|
196
|
+
hasPolicy,
|
|
197
|
+
abilities,
|
|
198
|
+
getPolicyFor,
|
|
199
|
+
flush,
|
|
200
|
+
AuthorizationResponse,
|
|
201
|
+
AuthorizationException
|
|
202
|
+
};
|
|
203
|
+
export default Gate;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type { SeedDefaultRolesResult } from './rbac-seed';
|
|
2
|
+
export * from './authentication';
|
|
3
|
+
export * from './authenticator';
|
|
4
|
+
export * from './client';
|
|
5
|
+
export * from './middleware';
|
|
6
|
+
export * from './rate-limiter';
|
|
7
|
+
// WebAuthn/Passkey support (now using ts-auth - no external dependencies)
|
|
8
|
+
export * from './passkey';
|
|
9
|
+
export * from './password/reset';
|
|
10
|
+
export * from './register';
|
|
11
|
+
export * from './user';
|
|
12
|
+
// Token management (Laravel Passport-style)
|
|
13
|
+
export * from './tokens';
|
|
14
|
+
// Authorization Gates & Policies (Laravel-style)
|
|
15
|
+
export * from './gate';
|
|
16
|
+
export * from './policy';
|
|
17
|
+
export * from './authorizable';
|
|
18
|
+
// Role-Based Access Control (RBAC)
|
|
19
|
+
export * from './rbac';
|
|
20
|
+
export { createBqbRbacStore } from './rbac-store-bqb';
|
|
21
|
+
export { DEFAULT_ROLE_PACKS, seedDefaultRoles } from './rbac-seed';
|
|
22
|
+
// Email Verification
|
|
23
|
+
export * from './email-verification';
|
|
24
|
+
// Session-based Authentication (SPA Cookie Auth)
|
|
25
|
+
export * from './session-auth';
|
|
26
|
+
// TOTP (Two-Factor Authentication) - re-export from ts-auth
|
|
27
|
+
export {
|
|
28
|
+
generateTOTP,
|
|
29
|
+
verifyTOTP,
|
|
30
|
+
generateTOTPSecret,
|
|
31
|
+
totpKeyUri,
|
|
32
|
+
} from '@stacksjs/ts-auth';
|
|
33
|
+
// TOTP setup/enable/disable + login-challenge persistence
|
|
34
|
+
export * from './two-factor';
|
|
35
|
+
// Team resolution from auth credentials (dashboard-form scoping)
|
|
36
|
+
export * from './team';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from "./authentication";
|
|
2
|
+
export * from "./authenticator";
|
|
3
|
+
export * from "./client";
|
|
4
|
+
export * from "./middleware";
|
|
5
|
+
export * from "./rate-limiter";
|
|
6
|
+
export * from "./passkey";
|
|
7
|
+
export * from "./password/reset";
|
|
8
|
+
export * from "./register";
|
|
9
|
+
export * from "./user";
|
|
10
|
+
export * from "./tokens";
|
|
11
|
+
export * from "./gate";
|
|
12
|
+
export * from "./policy";
|
|
13
|
+
export * from "./authorizable";
|
|
14
|
+
export * from "./rbac";
|
|
15
|
+
export { createBqbRbacStore } from "./rbac-store-bqb";
|
|
16
|
+
export { DEFAULT_ROLE_PACKS, seedDefaultRoles } from "./rbac-seed";
|
|
17
|
+
export * from "./email-verification";
|
|
18
|
+
export * from "./session-auth";
|
|
19
|
+
export {
|
|
20
|
+
generateTOTP,
|
|
21
|
+
verifyTOTP,
|
|
22
|
+
generateTOTPSecret,
|
|
23
|
+
totpKeyUri
|
|
24
|
+
} from "@stacksjs/ts-auth";
|
|
25
|
+
export * from "./two-factor";
|
|
26
|
+
export * from "./team";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal constants shared across the auth package.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own file so consumers like `authentication.ts` and
|
|
5
|
+
* `session-auth.ts` can pull the same literal without re-declaring it
|
|
6
|
+
* (stacksjs/stacks#1861 L-1).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Pre-computed bcrypt hash of nothing — used to keep timing constant
|
|
10
|
+
* when the lookup-by-email half of a login fails. Without it, the
|
|
11
|
+
* "user not found" branch would short-circuit fast and the "wrong
|
|
12
|
+
* password" branch would always pay the bcrypt cost, leaking
|
|
13
|
+
* "does this email exist?" via response time.
|
|
14
|
+
*
|
|
15
|
+
* The hash itself is intentionally non-verifiable: nothing the
|
|
16
|
+
* attacker types will ever match it, so the dummy compare always
|
|
17
|
+
* returns false but spends the same CPU as a real one would.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DUMMY_BCRYPT_HASH: '$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DUMMY_BCRYPT_HASH = "$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in auth middleware handler
|
|
3
|
+
* Validates bearer token and sets the authenticated user on Auth
|
|
4
|
+
*/
|
|
5
|
+
export declare function authMiddleware(request: any): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Auth middleware object with handle method (for compatibility with middleware loader)
|
|
8
|
+
* @defaultValue `{ name: 'auth' }`
|
|
9
|
+
*/
|
|
10
|
+
export declare const authMiddlewareHandler: {
|
|
11
|
+
/** @defaultValue 'auth' */
|
|
12
|
+
name: string;
|
|
13
|
+
handle: unknown
|
|
14
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Auth } from "./authentication";
|
|
2
|
+
export async function authMiddleware(request) {
|
|
3
|
+
let bearerToken = request.bearerToken?.();
|
|
4
|
+
if (!bearerToken) {
|
|
5
|
+
const authHeader = request.headers?.get?.("authorization") || request.headers?.get?.("Authorization");
|
|
6
|
+
if (authHeader && authHeader.startsWith("Bearer "))
|
|
7
|
+
bearerToken = authHeader.substring(7);
|
|
8
|
+
}
|
|
9
|
+
if (!bearerToken) {
|
|
10
|
+
const error = Error("No authentication token provided.");
|
|
11
|
+
error.statusCode = 401;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
const user = await Auth.getUserFromToken(bearerToken);
|
|
15
|
+
if (!user) {
|
|
16
|
+
const error = Error("Invalid or expired authentication token.");
|
|
17
|
+
error.statusCode = 401;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
Auth.setUser(user);
|
|
21
|
+
request._authenticatedUser = user;
|
|
22
|
+
const accessToken = await Auth.currentAccessToken();
|
|
23
|
+
request._currentAccessToken = accessToken;
|
|
24
|
+
}
|
|
25
|
+
export const authMiddlewareHandler = {
|
|
26
|
+
name: "auth",
|
|
27
|
+
handle: authMiddleware
|
|
28
|
+
};
|