@mahiframework/permissions 0.1.4

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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/assignee.d.ts +57 -0
  4. package/dist/assignee.d.ts.map +1 -0
  5. package/dist/assignee.js +46 -0
  6. package/dist/assignee.js.map +1 -0
  7. package/dist/commands/permissions-cache-reset.d.ts +20 -0
  8. package/dist/commands/permissions-cache-reset.d.ts.map +1 -0
  9. package/dist/commands/permissions-cache-reset.js +24 -0
  10. package/dist/commands/permissions-cache-reset.js.map +1 -0
  11. package/dist/commands/permissions-check.d.ts +37 -0
  12. package/dist/commands/permissions-check.d.ts.map +1 -0
  13. package/dist/commands/permissions-check.js +92 -0
  14. package/dist/commands/permissions-check.js.map +1 -0
  15. package/dist/commands/permissions-show.d.ts +20 -0
  16. package/dist/commands/permissions-show.d.ts.map +1 -0
  17. package/dist/commands/permissions-show.js +49 -0
  18. package/dist/commands/permissions-show.js.map +1 -0
  19. package/dist/errors.d.ts +77 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +106 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +25 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +18 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/listeners/invalidate-permission-cache.listener.d.ts +39 -0
  28. package/dist/listeners/invalidate-permission-cache.listener.d.ts.map +1 -0
  29. package/dist/listeners/invalidate-permission-cache.listener.js +50 -0
  30. package/dist/listeners/invalidate-permission-cache.listener.js.map +1 -0
  31. package/dist/middleware/current-subject.d.ts +23 -0
  32. package/dist/middleware/current-subject.d.ts.map +1 -0
  33. package/dist/middleware/current-subject.js +31 -0
  34. package/dist/middleware/current-subject.js.map +1 -0
  35. package/dist/middleware/permission.d.ts +19 -0
  36. package/dist/middleware/permission.d.ts.map +1 -0
  37. package/dist/middleware/permission.js +34 -0
  38. package/dist/middleware/permission.js.map +1 -0
  39. package/dist/middleware/role-or-permission.d.ts +29 -0
  40. package/dist/middleware/role-or-permission.d.ts.map +1 -0
  41. package/dist/middleware/role-or-permission.js +45 -0
  42. package/dist/middleware/role-or-permission.js.map +1 -0
  43. package/dist/middleware/role.d.ts +21 -0
  44. package/dist/middleware/role.d.ts.map +1 -0
  45. package/dist/middleware/role.js +36 -0
  46. package/dist/middleware/role.js.map +1 -0
  47. package/dist/migrations/0001_create_permission_tables.d.ts +63 -0
  48. package/dist/migrations/0001_create_permission_tables.d.ts.map +1 -0
  49. package/dist/migrations/0001_create_permission_tables.js +113 -0
  50. package/dist/migrations/0001_create_permission_tables.js.map +1 -0
  51. package/dist/models/permission.model.d.ts +58 -0
  52. package/dist/models/permission.model.d.ts.map +1 -0
  53. package/dist/models/permission.model.js +18 -0
  54. package/dist/models/permission.model.js.map +1 -0
  55. package/dist/models/role.model.d.ts +60 -0
  56. package/dist/models/role.model.d.ts.map +1 -0
  57. package/dist/models/role.model.js +18 -0
  58. package/dist/models/role.model.js.map +1 -0
  59. package/dist/permission-map.d.ts +97 -0
  60. package/dist/permission-map.d.ts.map +1 -0
  61. package/dist/permission-map.js +93 -0
  62. package/dist/permission-map.js.map +1 -0
  63. package/dist/permission-registrar.d.ts +183 -0
  64. package/dist/permission-registrar.d.ts.map +1 -0
  65. package/dist/permission-registrar.js +573 -0
  66. package/dist/permission-registrar.js.map +1 -0
  67. package/dist/permissions-config.d.ts +80 -0
  68. package/dist/permissions-config.d.ts.map +1 -0
  69. package/dist/permissions-config.js +22 -0
  70. package/dist/permissions-config.js.map +1 -0
  71. package/dist/permissions-facade.d.ts +74 -0
  72. package/dist/permissions-facade.d.ts.map +1 -0
  73. package/dist/permissions-facade.js +131 -0
  74. package/dist/permissions-facade.js.map +1 -0
  75. package/dist/permissions-service-provider.d.ts +91 -0
  76. package/dist/permissions-service-provider.d.ts.map +1 -0
  77. package/dist/permissions-service-provider.js +137 -0
  78. package/dist/permissions-service-provider.js.map +1 -0
  79. package/dist/relations.d.ts +63 -0
  80. package/dist/relations.d.ts.map +1 -0
  81. package/dist/relations.js +62 -0
  82. package/dist/relations.js.map +1 -0
  83. package/dist/request-cache.d.ts +50 -0
  84. package/dist/request-cache.d.ts.map +1 -0
  85. package/dist/request-cache.js +40 -0
  86. package/dist/request-cache.js.map +1 -0
  87. package/dist/tokens.d.ts +15 -0
  88. package/dist/tokens.d.ts.map +1 -0
  89. package/dist/tokens.js +15 -0
  90. package/dist/tokens.js.map +1 -0
  91. package/package.json +69 -0
