@veloxts/auth 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @veloxts/auth
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: add business logic primitives for B2B SaaS apps
8
+ - Updated dependencies
9
+ - @veloxts/core@0.8.1
10
+ - @veloxts/router@0.8.1
11
+
3
12
  ## 0.8.0
4
13
 
5
14
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * @module @veloxts/auth
9
9
  */
10
10
  export { AUTH_VERSION } from './plugin.js';
11
- export type { AdapterAuthContext, AuthConfig, AuthContext, AuthMiddlewareOptions, BaseAuthContext, GuardDefinition, GuardFunction, HashConfig, JwtConfig, NativeAuthContext, PolicyAction, PolicyDefinition, RateLimitConfig, TokenPair, TokenPayload, User, } from './types.js';
11
+ export type { AdapterAuthContext, AuthConfig, AuthContext, AuthMiddlewareOptions, BaseAuthContext, GuardDefinition, GuardFunction, HashConfig, JwtConfig, NativeAuthContext, PolicyAction, PolicyActionParams, PolicyActionRef, PolicyDefinition, PolicyObject, RateLimitConfig, TokenPair, TokenPayload, User, } from './types.js';
12
12
  export { AuthError } from './types.js';
13
13
  export { AUTH_REGISTERED, checkDoubleRegistration, decorateAuth, getRequestAuth, getRequestUser, setRequestAuth, } from './decoration.js';
14
14
  export type { TokenStore } from './jwt.js';
@@ -2,20 +2,22 @@
2
2
  * Resource-level authorization policies for @veloxts/auth
3
3
  * @module auth/policies
4
4
  */
5
- import type { PolicyAction, PolicyDefinition, User } from './types.js';
5
+ import type { PolicyAction, PolicyDefinition, PolicyObject, User } from './types.js';
6
6
  /**
7
7
  * Registers a policy for a resource type
8
8
  *
9
9
  * @example
10
10
  * ```typescript
11
- * registerPolicy('Post', {
12
- * view: (user, post) => true, // Anyone can view
13
- * update: (user, post) => user.id === post.authorId,
14
- * delete: (user, post) => user.id === post.authorId || user.role === 'admin',
11
+ * const PostPolicy = definePolicy<User, Post>('Post', {
12
+ * view: () => true,
13
+ * update: ({ user, resource }) => user.id === resource.authorId,
14
+ * delete: ({ user, resource }) => user.id === resource.authorId || user.role === 'admin',
15
15
  * });
16
+ *
17
+ * registerPolicy(PostPolicy);
16
18
  * ```
17
19
  */
18
- export declare function registerPolicy<TUser = User, TResource = unknown>(resourceName: string, policy: PolicyDefinition<TUser, TResource>): void;
20
+ export declare function registerPolicy<TUser = User, TResource = unknown>(policyOrName: string | PolicyObject<TUser, TResource>, policy?: PolicyDefinition<TUser, TResource>): void;
19
21
  /**
20
22
  * Gets a registered policy by resource name
21
23
  */
@@ -29,19 +31,24 @@ export declare function clearPolicies(): void;
29
31
  *
30
32
  * @example
31
33
  * ```typescript
32
- * const PostPolicy = definePolicy<User, Post>({
34
+ * const PostPolicy = definePolicy<User, Post>('Post', {
33
35
  * view: () => true,
34
- * create: (user) => user.emailVerified,
35
- * update: (user, post) => user.id === post.authorId,
36
- * delete: (user, post) => user.id === post.authorId || user.role === 'admin',
37
- * publish: (user, post) => user.role === 'editor' && user.id === post.authorId,
36
+ * create: ({ user }) => user.emailVerified,
37
+ * update: ({ user, resource }) => user.id === resource.authorId,
38
+ * delete: ({ user, resource }) => user.id === resource.authorId || user.role === 'admin',
39
+ * publish: ({ user, resource }) => user.role === 'editor' && user.id === resource.authorId,
38
40
  * });
39
41
  *
42
+ * // Each action is a PolicyActionRef:
43
+ * PostPolicy.update.actionName // 'update'
44
+ * PostPolicy.update.resourceName // 'Post'
45
+ * PostPolicy.update.check(user, post) // boolean | Promise<boolean>
46
+ *
40
47
  * // Register it
41
- * registerPolicy('Post', PostPolicy);
48
+ * registerPolicy(PostPolicy);
42
49
  * ```
43
50
  */
