@stacksjs/auth 0.75.95 → 0.75.96

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
@@ -124,6 +124,42 @@ export declare function authorize<const A extends Ability>(ability: A, user: Use
124
124
  * Get detailed inspection result for an ability check
125
125
  */
126
126
  export declare function inspect<const A extends Ability>(ability: A, user: UserModel | null, ...args: AbilityArgs<A>): Promise<AuthorizationResponse>;
127
+ /**
128
+ * Check an ability against the policy registered for a model NAME, answering
129
+ * three states: no policy (null), allowed, denied.
130
+ *
131
+ * The vocabulary in `policy.ts` was unreachable from the code that wanted it.
132
+ * Resolution keys on `args[0].constructor.name`, so it needs a model INSTANCE,
133
+ * and the generated API and the dashboard actions both hold rows the query
134
+ * builder returned - plain objects, whose constructor is `Object`. There was no
135
+ * path from a model name to its policy, which is why 0 of the 750 default
136
+ * actions consult one (stacksjs/stacks#2883).
137
+ *
138
+ * The three-way answer is the point. `allows()` DENIES when nothing is
139
+ * registered, which is right for a named gate and catastrophic for a generated
140
+ * route: it would refuse every one of the 107 models that has no policy. A
141
+ * caller must be able to tell "nobody has an opinion" from "the answer is no",
142
+ * and null makes that impossible to read as fail-open by accident.
143
+ *
144
+ * The global before/after callbacks run when a policy IS registered, so a
145
+ * `Gate.after()` lockdown still applies. They do not run otherwise: with no
146
+ * policy there is no decision to override, and inventing one would turn this
147
+ * back into a deny for every unpoliced model.
148
+ *
149
+ * @example
150
+ * const verdict = await Gate.inspectFor('Order', 'delete', user, row)
151
+ * if (verdict && !verdict.isAllowed)
152
+ * return response.json({ message: verdict.message }, 403)
153
+ */
154
+ export declare function inspectFor(model: string | { name: string }, ability: Ability, user: UserModel | null, ...args: any[]): Promise<AuthorizationResponse | null>;
155
+ /**
156
+ * {@link inspectFor} as a boolean, reading "no policy registered" as allowed.
157
+ *
158
+ * The convenience form for a caller with nothing of its own to add: no policy
159
+ * means this layer is not the one saying no. Anything that needs to tell that
160
+ * apart from a permission - to log it, or to skip work - uses `inspectFor`.
161
+ */
162
+ export declare function allowsFor(model: string | { name: string }, ability: Ability, user: UserModel | null, ...args: any[]): Promise<boolean>;
127
163
  /**
128
164
  * Get a policy instance for a model
129
165
  */
@@ -153,6 +189,7 @@ export declare const Gate: {
153
189
  before: typeof before;
154
190
  after: typeof after;
155
191
  allows: typeof allows;
192
+ allowsFor: typeof allowsFor;
156
193
  denies: typeof denies;
157
194
  can: typeof can;
158
195
  cannot: typeof cannot;
@@ -161,6 +198,7 @@ export declare const Gate: {
161
198
  none: typeof none;
162
199
  authorize: typeof authorize;
163
200
  inspect: typeof inspect;
201
+ inspectFor: typeof inspectFor;
164
202
  has: typeof has;
165
203
  hasPolicy: typeof hasPolicy;
166
204
  abilities: typeof abilities;
package/dist/gate.js CHANGED
@@ -1 +1 @@
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;
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 policyClass=state.policies.get(model.constructor?.name);if(policyClass){const response=await runPolicy(policyClass,ability,user,args);if(response)return response}}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 runPolicy(policyClass,ability,user,args){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")return null;const result=await method.call(policyInstance,user,...args);return normalizeResponse(result??!1)}export async function inspectFor(model,ability,user,...args){const modelName=typeof model==="string"?model:model.name,policyClass=state.policies.get(modelName);if(!policyClass)return null;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}}response??=await runPolicy(policyClass,ability,user,args)??AuthorizationResponse.deny(`${modelName}'s policy defines no ${String(ability)} ability`);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}export async function allowsFor(model,ability,user,...args){const response=await inspectFor(model,ability,user,...args);return response?response.isAllowed:!0}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,allowsFor,denies,can,cannot,any,all,none,authorize,inspect,inspectFor,has,hasPolicy,abilities,getPolicyFor,flush,AuthorizationResponse,AuthorizationException};export default Gate;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.95",
5
+ "version": "0.75.96",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,18 +58,18 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@stacksjs/bun-router": "^0.1.24",
61
- "@stacksjs/cache": "0.75.95",
62
- "@stacksjs/config": "0.75.95",
63
- "@stacksjs/database": "0.75.95",
64
- "@stacksjs/email": "0.75.95",
65
- "@stacksjs/env": "0.75.95",
66
- "@stacksjs/error-handling": "0.75.95",
67
- "@stacksjs/logging": "0.75.95",
68
- "@stacksjs/orm": "0.75.95",
69
- "@stacksjs/path": "0.75.95",
70
- "@stacksjs/router": "0.75.95",
71
- "@stacksjs/security": "0.75.95",
72
- "@stacksjs/storage": "0.75.95",
61
+ "@stacksjs/cache": "0.75.96",
62
+ "@stacksjs/config": "0.75.96",
63
+ "@stacksjs/database": "0.75.96",
64
+ "@stacksjs/email": "0.75.96",
65
+ "@stacksjs/env": "0.75.96",
66
+ "@stacksjs/error-handling": "0.75.96",
67
+ "@stacksjs/logging": "0.75.96",
68
+ "@stacksjs/orm": "0.75.96",
69
+ "@stacksjs/path": "0.75.96",
70
+ "@stacksjs/router": "0.75.96",
71
+ "@stacksjs/security": "0.75.96",
72
+ "@stacksjs/storage": "0.75.96",
73
73
  "@stacksjs/ts-auth": "^0.4.5",
74
74
  "ts-qr-codes": "^0.1.8"
75
75
  },