@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
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export { PermissionsServiceProvider, PERMISSIONS_TOKEN } from "./permissions-service-provider.js";
2
+ export { Permissions } from "./permissions-facade.js";
3
+ export { PermissionRegistrar } from "./permission-registrar.js";
4
+ export { Role } from "./models/role.model.js";
5
+ export { Permission } from "./models/permission.model.js";
6
+ export { rolesRelation, permissionsRelation } from "./relations.js";
7
+ export { resolveAssignee, assigneeCacheKey } from "./assignee.js";
8
+ export { resolveConfig } from "./permissions-config.js";
9
+ export { role } from "./middleware/role.js";
10
+ export { permission } from "./middleware/permission.js";
11
+ export { roleOrPermission } from "./middleware/role-or-permission.js";
12
+ export { withPermissionCache } from "./request-cache.js";
13
+ export { InvalidatePermissionCacheListener } from "./listeners/invalidate-permission-cache.listener.js";
14
+ export { PermissionsCacheResetCommand } from "./commands/permissions-cache-reset.js";
15
+ export { PermissionsCheckCommand } from "./commands/permissions-check.js";
16
+ export { PermissionsShowCommand } from "./commands/permissions-show.js";
17
+ export { PermissionsError, RoleNotFoundError, PermissionNotFoundError, DuplicateNameError, UnsupportedAssigneeKeyError, UnresolvedGuardError, UnknownMorphAliasError, } from "./errors.js";
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAClG,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAOxD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAGtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,iCAAiC,EAAE,MAAM,qDAAqD,CAAC;AAExG,OAAO,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAYxE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { Application } from "@mahiframework/core";
2
+ import type { Listener } from "@mahiframework/events";
3
+ import type { ModelLifecycleEvent } from "@mahiframework/database";
4
+ /**
5
+ * Forgets the cached role/permission map when a `Role` or `Permission`
6
+ * row is written outside this package.
7
+ *
8
+ * `PermissionRegistrar` already forgets the cache on every write it
9
+ * performs, so this listener exists entirely for the writes it never
10
+ * sees: a seeder calling `Role.create(...)`, a migration backfilling
11
+ * `guard_name`, an admin screen saving a renamed role through the model.
12
+ * Without it, the map would keep answering from the old names until the
13
+ * TTL expired — which is precisely the class of bug a permission system
14
+ * cannot ship with.
15
+ *
16
+ * Subscribed to the three past-tense classes individually, not a
17
+ * `"model.*"` pattern. The pattern would also match `retrieved`, which
18
+ * fires on every row read in the application, so this would sit on the
19
+ * hottest path only to filter itself out. `ModelSaved` is skipped: it
20
+ * fires alongside both `ModelCreated` and `ModelUpdated` and would
21
+ * double the work. `ModelRestored` is skipped too — neither model uses
22
+ * soft deletes, so it cannot fire for them.
23
+ *
24
+ * ## Errors are not swallowed
25
+ *
26
+ * `EventDispatcher` awaits listeners with no isolation and
27
+ * `dispatchModelEvent` is awaited inside `save()`, so a throw here fails
28
+ * the caller's `Role.create()`. That is the right trade, and the opposite
29
+ * of the one `activity-logs` makes: a missing audit row is a lost record,
30
+ * but a cache that still grants a deleted role's permissions is a
31
+ * security failure. Better the write fails loudly and is retried than
32
+ * succeeds while leaving the cache lying.
33
+ */
34
+ export declare class InvalidatePermissionCacheListener implements Listener<ModelLifecycleEvent> {
35
+ private readonly app;
36
+ constructor(app: Application);
37
+ handle(event: ModelLifecycleEvent): Promise<void>;
38
+ }
39
+ //# sourceMappingURL=invalidate-permission-cache.listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidate-permission-cache.listener.d.ts","sourceRoot":"","sources":["../../src/listeners/invalidate-permission-cache.listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAMnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,iCAAkC,YAAW,QAAQ,CAAC,mBAAmB,CAAC;IACzE,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,WAAW;IAEvC,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAWxD"}
@@ -0,0 +1,50 @@
1
+ import { PERMISSIONS_TOKEN } from "../tokens.js";
2
+ import { Permission } from "../models/permission.model.js";
3
+ import { Role } from "../models/role.model.js";
4
+ /**
5
+ * Forgets the cached role/permission map when a `Role` or `Permission`
6
+ * row is written outside this package.
7
+ *
8
+ * `PermissionRegistrar` already forgets the cache on every write it
9
+ * performs, so this listener exists entirely for the writes it never
10
+ * sees: a seeder calling `Role.create(...)`, a migration backfilling
11
+ * `guard_name`, an admin screen saving a renamed role through the model.
12
+ * Without it, the map would keep answering from the old names until the
13
+ * TTL expired — which is precisely the class of bug a permission system
14
+ * cannot ship with.
15
+ *
16
+ * Subscribed to the three past-tense classes individually, not a
17
+ * `"model.*"` pattern. The pattern would also match `retrieved`, which
18
+ * fires on every row read in the application, so this would sit on the
19
+ * hottest path only to filter itself out. `ModelSaved` is skipped: it
20
+ * fires alongside both `ModelCreated` and `ModelUpdated` and would
21
+ * double the work. `ModelRestored` is skipped too — neither model uses
22
+ * soft deletes, so it cannot fire for them.
23
+ *
24
+ * ## Errors are not swallowed
25
+ *
26
+ * `EventDispatcher` awaits listeners with no isolation and
27
+ * `dispatchModelEvent` is awaited inside `save()`, so a throw here fails
28
+ * the caller's `Role.create()`. That is the right trade, and the opposite
29
+ * of the one `activity-logs` makes: a missing audit row is a lost record,
30
+ * but a cache that still grants a deleted role's permissions is a
31
+ * security failure. Better the write fails loudly and is retried than
32
+ * succeeds while leaving the cache lying.
33
+ */
34
+ export class InvalidatePermissionCacheListener {
35
+ app;
36
+ constructor(app) {
37
+ this.app = app;
38
+ }
39
+ async handle(event) {
40
+ // Identity, not `morphAlias()`: an app may legitimately map `Role` to
41
+ // some other alias, and the question here is only whether the row
42
+ // that moved belongs to one of the two tables this cache is built
43
+ // from.
44
+ if (event.model !== Role && event.model !== Permission) {
45
+ return;
46
+ }
47
+ await this.app.make(PERMISSIONS_TOKEN).forgetCache();
48
+ }
49
+ }
50
+ //# sourceMappingURL=invalidate-permission-cache.listener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidate-permission-cache.listener.js","sourceRoot":"","sources":["../../src/listeners/invalidate-permission-cache.listener.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,OAAO,iCAAiC;IACf;IAA7B,YAA6B,GAAgB;QAAhB,QAAG,GAAH,GAAG,CAAa;IAAG,CAAC;IAEjD,KAAK,CAAC,MAAM,CAAC,KAA0B;QACrC,sEAAsE;QACtE,kEAAkE;QAClE,kEAAkE;QAClE,QAAQ;QACR,IAAI,KAAK,CAAC,KAAK,KAAM,IAAc,IAAI,KAAK,CAAC,KAAK,KAAM,UAAoB,EAAE,CAAC;YAC7E,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAAsB,iBAAiB,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5E,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ import type { Assignee } from "../assignee.js";
2
+ /**
3
+ * The authenticated subject, or null for a guest.
4
+ *
5
+ * Reads `AUTH_TOKEN` directly rather than `request.user()`, which wraps
6
+ * its lookup in a `try`/`catch` and returns `undefined` on any failure.
7
+ * That is the wrong shape here: it would turn "the route forgot
8
+ * `authenticate()`, so there is no auth scope at all" into "this is a
9
+ * guest", which is an authorization decision made by accident. The
10
+ * underlying `MissingAuthContextError` must propagate and 500, exactly
11
+ * as it does through `can()`.
12
+ *
13
+ * Returns null when auth isn't bound at all, so these pipes still behave
14
+ * (as deny) in an app with no authentication.
15
+ *
16
+ * Only a `BaseModel` is usable: the pivots need a morph alias and a
17
+ * `bigint` key, and a non-model user (a token-guard adapter, a stub)
18
+ * has neither. Treated as a guest rather than thrown, because a pipe's
19
+ * job is to answer allow/deny and a subject that cannot hold a role
20
+ * holds none.
21
+ */
22
+ export declare function currentSubject(): Assignee | null;
23
+ //# sourceMappingURL=current-subject.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"current-subject.d.ts","sourceRoot":"","sources":["../../src/middleware/current-subject.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAO/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,IAAI,QAAQ,GAAG,IAAI,CAUhD"}
@@ -0,0 +1,31 @@
1
+ import { app, AUTH_TOKEN } from "@mahiframework/core";
2
+ import { BaseModel } from "@mahiframework/database";
3
+ /**
4
+ * The authenticated subject, or null for a guest.
5
+ *
6
+ * Reads `AUTH_TOKEN` directly rather than `request.user()`, which wraps
7
+ * its lookup in a `try`/`catch` and returns `undefined` on any failure.
8
+ * That is the wrong shape here: it would turn "the route forgot
9
+ * `authenticate()`, so there is no auth scope at all" into "this is a
10
+ * guest", which is an authorization decision made by accident. The
11
+ * underlying `MissingAuthContextError` must propagate and 500, exactly
12
+ * as it does through `can()`.
13
+ *
14
+ * Returns null when auth isn't bound at all, so these pipes still behave
15
+ * (as deny) in an app with no authentication.
16
+ *
17
+ * Only a `BaseModel` is usable: the pivots need a morph alias and a
18
+ * `bigint` key, and a non-model user (a token-guard adapter, a stub)
19
+ * has neither. Treated as a guest rather than thrown, because a pipe's
20
+ * job is to answer allow/deny and a subject that cannot hold a role
21
+ * holds none.
22
+ */
23
+ export function currentSubject() {
24
+ const container = app();
25
+ if (!container.has(AUTH_TOKEN)) {
26
+ return null;
27
+ }
28
+ const user = container.make(AUTH_TOKEN).userOrNull();
29
+ return user instanceof BaseModel ? user : null;
30
+ }
31
+ //# sourceMappingURL=current-subject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"current-subject.js","sourceRoot":"","sources":["../../src/middleware/current-subject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAQpD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC;IAExB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAoB,UAAU,CAAC,CAAC,UAAU,EAAE,CAAC;IAExE,OAAO,IAAI,YAAY,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type HttpPipeFn } from "@mahiframework/http";
2
+ import type { GuardOption } from "../permission-registrar.js";
3
+ /**
4
+ * Require the authenticated subject to hold one of `permissions`, by
5
+ * either route (a role, or directly).
6
+ *
7
+ * posts.patch("/{post}", update).middleware(authenticate(), permission("posts.edit"));
8
+ *
9
+ * ANY-of, as `role()` is. See that pipe for the guest/403 reasoning and
10
+ * the ordering constraint.
11
+ *
12
+ * `can("posts.edit")` from `@mahiframework/authorization` does the same
13
+ * thing, via the gate hook this package registers. Reach for this one
14
+ * when the gate hook is disabled (`permissions.gate: false`), when the
15
+ * check needs a non-default guard, or when the route should read as a
16
+ * permission check rather than an ability check.
17
+ */
18
+ export declare function permission(permissions: string | string[], options?: GuardOption): HttpPipeFn;
19
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../src/middleware/permission.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAuB,MAAM,4BAA4B,CAAC;AAInF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,GAAE,WAAgB,GAAG,UAAU,CAkBhG"}
@@ -0,0 +1,34 @@
1
+ import { app } from "@mahiframework/core";
2
+ import { HttpError } from "@mahiframework/http";
3
+ import { PERMISSIONS_TOKEN } from "../tokens.js";
4
+ import { currentSubject } from "./current-subject.js";
5
+ /**
6
+ * Require the authenticated subject to hold one of `permissions`, by
7
+ * either route (a role, or directly).
8
+ *
9
+ * posts.patch("/{post}", update).middleware(authenticate(), permission("posts.edit"));
10
+ *
11
+ * ANY-of, as `role()` is. See that pipe for the guest/403 reasoning and
12
+ * the ordering constraint.
13
+ *
14
+ * `can("posts.edit")` from `@mahiframework/authorization` does the same
15
+ * thing, via the gate hook this package registers. Reach for this one
16
+ * when the gate hook is disabled (`permissions.gate: false`), when the
17
+ * check needs a non-default guard, or when the route should read as a
18
+ * permission check rather than an ability check.
19
+ */
20
+ export function permission(permissions, options = {}) {
21
+ const names = Array.isArray(permissions) ? permissions : [permissions];
22
+ return async (request, next) => {
23
+ const subject = currentSubject();
24
+ if (subject === null) {
25
+ throw HttpError.forbidden();
26
+ }
27
+ const registrar = app().make(PERMISSIONS_TOKEN);
28
+ if (!(await registrar.hasAnyPermission(subject, names, options))) {
29
+ throw HttpError.forbidden();
30
+ }
31
+ return next(request);
32
+ };
33
+ }
34
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../../src/middleware/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,UAAU,CAAC,WAA8B,EAAE,UAAuB,EAAE;IAClF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;QAEjC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAsB,iBAAiB,CAAC,CAAC;QAErE,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { type HttpPipeFn } from "@mahiframework/http";
2
+ import type { GuardOption } from "../permission-registrar.js";
3
+ export interface RoleOrPermissionOptions extends GuardOption {
4
+ roles?: string[];
5
+ permissions?: string[];
6
+ }
7
+ /**
8
+ * Require the subject to hold one of `roles` OR one of `permissions`.
9
+ *
10
+ * group.middleware(
11
+ * authenticate(),
12
+ * roleOrPermission({ roles: ["admin"], permissions: ["posts.edit"] }),
13
+ * );
14
+ *
15
+ * A separate pipe rather than stacking `role()` and `permission()`,
16
+ * because stacking them is an AND: both would have to pass. This is the
17
+ * OR, and spatie's `role_or_permission:` middleware exists for the same
18
+ * reason.
19
+ *
20
+ * Takes a named object rather than two positional arrays: `roleOrPermission(
21
+ * ["admin"], ["posts.edit"])` is unreadable at the call site and silently
22
+ * wrong if the arguments are swapped, which is undetectable because both
23
+ * are `string[]`.
24
+ *
25
+ * Roles are checked first, so an admin costs no permission lookup. Both
26
+ * reads come from the same per-request memo regardless.
27
+ */
28
+ export declare function roleOrPermission(options: RoleOrPermissionOptions): HttpPipeFn;
29
+ //# sourceMappingURL=role-or-permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-or-permission.d.ts","sourceRoot":"","sources":["../../src/middleware/role-or-permission.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAuB,MAAM,4BAA4B,CAAC;AAInF,MAAM,WAAW,uBAAwB,SAAQ,WAAW;IAC1D,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CA0B7E"}
@@ -0,0 +1,45 @@
1
+ import { app } from "@mahiframework/core";
2
+ import { HttpError } from "@mahiframework/http";
3
+ import { PERMISSIONS_TOKEN } from "../tokens.js";
4
+ import { currentSubject } from "./current-subject.js";
5
+ /**
6
+ * Require the subject to hold one of `roles` OR one of `permissions`.
7
+ *
8
+ * group.middleware(
9
+ * authenticate(),
10
+ * roleOrPermission({ roles: ["admin"], permissions: ["posts.edit"] }),
11
+ * );
12
+ *
13
+ * A separate pipe rather than stacking `role()` and `permission()`,
14
+ * because stacking them is an AND: both would have to pass. This is the
15
+ * OR, and spatie's `role_or_permission:` middleware exists for the same
16
+ * reason.
17
+ *
18
+ * Takes a named object rather than two positional arrays: `roleOrPermission(
19
+ * ["admin"], ["posts.edit"])` is unreadable at the call site and silently
20
+ * wrong if the arguments are swapped, which is undetectable because both
21
+ * are `string[]`.
22
+ *
23
+ * Roles are checked first, so an admin costs no permission lookup. Both
24
+ * reads come from the same per-request memo regardless.
25
+ */
26
+ export function roleOrPermission(options) {
27
+ const roles = options.roles ?? [];
28
+ const permissions = options.permissions ?? [];
29
+ return async (request, next) => {
30
+ const subject = currentSubject();
31
+ if (subject === null) {
32
+ throw HttpError.forbidden();
33
+ }
34
+ const registrar = app().make(PERMISSIONS_TOKEN);
35
+ if (roles.length > 0 && (await registrar.hasAnyRole(subject, roles, options))) {
36
+ return next(request);
37
+ }
38
+ if (permissions.length > 0 &&
39
+ (await registrar.hasAnyPermission(subject, permissions, options))) {
40
+ return next(request);
41
+ }
42
+ throw HttpError.forbidden();
43
+ };
44
+ }
45
+ //# sourceMappingURL=role-or-permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-or-permission.js","sourceRoot":"","sources":["../../src/middleware/role-or-permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAOtD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgC;IAC/D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;IAClC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;IAE9C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;QAEjC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAsB,iBAAiB,CAAC,CAAC;QAErE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;YAC9E,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;QAED,IACE,WAAW,CAAC,MAAM,GAAG,CAAC;YACtB,CAAC,MAAM,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,EACjE,CAAC;YACD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;QAED,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { type HttpPipeFn } from "@mahiframework/http";
2
+ import type { GuardOption } from "../permission-registrar.js";
3
+ /**
4
+ * Require the authenticated subject to hold one of `roles`.
5
+ *
6
+ * admin.middleware(authenticate(), role("admin"));
7
+ * admin.middleware(authenticate(), role(["admin", "editor"]));
8
+ *
9
+ * ANY-of, not all-of, matching spatie's `role:` middleware. For all-of,
10
+ * use `Permissions.hasAllRoles()` in the controller, where the intent is
11
+ * legible; a route string cannot express the difference and silently
12
+ * choosing one is how an app ends up with the wrong one.
13
+ *
14
+ * Place AFTER `authenticate()`. A guest is denied with 403, not 401,
15
+ * because 401 is not an authorization decision — the same stance the
16
+ * gate takes. `Router.middleware()` throws when called after a route is
17
+ * registered, so getting the order wrong inside a group is a boot
18
+ * failure rather than a hole.
19
+ */
20
+ export declare function role(roles: string | string[], options?: GuardOption): HttpPipeFn;
21
+ //# sourceMappingURL=role.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/middleware/role.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAuB,MAAM,4BAA4B,CAAC;AAInF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,GAAE,WAAgB,GAAG,UAAU,CAkBpF"}
@@ -0,0 +1,36 @@
1
+ import { app } from "@mahiframework/core";
2
+ import { HttpError } from "@mahiframework/http";
3
+ import { PERMISSIONS_TOKEN } from "../tokens.js";
4
+ import { currentSubject } from "./current-subject.js";
5
+ /**
6
+ * Require the authenticated subject to hold one of `roles`.
7
+ *
8
+ * admin.middleware(authenticate(), role("admin"));
9
+ * admin.middleware(authenticate(), role(["admin", "editor"]));
10
+ *
11
+ * ANY-of, not all-of, matching spatie's `role:` middleware. For all-of,
12
+ * use `Permissions.hasAllRoles()` in the controller, where the intent is
13
+ * legible; a route string cannot express the difference and silently
14
+ * choosing one is how an app ends up with the wrong one.
15
+ *
16
+ * Place AFTER `authenticate()`. A guest is denied with 403, not 401,
17
+ * because 401 is not an authorization decision — the same stance the
18
+ * gate takes. `Router.middleware()` throws when called after a route is
19
+ * registered, so getting the order wrong inside a group is a boot
20
+ * failure rather than a hole.
21
+ */
22
+ export function role(roles, options = {}) {
23
+ const names = Array.isArray(roles) ? roles : [roles];
24
+ return async (request, next) => {
25
+ const subject = currentSubject();
26
+ if (subject === null) {
27
+ throw HttpError.forbidden();
28
+ }
29
+ const registrar = app().make(PERMISSIONS_TOKEN);
30
+ if (!(await registrar.hasAnyRole(subject, names, options))) {
31
+ throw HttpError.forbidden();
32
+ }
33
+ return next(request);
34
+ };
35
+ }
36
+ //# sourceMappingURL=role.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role.js","sourceRoot":"","sources":["../../src/middleware/role.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,IAAI,CAAC,KAAwB,EAAE,UAAuB,EAAE;IACtE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAErD,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;QAEjC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAsB,iBAAiB,CAAC,CAAC;QAErE,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;YAC3D,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { type Migration } from "@mahiframework/database";
2
+ /**
3
+ * All five permission tables in one migration, mirroring spatie's single
4
+ * `create_permission_tables`.
5
+ *
6
+ * One unit rather than five because the three pivots are meaningless
7
+ * without the two entity tables, and a partial rollback that left
8
+ * `model_has_roles` pointing at a dropped `roles` would be a worse state
9
+ * than either end of the migration.
10
+ *
11
+ * `roles` and `permissions` are identical in shape and deliberately kept
12
+ * as separate tables rather than one table with a `kind` column: the two
13
+ * are joined to each other (`role_has_permissions`), so a single table
14
+ * would need a self-referential pivot and every query would carry a
15
+ * `kind` predicate that the schema should have made impossible.
16
+ *
17
+ * `guard_name` is NOT NULL on both, and `(name, guard_name)` is unique.
18
+ * There is no "applies to every guard" value: a nullable `guard_name`
19
+ * cannot be made unique portably, because every engine treats NULLs as
20
+ * distinct in a unique index and the fix (`nullsNotDistinct`) is Postgres
21
+ * 15+ only and throws on SQLite/MySQL. Nullable would therefore have
22
+ * allowed unlimited duplicate `('admin', NULL)` rows, which is exactly
23
+ * the integrity this table exists to have.
24
+ *
25
+ * `id` on both is a `bigInteger` primary key, never auto-increment: the
26
+ * models assign a snowflake via `keyType: snowflake()`.
27
+ *
28
+ * COMPOSITE PRIMARY KEYS, not surrogate ids, on all three pivots. They
29
+ * ARE the dedupe mechanism: nothing in the framework deduplicates pivot
30
+ * inserts, so "assign the same role twice" has to be either a database
31
+ * error or a diffed write, and the constraint is what makes the first
32
+ * possible. `model_has_*` leads with the foreign id so the index also
33
+ * serves "who holds this role", with a second index on
34
+ * `(model_id, model_type)` for the far commoner "what does this subject
35
+ * hold".
36
+ *
37
+ * FOREIGN KEYS ON THE PACKAGE-OWNED SIDE ONLY. `role_id` and
38
+ * `permission_id` cascade on delete, because both tables are this
39
+ * package's and deleting a role must not leave assignments behind.
40
+ * `model_id` carries no foreign key at all: `users` is app-owned so the
41
+ * framework cannot assume its name, and the column holds the key of any
42
+ * assignable model. Same reasoning as `activity_logs.model_id` and
43
+ * `sessions.user_id`.
44
+ *
45
+ * `model_id` is a `bigInteger`, NOT text, which is the one place this
46
+ * schema diverges from `activity_logs`/`notifications`. Those tables are
47
+ * only ever read back by equality from code that already knows the type,
48
+ * so text (which holds every key type losslessly) costs them nothing.
49
+ * Here the column is the local side of a `morphToMany` pivot, and
50
+ * `buildPivotQuery()` binds the local key value RAW — a `bigint` against
51
+ * a `varchar` column makes Postgres raise `operator does not exist`. Text
52
+ * would therefore have broken the exported relation helpers, and with
53
+ * them `with("roles")` and `whereHas("roles", ...)`. The cost is that
54
+ * only snowflake-keyed models can hold roles, which
55
+ * `resolveAssignee()` enforces with a clear error rather than letting it
56
+ * reach SQL.
57
+ *
58
+ * No timestamps on the pivots. spatie has none either, and there is
59
+ * nothing to record: the composite key carries the entire fact.
60
+ */
61
+ declare const migration: Migration;
62
+ export default migration;
63
+ //# sourceMappingURL=0001_create_permission_tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0001_create_permission_tables.d.ts","sourceRoot":"","sources":["../../src/migrations/0001_create_permission_tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,QAAA,MAAM,SAAS,EAAE,SAuDhB,CAAC;AAEF,eAAe,SAAS,CAAC"}
@@ -0,0 +1,113 @@
1
+ import { Schema } from "@mahiframework/database";
2
+ /**
3
+ * All five permission tables in one migration, mirroring spatie's single
4
+ * `create_permission_tables`.
5
+ *
6
+ * One unit rather than five because the three pivots are meaningless
7
+ * without the two entity tables, and a partial rollback that left
8
+ * `model_has_roles` pointing at a dropped `roles` would be a worse state
9
+ * than either end of the migration.
10
+ *
11
+ * `roles` and `permissions` are identical in shape and deliberately kept
12
+ * as separate tables rather than one table with a `kind` column: the two
13
+ * are joined to each other (`role_has_permissions`), so a single table
14
+ * would need a self-referential pivot and every query would carry a
15
+ * `kind` predicate that the schema should have made impossible.
16
+ *
17
+ * `guard_name` is NOT NULL on both, and `(name, guard_name)` is unique.
18
+ * There is no "applies to every guard" value: a nullable `guard_name`
19
+ * cannot be made unique portably, because every engine treats NULLs as
20
+ * distinct in a unique index and the fix (`nullsNotDistinct`) is Postgres
21
+ * 15+ only and throws on SQLite/MySQL. Nullable would therefore have
22
+ * allowed unlimited duplicate `('admin', NULL)` rows, which is exactly
23
+ * the integrity this table exists to have.
24
+ *
25
+ * `id` on both is a `bigInteger` primary key, never auto-increment: the
26
+ * models assign a snowflake via `keyType: snowflake()`.
27
+ *
28
+ * COMPOSITE PRIMARY KEYS, not surrogate ids, on all three pivots. They
29
+ * ARE the dedupe mechanism: nothing in the framework deduplicates pivot
30
+ * inserts, so "assign the same role twice" has to be either a database
31
+ * error or a diffed write, and the constraint is what makes the first
32
+ * possible. `model_has_*` leads with the foreign id so the index also
33
+ * serves "who holds this role", with a second index on
34
+ * `(model_id, model_type)` for the far commoner "what does this subject
35
+ * hold".
36
+ *
37
+ * FOREIGN KEYS ON THE PACKAGE-OWNED SIDE ONLY. `role_id` and
38
+ * `permission_id` cascade on delete, because both tables are this
39
+ * package's and deleting a role must not leave assignments behind.
40
+ * `model_id` carries no foreign key at all: `users` is app-owned so the
41
+ * framework cannot assume its name, and the column holds the key of any
42
+ * assignable model. Same reasoning as `activity_logs.model_id` and
43
+ * `sessions.user_id`.
44
+ *
45
+ * `model_id` is a `bigInteger`, NOT text, which is the one place this
46
+ * schema diverges from `activity_logs`/`notifications`. Those tables are
47
+ * only ever read back by equality from code that already knows the type,
48
+ * so text (which holds every key type losslessly) costs them nothing.
49
+ * Here the column is the local side of a `morphToMany` pivot, and
50
+ * `buildPivotQuery()` binds the local key value RAW — a `bigint` against
51
+ * a `varchar` column makes Postgres raise `operator does not exist`. Text
52
+ * would therefore have broken the exported relation helpers, and with
53
+ * them `with("roles")` and `whereHas("roles", ...)`. The cost is that
54
+ * only snowflake-keyed models can hold roles, which
55
+ * `resolveAssignee()` enforces with a clear error rather than letting it
56
+ * reach SQL.
57
+ *
58
+ * No timestamps on the pivots. spatie has none either, and there is
59
+ * nothing to record: the composite key carries the entire fact.
60
+ */
61
+ const migration = {
62
+ async up() {
63
+ await Schema.create("roles", (table) => {
64
+ table.bigInteger("id").primary();
65
+ table.string("name");
66
+ table.string("guard_name");
67
+ table.timestamp("created_at");
68
+ table.timestamp("updated_at");
69
+ table.unique(["name", "guard_name"]);
70
+ });
71
+ await Schema.create("permissions", (table) => {
72
+ table.bigInteger("id").primary();
73
+ table.string("name");
74
+ table.string("guard_name");
75
+ table.timestamp("created_at");
76
+ table.timestamp("updated_at");
77
+ table.unique(["name", "guard_name"]);
78
+ });
79
+ await Schema.create("role_has_permissions", (table) => {
80
+ table.bigInteger("role_id");
81
+ table.bigInteger("permission_id");
82
+ table.primary(["role_id", "permission_id"]);
83
+ table.foreign("role_id").references("id").on("roles").cascadeOnDelete();
84
+ table.foreign("permission_id").references("id").on("permissions").cascadeOnDelete();
85
+ });
86
+ await Schema.create("model_has_roles", (table) => {
87
+ table.bigInteger("role_id");
88
+ table.string("model_type");
89
+ table.bigInteger("model_id");
90
+ table.primary(["role_id", "model_id", "model_type"]);
91
+ table.index(["model_id", "model_type"]);
92
+ table.foreign("role_id").references("id").on("roles").cascadeOnDelete();
93
+ });
94
+ await Schema.create("model_has_permissions", (table) => {
95
+ table.bigInteger("permission_id");
96
+ table.string("model_type");
97
+ table.bigInteger("model_id");
98
+ table.primary(["permission_id", "model_id", "model_type"]);
99
+ table.index(["model_id", "model_type"]);
100
+ table.foreign("permission_id").references("id").on("permissions").cascadeOnDelete();
101
+ });
102
+ },
103
+ /** Reverse creation order, so a pivot never outlives the table it references. */
104
+ async down() {
105
+ await Schema.drop("model_has_permissions");
106
+ await Schema.drop("model_has_roles");
107
+ await Schema.drop("role_has_permissions");
108
+ await Schema.drop("permissions");
109
+ await Schema.drop("roles");
110
+ },
111
+ };
112
+ export default migration;
113
+ //# sourceMappingURL=0001_create_permission_tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0001_create_permission_tables.js","sourceRoot":"","sources":["../../src/migrations/0001_create_permission_tables.ts"],"names":[],"mappings":"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"}