44
- export declare function definePolicy<TUser = User, TResource = unknown>(actions: PolicyDefinition<TUser, TResource>): PolicyDefinition<TUser, TResource>;
51
+ export declare function definePolicy<TUser = User, TResource = unknown, const TResourceName extends string = string>(resourceName: TResourceName, actions: PolicyDefinition<TUser, TResource>): PolicyObject<TUser, TResource, TResourceName>;
45
52
  /**
46
53
  * Checks if a user can perform an action on a resource
47
54
  *
@@ -76,10 +83,10 @@ export declare function authorize<TResource = unknown>(user: User | null | undef
76
83
  * ```typescript
77
84
  * const CommentPolicy = createPolicyBuilder<User, Comment>()
78
85
  * .allow('view', () => true)
79
- * .allow('create', (user) => user.emailVerified)
80
- * .allow('update', (user, comment) => user.id === comment.authorId)
81
- * .allow('delete', (user, comment) =>
82
- * user.id === comment.authorId || user.role === 'admin'
86
+ * .allow('create', ({ user }) => user.emailVerified)
87
+ * .allow('update', ({ user, resource }) => user.id === resource.authorId)
88
+ * .allow('delete', ({ user, resource }) =>
89
+ * user.id === resource.authorId || user.role === 'admin'
83
90
  * )
84
91
  * .build();
85
92
  * ```
package/dist/policies.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Resource-level authorization policies for @veloxts/auth
3
3
  * @module auth/policies
4
4
  */
5
- import { createLogger } from '@veloxts/core';
5
+ import { createLogger, ForbiddenError } from '@veloxts/core';
6
6
  const log = createLogger('auth');
7
7
  // ============================================================================
8
8
  // Policy Registry
@@ -17,15 +17,37 @@ const policyRegistry = new Map();
17
17
  *
18
18
  * @example
19
19
  * ```typescript
20
- * registerPolicy('Post', {
21
- * view: (user, post) => true, // Anyone can view
22
- * update: (user, post) => user.id === post.authorId,
23
- * delete: (user, post) => user.id === post.authorId || user.role === 'admin',
20
+ * const PostPolicy = definePolicy<User, Post>('Post', {
21
+ * view: () => true,
22
+ * update: ({ user, resource }) => user.id === resource.authorId,
23
+ * delete: ({ user, resource }) => user.id === resource.authorId || user.role === 'admin',
24
24
  * });
25
+ *
26
+ * registerPolicy(PostPolicy);
25
27
  * ```
26
28
  */
