@velajs/authz 1.0.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 ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 307ff5e: Modernize the package build, validation, and release toolchain.
8
+
9
+ All notable changes to `@velajs/authz` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
+
11
+ ## 1.0.0
12
+
13
+ Initial release — the framework-agnostic permission/role engine for Vela.
14
+
15
+ ### Added
16
+
17
+ - **`Identity` / `PermissionResolver`** — the model at the core. `Identity` is who is asking (all fields optional, so `{}` and `anonymous` are valid zero-privilege identities); `PermissionResolver` is the single seam that resolves an identity to its granted-permission `Set` (sync or async). `anonymous` (`{ roles: [] }`, frozen) is the fail-closed default when no session is present.
18
+ - **`defineRole` / `definePermission`** — declare the role→permission table. `defineRole(name, permissions)` copies the permissions array, so the returned `RoleDef` never aliases the caller's input; `definePermission(name)` names a permission for the optional allow-list.
19
+ - **`createAuthz`** — builds an `Authz` (`{ can, resolver }`) over a role table or a custom `resolver`. Passing a `permissions` allow-list turns typos into a build-time error: `createAuthz` throws when a role grants an **undeclared** (non-wildcard) permission.
20
+ - **Fail-closed `can()` + wildcards** — `can(identity, permission, resolver)` (and the bound `authz.can`) default to **deny**. No session, unknown role, missing permission, or a resolver that **throws** all deny — there is no allow-on-error path. On the **granted** side, `*` grants everything and `resource:*` grants any action under that resource (e.g. `posts:*` grants `posts:delete`).
21
+ - **`anyOf` / `allOf` / `hasPerm` / `mask`** — composition + masking over a `Policy` (a plain predicate over a context and a resource). `anyOf` is OR/read semantics (empty → **denied**); `allOf` is AND/write semantics (empty → vacuously true); `hasPerm(authz, permission)` bridges a capability check into a `Policy` reading only `ctx.identity`; `mask(fn)` wraps a field transform so a throw redacts to `null` instead of leaking the raw value. A policy that throws inside `anyOf`/`allOf` denies that branch — it can never allow.
22
+ - **`@velajs/authz/vela` — `AuthzModule`** — optional Vela integration behind a subpath. Built on `@velajs/vela`'s `defineModule`, `AuthzModule.forRoot(options)` provides an `Authz` instance (`createAuthz(options)`) under the `AUTHZ` token and exports it for other modules to inject. `@velajs/vela` is an **optional** peer dependency — the core engine has no framework coupling.
23
+
24
+ ### Notes
25
+
26
+ - Zero runtime dependencies; ESM; `sideEffects: false`; edge-runtime safe (no `node:*`, `Buffer`, or `process`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kauan Guesser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @velajs/authz
2
+
3
+ Framework-agnostic permission/role engine for Vela: `defineRole`, fail-closed `can()`, composition + masking helpers. **Zero runtime dependencies, edge-runtime safe** (no `node:*`, no `Buffer`, no `process`).
4
+
5
+ ## Why
6
+
7
+ Authorization is one decision — *"is this identity allowed to do this?"* — that has to be answered identically across HTTP, WebSocket, live queries, and background jobs. This package is that single answer. It resolves an `Identity` to a set of granted permissions and decides `can()` **fail-closed**: absent a session, the [`anonymous`](#identity) zero-privilege identity is used and nothing is granted.
8
+
9
+ The core is a plain function library — no decorators, no container, no framework coupling. Optional [Vela integration](#vela-integration) lives behind a subpath.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @velajs/authz
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createAuthz, defineRole } from '@velajs/authz';
21
+
22
+ const authz = createAuthz({
23
+ roles: [
24
+ defineRole('editor', ['posts:read', 'posts:write']),
25
+ defineRole('admin', ['*']),
26
+ ],
27
+ });
28
+
29
+ await authz.can({ roles: ['editor'] }, 'posts:write'); // true
30
+ await authz.can({ roles: ['editor'] }, 'posts:delete'); // false
31
+ await authz.can({ roles: ['admin'] }, 'anything:at:all'); // true (wildcard)
32
+ ```
33
+
34
+ ## The model
35
+
36
+ - **`Identity`** — who is asking. All fields optional, so `{}` and `anonymous` are valid zero-privilege identities.
37
+ - **`defineRole(name, permissions)` / `definePermission(name)`** — declare the role→permission table. `defineRole` copies the permissions array, so the returned `RoleDef` never aliases the caller's input.
38
+ - **`createAuthz(options)`** — builds an `Authz` over a role table (or a custom `resolver`). Returns `{ can, resolver }`. If you pass a `permissions` allow-list, `createAuthz` throws when a role grants an **undeclared** (non-wildcard) permission — a build-time guard against typos.
39
+ - **`can(identity, permission, resolver)`** — the standalone fail-closed check; `authz.can(identity, permission)` is the same check bound to the built resolver.
40
+
41
+ ```ts
42
+ interface Identity {
43
+ userId?: string;
44
+ roles?: string[];
45
+ claims?: Record<string, unknown>;
46
+ }
47
+
48
+ interface PermissionResolver {
49
+ grants(identity: Identity): Set<string> | Promise<Set<string>>;
50
+ }
51
+ ```
52
+
53
+ `anonymous` is the zero-privilege identity (`{ roles: [] }`, frozen) — the fail-closed default when no session is present.
54
+
55
+ ## Wildcards
56
+
57
+ A granted permission string matches the requested permission when:
58
+
59
+ - it is `*` — grants **everything**;
60
+ - it equals the requested permission exactly (e.g. `posts:write`);
61
+ - it is `resource:*` — grants **any action** under that resource (e.g. `posts:*` grants `posts:delete`).
62
+
63
+ Wildcards live on the **granted** side (what a role holds), not the requested side.
64
+
65
+ ## Fail-closed rules
66
+
67
+ Authorization defaults to **deny**. Every ambiguous or broken path denies rather than leaks:
68
+
69
+ - No session → use `anonymous` → grants nothing.
70
+ - Unknown role / missing permission → denied.
71
+ - A resolver that **throws** → denied (there is no allow-on-error path).
72
+ - **`anyOf()` with no policies → denied** (nothing grants access).
73
+ - A policy that **throws** inside `anyOf`/`allOf` → that branch is denied; it can never allow.
74
+ - **`mask`** whose transform throws → redacts to `null`, never leaks the raw value.
75
+
76
+ (`allOf()` with no policies is vacuously `true` — an empty AND — but an empty OR denies.)
77
+
78
+ ## Composition + masking
79
+
80
+ `Policy` is a plain predicate over a context and a resource:
81
+
82
+ ```ts
83
+ type Policy<C = { identity: Identity }, R = unknown> =
84
+ (ctx: C, resource: R) => boolean | Promise<boolean>;
85
+ ```
86
+
87
+ Combine capability checks with resource-level rules (ownership, tenancy, state):
88
+
89
+ - **`anyOf(...policies)`** — OR, read semantics. Any policy granting → allowed. Empty → denied.
90
+ - **`allOf(...policies)`** — AND, write semantics. All must allow.
91
+ - **`hasPerm(authz, permission)`** — bridge a capability check into a `Policy` that reads only `ctx.identity`.
92
+ - **`mask(fn)`** — wrap a field transform so a throw redacts to `null` instead of leaking.
93
+
94
+ ```ts
95
+ import { anyOf, allOf, hasPerm, mask } from '@velajs/authz';
96
+
97
+ const isOwner = (ctx: { identity: { userId?: string } }, post: { authorId: string }) =>
98
+ ctx.identity.userId === post.authorId;
99
+
100
+ // A reader may see a post if they own it OR hold posts:read.
101
+ const canRead = anyOf(isOwner, hasPerm(authz, 'posts:read'));
102
+
103
+ // A writer must own it AND hold posts:write.
104
+ const canWrite = allOf(isOwner, hasPerm(authz, 'posts:write'));
105
+
106
+ await canRead({ identity: { userId: 'u1', roles: [] } }, { authorId: 'u1' }); // true
107
+
108
+ // Redact a sensitive field, fail-closed to null on any error.
109
+ const lastFour = mask((_ctx: unknown, r: { ssn: string }) => r.ssn.slice(-4));
110
+ lastFour({}, { ssn: '123456789' }); // '6789'
111
+ ```
112
+
113
+ ## Vela integration
114
+
115
+ Optional. `@velajs/authz/vela` wires the engine into a Vela app. `@velajs/vela` is an **optional** peer dependency — the core engine has no framework coupling and runs anywhere (edge, Node, Workers, Deno, Bun).
116
+
117
+ ## License
118
+
119
+ MIT © Kauan Guesser
@@ -0,0 +1,37 @@
1
+ //#region src/identity.d.ts
2
+ interface Identity {
3
+ userId?: string;
4
+ roles?: string[];
5
+ claims?: Record<string, unknown>;
6
+ }
7
+ /** The zero-privilege identity. Fail-closed default when no session is present. */
8
+ declare const anonymous: Identity;
9
+ interface PermissionResolver {
10
+ grants(identity: Identity): Set<string> | Promise<Set<string>>;
11
+ }
12
+ //#endregion
13
+ //#region src/roles.d.ts
14
+ interface RoleDef {
15
+ readonly name: string;
16
+ readonly permissions: readonly string[];
17
+ }
18
+ interface PermissionDef {
19
+ readonly name: string;
20
+ }
21
+ declare const defineRole: (name: string, permissions: readonly string[]) => RoleDef;
22
+ declare const definePermission: (name: string) => PermissionDef;
23
+ //#endregion
24
+ //#region src/authz.d.ts
25
+ interface Authz {
26
+ can(identity: Identity, permission: string): Promise<boolean>;
27
+ readonly resolver: PermissionResolver;
28
+ }
29
+ interface CreateAuthzOptions {
30
+ roles?: RoleDef[];
31
+ permissions?: PermissionDef[];
32
+ resolver?: PermissionResolver;
33
+ }
34
+ declare const createAuthz: (options?: CreateAuthzOptions) => Authz;
35
+ //#endregion
36
+ export { RoleDef as a, Identity as c, PermissionDef as i, PermissionResolver as l, CreateAuthzOptions as n, definePermission as o, createAuthz as r, defineRole as s, Authz as t, anonymous as u };
37
+ //# sourceMappingURL=authz-BX3wL_WR.d.ts.map
@@ -0,0 +1,50 @@
1
+ //#region src/can.ts
2
+ /** Does the granted set satisfy `permission`, honoring wildcards? */
3
+ const granted = (grants, permission) => {
4
+ if (grants.has("*") || grants.has(permission)) return true;
5
+ const colon = permission.indexOf(":");
6
+ if (colon > 0 && grants.has(`${permission.slice(0, colon)}:*`)) return true;
7
+ return false;
8
+ };
9
+ /**
10
+ * The fail-closed capability check. Resolves the identity's granted permissions
11
+ * via the resolver and matches `permission` (exact or wildcard). A resolver
12
+ * that throws denies — there is no allow-on-error path.
13
+ */
14
+ const can = async (identity, permission, resolver) => {
15
+ try {
16
+ const grants = await resolver.grants(identity);
17
+ return granted(grants, permission);
18
+ } catch {
19
+ return false;
20
+ }
21
+ };
22
+ //#endregion
23
+ //#region src/authz.ts
24
+ const isWildcard = (p) => p === "*" || p.endsWith(":*");
25
+ /** Build a role→permission resolver over a role table (no module globals). */
26
+ const roleResolver = (roles) => {
27
+ const byRole = /* @__PURE__ */ new Map();
28
+ for (const r of roles) byRole.set(r.name, r.permissions);
29
+ return { grants(identity) {
30
+ const out = /* @__PURE__ */ new Set();
31
+ for (const name of identity.roles ?? []) for (const perm of byRole.get(name) ?? []) out.add(perm);
32
+ return out;
33
+ } };
34
+ };
35
+ const createAuthz = (options = {}) => {
36
+ const roles = options.roles ?? [];
37
+ if (options.permissions) {
38
+ const declared = new Set(options.permissions.map((p) => p.name));
39
+ for (const role of roles) for (const perm of role.permissions) if (!isWildcard(perm) && !declared.has(perm)) throw new Error(`role '${role.name}' grants undeclared permission '${perm}'`);
40
+ }
41
+ const resolver = options.resolver ?? roleResolver(roles);
42
+ return {
43
+ resolver,
44
+ can: (identity, permission) => can(identity, permission, resolver)
45
+ };
46
+ };
47
+ //#endregion
48
+ export { can as n, createAuthz as t };
49
+
50
+ //# sourceMappingURL=authz-DXUrOWVO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authz-DXUrOWVO.js","names":[],"sources":["../src/can.ts","../src/authz.ts"],"sourcesContent":["import type { Identity, PermissionResolver } from './identity';\n\n/** Does the granted set satisfy `permission`, honoring wildcards? */\nconst granted = (grants: Set<string>, permission: string): boolean => {\n if (grants.has('*') || grants.has(permission)) return true;\n const colon = permission.indexOf(':');\n if (colon > 0 && grants.has(`${permission.slice(0, colon)}:*`)) return true;\n return false;\n};\n\n/**\n * The fail-closed capability check. Resolves the identity's granted permissions\n * via the resolver and matches `permission` (exact or wildcard). A resolver\n * that throws denies — there is no allow-on-error path.\n */\nexport const can = async (\n identity: Identity,\n permission: string,\n resolver: PermissionResolver,\n): Promise<boolean> => {\n try {\n const grants = await resolver.grants(identity);\n return granted(grants, permission);\n } catch {\n return false;\n }\n};\n","import { can } from './can';\nimport type { Identity, PermissionResolver } from './identity';\nimport type { PermissionDef, RoleDef } from './roles';\n\nexport interface Authz {\n can(identity: Identity, permission: string): Promise<boolean>;\n readonly resolver: PermissionResolver;\n}\n\nexport interface CreateAuthzOptions {\n roles?: RoleDef[];\n permissions?: PermissionDef[];\n resolver?: PermissionResolver;\n}\n\nconst isWildcard = (p: string): boolean => p === '*' || p.endsWith(':*');\n\n/** Build a role→permission resolver over a role table (no module globals). */\nconst roleResolver = (roles: RoleDef[]): PermissionResolver => {\n const byRole = new Map<string, readonly string[]>();\n for (const r of roles) byRole.set(r.name, r.permissions);\n return {\n grants(identity: Identity): Set<string> {\n const out = new Set<string>();\n for (const name of identity.roles ?? []) {\n for (const perm of byRole.get(name) ?? []) out.add(perm);\n }\n return out;\n },\n };\n};\n\nexport const createAuthz = (options: CreateAuthzOptions = {}): Authz => {\n const roles = options.roles ?? [];\n if (options.permissions) {\n const declared = new Set(options.permissions.map((p) => p.name));\n for (const role of roles) {\n for (const perm of role.permissions) {\n if (!isWildcard(perm) && !declared.has(perm)) {\n throw new Error(`role '${role.name}' grants undeclared permission '${perm}'`);\n }\n }\n }\n }\n const resolver = options.resolver ?? roleResolver(roles);\n return {\n resolver,\n can: (identity, permission) => can(identity, permission, resolver),\n };\n};\n"],"mappings":";;AAGA,MAAM,WAAW,QAAqB,eAAgC;CACpE,IAAI,OAAO,IAAI,GAAG,KAAK,OAAO,IAAI,UAAU,GAAG,OAAO;CACtD,MAAM,QAAQ,WAAW,QAAQ,GAAG;CACpC,IAAI,QAAQ,KAAK,OAAO,IAAI,GAAG,WAAW,MAAM,GAAG,KAAK,EAAE,GAAG,GAAG,OAAO;CACvE,OAAO;AACT;;;;;;AAOA,MAAa,MAAM,OACjB,UACA,YACA,aACqB;CACrB,IAAI;EACF,MAAM,SAAS,MAAM,SAAS,OAAO,QAAQ;EAC7C,OAAO,QAAQ,QAAQ,UAAU;CACnC,QAAQ;EACN,OAAO;CACT;AACF;;;ACXA,MAAM,cAAc,MAAuB,MAAM,OAAO,EAAE,SAAS,IAAI;;AAGvE,MAAM,gBAAgB,UAAyC;CAC7D,MAAM,yBAAS,IAAI,IAA+B;CAClD,KAAK,MAAM,KAAK,OAAO,OAAO,IAAI,EAAE,MAAM,EAAE,WAAW;CACvD,OAAO,EACL,OAAO,UAAiC;EACtC,MAAM,sBAAM,IAAI,IAAY;EAC5B,KAAK,MAAM,QAAQ,SAAS,SAAS,CAAC,GACpC,KAAK,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,CAAC,GAAG,IAAI,IAAI,IAAI;EAEzD,OAAO;CACT,EACF;AACF;AAEA,MAAa,eAAe,UAA8B,CAAC,MAAa;CACtE,MAAM,QAAQ,QAAQ,SAAS,CAAC;CAChC,IAAI,QAAQ,aAAa;EACvB,MAAM,WAAW,IAAI,IAAI,QAAQ,YAAY,KAAK,MAAM,EAAE,IAAI,CAAC;EAC/D,KAAK,MAAM,QAAQ,OACjB,KAAK,MAAM,QAAQ,KAAK,aACtB,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,GACzC,MAAM,IAAI,MAAM,SAAS,KAAK,KAAK,kCAAkC,KAAK,EAAE;CAIpF;CACA,MAAM,WAAW,QAAQ,YAAY,aAAa,KAAK;CACvD,OAAO;EACL;EACA,MAAM,UAAU,eAAe,IAAI,UAAU,YAAY,QAAQ;CACnE;AACF"}
@@ -0,0 +1,30 @@
1
+ import { a as RoleDef, c as Identity, i as PermissionDef, l as PermissionResolver, n as CreateAuthzOptions, o as definePermission, r as createAuthz, s as defineRole, t as Authz, u as anonymous } from "./authz-BX3wL_WR.js";
2
+ //#region src/can.d.ts
3
+ /**
4
+ * The fail-closed capability check. Resolves the identity's granted permissions
5
+ * via the resolver and matches `permission` (exact or wildcard). A resolver
6
+ * that throws denies — there is no allow-on-error path.
7
+ */
8
+ declare const can: (identity: Identity, permission: string, resolver: PermissionResolver) => Promise<boolean>;
9
+ //#endregion
10
+ //#region src/policy.d.ts
11
+ type Policy<C = {
12
+ identity: Identity;
13
+ }, R = unknown> = (ctx: C, resource: R) => boolean | Promise<boolean>;
14
+ /** OR — read semantics. Any policy granting → allowed. Empty → denied. */
15
+ declare const anyOf: (...policies: Policy<{
16
+ identity: Identity;
17
+ }, never>[]) => Policy;
18
+ /** AND — write semantics. All must allow. Empty → allowed (vacuous truth). */
19
+ declare const allOf: (...policies: Policy<{
20
+ identity: Identity;
21
+ }, never>[]) => Policy;
22
+ /** Bridge a capability check into a Policy reading only ctx.identity. */
23
+ declare const hasPerm: (authz: Authz, permission: string) => Policy<{
24
+ identity: Identity;
25
+ }>;
26
+ /** Fail-closed field transform: a throwing mask redacts to null, never leaks. */
27
+ declare const mask: <C, R, T>(fn: (ctx: C, record: R) => T) => ((ctx: C, record: R) => T | null);
28
+ //#endregion
29
+ export { type Authz, type CreateAuthzOptions, type Identity, type PermissionDef, type PermissionResolver, type Policy, type RoleDef, allOf, anonymous, anyOf, can, createAuthz, definePermission, defineRole, hasPerm, mask };
30
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ import { n as can, t as createAuthz } from "./authz-DXUrOWVO.js";
2
+ //#region src/identity.ts
3
+ /** The zero-privilege identity. Fail-closed default when no session is present. */
4
+ const anonymous = Object.freeze({ roles: Object.freeze([]) });
5
+ //#endregion
6
+ //#region src/roles.ts
7
+ const defineRole = (name, permissions) => ({
8
+ name,
9
+ permissions: [...permissions]
10
+ });
11
+ const definePermission = (name) => ({ name });
12
+ //#endregion
13
+ //#region src/policy.ts
14
+ const runSafe = async (p, ctx, resource) => {
15
+ try {
16
+ return await p(ctx, resource) === true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ };
21
+ /** OR — read semantics. Any policy granting → allowed. Empty → denied. */
22
+ const anyOf = (...policies) => async (ctx, resource) => {
23
+ for (const p of policies) if (await runSafe(p, ctx, resource)) return true;
24
+ return false;
25
+ };
26
+ /** AND — write semantics. All must allow. Empty → allowed (vacuous truth). */
27
+ const allOf = (...policies) => async (ctx, resource) => {
28
+ for (const p of policies) if (!await runSafe(p, ctx, resource)) return false;
29
+ return true;
30
+ };
31
+ /** Bridge a capability check into a Policy reading only ctx.identity. */
32
+ const hasPerm = (authz, permission) => (ctx) => authz.can(ctx.identity, permission);
33
+ /** Fail-closed field transform: a throwing mask redacts to null, never leaks. */
34
+ const mask = (fn) => (ctx, record) => {
35
+ try {
36
+ return fn(ctx, record);
37
+ } catch {
38
+ return null;
39
+ }
40
+ };
41
+ //#endregion
42
+ export { allOf, anonymous, anyOf, can, createAuthz, definePermission, defineRole, hasPerm, mask };
43
+
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/identity.ts","../src/roles.ts","../src/policy.ts"],"sourcesContent":["export interface Identity {\n userId?: string;\n roles?: string[];\n claims?: Record<string, unknown>;\n}\n\n/** The zero-privilege identity. Fail-closed default when no session is present. */\nexport const anonymous: Identity = Object.freeze({\n roles: Object.freeze([] as string[]) as string[],\n});\n\nexport interface PermissionResolver {\n grants(identity: Identity): Set<string> | Promise<Set<string>>;\n}\n","export interface RoleDef {\n readonly name: string;\n readonly permissions: readonly string[];\n}\nexport interface PermissionDef {\n readonly name: string;\n}\n\nexport const defineRole = (name: string, permissions: readonly string[]): RoleDef => ({\n name,\n permissions: [...permissions],\n});\n\nexport const definePermission = (name: string): PermissionDef => ({ name });\n","import type { Authz } from './authz';\nimport type { Identity } from './identity';\n\nexport type Policy<C = { identity: Identity }, R = unknown> = (\n ctx: C,\n resource: R,\n) => boolean | Promise<boolean>;\n\nconst runSafe = async (\n p: Policy<{ identity: Identity }, never>,\n ctx: unknown,\n resource: unknown,\n): Promise<boolean> => {\n try {\n return (await (p as Policy<unknown, unknown>)(ctx, resource)) === true;\n } catch {\n return false; // a throwing policy never allows\n }\n};\n\n/** OR — read semantics. Any policy granting → allowed. Empty → denied. */\nexport const anyOf =\n (...policies: Policy<{ identity: Identity }, never>[]): Policy =>\n async (ctx, resource) => {\n for (const p of policies) if (await runSafe(p, ctx, resource)) return true;\n return false;\n };\n\n/** AND — write semantics. All must allow. Empty → allowed (vacuous truth). */\nexport const allOf =\n (...policies: Policy<{ identity: Identity }, never>[]): Policy =>\n async (ctx, resource) => {\n for (const p of policies) if (!(await runSafe(p, ctx, resource))) return false;\n return true;\n };\n\n/** Bridge a capability check into a Policy reading only ctx.identity. */\nexport const hasPerm =\n (authz: Authz, permission: string): Policy<{ identity: Identity }> =>\n (ctx) =>\n authz.can(ctx.identity, permission);\n\n/** Fail-closed field transform: a throwing mask redacts to null, never leaks. */\nexport const mask =\n <C, R, T>(fn: (ctx: C, record: R) => T): ((ctx: C, record: R) => T | null) =>\n (ctx, record) => {\n try {\n return fn(ctx, record);\n } catch {\n return null;\n }\n };\n"],"mappings":";;;AAOA,MAAa,YAAsB,OAAO,OAAO,EAC/C,OAAO,OAAO,OAAO,CAAC,CAAa,EACrC,CAAC;;;ACDD,MAAa,cAAc,MAAc,iBAA6C;CACpF;CACA,aAAa,CAAC,GAAG,WAAW;AAC9B;AAEA,MAAa,oBAAoB,UAAiC,EAAE,KAAK;;;ACLzE,MAAM,UAAU,OACd,GACA,KACA,aACqB;CACrB,IAAI;EACF,OAAQ,MAAO,EAA+B,KAAK,QAAQ,MAAO;CACpE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,MAAa,SACV,GAAG,aACJ,OAAO,KAAK,aAAa;CACvB,KAAK,MAAM,KAAK,UAAU,IAAI,MAAM,QAAQ,GAAG,KAAK,QAAQ,GAAG,OAAO;CACtE,OAAO;AACT;;AAGF,MAAa,SACV,GAAG,aACJ,OAAO,KAAK,aAAa;CACvB,KAAK,MAAM,KAAK,UAAU,IAAI,CAAE,MAAM,QAAQ,GAAG,KAAK,QAAQ,GAAI,OAAO;CACzE,OAAO;AACT;;AAGF,MAAa,WACV,OAAc,gBACd,QACC,MAAM,IAAI,IAAI,UAAU,UAAU;;AAGtC,MAAa,QACD,QACT,KAAK,WAAW;CACf,IAAI;EACF,OAAO,GAAG,KAAK,MAAM;CACvB,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -0,0 +1,38 @@
1
+ import { n as CreateAuthzOptions, t as Authz } from "../authz-BX3wL_WR.js";
2
+ import { InjectionToken } from "@velajs/vela";
3
+ //#region src/vela/tokens.d.ts
4
+ /**
5
+ * The auto-provided options bag passed to {@link AuthzModule}.forRoot. Distinct
6
+ * from {@link AUTHZ} — this token carries the raw {@link CreateAuthzOptions},
7
+ * the built {@link Authz} instance lives under {@link AUTHZ}.
8
+ */
9
+ declare const AUTHZ_OPTIONS: InjectionToken<CreateAuthzOptions>;
10
+ /** The built {@link Authz} instance provided and exported by {@link AuthzModule}. */
11
+ declare const AUTHZ: InjectionToken<Authz>;
12
+ //#endregion
13
+ //#region src/vela/authz.module.d.ts
14
+ /**
15
+ * Built on `@velajs/vela`'s `defineModule` engine (the same pattern as vela's
16
+ * own `ErrorsModule`). `defineModule` mints the `forRoot`/`forRootAsync`
17
+ * statics and auto-provides the options bag under {@link AUTHZ_OPTIONS}; `setup`
18
+ * turns those options into the built {@link AUTHZ} instance and exports it.
19
+ */
20
+ declare const ConfigurableModuleClass: import("@velajs/vela").ConfigurableModuleClassType<CreateAuthzOptions, "forRoot", "create", {
21
+ isGlobal?: boolean;
22
+ }>;
23
+ /**
24
+ * Vela module for `@velajs/authz`. `AuthzModule.forRoot(options)` provides an
25
+ * {@link Authz} instance (`createAuthz(options)`) under the {@link AUTHZ} token
26
+ * and exports it for other modules to inject.
27
+ *
28
+ * ```ts
29
+ * @Module({
30
+ * imports: [AuthzModule.forRoot({ roles: [defineRole('editor', ['posts:write'])] })],
31
+ * })
32
+ * class AppModule {}
33
+ * ```
34
+ */
35
+ declare class AuthzModule extends ConfigurableModuleClass {}
36
+ //#endregion
37
+ export { AUTHZ, AUTHZ_OPTIONS, AuthzModule };
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { t as createAuthz } from "../authz-DXUrOWVO.js";
2
+ import { InjectionToken, defineModule } from "@velajs/vela";
3
+ //#region src/vela/tokens.ts
4
+ /**
5
+ * The auto-provided options bag passed to {@link AuthzModule}.forRoot. Distinct
6
+ * from {@link AUTHZ} — this token carries the raw {@link CreateAuthzOptions},
7
+ * the built {@link Authz} instance lives under {@link AUTHZ}.
8
+ */
9
+ const AUTHZ_OPTIONS = new InjectionToken("AUTHZ_OPTIONS");
10
+ /** The built {@link Authz} instance provided and exported by {@link AuthzModule}. */
11
+ const AUTHZ = new InjectionToken("AUTHZ");
12
+ //#endregion
13
+ //#region src/vela/authz.module.ts
14
+ /**
15
+ * Built on `@velajs/vela`'s `defineModule` engine (the same pattern as vela's
16
+ * own `ErrorsModule`). `defineModule` mints the `forRoot`/`forRootAsync`
17
+ * statics and auto-provides the options bag under {@link AUTHZ_OPTIONS}; `setup`
18
+ * turns those options into the built {@link AUTHZ} instance and exports it.
19
+ */
20
+ const { ConfigurableModuleClass } = defineModule({
21
+ name: "Authz",
22
+ optionsToken: AUTHZ_OPTIONS,
23
+ setup: ({ options }) => ({
24
+ providers: [{
25
+ provide: AUTHZ,
26
+ useValue: createAuthz(options)
27
+ }],
28
+ exports: [AUTHZ]
29
+ })
30
+ });
31
+ /**
32
+ * Vela module for `@velajs/authz`. `AuthzModule.forRoot(options)` provides an
33
+ * {@link Authz} instance (`createAuthz(options)`) under the {@link AUTHZ} token
34
+ * and exports it for other modules to inject.
35
+ *
36
+ * ```ts
37
+ * @Module({
38
+ * imports: [AuthzModule.forRoot({ roles: [defineRole('editor', ['posts:write'])] })],
39
+ * })
40
+ * class AppModule {}
41
+ * ```
42
+ */
43
+ var AuthzModule = class extends ConfigurableModuleClass {};
44
+ //#endregion
45
+ export { AUTHZ, AUTHZ_OPTIONS, AuthzModule };
46
+
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/vela/tokens.ts","../../src/vela/authz.module.ts"],"sourcesContent":["import { InjectionToken } from '@velajs/vela';\nimport type { Authz, CreateAuthzOptions } from '../authz';\n\n/**\n * The auto-provided options bag passed to {@link AuthzModule}.forRoot. Distinct\n * from {@link AUTHZ} — this token carries the raw {@link CreateAuthzOptions},\n * the built {@link Authz} instance lives under {@link AUTHZ}.\n */\nexport const AUTHZ_OPTIONS = new InjectionToken<CreateAuthzOptions>('AUTHZ_OPTIONS');\n\n/** The built {@link Authz} instance provided and exported by {@link AuthzModule}. */\nexport const AUTHZ = new InjectionToken<Authz>('AUTHZ');\n","import { defineModule } from '@velajs/vela';\nimport { createAuthz } from '../authz';\nimport type { CreateAuthzOptions } from '../authz';\nimport { AUTHZ, AUTHZ_OPTIONS } from './tokens';\n\n/**\n * Built on `@velajs/vela`'s `defineModule` engine (the same pattern as vela's\n * own `ErrorsModule`). `defineModule` mints the `forRoot`/`forRootAsync`\n * statics and auto-provides the options bag under {@link AUTHZ_OPTIONS}; `setup`\n * turns those options into the built {@link AUTHZ} instance and exports it.\n */\nconst { ConfigurableModuleClass } = defineModule<CreateAuthzOptions>({\n name: 'Authz',\n // Reuse the public AUTHZ_OPTIONS token for the auto-provided options bag,\n // kept DISTINCT from the AUTHZ instance token below.\n optionsToken: AUTHZ_OPTIONS,\n setup: ({ options }) => ({\n providers: [{ provide: AUTHZ, useValue: createAuthz(options) }],\n exports: [AUTHZ],\n }),\n});\n\n/**\n * Vela module for `@velajs/authz`. `AuthzModule.forRoot(options)` provides an\n * {@link Authz} instance (`createAuthz(options)`) under the {@link AUTHZ} token\n * and exports it for other modules to inject.\n *\n * ```ts\n * @Module({\n * imports: [AuthzModule.forRoot({ roles: [defineRole('editor', ['posts:write'])] })],\n * })\n * class AppModule {}\n * ```\n */\nexport class AuthzModule extends ConfigurableModuleClass {}\n"],"mappings":";;;;;;;;AAQA,MAAa,gBAAgB,IAAI,eAAmC,eAAe;;AAGnF,MAAa,QAAQ,IAAI,eAAsB,OAAO;;;;;;;;;ACAtD,MAAM,EAAE,4BAA4B,aAAiC;CACnE,MAAM;CAGN,cAAc;CACd,QAAQ,EAAE,eAAe;EACvB,WAAW,CAAC;GAAE,SAAS;GAAO,UAAU,YAAY,OAAO;EAAE,CAAC;EAC9D,SAAS,CAAC,KAAK;CACjB;AACF,CAAC;;;;;;;;;;;;;AAcD,IAAa,cAAb,cAAiC,wBAAwB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@velajs/authz",
3
+ "version": "1.0.1",
4
+ "description": "Framework-agnostic permission/role engine for Vela: defineRole, fail-closed can(), composition + masking helpers",
5
+ "keywords": [
6
+ "authorization",
7
+ "authz",
8
+ "framework",
9
+ "permissions",
10
+ "rbac",
11
+ "vela"
12
+ ],
13
+ "homepage": "https://github.com/velajs/authz#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/velajs/authz/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "ksh",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/velajs/authz.git"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./vela": {
39
+ "types": "./dist/vela/index.d.ts",
40
+ "import": "./dist/vela/index.js"
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@arethetypeswrong/cli": "^0.18.5",
45
+ "@changesets/cli": "^2.31.0",
46
+ "@velajs/vela": "^1.19.0",
47
+ "oxfmt": "^0.58.0",
48
+ "oxlint": "^1.73.0",
49
+ "publint": "^0.3.21",
50
+ "tsdown": "^0.22.4",
51
+ "typescript": "^7.0.2",
52
+ "vitest": "^4.1.10"
53
+ },
54
+ "peerDependencies": {
55
+ "@velajs/vela": ">=1.11.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@velajs/vela": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "engines": {
63
+ "node": ">=24"
64
+ },
65
+ "scripts": {
66
+ "build": "tsdown",
67
+ "test": "vitest run",
68
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
69
+ "lint": "oxlint .",
70
+ "format": "oxfmt .",
71
+ "format:check": "oxfmt --check .",
72
+ "publint": "publint",
73
+ "attw": "attw --pack . --profile esm-only",
74
+ "changeset": "changeset",
75
+ "version-packages": "changeset version",
76
+ "release": "pnpm build && changeset publish",
77
+ "verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm publint && pnpm attw"
78
+ }
79
+ }