@stacksjs/auth 0.72.98 → 0.72.100

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/gate.d.ts CHANGED
@@ -1,4 +1,41 @@
1
1
  import type { UserModel as OrmUserModel } from '@stacksjs/orm';
2
+ /**
3
+ * Define the application's gates, policy mappings and before/after callbacks.
4
+ *
5
+ * The `define*` helper for `app/Gates.ts`. Both halves of `policies` are
6
+ * checked - the key names a model that exists, the value names a policy file
7
+ * that exists - where the type used to be
8
+ * `Record<string, string | { policy: string }>` on both sides, so a mapping to
9
+ * a policy that is not there registered nothing and denied every check on that
10
+ * model with no error anywhere.
11
+ *
12
+ * The `const` type parameter keeps the ability names, which is what
13
+ * `storage/framework/types/gates.d.ts` reads back to fill `AppGates`.
14
+ *
15
+ * The second half of the constraint is what rejects a key that is not a model,
16
+ * and it is not redundant with `PolicyMapping`. Excess-property checking is a
17
+ * freshness rule on the object literal and stops applying as soon as inference
18
+ * has a matching property to work with: `{ Psot: 'PostPolicy' }` alone was
19
+ * caught, and the same typo beside one correct entry was not. Requiring every
20
+ * key outside the model list to hold something no policy name can be makes the
21
+ * check structural.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * // app/Gates.ts
26
+ * import { defineGates } from '@stacksjs/auth'
27
+ *
28
+ * export default defineGates({
29
+ * gates: {
30
+ * 'access-admin': user => user?.email?.endsWith('@stacksjs.org') ?? false,
31
+ * },
32
+ * policies: {
33
+ * Post: 'PostPolicy',
34
+ * },
35
+ * })
36
+ * ```
37
+ */
38
+ export declare function defineGates<const T extends GatesDefinition>(definition: T & { policies?: OnlyKnownModels<T['policies']> }): T;
2
39
  /**
3
40
  * Define a new authorization gate
4
41
  *
@@ -14,7 +51,7 @@ export declare function define<T = any>(ability: string, callback: GateCallback<
14
51
  * policy('Post', PostPolicy)
15
52
  * policy(Post, PostPolicy)
16
53
  */
17
- export declare function policy(model: string | { name: string }, policyClass: new () => Policy): void;
54
+ export declare function policy(model: PolicyModelName | { name: PolicyModelName }, policyClass: new () => Policy): void;
18
55
  /**
19
56
  * Register a callback to run before all gate checks
20
57
  *
@@ -24,11 +61,11 @@ export declare function policy(model: string | { name: string }, policyClass: ne
24
61
  * return null // Continue to normal checks
25
62
  * })
26
63
  */
27
- export declare function before(callback: (user: UserModel | null, ability: string, args: any[]) => boolean | null | Promise<boolean | null>): void;
64
+ export declare function before(callback: GateBeforeCallback): void;
28
65
  /**
29
66
  * Register a callback to run after all gate checks
30
67
  */
31
- export declare function after(callback: (user: UserModel | null, ability: string, result: boolean, args: any[]) => boolean | void | Promise<boolean | void>): void;
68
+ export declare function after(callback: GateAfterCallback): void;
32
69
  /**
33
70
  * Check if the user is allowed to perform an ability
34
71
  *
@@ -36,51 +73,51 @@ export declare function after(callback: (user: UserModel | null, ability: string
36
73
  * if (await allows('edit-settings', user)) { ... }
37
74
  * if (await allows('update', user, post)) { ... }
38
75
  */