27
- export function registerPolicy(resourceName, policy) {
28
- policyRegistry.set(resourceName, policy);
29
+ export function registerPolicy(policyOrName, policy) {
30
+ if (typeof policyOrName === 'string') {
31
+ // Legacy: registerPolicy('Post', policyDef)
32
+ if (policy) {
33
+ policyRegistry.set(policyOrName, policy);
34
+ }
35
+ }
36
+ else {
37
+ // New: registerPolicy(PostPolicy) — extracts resourceName and rebuilds definition
38
+ const policyObj = policyOrName;
39
+ const definition = {};
40
+ for (const key of Object.keys(policyObj)) {
41
+ if (key === 'resourceName')
42
+ continue;
43
+ const ref = policyObj[key];
44
+ if (ref && typeof ref === 'object' && 'check' in ref && 'actionName' in ref) {
45
+ const actionRef = ref;
46
+ definition[key] = ({ user, resource }) => actionRef.check(user, resource);
47
+ }
48
+ }
49
+ policyRegistry.set(policyObj.resourceName, definition);
50
+ }
29
51
  }
30
52
  /**
31
53
  * Gets a registered policy by resource name
@@ -47,20 +69,41 @@ export function clearPolicies() {
47
69
  *
48
70
  * @example
49
71
  * ```typescript
50
- * const PostPolicy = definePolicy<User, Post>({
72
+ * const PostPolicy = definePolicy<User, Post>('Post', {
51
73
  * view: () => true,
52
- * create: (user) => user.emailVerified,
53
- * update: (user, post) => user.id === post.authorId,
54
- * delete: (user, post) => user.id === post.authorId || user.role === 'admin',
55
- * publish: (user, post) => user.role === 'editor' && user.id === post.authorId,
74
+ * create: ({ user }) => user.emailVerified,
75
+ * update: ({ user, resource }) => user.id === resource.authorId,
76
+ * delete: ({ user, resource }) => user.id === resource.authorId || user.role === 'admin',
77
+ * publish: ({ user, resource }) => user.role === 'editor' && user.id === resource.authorId,
56
78
  * });
57
79
  *
80
+ * // Each action is a PolicyActionRef:
81
+ * PostPolicy.update.actionName // 'update'
82
+ * PostPolicy.update.resourceName // 'Post'
83
+ * PostPolicy.update.check(user, post) // boolean | Promise<boolean>
84
+ *
58
85
  * // Register it
59
- * registerPolicy('Post', PostPolicy);
86
+ * registerPolicy(PostPolicy);
60
87
  * ```
61
88
  */
62
- export function definePolicy(actions) {
63
- return actions;
89
+ export function definePolicy(resourceName, actions) {
90
+ const result = {
91
+ resourceName,
92
+ };
93
+ for (const [actionName, handler] of Object.entries(actions)) {
94
+ if (handler) {
95
+ const actionHandler = handler;
96
+ const ref = {
97
+ actionName,
98
+ resourceName,
99
+ check: (user, resource) => {
100
+ return actionHandler({ user, resource: resource });
101
+ },
102
+ };
103
+ result[actionName] = ref;
104
+ }
105
+ }
106
+ return result;
64
107
  }
65
108
  // ============================================================================
66
109
  // Authorization Checks
@@ -92,7 +135,7 @@ export async function can(user, action, resourceName, resource) {
92
135
  // Action not defined = deny by default
93
136
  return false;
94
137
  }
95
- return actionHandler(user, resource);
138
+ return actionHandler({ user, resource: resource });
96
139
  }
97
140
  /**
98
141
  * Checks if a user cannot perform an action (inverse of can)
@@ -113,9 +156,7 @@ export async function cannot(user, action, resourceName, resource) {
113
156
  export async function authorize(user, action, resourceName, resource) {
114
157
  const allowed = await can(user, action, resourceName, resource);
115
158
  if (!allowed) {
116
- const error = new Error(`Unauthorized: cannot ${action} ${resourceName}${resource ? ` (id: ${resource.id ?? 'unknown'})` : ''}`);
117
- error.statusCode = 403;
118
- throw error;
159
+ throw new ForbiddenError(`Unauthorized: cannot ${action} ${resourceName}${resource ? ` (id: ${resource.id ?? 'unknown'})` : ''}`);
119
160
  }
120
161
  }
121
162
  // ============================================================================
@@ -128,10 +169,10 @@ export async function authorize(user, action, resourceName, resource) {
128
169
  * ```typescript
129
170
  * const CommentPolicy = createPolicyBuilder<User, Comment>()
130
171
  * .allow('view', () => true)
131
- * .allow('create', (user) => user.emailVerified)
132
- * .allow('update', (user, comment) => user.id === comment.authorId)
133
- * .allow('delete', (user, comment) =>
134
- * user.id === comment.authorId || user.role === 'admin'
172
+ * .allow('create', ({ user }) => user.emailVerified)
173
+ * .allow('update', ({ user, resource }) => user.id === resource.authorId)
174
+ * .allow('delete', ({ user, resource }) =>
175
+ * user.id === resource.authorId || user.role === 'admin'
135
176
  * )
136
177
  * .build();
137
178
  * ```
@@ -160,7 +201,7 @@ class PolicyBuilder {
160
201
  * Assumes resource has a userId or authorId field
161
202
  */
162
203
  allowOwner(action, ownerField = 'userId') {
163
- this.actions[action] = (user, resource) => {
204
+ this.actions[action] = ({ user, resource }) => {
164
205
  const ownerId = resource?.[ownerField];
165
206
  return ownerId === user.id;
166
207
  };
@@ -170,7 +211,7 @@ class PolicyBuilder {
170
211
  * Allows action for owner OR users with specific role
171
212
  */
172
213
  allowOwnerOr(action, roles, ownerField = 'userId') {
173
- this.actions[action] = (user, resource) => {
214
+ this.actions[action] = ({ user, resource }) => {
174
215
  const ownerId = resource?.[ownerField];
175
216
  const userRole = user.role;
176
217
  const isOwner = ownerId === user.id;
@@ -202,7 +243,7 @@ class PolicyBuilder {
202
243
  * and owner-only for regular users
203
244
  */
204
245
  export function createOwnerOrAdminPolicy(ownerField = 'userId') {
205
- const isOwnerOrAdmin = (user, resource) => {
246
+ const isOwnerOrAdmin = ({ user, resource, }) => {
206
247
  if (user.role === 'admin') {
207
248
  return true;
208
249
  }
@@ -231,7 +272,7 @@ export function createReadOnlyPolicy() {
231
272
  * Creates a policy for admin-only resources
232
273
  */
233
274
  export function createAdminOnlyPolicy() {
234
- const isAdmin = (user) => user.role === 'admin';
275
+ const isAdmin = ({ user }) => user.role === 'admin';
235
276
  return {
236
277
  view: isAdmin,
237
278
  create: isAdmin,
package/dist/types.d.ts CHANGED
@@ -286,11 +286,20 @@ export interface GuardDefinition<TContext = unknown> {
286
286
  /** HTTP status code on failure (default: 403) */
287
287
  statusCode?: number;
288
288
  }
289
+ /**
290
+ * Parameters passed to a policy action handler
291
+ */
292
+ export interface PolicyActionParams<TUser = User, TResource = unknown> {
293
+ /** The authenticated user performing the action */
294
+ user: TUser;
295
+ /** The resource being acted upon (optional for create-like actions) */
296
+ resource: TResource;
297
+ }
289
298
  /**
290
299
  * Policy action handler
291
300
  * Returns true if user can perform action on resource
292
301
  */
293
- export type PolicyAction<TUser = User, TResource = unknown> = (user: TUser, resource: TResource) => boolean | Promise<boolean>;
302
+ export type PolicyAction<TUser = User, TResource = unknown> = (params: PolicyActionParams<TUser, TResource>) => boolean | Promise<boolean>;
294
303
  /**
295
304
  * Policy definition with named actions
296
305
  */
@@ -306,6 +315,31 @@ export interface PolicyDefinition<TUser = User, TResource = unknown> {
306
315
  /** Custom actions */
307
316
  [action: string]: PolicyAction<TUser, TResource> | undefined;
308
317
  }
318
+ /**
319
+ * A reference to a single policy action with its metadata.
320
+ *
321
+ * Returned as a property on a PolicyObject for each defined action.
322
+ * Can be used for introspection or passed to authorization helpers.
323
+ */
324
+ export interface PolicyActionRef<TUser = unknown, TResource = unknown, TResourceName extends string = string> {
325
+ /** The name of the action (e.g., 'update', 'delete') */
326
+ readonly actionName: string;
327
+ /** The name of the resource this policy applies to (e.g., 'Post') */
328
+ readonly resourceName: TResourceName;
329
+ /** Execute the policy check for this action */
330
+ readonly check: (user: TUser, resource?: TResource) => boolean | Promise<boolean>;
331
+ }
332
+ /**
333
+ * A policy object returned by definePolicy.
334
+ *
335
+ * Contains the resource name and each action as a PolicyActionRef.
336
+ */
337
+ export type PolicyObject<TUser = User, TResource = unknown, TResourceName extends string = string, TActions extends string = string> = {
338
+ /** The resource name this policy applies to */
339
+ readonly resourceName: TResourceName;
340
+ } & {
341
+ readonly [K in TActions]: PolicyActionRef<TUser, TResource, TResourceName>;
342
+ };
309
343
  /**
310
344
  * Auth middleware options
311
345
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/auth",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Authentication and authorization system for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,9 +60,9 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@fastify/cookie": "11.0.2",
63
- "fastify": "5.7.4",
64
- "@veloxts/core": "0.8.0",
65
- "@veloxts/router": "0.8.0"
63
+ "fastify": "5.8.2",
64
+ "@veloxts/core": "0.8.1",
65
+ "@veloxts/router": "0.8.1"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "argon2": ">=0.30.0",
@@ -82,11 +82,11 @@
82
82
  },
83
83
  "devDependencies": {
84
84
  "@types/bcrypt": "6.0.0",
85
- "@vitest/coverage-v8": "4.0.18",
85
+ "@vitest/coverage-v8": "4.1.0",
86
86
  "typescript": "5.9.3",
87
- "vitest": "4.0.18",
88
- "@veloxts/testing": "0.8.0",
89
- "@veloxts/validation": "0.8.0"
87
+ "vitest": "4.1.0",
88
+ "@veloxts/testing": "0.8.1",
89
+ "@veloxts/validation": "0.8.1"
90
90
  },
91
91
  "keywords": [
92
92
  "velox",