@nwire/rbac 0.7.0

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Query filter helpers — the synthesis's "getFilterFor to prevent N+1".
3
+ *
4
+ * When listing resources ("show me all posts I can read"), checking
5
+ * permission per row is the classic N+1. CASL solves this with
6
+ * `rulesToQuery(ability, action, subjectType, ...)` from `@casl/ability/extra`,
7
+ * which compiles the ability's matching rules into a single disjunctive
8
+ * MongoDB-style query.
9
+ *
10
+ * import { conditionsFor } from "@nwire/rbac";
11
+ *
12
+ * const q = conditionsFor(ability, "read", "Post");
13
+ * // null → no rules → user can read NOTHING (return [])
14
+ * // { $or: [{}] } → an unrestricted rule exists → user can read ALL
15
+ * // { $or: [{ authorId: user.id }, { teamId: ... }] } → disjunctive
16
+ *
17
+ * // For Drizzle, translate via @casl/drizzle (community) or roll your own.
18
+ *
19
+ * The raw query lives in the canonical shape from CASL so the ecosystem
20
+ * bridges (`@casl/mongoose`, `@casl/prisma`, `@casl/drizzle`) drop right
21
+ * in. We expose it; users pick their bridge.
22
+ */
23
+ import type { Ability } from "./rbac";
24
+ /**
25
+ * The MongoDB-style disjunctive query the ability allows for the given
26
+ * (action, subjectType). Three return shapes:
27
+ * - `null` — no allowing rules; user can access nothing
28
+ * - `{ $or: [{}] }` — an unrestricted rule; user can access all
29
+ * - `{ $or: [{...}, ...]}` — conditional; OR over each allowed shape
30
+ */
31
+ export declare function conditionsFor(ability: Ability, action: string, subjectType: string): {
32
+ $or?: object[];
33
+ $and?: object[];
34
+ } | null;
35
+ /**
36
+ * Convenience: a Drizzle-friendly variant that returns a function
37
+ * producing a where-clause expression, given the ORM-specific column
38
+ * references. Keeps the bridge logic explicit and tested.
39
+ */
40
+ export interface DrizzleFilterBuilder {
41
+ /** Return `null` when no filter needed; falsy expr when denied; otherwise the where clause. */
42
+ build<TColumns>(columns: TColumns): unknown;
43
+ }
44
+ //# sourceMappingURL=filters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filters.d.ts","sourceRoot":"","sources":["../src/filters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB;IAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAE5C;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,+FAA+F;IAC/F,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC7C"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Query filter helpers — the synthesis's "getFilterFor to prevent N+1".
3
+ *
4
+ * When listing resources ("show me all posts I can read"), checking
5
+ * permission per row is the classic N+1. CASL solves this with
6
+ * `rulesToQuery(ability, action, subjectType, ...)` from `@casl/ability/extra`,
7
+ * which compiles the ability's matching rules into a single disjunctive
8
+ * MongoDB-style query.
9
+ *
10
+ * import { conditionsFor } from "@nwire/rbac";
11
+ *
12
+ * const q = conditionsFor(ability, "read", "Post");
13
+ * // null → no rules → user can read NOTHING (return [])
14
+ * // { $or: [{}] } → an unrestricted rule exists → user can read ALL
15
+ * // { $or: [{ authorId: user.id }, { teamId: ... }] } → disjunctive
16
+ *
17
+ * // For Drizzle, translate via @casl/drizzle (community) or roll your own.
18
+ *
19
+ * The raw query lives in the canonical shape from CASL so the ecosystem
20
+ * bridges (`@casl/mongoose`, `@casl/prisma`, `@casl/drizzle`) drop right
21
+ * in. We expose it; users pick their bridge.
22
+ */
23
+ import { rulesToQuery } from "@casl/ability/extra";
24
+ /**
25
+ * The MongoDB-style disjunctive query the ability allows for the given
26
+ * (action, subjectType). Three return shapes:
27
+ * - `null` — no allowing rules; user can access nothing
28
+ * - `{ $or: [{}] }` — an unrestricted rule; user can access all
29
+ * - `{ $or: [{...}, ...]}` — conditional; OR over each allowed shape
30
+ */
31
+ export function conditionsFor(ability, action, subjectType) {
32
+ return rulesToQuery(ability, action, subjectType, (rule) => rule.conditions ?? {});
33
+ }
34
+ //# sourceMappingURL=filters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filters.js","sourceRoot":"","sources":["../src/filters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAgB,EAChB,MAAc,EACd,WAAmB;IAEnB,OAAO,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;AACrF,CAAC"}
package/dist/rbac.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `@nwire/rbac` — declarative permissions powered by CASL.
3
+ *
4
+ * import { defineAbility, rbacPlugin, can } from "@nwire/rbac";
5
+ *
6
+ * // 1. Declare what each role can do
7
+ * export const buildAbility = defineAbility((user, { allow, deny }) => {
8
+ * if (!user) return;
9
+ * if (user.roles?.includes("admin")) { allow("manage", "all"); return; }
10
+ * allow("read", "Post");
11
+ * allow("create", "Post");
12
+ * allow("update", "Post", { authorId: user.id });
13
+ * allow("delete", "Post", { authorId: user.id });
14
+ * });
15
+ *
16
+ * // 2. Plug it in
17
+ * defineApp("my-app", {
18
+ * plugins: [identityPlugin({ adapter }), rbacPlugin({ buildAbility })],
19
+ * });
20
+ *
21
+ * // 3a. Declarative — gate an HTTP route binding
22
+ * httpInterface()
23
+ * .wire(post("/posts/:id", { policy: ["update", "Post"] }), async ({ input }) => { ... })
24
+ * .run();
25
+ *
26
+ * // 3b. Programmatic — gate inside a handler
27
+ * defineAction({
28
+ * name: "posts.delete",
29
+ * handler: async (input, ctx) => {
30
+ * const post = await db.query.posts.findFirst({ where: eq(posts.id, input.id) });
31
+ * if (!post) throw NotFound;
32
+ * const ability = ctx.ability();
33
+ * if (ability.cannot("delete", subject("Post", post))) throw Forbidden;
34
+ * ...
35
+ * }
36
+ * });
37
+ *
38
+ * Why CASL: 8-yr-old, mature, MongoDB-style conditions (`{ authorId: user.id }`),
39
+ * field-level permissions, full TypeScript support, used by NestJS / RedwoodJS /
40
+ * many production teams. We don't reinvent — we glue.
41
+ */
42
+ import { type MongoAbility, type MongoQuery, subject as caslSubject } from "@casl/ability";
43
+ import { type User } from "@nwire/auth";
44
+ import { type HandlerContext, type PluginDefinition } from "@nwire/forge";
45
+ /**
46
+ * The Ability type — what `buildAbility(user)` returns and what
47
+ * `ctx.ability()` exposes. We use CASL's `MongoAbility` so MongoDB-style
48
+ * conditions (e.g. `{ authorId: user.id }`) work out of the box without
49
+ * any extra `conditionsMatcher` wiring at the call site.
50
+ */
51
+ export type Ability = MongoAbility;
52
+ /**
53
+ * Builder API exposed to `defineAbility(user, { allow, deny }) => ...`.
54
+ * Wraps CASL's `AbilityBuilder.can/cannot` under nicer names (`allow/deny`
55
+ * read better in business code; `can/cannot` is already overloaded with
56
+ * the query verbs we expose elsewhere).
57
+ */
58
+ export interface AbilityBuilderCtx {
59
+ allow: (action: string | string[], subject: any, conditions?: MongoQuery, fields?: string | string[]) => void;
60
+ deny: (action: string | string[], subject: any, conditions?: MongoQuery, fields?: string | string[]) => void;
61
+ }
62
+ /** Re-export CASL's subject() helper — tag a plain object with its type. */
63
+ export declare const subject: typeof caslSubject;
64
+ /**
65
+ * Define an ability factory — pure function from User to a CASL Ability.
66
+ * Called once per dispatch (cached per envelope by the middleware).
67
+ *
68
+ * const buildAbility = defineAbility((user, { allow, deny }) => {
69
+ * allow("read", "Post");
70
+ * if (user.role === "admin") allow("manage", "all");
71
+ * });
72
+ */
73
+ export declare function defineAbility(factory: (user: User | null, ctx: AbilityBuilderCtx) => void): (user: User | null) => Ability;
74
+ export interface RbacPluginOptions {
75
+ readonly buildAbility: (user: User | null) => Ability;
76
+ /**
77
+ * Container token the ability factory is registered under. Default:
78
+ * `"buildAbility"`. The middleware looks it up by this key, so custom
79
+ * names let an app register multiple factories (e.g. for different
80
+ * tenants).
81
+ */
82
+ readonly name?: string;
83
+ }
84
+ /**
85
+ * RBAC plugin — registers `buildAbility` on the container and installs a
86
+ * dispatch middleware that resolves the current user's Ability, stashes it
87
+ * on the handler ctx, and enforces `action.policy` when it's an
88
+ * `[action, subject]` tuple.
89
+ */
90
+ export declare function rbacPlugin(options: RbacPluginOptions): PluginDefinition;
91
+ /**
92
+ * Test helper — build an ability and return it directly. Used in unit
93
+ * tests that don't want to spin up the full app/plugin stack.
94
+ */
95
+ export declare function abilityFor(buildAbility: (user: User | null) => Ability, user: User | null): Ability;
96
+ /**
97
+ * Koa middleware variant of `can(verb, subj)` for use with the http
98
+ * builder's `RouteBinding.middleware`:
99
+ *
100
+ * .wire(post("/posts/:id", { params, body, middleware: [canKoa("update", "Post")] }), handler)
101
+ *
102
+ * Lifts `buildAbility` off the container (registered by `rbacPlugin`),
103
+ * resolves the current user from `ctx.state.user` (set by an upstream
104
+ * auth middleware), and throws `ForbiddenError` if the verb is denied.
105
+ *
106
+ * For instance-level checks (e.g. owner-only `update`), the handler
107
+ * should still call `ability.cannot(verb, subject("Post", record))`
108
+ * after loading the instance — same as in dispatch handlers.
109
+ */
110
+ export declare function canKoa(verb: string, subj: string): import("koa").Middleware;
111
+ export declare function abilityFromCtx(ctx: HandlerContext): Ability | null;
112
+ export { permission, parsePermission, type Permission, } from "./typed";
113
+ export { resolver, ownedBy, sameTenant, authorize, type Resolver, type ResolverFn, } from "./resolvers";
114
+ export { conditionsFor, type DrizzleFilterBuilder, } from "./filters";
115
+ export { ActionDeniedError, OwnershipMismatchError, ScopeMissingError, RoleMissingError, isOwnershipMismatchError, isScopeMissingError, isRoleMissingError, } from "./errors";
116
+ //# sourceMappingURL=rbac.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rbac.d.ts","sourceRoot":"","sources":["../src/rbac.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,OAAO,IAAI,WAAW,EACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAkB,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,YAAY,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAEhC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;IAE9G,IAAI,EAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC/G;AAED,4EAA4E;AAC5E,eAAO,MAAM,OAAO,oBAAc,CAAC;AAEnC;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAC3D,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,OAAO,CAShC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAsBD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA4BvE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,OAAO,EAC5C,IAAI,EAAE,IAAI,GAAG,IAAI,GAChB,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,KAAK,EAAE,UAAU,CAuB3E;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,GAAG,IAAI,CAGlE;AAWD,OAAO,EACL,UAAU,EACV,eAAe,EACf,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,QAAQ,EACR,OAAO,EACP,UAAU,EACV,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,UAAU,CAAC"}
package/dist/rbac.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * `@nwire/rbac` — declarative permissions powered by CASL.
3
+ *
4
+ * import { defineAbility, rbacPlugin, can } from "@nwire/rbac";
5
+ *
6
+ * // 1. Declare what each role can do
7
+ * export const buildAbility = defineAbility((user, { allow, deny }) => {
8
+ * if (!user) return;
9
+ * if (user.roles?.includes("admin")) { allow("manage", "all"); return; }
10
+ * allow("read", "Post");
11
+ * allow("create", "Post");
12
+ * allow("update", "Post", { authorId: user.id });
13
+ * allow("delete", "Post", { authorId: user.id });
14
+ * });
15
+ *
16
+ * // 2. Plug it in
17
+ * defineApp("my-app", {
18
+ * plugins: [identityPlugin({ adapter }), rbacPlugin({ buildAbility })],
19
+ * });
20
+ *
21
+ * // 3a. Declarative — gate an HTTP route binding
22
+ * httpInterface()
23
+ * .wire(post("/posts/:id", { policy: ["update", "Post"] }), async ({ input }) => { ... })
24
+ * .run();
25
+ *
26
+ * // 3b. Programmatic — gate inside a handler
27
+ * defineAction({
28
+ * name: "posts.delete",
29
+ * handler: async (input, ctx) => {
30
+ * const post = await db.query.posts.findFirst({ where: eq(posts.id, input.id) });
31
+ * if (!post) throw NotFound;
32
+ * const ability = ctx.ability();
33
+ * if (ability.cannot("delete", subject("Post", post))) throw Forbidden;
34
+ * ...
35
+ * }
36
+ * });
37
+ *
38
+ * Why CASL: 8-yr-old, mature, MongoDB-style conditions (`{ authorId: user.id }`),
39
+ * field-level permissions, full TypeScript support, used by NestJS / RedwoodJS /
40
+ * many production teams. We don't reinvent — we glue.
41
+ */
42
+ import { AbilityBuilder, createMongoAbility, subject as caslSubject, } from "@casl/ability";
43
+ import { ForbiddenError } from "@nwire/auth";
44
+ import { definePlugin, } from "@nwire/forge";
45
+ /** Re-export CASL's subject() helper — tag a plain object with its type. */
46
+ export const subject = caslSubject;
47
+ /**
48
+ * Define an ability factory — pure function from User to a CASL Ability.
49
+ * Called once per dispatch (cached per envelope by the middleware).
50
+ *
51
+ * const buildAbility = defineAbility((user, { allow, deny }) => {
52
+ * allow("read", "Post");
53
+ * if (user.role === "admin") allow("manage", "all");
54
+ * });
55
+ */
56
+ export function defineAbility(factory) {
57
+ return (user) => {
58
+ const builder = new AbilityBuilder(createMongoAbility);
59
+ factory(user, {
60
+ allow: builder.can.bind(builder),
61
+ deny: builder.cannot.bind(builder),
62
+ });
63
+ return builder.build();
64
+ };
65
+ }
66
+ const ABILITY_BUILDER_KEY = Symbol.for("nwire.rbac.builder");
67
+ const CURRENT_ABILITY_KEY = Symbol.for("nwire.rbac.ability");
68
+ /**
69
+ * Stash the ability + builder on the handler ctx so:
70
+ * 1. The `can()` middleware can read the resolved ability for the gate.
71
+ * 2. Handlers can call `ctx.ability()` programmatically.
72
+ * Implementation: we side-channel through `handlerCtx` (passed across
73
+ * middleware) using well-known Symbol keys — type-safe AND framework-agnostic.
74
+ */
75
+ function getAbility(ctx) {
76
+ if (!ctx || typeof ctx !== "object")
77
+ return null;
78
+ return ctx[CURRENT_ABILITY_KEY] ?? null;
79
+ }
80
+ function setAbility(ctx, ability) {
81
+ if (!ctx || typeof ctx !== "object")
82
+ return;
83
+ ctx[CURRENT_ABILITY_KEY] = ability;
84
+ }
85
+ /**
86
+ * RBAC plugin — registers `buildAbility` on the container and installs a
87
+ * dispatch middleware that resolves the current user's Ability, stashes it
88
+ * on the handler ctx, and enforces `action.policy` when it's an
89
+ * `[action, subject]` tuple.
90
+ */
91
+ export function rbacPlugin(options) {
92
+ const containerKey = options.name ?? "buildAbility";
93
+ const middleware = async (next, action, _input, ctx) => {
94
+ const user = (ctx.envelope?.user ?? null);
95
+ const ability = options.buildAbility(user);
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ setAbility(ctx, ability);
98
+ // Tuple-form policy on actions: `policy: ["update", "Post"]`.
99
+ if (Array.isArray(action.policy) && action.policy.length === 2) {
100
+ const [verb, subj] = action.policy;
101
+ if (ability.cannot(verb, subj)) {
102
+ throw new ForbiddenError(`Not allowed to ${verb} ${subj}`);
103
+ }
104
+ }
105
+ return next();
106
+ };
107
+ return definePlugin("rbac", {
108
+ register: (container) => {
109
+ // Container factory: wrapping in `() => fn` so Awilix returns the
110
+ // function itself instead of treating it as a factory of its return.
111
+ container.register(containerKey, () => options.buildAbility);
112
+ container.register(ABILITY_BUILDER_KEY.toString(), () => options.buildAbility);
113
+ },
114
+ middleware: [middleware],
115
+ });
116
+ }
117
+ /**
118
+ * Test helper — build an ability and return it directly. Used in unit
119
+ * tests that don't want to spin up the full app/plugin stack.
120
+ */
121
+ export function abilityFor(buildAbility, user) {
122
+ return buildAbility(user);
123
+ }
124
+ /**
125
+ * Koa middleware variant of `can(verb, subj)` for use with the http
126
+ * builder's `RouteBinding.middleware`:
127
+ *
128
+ * .wire(post("/posts/:id", { params, body, middleware: [canKoa("update", "Post")] }), handler)
129
+ *
130
+ * Lifts `buildAbility` off the container (registered by `rbacPlugin`),
131
+ * resolves the current user from `ctx.state.user` (set by an upstream
132
+ * auth middleware), and throws `ForbiddenError` if the verb is denied.
133
+ *
134
+ * For instance-level checks (e.g. owner-only `update`), the handler
135
+ * should still call `ability.cannot(verb, subject("Post", record))`
136
+ * after loading the instance — same as in dispatch handlers.
137
+ */
138
+ export function canKoa(verb, subj) {
139
+ return async (koaCtx, next) => {
140
+ // The container reaches Koa through `ctx.state._container` (set by
141
+ // `@nwire/http` when an app is being served) or by reading the
142
+ // app's container via the http builder's `.provide(...)` plumbing.
143
+ // We don't want a hard dep on those internals — the builder
144
+ // exposes the container indirectly on the request scope. Read it
145
+ // via the documented Koa `state` channel.
146
+ const state = koaCtx.state;
147
+ const container = state._container;
148
+ if (!container) {
149
+ throw new ForbiddenError("RBAC: container not available on Koa state (mount canKoa downstream of provide(container))");
150
+ }
151
+ const buildAbility = container.resolve("buildAbility");
152
+ const ability = buildAbility(state.user ?? null);
153
+ if (ability.cannot(verb, subj)) {
154
+ throw new ForbiddenError(`Not allowed to ${verb} ${subj}`);
155
+ }
156
+ await next();
157
+ };
158
+ }
159
+ // Expose the dispatch-ctx accessor so handler code (or higher-level helpers)
160
+ // can retrieve the ability without poking at Symbols directly.
161
+ export function abilityFromCtx(ctx) {
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ return getAbility(ctx);
164
+ }
165
+ // ─── Polish layer ──────────────────────────────────────────────────
166
+ //
167
+ // Adds the synthesis's "best DX" affordances on top of the CASL core:
168
+ // - typed `resource:action` permission strings (template literals)
169
+ // - composable ownership/relation resolvers
170
+ // - conditionsFor → CASL accessibleBy for query-time filtering (no N+1)
171
+ // - structured error subclasses (OwnershipMismatchError, ScopeMissing,
172
+ // RoleMissing) so the API can tell the UI *why* a denial happened
173
+ export { permission, parsePermission, } from "./typed";
174
+ export { resolver, ownedBy, sameTenant, authorize, } from "./resolvers";
175
+ export { conditionsFor, } from "./filters";
176
+ export { ActionDeniedError, OwnershipMismatchError, ScopeMissingError, RoleMissingError, isOwnershipMismatchError, isScopeMissingError, isRoleMissingError, } from "./errors";
177
+ //# sourceMappingURL=rbac.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rbac.js","sourceRoot":"","sources":["../src/rbac.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Composable resolver helpers — the synthesis's "named rules" pattern.
3
+ *
4
+ * Real apps have a handful of relationship predicates that show up over
5
+ * and over: "is the owner of this resource", "is a member of the same
6
+ * team", "is in the same tenant", "is the assignee". The synthesis
7
+ * recommends naming them once and composing.
8
+ *
9
+ * const isOwner = resolver<User, Post>((u, p) => u.id === p.authorId);
10
+ * const isTeammate = resolver<User, Post>(async (u, p) => (
11
+ * await db.query.memberships.findFirst({
12
+ * where: and(eq(memberships.userId, u.id), eq(memberships.teamId, p.teamId)),
13
+ * }) != null
14
+ * ));
15
+ *
16
+ * // Compose
17
+ * const canEdit = isOwner.or(isTeammate);
18
+ *
19
+ * // Use programmatically
20
+ * if (!(await canEdit(user, post))) throw new OwnershipMismatchError({...});
21
+ *
22
+ * // Or as a CASL ability condition (returns the matcher function)
23
+ * allow("update", "Post", canEdit.condition(user));
24
+ *
25
+ * Why a separate primitive from CASL's MongoQuery conditions: the
26
+ * MongoQuery form is great for "fields on the row" checks (e.g.
27
+ * `{ authorId: user.id }`), but it can't express async I/O ("is the
28
+ * user in this team's membership table"). `Resolver` handles both.
29
+ */
30
+ import type { User } from "@nwire/auth";
31
+ export type ResolverFn<TSubject> = (user: User, subject: TSubject) => boolean | Promise<boolean>;
32
+ export interface Resolver<TSubject> {
33
+ readonly fn: ResolverFn<TSubject>;
34
+ (user: User, subject: TSubject): Promise<boolean>;
35
+ or(other: Resolver<TSubject> | ResolverFn<TSubject>): Resolver<TSubject>;
36
+ and(other: Resolver<TSubject> | ResolverFn<TSubject>): Resolver<TSubject>;
37
+ not(): Resolver<TSubject>;
38
+ }
39
+ /**
40
+ * Wrap a `(user, subject) => boolean` predicate as a composable resolver.
41
+ * The returned value is both callable and chainable.
42
+ */
43
+ export declare function resolver<TSubject>(fn: ResolverFn<TSubject>): Resolver<TSubject>;
44
+ /**
45
+ * Convenience — the most common predicate. Pass the field name on the
46
+ * subject that holds the owner id (defaults to `"authorId"`, which
47
+ * matches the synthesis's "article.authorId" example).
48
+ */
49
+ export declare function ownedBy<TSubject extends Record<string, unknown>>(field?: string): Resolver<TSubject>;
50
+ /**
51
+ * Convenience — same-tenant predicate. The synthesis calls this out as
52
+ * one of the most common reusable rules in multi-tenant SaaS.
53
+ */
54
+ export declare function sameTenant<TSubject extends {
55
+ tenant?: string;
56
+ tenantId?: string;
57
+ }>(): Resolver<TSubject>;
58
+ export declare function authorize<TSubject>(user: User | null | undefined, action: string, subject: TSubject, rule: Resolver<TSubject> | ResolverFn<TSubject>, opts?: {
59
+ readonly subjectName?: string;
60
+ }): Promise<void>;
61
+ //# sourceMappingURL=resolvers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,UAAU,CAAC,QAAQ,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjG,MAAM,WAAW,QAAQ,CAAC,QAAQ;IAChC,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzE,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC1E,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;CAC3B;AAQD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAgB/E;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,KAAK,GAAE,MAAmB,GACzB,QAAQ,CAAC,QAAQ,CAAC,CAEpB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,SAAS;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAKxG;AAYD,wBAAsB,SAAS,CAAC,QAAQ,EACtC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,EAC7B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,QAAQ,EACjB,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,EAC/C,IAAI,GAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3C,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Composable resolver helpers — the synthesis's "named rules" pattern.
3
+ *
4
+ * Real apps have a handful of relationship predicates that show up over
5
+ * and over: "is the owner of this resource", "is a member of the same
6
+ * team", "is in the same tenant", "is the assignee". The synthesis
7
+ * recommends naming them once and composing.
8
+ *
9
+ * const isOwner = resolver<User, Post>((u, p) => u.id === p.authorId);
10
+ * const isTeammate = resolver<User, Post>(async (u, p) => (
11
+ * await db.query.memberships.findFirst({
12
+ * where: and(eq(memberships.userId, u.id), eq(memberships.teamId, p.teamId)),
13
+ * }) != null
14
+ * ));
15
+ *
16
+ * // Compose
17
+ * const canEdit = isOwner.or(isTeammate);
18
+ *
19
+ * // Use programmatically
20
+ * if (!(await canEdit(user, post))) throw new OwnershipMismatchError({...});
21
+ *
22
+ * // Or as a CASL ability condition (returns the matcher function)
23
+ * allow("update", "Post", canEdit.condition(user));
24
+ *
25
+ * Why a separate primitive from CASL's MongoQuery conditions: the
26
+ * MongoQuery form is great for "fields on the row" checks (e.g.
27
+ * `{ authorId: user.id }`), but it can't express async I/O ("is the
28
+ * user in this team's membership table"). `Resolver` handles both.
29
+ */
30
+ function intoResolver(x) {
31
+ return typeof x === "function" && "fn" in x === false
32
+ ? resolver(x)
33
+ : x;
34
+ }
35
+ /**
36
+ * Wrap a `(user, subject) => boolean` predicate as a composable resolver.
37
+ * The returned value is both callable and chainable.
38
+ */
39
+ export function resolver(fn) {
40
+ const call = async (user, subject) => Boolean(await fn(user, subject));
41
+ const r = call;
42
+ // Attach properties — `r` is the call function so `r(u, s)` works.
43
+ Object.defineProperty(r, "fn", { value: fn });
44
+ r.or = (other) => {
45
+ const o = intoResolver(other);
46
+ return resolver(async (u, s) => (await call(u, s)) || (await o(u, s)));
47
+ };
48
+ r.and = (other) => {
49
+ const o = intoResolver(other);
50
+ return resolver(async (u, s) => (await call(u, s)) && (await o(u, s)));
51
+ };
52
+ r.not = () => resolver(async (u, s) => !(await call(u, s)));
53
+ return r;
54
+ }
55
+ /**
56
+ * Convenience — the most common predicate. Pass the field name on the
57
+ * subject that holds the owner id (defaults to `"authorId"`, which
58
+ * matches the synthesis's "article.authorId" example).
59
+ */
60
+ export function ownedBy(field = "authorId") {
61
+ return resolver((user, subject) => subject[field] === user.id);
62
+ }
63
+ /**
64
+ * Convenience — same-tenant predicate. The synthesis calls this out as
65
+ * one of the most common reusable rules in multi-tenant SaaS.
66
+ */
67
+ export function sameTenant() {
68
+ return resolver((user, subject) => {
69
+ const subTenant = (subject.tenant ?? subject.tenantId);
70
+ return Boolean(user.tenant && subTenant && user.tenant === subTenant);
71
+ });
72
+ }
73
+ /**
74
+ * Higher-order helper: turn a resolver into an authorize call that
75
+ * throws the right structured error on denial. The synthesis's
76
+ * "structured error" pattern.
77
+ *
78
+ * import { authorize, ownedBy } from "@nwire/rbac";
79
+ * await authorize(user, "update", post, ownedBy());
80
+ */
81
+ import { OwnershipMismatchError } from "./errors";
82
+ export async function authorize(user, action, subject, rule, opts = {}) {
83
+ if (!user) {
84
+ throw new OwnershipMismatchError({
85
+ action,
86
+ subject: opts.subjectName ?? "resource",
87
+ reason: "Not authenticated",
88
+ });
89
+ }
90
+ const r = intoResolver(rule);
91
+ const ok = await r(user, subject);
92
+ if (!ok) {
93
+ throw new OwnershipMismatchError({
94
+ action,
95
+ subject: opts.subjectName ?? "resource",
96
+ userId: user.id,
97
+ });
98
+ }
99
+ }
100
+ //# sourceMappingURL=resolvers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolvers.js","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Typed permission helpers — the "Read-Like-English" + autocomplete layer
3
+ * over CASL. Apps declare their vocabulary once (verbs + subjects); the
4
+ * rest of the codebase gets autocompletion + typo-prevention on every
5
+ * `can("update", "Post")` call.
6
+ *
7
+ * // Declare your vocabulary once.
8
+ * type AppVerbs = "read" | "create" | "update" | "delete" | "manage";
9
+ * type AppSubjects = "Post" | "Comment" | "User" | "all";
10
+ * export type AppPermission = Permission<AppVerbs, AppSubjects>;
11
+ * // ^ "read:Post" | "create:Post" | ... | "manage:all"
12
+ *
13
+ * // Now per-route middleware is autocomplete-driven:
14
+ * http().wire(post("/posts", {
15
+ * body, middleware: [canKoa<AppVerbs, AppSubjects>("update", "Post")],
16
+ * }), handler);
17
+ *
18
+ * The `resource:action` string form is also exposed via `parsePermission`
19
+ * for places where a single string is more ergonomic (config files, route
20
+ * tags, audit logs).
21
+ */
22
+ /**
23
+ * The `resource:action` permission string — Template Literal Type the
24
+ * synthesis recommends. Apps narrow Verbs/Subjects to their domain
25
+ * vocabulary and get IDE autocomplete + typo-prevention everywhere.
26
+ */
27
+ export type Permission<TVerb extends string = string, TSubject extends string = string> = `${TVerb}:${TSubject}`;
28
+ /**
29
+ * Build a permission string. Type-narrow when called with const-literal
30
+ * arguments; the inferred string lands in IDE tooltips.
31
+ *
32
+ * const p = permission("update", "Post"); // typed `update:Post`
33
+ */
34
+ export declare function permission<TVerb extends string, TSubject extends string>(verb: TVerb, subject: TSubject): Permission<TVerb, TSubject>;
35
+ /** Split a `resource:action` permission string into a tuple. */
36
+ export declare function parsePermission<P extends Permission>(p: P): readonly [string, string];
37
+ //# sourceMappingURL=typed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.d.ts","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,MAAM,GAAG,MAAM,IACpF,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC;AAEzB;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,MAAM,EACtE,IAAI,EAAK,KAAK,EACd,OAAO,EAAE,QAAQ,GAChB,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAE7B;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAIrF"}
package/dist/typed.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Typed permission helpers — the "Read-Like-English" + autocomplete layer
3
+ * over CASL. Apps declare their vocabulary once (verbs + subjects); the
4
+ * rest of the codebase gets autocompletion + typo-prevention on every
5
+ * `can("update", "Post")` call.
6
+ *
7
+ * // Declare your vocabulary once.
8
+ * type AppVerbs = "read" | "create" | "update" | "delete" | "manage";
9
+ * type AppSubjects = "Post" | "Comment" | "User" | "all";
10
+ * export type AppPermission = Permission<AppVerbs, AppSubjects>;
11
+ * // ^ "read:Post" | "create:Post" | ... | "manage:all"
12
+ *
13
+ * // Now per-route middleware is autocomplete-driven:
14
+ * http().wire(post("/posts", {
15
+ * body, middleware: [canKoa<AppVerbs, AppSubjects>("update", "Post")],
16
+ * }), handler);
17
+ *
18
+ * The `resource:action` string form is also exposed via `parsePermission`
19
+ * for places where a single string is more ergonomic (config files, route
20
+ * tags, audit logs).
21
+ */
22
+ /**
23
+ * Build a permission string. Type-narrow when called with const-literal
24
+ * arguments; the inferred string lands in IDE tooltips.
25
+ *
26
+ * const p = permission("update", "Post"); // typed `update:Post`
27
+ */
28
+ export function permission(verb, subject) {
29
+ return `${verb}:${subject}`;
30
+ }
31
+ /** Split a `resource:action` permission string into a tuple. */
32
+ export function parsePermission(p) {
33
+ const idx = p.indexOf(":");
34
+ if (idx < 0)
35
+ return [p, ""];
36
+ return [p.slice(0, idx), p.slice(idx + 1)];
37
+ }
38
+ //# sourceMappingURL=typed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.js","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAUH;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,IAAc,EACd,OAAiB;IAEjB,OAAO,GAAG,IAAI,IAAI,OAAO,EAAiC,CAAC;AAC7D,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,eAAe,CAAuB,CAAI;IACxD,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC5B,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAC7C,CAAC"}