39
- export declare function allows(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
76
+ export declare function allows(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
40
77
  /**
41
78
  * Check if the user is denied from performing an ability
42
79
  *
43
80
  * @example
44
81
  * if (await denies('delete', user, post)) { ... }
45
82
  */
46
- export declare function denies(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
83
+ export declare function denies(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
47
84
  /**
48
85
  * Check if the user can perform an ability (alias for allows)
49
86
  */
50
- export declare function can(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
87
+ export declare function can(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
51
88
  /**
52
89
  * Check if the user cannot perform an ability (alias for denies)
53
90
  */
54
- export declare function cannot(ability: string, user: UserModel | null, ...args: any[]): Promise<boolean>;
91
+ export declare function cannot(ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
55
92
  /**
56
93
  * Check if the user can perform any of the given abilities
57
94
  *
58
95
  * @example
59
96
  * if (await any(['update', 'delete'], user, post)) { ... }
60
97
  */
61
- export declare function any(abilities: string[], user: UserModel | null, ...args: any[]): Promise<boolean>;
98
+ export declare function any(abilities: readonly Ability[], user: UserModel | null, ...args: any[]): Promise<boolean>;
62
99
  /**
63
100
  * Check if the user can perform all of the given abilities
64
101
  *
65
102
  * @example
66
103
  * if (await all(['view', 'update'], user, post)) { ... }
67
104
  */
68
- export declare function all(abilities: string[], user: UserModel | null, ...args: any[]): Promise<boolean>;
105
+ export declare function all(abilities: readonly Ability[], user: UserModel | null, ...args: any[]): Promise<boolean>;
69
106
  /**
70
107
  * Check if the user can perform none of the given abilities
71
108
  */
72
- export declare function none(abilities: string[], user: UserModel | null, ...args: any[]): Promise<boolean>;
109
+ export declare function none(abilities: readonly Ability[], user: UserModel | null, ...args: any[]): Promise<boolean>;
73
110
  /**
74
111
  * Authorize an ability or throw an exception
75
112
  *
76
113
  * @example
77
114
  * await authorize('update', user, post) // Throws if not allowed
78
115
  */
79
- export declare function authorize(ability: string, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
116
+ export declare function authorize(ability: Ability, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
80
117
  /**
81
118
  * Get detailed inspection result for an ability check
82
119
  */
83
- export declare function inspect(ability: string, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
120
+ export declare function inspect(ability: Ability, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse>;
84
121
  /**
85
122
  * Get a policy instance for a model
86
123
  */
@@ -88,7 +125,7 @@ export declare function getPolicyFor<T = any>(model: T): Policy<T> | null;
88
125
  /**
89
126
  * Check if a gate is defined
90
127
  */
91
- export declare function has(ability: string): boolean;
128
+ export declare function has(ability: Ability): boolean;
92
129
  /**
93
130
  * Check if a policy is registered for a model
94
131
  */
@@ -126,6 +163,48 @@ export declare const Gate: {
126
163
  AuthorizationResponse: typeof AuthorizationResponse;
127
164
  AuthorizationException: typeof AuthorizationException
128
165
  };
166
+ /**
167
+ * Augmentation target: the ability names this application's own gates define.
168
+ *
169
+ * Derived from `app/Gates.ts` itself by
170
+ * `storage/framework/types/gates.d.ts`, so it cannot drift from the file it
171
+ * describes - the gates are the declaration.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * declare module '@stacksjs/auth' {
176
+ * interface AppGates {
177
+ * 'access-admin': true
178
+ * }
179
+ * }
180
+ * ```
181
+ */
182
+ // eslint-disable-next-line ts/no-empty-object-type -- augmentation target; empty by design
183
+ export declare interface AppGates {}
184
+ /**
185
+ * Augmentation target: the policy classes under `app/Policies/`, and the
186
+ * framework defaults behind it, by filename.
187
+ *
188
+ * Filled by `storage/framework/types/registries.d.ts`, which reads the same
189
+ * name map `findPolicyFile` resolves through.
190
+ */
191
+ // eslint-disable-next-line ts/no-empty-object-type -- augmentation target; empty by design
192
+ export declare interface PolicyClasses {}
193
+ /**
194
+ * Augmentation target: the models a policy may be registered for.
195
+ *
196
+ * Derived from the models barrel, so it is the models that exist rather than a
197
+ * list somebody maintains alongside them.
198
+ */
199
+ // eslint-disable-next-line ts/no-empty-object-type -- augmentation target; empty by design
200
+ export declare interface PolicyModels {}
201
+ /** The shape of `app/Gates.ts`. */
202
+ export declare interface GatesDefinition {
203
+ gates: Readonly<Record<string, GateCallback>>
204
+ policies?: PolicyMapping
205
+ before?: readonly GateBeforeCallback[]
206
+ after?: readonly GateAfterCallback[]
207
+ }
129
208
  /**
130
209
  * Policy class interface
131
210
  */
@@ -148,6 +227,56 @@ declare type UserModel = OrmUserModel;
148
227
  * Gate callback function type
149
228
  */
150
229
  export type GateCallback<T = any> = (_user: UserModel | null, ..._args: T[]) => boolean | Promise<boolean> | AuthorizationResponse;
230
+ /** An ability name defined by one of the application's gates. */
231
+ export type GateName = keyof AppGates extends never ? string : keyof AppGates & string;
232
+ /**
233
+ * The abilities `BasePolicy` resolves. A policy may add its own methods, which
234
+ * is why `Ability` below stays open.
235
+ */
236
+ export type PolicyAbility = 'viewAny' | 'view' | 'create' | 'update' | 'delete' | 'restore' | 'forceDelete';
237
+ /**
238
+ * Any ability that can be checked.
239
+ *
240
+ * Deliberately open. An ability is legitimately dynamic - a `/can/:ability`
241
+ * route passes one straight through, which is the reason
242
+ * `RESERVED_POLICY_MEMBERS` exists at all - so narrowing this to the declared
243
+ * set would reject correct code and break the fail-closed tests that check
244
+ * what an UNKNOWN ability does. The union is here for the editor: the gates
245
+ * and policy abilities are offered, and anything else still compiles.
246
+ */
247
+ // eslint-disable-next-line ts/ban-types -- `string & {}` keeps literal completions alive
248
+ export type Ability = GateName | PolicyAbility | (string & {});
249
+ /** A policy class name, as narrow as the application has made it. */
250
+ export type PolicyName = keyof PolicyClasses extends never ? string : keyof PolicyClasses & string;
251
+ /** A model name a policy may be registered for. */
252
+ export type PolicyModelName = keyof PolicyModels extends never ? string : keyof PolicyModels & string;
253
+ /** Runs before every check. `true` allows, `false` denies, `null` continues. */
254
+ export type GateBeforeCallback = (_user: UserModel | null, _ability: string, _args: unknown[]) => boolean | null | Promise<boolean | null>;
255
+ /** Runs after every check. A boolean overrides the result; anything else keeps it. */
256
+ export type GateAfterCallback = (_user: UserModel | null, _ability: string, _result: boolean, _args: unknown[]) => boolean | void | Promise<boolean | void>;
257
+ /**
258
+ * How `app/Gates.ts` maps a model to the policy that authorizes it.
259
+ *
260
+ * Every key optional: a mapping is written only for the models whose policy
261
+ * does not follow the `<Model>Policy` convention.
262
+ */
263
+ export type PolicyMapping = {
264
+ readonly [K in PolicyModelName]?: PolicyName | { policy: PolicyName, model?: PolicyModelName }
265
+ }
266
+ /**
267
+ * Every key of a policy map that is not a model, required to hold something no
268
+ * policy name can be.
269
+ *
270
+ * Applied to the PARAMETER rather than to the type parameter's constraint: a
271
+ * constraint that reads `T['policies']` is a self-reference and TypeScript
272
+ * refuses it, while the parameter may name `T` freely because inference has
273
+ * already run against the `T &` half.
274
+ */
275
+ declare type OnlyKnownModels<TPolicies> = {
276
+ [K in keyof TPolicies]: K extends PolicyModelName
277
+ ? TPolicies[K]
278
+ : { 'this is not a model in this application': never }
279
+ }
151
280
  /**
152
281
  * Policy method type. The return type intentionally allows `null` so that
153
282
  * a policy's `before()` hook (which returns `null` to delegate to the
package/dist/gate.js CHANGED
@@ -1 +1 @@
1
- const RESERVED_POLICY_MEMBERS=new Set(["before","allow","deny","denyIf","denyUnless","allowIf","constructor"]);export class AuthorizationResponse{isAllowed;message;code;constructor(allowed,message,code){this.isAllowed=allowed;this.message=message;this.code=code}static allow(message){return new AuthorizationResponse(!0,message)}static deny(message,code){return new AuthorizationResponse(!1,message||"This action is unauthorized.",code)}allowed(){return this.isAllowed}denied(){return!this.isAllowed}authorize(){if(!this.isAllowed)throw new AuthorizationException(this.message||"This action is unauthorized.",this.code)}}export class AuthorizationException extends Error{code;status;constructor(message="This action is unauthorized.",code,status=403){super(message);this.code=code;this.status=status;this.name="AuthorizationException"}}const state={gates:new Map,policies:new Map,beforeCallbacks:[],afterCallbacks:[]};export function define(ability,callback){state.gates.set(ability,callback)}export function policy(model,policyClass){const modelName=typeof model==="string"?model:model.name;state.policies.set(modelName,policyClass)}export function before(callback){state.beforeCallbacks.push(callback)}export function after(callback){state.afterCallbacks.push(callback)}export async function allows(ability,user,...args){return check(ability,user,...args)}export async function denies(ability,user,...args){return!await check(ability,user,...args)}export async function can(ability,user,...args){return check(ability,user,...args)}export async function cannot(ability,user,...args){return!await check(ability,user,...args)}export async function any(abilities,user,...args){for(const ability of abilities)if(await check(ability,user,...args))return!0;return!1}export async function all(abilities,user,...args){for(const ability of abilities)if(!await check(ability,user,...args))return!1;return!0}export async function none(abilities,user,...args){return!await any(abilities,user,...args)}export async function authorize(ability,user,...args){const result=await inspect(ability,user,...args);if(!result.isAllowed)throw new AuthorizationException(result.message,result.code);return result}export async function inspect(ability,user,...args){let response=null;for(const callback of state.beforeCallbacks){const beforeResult=await callback(user,ability,args);if(beforeResult===!0){response=AuthorizationResponse.allow();break}if(beforeResult===!1){response=AuthorizationResponse.deny();break}}if(!response)response=await resolveAbility(ability,user,args);for(const callback of state.afterCallbacks){const afterResult=await callback(user,ability,response.isAllowed,args);if(typeof afterResult==="boolean")return afterResult?AuthorizationResponse.allow():AuthorizationResponse.deny()}return response}async function resolveAbility(ability,user,args){const model=args[0];if(model&&typeof model==="object"){const modelName=model.constructor?.name,policyClass=state.policies.get(modelName);if(policyClass){const policyInstance=new policyClass;if(policyInstance.before){const beforeResult=await policyInstance.before(user,ability);if(beforeResult===!0)return AuthorizationResponse.allow();if(beforeResult===!1)return AuthorizationResponse.deny()}const method=RESERVED_POLICY_MEMBERS.has(ability)?void 0:policyInstance[ability];if(typeof method==="function"){const result=await method.call(policyInstance,user,...args);return normalizeResponse(result??!1)}}}const gate=state.gates.get(ability);if(gate)return normalizeResponse(await gate(user,...args));return AuthorizationResponse.deny(`No gate or policy defined for ability: ${ability}`)}async function check(ability,user,...args){return(await inspect(ability,user,...args)).isAllowed}function normalizeResponse(result){if(result instanceof AuthorizationResponse)return result;if(typeof result!=="boolean")throw TypeError(`[gate] Policy must return boolean or AuthorizationResponse; got ${typeof result}. If you returned a model/value by mistake, return \`true\`/\`false\` instead.`);return result?AuthorizationResponse.allow():AuthorizationResponse.deny()}export function getPolicyFor(model){if(!model||typeof model!=="object")return null;const modelName=model.constructor?.name,policyClass=state.policies.get(modelName);if(policyClass)return new policyClass;return null}export function has(ability){return state.gates.has(ability)}export function hasPolicy(model){const modelName=typeof model==="string"?model:model.name;return state.policies.has(modelName)}export function abilities(){return Array.from(state.gates.keys())}export function flush(){state.gates.clear();state.policies.clear();state.beforeCallbacks=[];state.afterCallbacks=[]}export const Gate={define,policy,before,after,allows,denies,can,cannot,any,all,none,authorize,inspect,has,hasPolicy,abilities,getPolicyFor,flush,AuthorizationResponse,AuthorizationException};export default Gate;
1
+ export function defineGates(definition){return definition}const RESERVED_POLICY_MEMBERS=new Set(["before","allow","deny","denyIf","denyUnless","allowIf","constructor"]);export class AuthorizationResponse{isAllowed;message;code;constructor(allowed,message,code){this.isAllowed=allowed;this.message=message;this.code=code}static allow(message){return new AuthorizationResponse(!0,message)}static deny(message,code){return new AuthorizationResponse(!1,message||"This action is unauthorized.",code)}allowed(){return this.isAllowed}denied(){return!this.isAllowed}authorize(){if(!this.isAllowed)throw new AuthorizationException(this.message||"This action is unauthorized.",this.code)}}export class AuthorizationException extends Error{code;status;constructor(message="This action is unauthorized.",code,status=403){super(message);this.code=code;this.status=status;this.name="AuthorizationException"}}const state={gates:new Map,policies:new Map,beforeCallbacks:[],afterCallbacks:[]};export function define(ability,callback){state.gates.set(ability,callback)}export function policy(model,policyClass){const modelName=typeof model==="string"?model:model.name;state.policies.set(modelName,policyClass)}export function before(callback){state.beforeCallbacks.push(callback)}export function after(callback){state.afterCallbacks.push(callback)}export async function allows(ability,user,...args){return check(ability,user,...args)}export async function denies(ability,user,...args){return!await check(ability,user,...args)}export async function can(ability,user,...args){return check(ability,user,...args)}export async function cannot(ability,user,...args){return!await check(ability,user,...args)}export async function any(abilities,user,...args){for(const ability of abilities)if(await check(ability,user,...args))return!0;return!1}export async function all(abilities,user,...args){for(const ability of abilities)if(!await check(ability,user,...args))return!1;return!0}export async function none(abilities,user,...args){return!await any(abilities,user,...args)}export async function authorize(ability,user,...args){const result=await inspect(ability,user,...args);if(!result.isAllowed)throw new AuthorizationException(result.message,result.code);return result}export async function inspect(ability,user,...args){let response=null;for(const callback of state.beforeCallbacks){const beforeResult=await callback(user,ability,args);if(beforeResult===!0){response=AuthorizationResponse.allow();break}if(beforeResult===!1){response=AuthorizationResponse.deny();break}}if(!response)response=await resolveAbility(ability,user,args);for(const callback of state.afterCallbacks){const afterResult=await callback(user,ability,response.isAllowed,args);if(typeof afterResult==="boolean")return afterResult?AuthorizationResponse.allow():AuthorizationResponse.deny()}return response}async function resolveAbility(ability,user,args){const model=args[0];if(model&&typeof model==="object"){const modelName=model.constructor?.name,policyClass=state.policies.get(modelName);if(policyClass){const policyInstance=new policyClass;if(policyInstance.before){const beforeResult=await policyInstance.before(user,ability);if(beforeResult===!0)return AuthorizationResponse.allow();if(beforeResult===!1)return AuthorizationResponse.deny()}const method=RESERVED_POLICY_MEMBERS.has(ability)?void 0:policyInstance[ability];if(typeof method==="function"){const result=await method.call(policyInstance,user,...args);return normalizeResponse(result??!1)}}}const gate=state.gates.get(ability);if(gate)return normalizeResponse(await gate(user,...args));return AuthorizationResponse.deny(`No gate or policy defined for ability: ${ability}`)}async function check(ability,user,...args){return(await inspect(ability,user,...args)).isAllowed}function normalizeResponse(result){if(result instanceof AuthorizationResponse)return result;if(typeof result!=="boolean")throw TypeError(`[gate] Policy must return boolean or AuthorizationResponse; got ${typeof result}. If you returned a model/value by mistake, return \`true\`/\`false\` instead.`);return result?AuthorizationResponse.allow():AuthorizationResponse.deny()}export function getPolicyFor(model){if(!model||typeof model!=="object")return null;const modelName=model.constructor?.name,policyClass=state.policies.get(modelName);if(policyClass)return new policyClass;return null}export function has(ability){return state.gates.has(ability)}export function hasPolicy(model){const modelName=typeof model==="string"?model:model.name;return state.policies.has(modelName)}export function abilities(){return Array.from(state.gates.keys())}export function flush(){state.gates.clear();state.policies.clear();state.beforeCallbacks=[];state.afterCallbacks=[]}export const Gate={define,policy,before,after,allows,denies,can,cannot,any,all,none,authorize,inspect,has,hasPolicy,abilities,getPolicyFor,flush,AuthorizationResponse,AuthorizationException};export default Gate;
package/dist/policy.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { AuthorizationResponse } from './gate';
2
2
  import type { UserModel as OrmUserModel } from '@stacksjs/orm';
3
+ /** For tests, and for a dev server that regenerated the registry. */
4
+ export declare function resetPolicyRegistry(): void;
3
5
  /**
4
- * Discover and register policies from app/Policies directory
6
+ * Discover and register policies: the explicit mappings in `app/Gates.ts`
7
+ * first, then anything under `app/Policies/` that follows the `ModelPolicy`
8
+ * convention.
5
9
  */
6
10
  export declare function discoverPolicies(): Promise<void>;
7
11
  /**
@@ -9,7 +13,20 @@ export declare function discoverPolicies(): Promise<void>;
9
13
  */
10
14
  export declare function registerGates(): Promise<void>;
11
15
  /**
12
- * Initialize authorization system
16
+ * Initialize the authorization system: register the gates and before/after
17
+ * callbacks from `app/Gates.ts`, then the policies.
18
+ *
19
+ * Called from `injectGlobalAutoImports()`, which is the one place every entry
20
+ * point comes through - HTTP, `buddy seed`, a scheduled job, a console command.
21
+ * Nothing called it before. It was exported, documented, and dead, so every
22
+ * gate an application defined was never registered and every `Gate.allows(...)`
23
+ * fell through to the default deny. That is the failure mode authorization is
24
+ * least able to report, because a gate that denies everything and a gate that
25
+ * was never registered are the same answer.
26
+ *
27
+ * Never throws: an application that cannot boot over a typo in a gate is worse
28
+ * than one that logs the typo. A `Gates.ts` that fails to load is reported at
29
+ * error level, not swallowed at debug.
13
30
  */
14
31
  export declare function initializeAuthorization(): Promise<void>;
15
32
  // Use the row/instance shape from orm so policies operate on the
package/dist/policy.js CHANGED
@@ -1 +1 @@
1
- import{AuthorizationResponse}from"./gate";export class BasePolicy{allow(message){return AuthorizationResponse.allow(message)}deny(message,code){return AuthorizationResponse.deny(message,code)}denyIf(condition,message){if(condition)return this.deny(message);return!0}denyUnless(condition,message){if(!condition)return this.deny(message);return!0}allowIf(condition,message){if(condition)return this.allow(message);return!1}}import{log}from"@stacksjs/logging";import*as p from"@stacksjs/path";import{policy as registerPolicy}from"./gate";export async function discoverPolicies(){const{fs}=await import("@stacksjs/storage"),policiesDir=p.appPath("Policies");if(!fs.existsSync(policiesDir)){log.debug("No Policies directory found");return}try{const mappings=(await import(p.appPath("Gates.ts"))).policies||{};for(const[modelName,config]of Object.entries(mappings)){const policyFile=typeof config==="string"?config:config.policy,policyPath=`${policiesDir}/${policyFile}.ts`;if(fs.existsSync(policyPath)){const policyModule=await import(policyPath),PolicyClass=policyModule.default||policyModule[policyFile];if(PolicyClass){registerPolicy(modelName,PolicyClass);log.debug(`Registered policy: ${policyFile} for ${modelName}`)}}}}catch{log.debug("No Gates.ts found, using convention-based discovery")}const policyFiles=fs.readdirSync(policiesDir).filter((file)=>file.endsWith("Policy.ts"));for(const file of policyFiles){const policyName=file.replace(".ts",""),modelName=policyName.replace("Policy",""),policyPath=`${policiesDir}/${file}`;try{const policyModule=await import(policyPath),PolicyClass=policyModule.default||policyModule[policyName];if(PolicyClass){registerPolicy(modelName,PolicyClass);log.debug(`Auto-discovered policy: ${policyName} for ${modelName}`)}}catch(error){log.error(`Failed to load policy ${policyName}:`,error)}}}export async function registerGates(){const{define,before,after}=await import("./gate");try{const gatesModule=await import(p.appPath("Gates.ts")),gates=gatesModule.gates||gatesModule.default?.gates||{};for(const[ability,callback]of Object.entries(gates))if(typeof callback==="function"){define(ability,callback);log.debug(`Registered gate: ${ability}`)}const beforeCallbacks=gatesModule.before||gatesModule.default?.before||[];for(const callback of beforeCallbacks)if(typeof callback==="function")before(callback);const afterCallbacks=gatesModule.after||gatesModule.default?.after||[];for(const callback of afterCallbacks)if(typeof callback==="function")after(callback);log.debug("Gates registered successfully")}catch{log.debug("No Gates.ts found or failed to load")}}export async function initializeAuthorization(){await registerGates();await discoverPolicies()}export{AuthorizationResponse};
1
+ import{AuthorizationResponse}from"./gate";export class BasePolicy{allow(message){return AuthorizationResponse.allow(message)}deny(message,code){return AuthorizationResponse.deny(message,code)}denyIf(condition,message){if(condition)return this.deny(message);return!0}denyUnless(condition,message){if(!condition)return this.deny(message);return!0}allowIf(condition,message){if(condition)return this.allow(message);return!1}}import{log}from"@stacksjs/logging";import*as p from"@stacksjs/path";import{policy as registerPolicy}from"./gate";let policyRegistry=null;async function loadPolicyRegistry(){if(policyRegistry)return policyRegistry;policyRegistry=(async()=>{try{const dir=p.storagePath("framework/auto-imports"),module=await import(`${dir}/policies.ts`);if(!module.policies)return null;const{resolve}=await import("node:path");return Object.fromEntries(Object.entries(module.policies).map(([name,file])=>[name,resolve(dir,file)]))}catch{return null}})();return policyRegistry}function policyDirectories(){return[p.appPath("Policies"),p.storagePath("framework/defaults/app/Policies")]}async function findPolicyFile(name){const registry=await loadPolicyRegistry();if(registry)return registry[name]??null;const{fs}=await import("@stacksjs/storage");for(const dir of policyDirectories()){const candidate=`${dir}/${name}.ts`;if(fs.existsSync(candidate))return candidate}return null}export function resetPolicyRegistry(){policyRegistry=null}export async function discoverPolicies(){const{fs}=await import("@stacksjs/storage"),explicit=new Set,mappings=(await loadGatesModule())?.policies??{};for(const[modelName,config]of Object.entries(mappings)){const policyFile=typeof config==="string"?config:config.policy,policyPath=await findPolicyFile(policyFile);if(!policyPath){log.warn(`[auth] app/Gates.ts maps ${modelName} to ${policyFile}, and no such policy exists`);continue}try{const policyModule=await import(policyPath),PolicyClass=policyModule.default||policyModule[policyFile];if(!PolicyClass){log.warn(`[auth] ${policyPath} has no default export, so ${modelName} has no policy`);continue}registerPolicy(modelName,PolicyClass);explicit.add(modelName);log.debug(`Registered policy: ${policyFile} for ${modelName}`)}catch(error){log.error(`Failed to load policy ${policyFile}:`,error)}}const registry=await loadPolicyRegistry();let discovered;if(registry)discovered=Object.entries(registry).filter(([name])=>name.endsWith("Policy")).map(([policyName,policyPath])=>({policyName,policyPath}));else{const policiesDir=p.appPath("Policies");if(!fs.existsSync(policiesDir)){log.debug("No Policies directory found");return}discovered=fs.readdirSync(policiesDir).filter((file)=>file.endsWith("Policy.ts")).map((file)=>({policyName:file.replace(".ts",""),policyPath:`${policiesDir}/${file}`}))}for(const{policyName,policyPath}of discovered){const modelName=policyName.replace("Policy","");if(explicit.has(modelName))continue;try{const policyModule=await import(policyPath),PolicyClass=policyModule.default||policyModule[policyName];if(PolicyClass){registerPolicy(modelName,PolicyClass);log.debug(`Auto-discovered policy: ${policyName} for ${modelName}`)}}catch(error){log.error(`Failed to load policy ${policyName}:`,error)}}}async function loadGatesModule(){const{fs}=await import("@stacksjs/storage"),gatesPath=p.appPath("Gates.ts");if(!fs.existsSync(gatesPath))return null;const module=await import(gatesPath);return{gates:module.default?.gates??module.gates,policies:module.default?.policies??module.policies,before:module.default?.before??module.before,after:module.default?.after??module.after}}export async function registerGates(){const{define,before,after}=await import("./gate"),authorization=await loadGatesModule();if(!authorization){log.debug("No Gates.ts found");return}for(const[ability,callback]of Object.entries(authorization.gates??{}))if(typeof callback==="function"){define(ability,callback);log.debug(`Registered gate: ${ability}`)}for(const callback of authorization.before??[])if(typeof callback==="function")before(callback);for(const callback of authorization.after??[])if(typeof callback==="function")after(callback);log.debug("Gates registered successfully")}export async function initializeAuthorization(){try{await registerGates()}catch(error){log.error("[auth] app/Gates.ts failed to load - no gates are registered and every Gate check will deny:",error)}try{await discoverPolicies()}catch(error){log.error("[auth] policy discovery failed - model authorization will deny:",error)}}export{AuthorizationResponse};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.98",
5
+ "version": "0.72.100",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -53,6 +53,7 @@
53
53
  "scripts": {
54
54
  "build": "bun build.ts",
55
55
  "typecheck": "bun tsc --noEmit",
56
+ "typecheck:types": "bun tsc --noEmit -p tsconfig.type-tests.json --pretty false",
56
57
  "prepublishOnly": "bun run build"
57
58
  },
58
59
  "dependencies": {
@@ -61,7 +62,7 @@
61
62
  },
62
63
  "devDependencies": {
63
64
  "better-dx": "^0.2.24",
64
- "@stacksjs/error-handling": "0.72.98",
65
- "@stacksjs/router": "0.72.98"
65
+ "@stacksjs/error-handling": "0.72.100",
66
+ "@stacksjs/router": "0.72.100"
66
67
  }
67
68
  }