@tekki-forge/runtime-permissions 0.1.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.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @tekki-forge/runtime-permissions
2
+
3
+ Forge RBAC/ownership permission evaluation (shared by API and UI).
4
+
5
+ Part of [Tekki Forge](https://www.npmjs.com/package/tekki-forge), which turns a `forge.config.ts` into a production-ready full-stack app. Apps created with `forge new` install this package for you; see the [tekki-forge](https://www.npmjs.com/package/tekki-forge) package for the documentation.
@@ -0,0 +1,56 @@
1
+ import type { EntityIR, Operation, PermissionIR } from "@tekki-forge/compiler";
2
+ /** The authenticated caller as seen by the authorization layer. */
3
+ export interface Principal {
4
+ id: string;
5
+ roles: string[];
6
+ organizationId?: string | null;
7
+ /** Teams of the active organization the caller belongs to ("team" rules). */
8
+ teamIds?: string[];
9
+ /**
10
+ * Set for API keys: what the key may touch ("invoices:read", "invoices:write", "*:read", "*").
11
+ * Applied on top of the roles. Undefined for people signed in with a session (no restriction).
12
+ */
13
+ scopes?: string[];
14
+ /** Set when the caller is an API key (a service principal, not a user). */
15
+ apiKeyId?: string;
16
+ }
17
+ export type Decision = {
18
+ allowed: false;
19
+ reason: "unauthenticated" | "forbidden";
20
+ } | {
21
+ allowed: true;
22
+ scope: "all";
23
+ }
24
+ /** Allowed only for records matching the owner/team/policy conditions. */
25
+ | {
26
+ allowed: true;
27
+ scope: "conditional";
28
+ owner: boolean;
29
+ team?: boolean;
30
+ policies: string[];
31
+ };
32
+ /**
33
+ * Merge the global user role (Better Auth admin plugin, comma separated) with the
34
+ * member role in the active organization. Organization owners count as admins.
35
+ */
36
+ export declare function effectiveRoles(userRole: string | null | undefined, memberRole?: string | null, tenancy?: boolean): string[];
37
+ export declare function evaluate(rule: PermissionIR, principal: Principal | null): Decision;
38
+ /**
39
+ * Does an API key scope list allow `op` on the resource at `path`? Scopes are `<path>:read`, `<path>:write`
40
+ * (create, update, delete, actions, transitions, import), `<path>:*`, `*:read`, `*:write` and `*`.
41
+ */
42
+ export declare function scopeAllows(scopes: readonly string[], path: string, op: Operation | "read" | "write"): boolean;
43
+ export declare function authorize(entity: EntityIR, op: Operation, principal: Principal | null): Decision;
44
+ export type PolicyCheck = (name: string, record: Record<string, unknown>) => boolean | Promise<boolean>;
45
+ /** Foreign key of the team relation (`teamId`), if the entity has features.teams. */
46
+ export declare function teamKey(entity: EntityIR): string | undefined;
47
+ /** Decide whether `principal` may apply `decision` to one concrete record. */
48
+ export declare function allowsRecord(entity: EntityIR, decision: Decision, principal: Principal | null, record: Record<string, unknown>, checkPolicy?: PolicyCheck): Promise<boolean>;
49
+ /** UI helper: may the user *possibly* perform `op` (ignoring record-level conditions)? */
50
+ export declare function can(entity: EntityIR, op: Operation, principal: Principal | null): boolean;
51
+ /** UI helper: may the user perform `op` on this specific record (owner and team rules only)? */
52
+ export declare function canOnRecord(entity: EntityIR, op: Operation, principal: Principal | null, record: Record<string, unknown>): boolean;
53
+ /** UI helper: may the user change `field` of this record (entity update permission + field write permission)? */
54
+ export declare function canWriteField(entity: EntityIR, field: string, principal: Principal | null, record: Record<string, unknown>): boolean;
55
+ /** UI helper: does `rule` (e.g. a workflow transition permission) allow the user on this record? */
56
+ export declare function ruleAllowsRecord(entity: EntityIR, rule: PermissionIR, principal: Principal | null, record: Record<string, unknown>): boolean;
package/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Merge the global user role (Better Auth admin plugin, comma separated) with the
3
+ * member role in the active organization. Organization owners count as admins.
4
+ */
5
+ export function effectiveRoles(userRole, memberRole, tenancy = false) {
6
+ const roles = new Set();
7
+ for (const r of (userRole ?? "").split(",").map((s) => s.trim()).filter(Boolean))
8
+ roles.add(r);
9
+ if (tenancy && memberRole) {
10
+ for (const r of memberRole.split(",").map((s) => s.trim()).filter(Boolean))
11
+ roles.add(r === "owner" ? "admin" : r);
12
+ roles.add("member");
13
+ }
14
+ if (!tenancy)
15
+ roles.add("user");
16
+ return [...roles];
17
+ }
18
+ export function evaluate(rule, principal) {
19
+ if (rule.public)
20
+ return { allowed: true, scope: "all" };
21
+ if (!principal)
22
+ return { allowed: false, reason: "unauthenticated" };
23
+ if (rule.authenticated || rule.roles.some((r) => principal.roles.includes(r)))
24
+ return { allowed: true, scope: "all" };
25
+ if (rule.owner || rule.team || rule.policies.length > 0)
26
+ return { allowed: true, scope: "conditional", owner: rule.owner, ...(rule.team ? { team: true } : {}), policies: rule.policies };
27
+ return { allowed: false, reason: "forbidden" };
28
+ }
29
+ /**
30
+ * Does an API key scope list allow `op` on the resource at `path`? Scopes are `<path>:read`, `<path>:write`
31
+ * (create, update, delete, actions, transitions, import), `<path>:*`, `*:read`, `*:write` and `*`.
32
+ */
33
+ export function scopeAllows(scopes, path, op) {
34
+ const kind = op === "read" ? "read" : "write";
35
+ return scopes.some((s) => s === "*" || s === "*:*" || s === `*:${kind}` || s === `${path}:*` || s === `${path}:${kind}`);
36
+ }
37
+ export function authorize(entity, op, principal) {
38
+ if (principal?.scopes && !scopeAllows(principal.scopes, entity.path, op))
39
+ return { allowed: false, reason: "forbidden" };
40
+ return evaluate(entity.permissions[op], principal);
41
+ }
42
+ /** Foreign key of the owner relation (`ownerId`), if the entity has one. */
43
+ function ownerKey(entity) {
44
+ return entity.ownerField ? entity.fields.find((f) => f.name === entity.ownerField)?.relation?.foreignKey : undefined;
45
+ }
46
+ /** Foreign key of the team relation (`teamId`), if the entity has features.teams. */
47
+ export function teamKey(entity) {
48
+ return entity.teamField ? entity.fields.find((f) => f.name === entity.teamField)?.relation?.foreignKey : undefined;
49
+ }
50
+ /** Owner or team condition satisfied by this record (policies aside). */
51
+ function matchesScope(entity, decision, principal, record) {
52
+ if (!principal)
53
+ return false;
54
+ if (decision.owner) {
55
+ const fk = ownerKey(entity);
56
+ if (fk && record[fk] === principal.id)
57
+ return true;
58
+ }
59
+ if (decision.team) {
60
+ const fk = teamKey(entity);
61
+ const team = fk ? record[fk] : undefined;
62
+ if (typeof team === "string" && (principal.teamIds ?? []).includes(team))
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+ /** Decide whether `principal` may apply `decision` to one concrete record. */
68
+ export async function allowsRecord(entity, decision, principal, record, checkPolicy) {
69
+ if (!decision.allowed)
70
+ return false;
71
+ if (decision.scope === "all")
72
+ return true;
73
+ if (matchesScope(entity, decision, principal, record))
74
+ return true;
75
+ for (const policy of decision.policies) {
76
+ if (checkPolicy && (await checkPolicy(policy, record)))
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ /** UI helper: may the user *possibly* perform `op` (ignoring record-level conditions)? */
82
+ export function can(entity, op, principal) {
83
+ return authorize(entity, op, principal).allowed;
84
+ }
85
+ /** UI helper: may the user perform `op` on this specific record (owner and team rules only)? */
86
+ export function canOnRecord(entity, op, principal, record) {
87
+ return decidesRecord(entity, authorize(entity, op, principal), principal, record);
88
+ }
89
+ /** UI helper: may the user change `field` of this record (entity update permission + field write permission)? */
90
+ export function canWriteField(entity, field, principal, record) {
91
+ if (!canOnRecord(entity, "update", principal, record))
92
+ return false;
93
+ const rule = entity.fields.find((f) => f.name === field)?.access?.write;
94
+ return !rule || decidesRecord(entity, evaluate(rule, principal), principal, record);
95
+ }
96
+ /** UI helper: does `rule` (e.g. a workflow transition permission) allow the user on this record? */
97
+ export function ruleAllowsRecord(entity, rule, principal, record) {
98
+ return decidesRecord(entity, evaluate(rule, principal), principal, record);
99
+ }
100
+ function decidesRecord(entity, decision, principal, record) {
101
+ if (!decision.allowed)
102
+ return false;
103
+ if (decision.scope === "all")
104
+ return true;
105
+ if (matchesScope(entity, decision, principal, record))
106
+ return true;
107
+ // Policies are only known server-side; optimistically show the control and let the API decide.
108
+ return decision.policies.length > 0;
109
+ }
110
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,QAAmC,EAAE,UAA0B,EAAE,OAAO,GAAG,KAAK;IAC7G,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/F,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnH,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,CAAC,OAAO;QAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAkB,EAAE,SAA2B;IACtE,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACxD,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACrE,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtH,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC1L,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AACjD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,MAAyB,EAAE,IAAY,EAAE,EAAgC;IACnG,MAAM,IAAI,GAAG,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAC9C,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;AAC3H,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAgB,EAAE,EAAa,EAAE,SAA2B;IACpF,IAAI,SAAS,EAAE,MAAM,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACzH,OAAO,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;AACrD,CAAC;AAID,4EAA4E;AAC5E,SAAS,QAAQ,CAAC,MAAgB;IAChC,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;AACvH,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,OAAO,CAAC,MAAgB;IACtC,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;AACrH,CAAC;AAED,yEAAyE;AACzE,SAAS,YAAY,CAAC,MAAgB,EAAE,QAAqD,EAAE,SAA2B,EAAE,MAA+B;IACzJ,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7B,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,EAAE,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,SAAS,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;IACrD,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACxF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAgB,EAChB,QAAkB,EAClB,SAA2B,EAC3B,MAA+B,EAC/B,WAAyB;IAEzB,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,QAAQ,CAAC,KAAK,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACvC,IAAI,WAAW,IAAI,CAAC,MAAM,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IACtE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,GAAG,CAAC,MAAgB,EAAE,EAAa,EAAE,SAA2B;IAC9E,OAAO,SAAS,CAAC,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC,OAAO,CAAC;AAClD,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,WAAW,CAAC,MAAgB,EAAE,EAAa,EAAE,SAA2B,EAAE,MAA+B;IACvH,OAAO,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AACpF,CAAC;AAED,iHAAiH;AACjH,MAAM,UAAU,aAAa,CAAC,MAAgB,EAAE,KAAa,EAAE,SAA2B,EAAE,MAA+B;IACzH,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACpE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC;IACxE,OAAO,CAAC,IAAI,IAAI,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AACtF,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,gBAAgB,CAAC,MAAgB,EAAE,IAAkB,EAAE,SAA2B,EAAE,MAA+B;IACjI,OAAO,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,aAAa,CAAC,MAAgB,EAAE,QAAkB,EAAE,SAA2B,EAAE,MAA+B;IACvH,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,QAAQ,CAAC,KAAK,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,YAAY,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,+FAA+F;IAC/F,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@tekki-forge/runtime-permissions",
3
+ "version": "0.1.0",
4
+ "description": "Forge RBAC/ownership permission evaluation (shared by API and UI)",
5
+ "keywords": [
6
+ "code-generator",
7
+ "forge",
8
+ "full-stack",
9
+ "tekki-forge"
10
+ ],
11
+ "author": "Mahy KANE",
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "@tekki-forge/compiler": "^0.1.0"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit"
37
+ }
38
+ }