@@ -0,0 +1,62 @@
1
+ import { morphToMany } from "@mahiframework/database";
2
+ import { Permission } from "./models/permission.model.js";
3
+ import { Role } from "./models/role.model.js";
4
+ /**
5
+ * The `roles` relation, for an app model that wants to eager-load them.
6
+ *
7
+ * Opt-in per model rather than shipped on anything, because the package
8
+ * owns neither the app's models nor its attribute interfaces:
9
+ *
10
+ * export interface UserAttributes {
11
+ * // ...
12
+ * roles: MorphToMany<Role>;
13
+ * }
14
+ *
15
+ * export class User extends Model<UserAttributes>()({ ... }) {
16
+ * static override relationships = {
17
+ * roles: rolesRelation(),
18
+ * };
19
+ * }
20
+ *
21
+ * That buys `User.query().with("roles.permissions")` and
22
+ * `whereHas("roles", (q) => q.where("name", "admin"))`.
23
+ *
24
+ * READ-ONLY, in practice. `Permissions.assignRole()` writes the pivot
25
+ * directly (it takes a subject, not a relation handle, so it can also
26
+ * work from a `{ type, id }` descriptor in a job), so a collection
27
+ * loaded by `with("roles")` will not reflect an assignment made later in
28
+ * the same request. Re-`load()` it if that matters.
29
+ *
30
+ * `type` is deliberately omitted so it defaults to the DECLARING model's
31
+ * `morphAlias()` — which is what a `morphToMany` wants, and the opposite
32
+ * of what `morphedByMany` would. Note that `morphAlias()` falls back to
33
+ * the table name, so an app without a `Relation.morphMap()` entry has
34
+ * made its assignment rows depend on its table name.
35
+ */
36
+ export function rolesRelation() {
37
+ return morphToMany(() => Role, {
38
+ pivotTable: "model_has_roles",
39
+ morphType: "model_type",
40
+ morphId: "model_id",
41
+ relatedPivotKey: "role_id",
42
+ });
43
+ }
44
+ /**
45
+ * The `permissions` relation: permissions granted to this model
46
+ * DIRECTLY, never those inherited from its roles.
47
+ *
48
+ * The inherited ones have no relation to declare — they are two hops
49
+ * through `model_has_roles` and `role_has_permissions`, which is a
50
+ * `hasManyThrough` the ORM cannot express across a polymorphic pivot.
51
+ * `Permissions.getAllPermissions(user)` is the union, and it answers
52
+ * from the cached map rather than a join.
53
+ */
54
+ export function permissionsRelation() {
55
+ return morphToMany(() => Permission, {
56
+ pivotTable: "model_has_permissions",
57
+ morphType: "model_type",
58
+ morphId: "model_id",
59
+ relatedPivotKey: "permission_id",
60
+ });
61
+ }
62
+ //# sourceMappingURL=relations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.js","sourceRoot":"","sources":["../src/relations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;QAC7B,UAAU,EAAE,iBAAiB;QAC7B,SAAS,EAAE,YAAY;QACvB,OAAO,EAAE,UAAU;QACnB,eAAe,EAAE,SAAS;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE;QACnC,UAAU,EAAE,uBAAuB;QACnC,SAAS,EAAE,YAAY;QACvB,OAAO,EAAE,UAAU;QACnB,eAAe,EAAE,eAAe;KACjC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A per-request memo of each subject's raw assignments.
3
+ *
4
+ * This exists because of how the gate hook works, not as a general
5
+ * optimisation. `GateRegistry.before()` callbacks run on EVERY
6
+ * authorization check, so a controller doing five `can()` calls would
7
+ * otherwise issue ten queries (roles and direct permissions, twice per
8
+ * call) against rows that cannot have changed mid-request unless this
9
+ * request changed them.
10
+ *
11
+ * Scoped rather than global for the obvious reason: a long-lived worker
12
+ * process would otherwise serve one request's answers to the next, and
13
+ * "why does this user still have admin" is not a bug anybody enjoys.
14
+ *
15
+ * The provider's `middleware()` pipe opens a scope per request. Outside
16
+ * one, `assignmentMemo()` returns null and every read goes to the
17
+ * database — correct by default, since a queue job or a CLI command has
18
+ * no natural boundary at which the memo should expire. A job that wants
19
+ * one wraps itself in `withPermissionCache()`.
20
+ *
21
+ * Writes through the registrar clear the memo rather than patching it:
22
+ * patching means keeping two representations in step, and clearing costs
23
+ * one query on the next check.
24
+ */
25
+ export interface AssignmentRecord {
26
+ roleIds: bigint[];
27
+ permissionIds: bigint[];
28
+ }
29
+ /** Run `fn` inside a memo scope. Called once per request by the provider's pipe. */
30
+ export declare function runWithPermissionCache<T>(fn: () => T): T;
31
+ /**
32
+ * Open a memo scope around a job, command, or test.
33
+ *
34
+ * Async-returning so a caller cannot forget to await it, and so the scope
35
+ * genuinely covers the whole callback rather than just its synchronous
36
+ * prefix.
37
+ */
38
+ export declare function withPermissionCache<T>(callback: () => T | Promise<T>): Promise<T>;
39
+ /** The active memo, or null outside a scope. */
40
+ export declare function assignmentMemo(): Map<string, AssignmentRecord> | null;
41
+ /**
42
+ * Drop memoised assignments.
43
+ *
44
+ * With no argument, clears everything: a role's permissions changing
45
+ * affects every subject holding it, and the registrar has no index from
46
+ * role to subject. Pass a key to drop just one subject, which is what an
47
+ * assignment write does.
48
+ */
49
+ export declare function forgetMemoisedAssignments(cacheKey?: string): void;
50
+ //# sourceMappingURL=request-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-cache.d.ts","sourceRoot":"","sources":["../src/request-cache.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAID,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAExD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAEjF;AAED,gDAAgD;AAChD,wBAAgB,cAAc,IAAI,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,IAAI,CAErE;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAcjE"}
@@ -0,0 +1,40 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const storage = new AsyncLocalStorage();
3
+ /** Run `fn` inside a memo scope. Called once per request by the provider's pipe. */
4
+ export function runWithPermissionCache(fn) {
5
+ return storage.run(new Map(), fn);
6
+ }
7
+ /**
8
+ * Open a memo scope around a job, command, or test.
9
+ *
10
+ * Async-returning so a caller cannot forget to await it, and so the scope
11
+ * genuinely covers the whole callback rather than just its synchronous
12
+ * prefix.
13
+ */
14
+ export function withPermissionCache(callback) {
15
+ return storage.run(new Map(), async () => callback());
16
+ }
17
+ /** The active memo, or null outside a scope. */
18
+ export function assignmentMemo() {
19
+ return storage.getStore() ?? null;
20
+ }
21
+ /**
22
+ * Drop memoised assignments.
23
+ *
24
+ * With no argument, clears everything: a role's permissions changing
25
+ * affects every subject holding it, and the registrar has no index from
26
+ * role to subject. Pass a key to drop just one subject, which is what an
27
+ * assignment write does.
28
+ */
29
+ export function forgetMemoisedAssignments(cacheKey) {
30
+ const memo = storage.getStore();
31
+ if (memo === undefined) {
32
+ return;
33
+ }
34
+ if (cacheKey === undefined) {
35
+ memo.clear();
36
+ return;
37
+ }
38
+ memo.delete(cacheKey);
39
+ }
40
+ //# sourceMappingURL=request-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-cache.js","sourceRoot":"","sources":["../src/request-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA+BrD,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAiC,CAAC;AAEvE,oFAAoF;AACpF,MAAM,UAAU,sBAAsB,CAAI,EAAW;IACnD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,EAA4B,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAI,QAA8B;IACnE,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,EAA4B,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;AAClF,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAAiB;IACzD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAEhC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO;IACT,CAAC;IAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,OAAO;IACT,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The `PermissionRegistrar` singleton.
3
+ *
4
+ * NOT in `@mahiframework/core`'s `well-known-tokens.ts`. That file is
5
+ * explicitly for tokens referenced ACROSS package boundaries by packages
6
+ * that deliberately avoid a compile-time dependency; nothing outside this
7
+ * package resolves this one by string. A local literal is correct until
8
+ * that changes.
9
+ *
10
+ * A separate module from the provider so the facade and the middleware
11
+ * can import the token without importing the provider, which would be a
12
+ * cycle.
13
+ */
14
+ export declare const PERMISSIONS_TOKEN = "permissions";
15
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,gBAAgB,CAAC"}
package/dist/tokens.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The `PermissionRegistrar` singleton.
3
+ *
4
+ * NOT in `@mahiframework/core`'s `well-known-tokens.ts`. That file is
5
+ * explicitly for tokens referenced ACROSS package boundaries by packages
6
+ * that deliberately avoid a compile-time dependency; nothing outside this
7
+ * package resolves this one by string. A local literal is correct until
8
+ * that changes.
9
+ *
10
+ * A separate module from the provider so the facade and the middleware
11
+ * can import the token without importing the provider, which would be a
12
+ * cycle.
13
+ */
14
+ export const PERMISSIONS_TOKEN = "permissions";
15
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@mahiframework/permissions",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "commander": "^12.1.0",
15
+ "@mahiframework/auth": "0.1.4",
16
+ "@mahiframework/cli": "0.1.4",
17
+ "@mahiframework/core": "0.1.4",
18
+ "@mahiframework/database": "0.1.4",
19
+ "@mahiframework/events": "0.1.4",
20
+ "@mahiframework/datetime": "0.1.4",
21
+ "@mahiframework/http": "0.1.4",
22
+ "@mahiframework/facades": "0.1.4",
23
+ "@mahiframework/cache": "0.1.4",
24
+ "@mahiframework/authorization": "0.1.4",
25
+ "@mahiframework/snowflake": "0.1.4"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.7.2",
29
+ "vite": "^8.0.0",
30
+ "vitest": "^4.1.11"
31
+ },
32
+ "license": "MIT",
33
+ "author": "Bradie Tilley",
34
+ "homepage": "https://github.com/mahiframework/mahi#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/mahiframework/mahi.git",
38
+ "directory": "framework/permissions"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/mahiframework/mahi/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=26"
45
+ },
46
+ "sideEffects": false,
47
+ "description": "Permissions — roles, permissions, and cached authorization checks for Mahi.",
48
+ "keywords": [
49
+ "mahi",
50
+ "permissions",
51
+ "roles",
52
+ "authorization",
53
+ "acl"
54
+ ],
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "LICENSE"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "scripts": {
64
+ "build": "tsc -b && node ../../scripts/prune-dist.mjs",
65
+ "typecheck": "tsc -b && tsc -p tsconfig.test.json",
66
+ "lint": "eslint src tests",
67
+ "test": "vitest run"
68
+ }
69
+ }