@awsmbiz/inbis-access-control 0.0.0-stage → 0.2.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 CHANGED
@@ -1,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # @awsmbiz/inbis-access-control
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Business permissions for Inbis applications: a typed permission catalogue and system roles
4
+ declared in code, roles and assignments in D1, `authorize()` and a `requirePermission()` Hono
5
+ middleware, and package-owned D1 migrations (ADR-4).
6
+
7
+ It is separate from the platform administrator role (`user.role = "admin"` in `@awsmbiz/inbis-auth`,
8
+ plan Q12): a platform admin gets **no** business permission implicitly (Q41).
9
+
10
+ ## Public API
11
+
12
+ | Entry | Contents |
13
+ |---|---|
14
+ | `@awsmbiz/inbis-access-control` | `defineAccessControl`, `createAccessControl`, `ACCESS_CONTROL_AUDIT_ACTIONS`, `ACCESS_CONTROL_MIGRATIONS_TABLE`; types `AccessControl`, `AccessControlDefinition`, `AccessControlInput`, `AccessControlOptions`, `AuthorizeInput`, `RoleAssignmentInput`, `RoleDeclaration`, `PermissionDefinition`, `SystemRoleDefinition`, `SeedResult`, `Role`, `RoleFields`, `CreateRoleInput`, `UpdateRoleInput`, `DeleteRoleInput`, `UserRole` |
15
+ | `@awsmbiz/inbis-access-control/hono` | `honoAccessControl` (→ `requirePermission`, `accessControlOf`), `mountRoleAdmin`; types `AccessControlResolver`, `AccessControlVariables`, `HonoAccessControl`, `HonoAccessControlOptions`, `MountRoleAdminOptions`, `RoleResponse`, `UserRoleResponse` |
16
+ | `migrations/` | Numbered, frozen SQL plus `migrations.lock.json`, shipped in the tarball |
17
+
18
+ Optional peer dependency: `hono` ^4.13.14 (for `/hono`).
19
+
20
+ ## Setting it up
21
+
22
+ 1. Declare an access-control binding to the app's database, after auth's (`user_role`
23
+ references auth's `user` table) and before audit's, with the same `database_id`:
24
+
25
+ ```jsonc
26
+ "d1_databases": [
27
+ { "binding": "AUTH_DB", "database_name": "myapp-dev", "database_id": "<id>",
28
+ "migrations_dir": "node_modules/@awsmbiz/inbis-auth/migrations", "migrations_table": "_inbis_auth_migrations" },
29
+ { "binding": "ACCESS_CONTROL_DB", "database_name": "myapp-dev", "database_id": "<id>",
30
+ "migrations_dir": "node_modules/@awsmbiz/inbis-access-control/migrations", "migrations_table": "_inbis_access_control_migrations" },
31
+ { "binding": "AUDIT_DB", ... },
32
+ { "binding": "DB", ... }
33
+ ]
34
+ ```
35
+
36
+ 2. Declare the catalogue once, in code:
37
+
38
+ ```ts
39
+ import { defineAccessControl } from "@awsmbiz/inbis-access-control";
40
+
41
+ export const access = defineAccessControl({
42
+ permissions: { "notes.read": "Read notes", "notes.write": "Create and edit notes" },
43
+ roles: {
44
+ reader: { name: "Reader", permissions: ["notes.read"] },
45
+ editor: { name: "Editor", permissions: ["notes.write"], inherits: ["reader"] },
46
+ },
47
+ });
48
+ ```
49
+
50
+ Permission names are dotted lowercase (`notes.read`) and typed: a role granting an
51
+ undeclared permission, inheriting an undeclared role, or a call to
52
+ `authorize({ permission: "notes.reed" })` is a compile error. At runtime
53
+ `defineAccessControl` also throws on malformed names, duplicate role names and inheritance
54
+ cycles.
55
+
56
+ 3. Protect routes (build the helpers once per isolate, the instance per request):
57
+
58
+ ```ts
59
+ import { createAccessControl } from "@awsmbiz/inbis-access-control";
60
+ import { honoAccessControl } from "@awsmbiz/inbis-access-control/hono";
61
+
62
+ const { requirePermission, accessControlOf } = honoAccessControl((c) =>
63
+ createAccessControl(requireBinding(c.env, "ACCESS_CONTROL_DB"), access, {
64
+ audit, // e.g. @awsmbiz/inbis-audit's sink
65
+ requestId: c.var.requestId,
66
+ logger: c.var.logger,
67
+ }),
68
+ );
69
+
70
+ app.get("/v1/notes", requireAuth(), requirePermission("notes.read"), handler);
71
+ // Or in code: await (await accessControlOf(c)).authorize({ userId, permission: "notes.write" })
72
+ ```
73
+
74
+ `requirePermission` reads the user from `c.var.user` (`@awsmbiz/inbis-auth/hono`'s
75
+ `sessionMiddleware`; pass `{ userId: (c) => ... }` otherwise). Signed out: `401
76
+ UNAUTHENTICATED`; not allowed: `403 FORBIDDEN` with `details.permission`. Both are
77
+ `AppError`s that `@awsmbiz/inbis-api` renders as the standard error envelope. Put it after
78
+ `requireAuth()`, which applies the session policy.
79
+
80
+ 4. Mount the role admin API behind the platform admin policy (changes also need a recent
81
+ sign-in):
82
+
83
+ ```ts
84
+ import { requireAdmin, requireRecentAdminSignIn } from "@awsmbiz/inbis-auth/hono";
85
+ import { mountRoleAdmin } from "@awsmbiz/inbis-access-control/hono";
86
+
87
+ mountRoleAdmin(app, { accessControl: accessControlOf, guard: requireAdmin(), changeGuard: requireRecentAdminSignIn() });
88
+ ```
89
+
90
+ 5. Register the audit actions with the app's trail:
91
+ `createAuditActions({ platform: [AUTH_AUDIT_ACTIONS, ACCESS_CONTROL_AUDIT_ACTIONS] })`.
92
+
93
+ ## Behaviour
94
+
95
+ - **Seeding.** `seed()` makes D1 hold exactly the declared catalogue in one batch: upserts
96
+ permissions and system roles, replaces system roles' grants and inheritance, and deletes
97
+ permissions and system roles no longer declared (with their grants and assignments). Custom
98
+ roles stay (losing grants of removed permissions). A fingerprint row makes it a single read
99
+ when nothing changed. `honoAccessControl` runs it on the isolate's first use. A custom role
100
+ with the same name as a new system role makes seeding fail (role names are unique).
101
+ - **`authorize({ userId, permission, resource? })`** reads D1 on every request: the user's
102
+ permissions through their roles and the roles those **explicitly** inherit (transitively),
103
+ memoised for the life of one `AccessControl` instance (one request), never cached in KV
104
+ (Q42). Removing a role takes effect on the next request. An undeclared permission denies,
105
+ even if a stale row grants it. `resource` is reserved for app-defined scoping and ignored
106
+ today.
107
+ - **Assignments.** `assignRole` / `unassignRole({ userId, roleId, actorId })` are idempotent
108
+ and record `access.role.assigned` / `access.role.unassigned` (subject `user:<id>`, metadata
109
+ `roleId`) through the injected `AuditSink` when they change something; a failing sink is
110
+ logged (`access.audit.record_failed`), never the caller's failure. Unknown role or user:
111
+ `404 ROLE_NOT_FOUND` / `404 USER_NOT_FOUND`. `rolesOf(userId)` lists direct assignments.
112
+ - **Custom roles.** Admins create roles from the declared permissions (`createRole`,
113
+ `updateRole`, `deleteRole`); system roles are read-only (`409 SYSTEM_ROLE_IMMUTABLE`). Names
114
+ are 1–64 characters, unique case-insensitively (`409 ROLE_NAME_TAKEN`); undeclared
115
+ permissions are `422 UNKNOWN_PERMISSION`, inheriting a missing role `422 UNKNOWN_ROLE`, and an
116
+ edge that would close a loop `422 ROLE_INHERITANCE_CYCLE` (`details.cycle`). Deleting a role
117
+ deletes its assignments, but is refused while another role inherits it
118
+ (`409 ROLE_INHERITED`, `details.roleIds`). Audited (subject `role:<id>`) as
119
+ `access.role.created` (the fields), `access.role.updated` (`changes`: `{ from, to }` for
120
+ `name`/`description`, `{ added, removed }` for `permissions`/`inherits`; an update that
121
+ changes nothing writes and audits nothing) and `access.role.deleted` (the fields and
122
+ `unassignedUserIds`). Role data holds no secrets. Concurrent edits are last-write-wins.
123
+
124
+ ## Role admin API
125
+
126
+ `mountRoleAdmin(app, { accessControl, guard, changeGuard, actorIdOf?, basePath? })` mounts, under
127
+ `/api/admin` (all `Cache-Control: no-store`, errors in the `@awsmbiz/inbis-api` envelope):
128
+
129
+ | Route | Body | Response |
130
+ |---|---|---|
131
+ | `GET /permissions` | | `{ items: [{ name, description }] }` |
132
+ | `GET /roles` | | `{ items: Role[] }`, system roles first, then by name |
133
+ | `GET /roles/:roleId` | | `Role` |
134
+ | `POST /roles` | `{ name, description?, permissions, inherits? }` | `201 Role` |
135
+ | `PATCH /roles/:roleId` | any of those fields | `Role` |
136
+ | `DELETE /roles/:roleId` | | `204` |
137
+ | `GET /users/:userId/roles` | | `{ items: UserRole[] }` |
138
+ | `PUT /users/:userId/roles/:roleId` | | `{ items: UserRole[] }` (idempotent) |
139
+ | `DELETE /users/:userId/roles/:roleId` | | `{ items: UserRole[] }` (idempotent) |
140
+
141
+ `Role` = `{ id, name, description, system, permissions, inherits, userCount, createdAt, updatedAt }`
142
+ and `UserRole` = `{ id, name, system, assignedAt, assignedBy }` (timestamps ISO 8601). Custom role
143
+ IDs are random UUIDs. Reads pass `guard` (the platform admin policy, `requireAdmin()`);
144
+ changes then pass `changeGuard` (`requireRecentAdminSignIn()`). Error codes: `422
145
+ VALIDATION_FAILED` (`details.issues[{ path, message }]`), `422 UNKNOWN_PERMISSION`, `422
146
+ UNKNOWN_ROLE`, `422 ROLE_INHERITANCE_CYCLE`, `409 ROLE_NAME_TAKEN`, `409 SYSTEM_ROLE_IMMUTABLE`,
147
+ `409 ROLE_INHERITED`, `404 ROLE_NOT_FOUND`, `404 USER_NOT_FOUND`, plus the guards' (`401
148
+ UNAUTHENTICATED`, `403 FORBIDDEN`, `403 TWO_FACTOR_REQUIRED`, `403 STEP_UP_REQUIRED`, `403
149
+ SESSION_NOT_FRESH`).
150
+ - **Account deletion.** `user_role.user_id` references `user(id)` `ON DELETE CASCADE`, so every
151
+ deletion path (`/delete-user`, the scheduled purge, `/admin/remove-user`) removes the user's
152
+ role assignments; no deletion hook is needed. The foreign key is deliberate, and it means
153
+ **access-control's migrations run after auth's** (ADR-4 platform order): an app installs
154
+ `@awsmbiz/inbis-auth` first, and a database without auth's `user` table cannot take them.
155
+ - **Actor.** Each change records the signed-in admin as its actor (`actorIdOf`, default
156
+ `c.var.user?.id`); a change that reaches the routes without one is refused with `401
157
+ UNAUTHENTICATED`, never audited without an actor.
158
+
159
+ ## Tables
160
+
161
+ `permission(name, description)`, `role(id, name unique, description, system, created_at,
162
+ updated_at)`, `role_permission(role_id, permission)`, `role_inheritance(role_id,
163
+ inherits_role_id)`, `user_role(user_id, role_id, assigned_at, assigned_by)` and
164
+ `access_control_seed` (the catalogue fingerprint). System role IDs are the keys declared in
165
+ code; timestamps are epoch milliseconds. Apps use the API, never the tables.
166
+
167
+ ## Changing the schema (maintainers)
168
+
169
+ Migrations are append-only: add `migrations/000N_*.sql`, then
170
+ `pnpm exec inbis-db lock migrations`. `pnpm check:migrations` fails if a released file changed.
@@ -0,0 +1,197 @@
1
+ import { AuditSink, Clock, Logger } from "@awsmbiz/inbis-core";
2
+ /**
3
+ * The permission catalogue and system roles, declared in the app's code
4
+ * (plan Q40). Permission names are typed: a typo is a compile error.
5
+ */
6
+ /** A system role as the app declares it. */
7
+ interface RoleDeclaration<P extends string, R extends string> {
8
+ /** Display name, unique among all roles (custom ones included). */
9
+ name: string;
10
+ description?: string;
11
+ /** Permissions the role grants itself. */
12
+ permissions: readonly P[];
13
+ /**
14
+ * Roles whose permissions this role also grants (transitively). Explicit
15
+ * inheritance is the only inheritance: nothing is implied by names or order.
16
+ */
17
+ inherits?: readonly R[];
18
+ }
19
+ interface AccessControlInput<P extends string, R extends string> {
20
+ /** Every permission the app checks, with a description for the admin UI: `{ "notes.read": "Read notes" }`. */
21
+ permissions: Readonly<Record<P, string>>;
22
+ /** System roles by ID: seeded into D1 and immutable through the API. */
23
+ roles: Readonly<Record<R, RoleDeclaration<NoInfer<P>, NoInfer<R>>>>;
24
+ }
25
+ interface PermissionDefinition<P extends string = string> {
26
+ name: P;
27
+ description: string;
28
+ }
29
+ interface SystemRoleDefinition<P extends string = string, R extends string = string> {
30
+ id: R;
31
+ name: string;
32
+ description: string | null;
33
+ permissions: readonly P[];
34
+ inherits: readonly R[];
35
+ }
36
+ /** A validated catalogue, from {@link defineAccessControl}. */
37
+ interface AccessControlDefinition<P extends string = string, R extends string = string> {
38
+ /** Sorted by name. */
39
+ readonly permissions: readonly PermissionDefinition<P>[];
40
+ /** Sorted by ID. */
41
+ readonly roles: readonly SystemRoleDefinition<P, R>[];
42
+ /** True for a declared permission name. */
43
+ hasPermission(name: string): name is P;
44
+ }
45
+ /**
46
+ * Declares the app's permissions and system roles. Throws on a malformed name,
47
+ * an undeclared permission or role, a duplicate role name or an inheritance
48
+ * cycle: all programming errors, found at startup.
49
+ *
50
+ * ```ts
51
+ * export const access = defineAccessControl({
52
+ * permissions: { "notes.read": "Read notes", "notes.write": "Write notes" },
53
+ * roles: {
54
+ * reader: { name: "Reader", permissions: ["notes.read"] },
55
+ * editor: { name: "Editor", permissions: ["notes.write"], inherits: ["reader"] },
56
+ * },
57
+ * });
58
+ * ```
59
+ */
60
+ declare function defineAccessControl<const P extends string, const R extends string>(input: AccessControlInput<P, R>): AccessControlDefinition<P, R>;
61
+ /** A custom role's editable fields. */
62
+ interface RoleFields {
63
+ /** Display name, 1–64 characters, unique among all roles (case-insensitive). */
64
+ name: string;
65
+ /** Up to 500 characters; `null` or `""` for none. */
66
+ description?: string | null;
67
+ /** Declared permission names the role grants itself. */
68
+ permissions: readonly string[];
69
+ /** IDs of roles (system or custom) whose permissions this role also grants. */
70
+ inherits?: readonly string[];
71
+ }
72
+ /** A role as the admin API shows it. */
73
+ interface Role {
74
+ id: string;
75
+ name: string;
76
+ description: string | null;
77
+ /** Declared in code: read-only through the API. */
78
+ system: boolean;
79
+ /** Permissions the role grants itself (declared ones only), sorted. */
80
+ permissions: string[];
81
+ /** IDs of the roles it inherits directly, sorted. */
82
+ inherits: string[];
83
+ /** Users the role is assigned to directly. */
84
+ userCount: number;
85
+ createdAt: Date;
86
+ updatedAt: Date;
87
+ }
88
+ /** A role assigned to a user directly. */
89
+ interface UserRole {
90
+ /** The role's ID. */
91
+ id: string;
92
+ name: string;
93
+ system: boolean;
94
+ assignedAt: Date;
95
+ /** The admin who assigned it; `null` for the system. */
96
+ assignedBy: string | null;
97
+ }
98
+ interface CreateRoleInput extends RoleFields {
99
+ actorId: string | null;
100
+ }
101
+ interface UpdateRoleInput {
102
+ roleId: string;
103
+ /** The fields to change; others keep their value. */
104
+ changes: Partial<RoleFields>;
105
+ actorId: string | null;
106
+ }
107
+ interface DeleteRoleInput {
108
+ roleId: string;
109
+ actorId: string | null;
110
+ }
111
+ /** What {@link seedCatalogue} did. */
112
+ interface SeedResult {
113
+ /** `false` when the database already held this exact catalogue (nothing written). */
114
+ changed: boolean;
115
+ }
116
+ interface AuthorizeInput<P extends string = string> {
117
+ userId: string;
118
+ permission: P;
119
+ /**
120
+ * The record the action applies to, e.g. `note:<id>`. Reserved: ignored
121
+ * today, so apps can add resource scoping later without a breaking change.
122
+ */
123
+ resource?: string;
124
+ }
125
+ interface RoleAssignmentInput {
126
+ userId: string;
127
+ roleId: string;
128
+ /** Who made the change (audited); `null` for the system. */
129
+ actorId: string | null;
130
+ }
131
+ /**
132
+ * Access control for one request (create one per request: answers are
133
+ * memoised for its life only, and forgotten when it changes roles or assignments).
134
+ */
135
+ interface AccessControl<P extends string = string> {
136
+ /**
137
+ * `true` when one of the user's roles, or a role they explicitly inherit,
138
+ * grants the permission. Reads D1 (once per user per instance, never a
139
+ * cache), so revocation applies on the next request. Unknown permissions
140
+ * deny. A platform admin (`role` `admin`) gets nothing implicitly.
141
+ */
142
+ authorize(input: AuthorizeInput<P>): Promise<boolean>;
143
+ /** IDs of the roles assigned to the user directly (not inherited), sorted. */
144
+ rolesOf(userId: string): Promise<string[]>;
145
+ /**
146
+ * Assigns a role (idempotent). Audited as `access.role.assigned` when it
147
+ * changes anything. `404 ROLE_NOT_FOUND` / `404 USER_NOT_FOUND`.
148
+ */
149
+ assignRole(input: RoleAssignmentInput): Promise<void>;
150
+ /** Removes a role assignment (idempotent). Audited as `access.role.unassigned` when it changes anything. */
151
+ unassignRole(input: RoleAssignmentInput): Promise<void>;
152
+ /** Seeds the catalogue and system roles into D1 (see `seedCatalogue`); run once per isolate before use. */
153
+ seed(): Promise<SeedResult>;
154
+ /** The declared permission catalogue, sorted by name. */
155
+ listPermissions(): PermissionDefinition<P>[];
156
+ /** Every role: system roles first, then by name. */
157
+ listRoles(): Promise<Role[]>;
158
+ /** `404 ROLE_NOT_FOUND` for an unknown ID. */
159
+ getRole(roleId: string): Promise<Role>;
160
+ /**
161
+ * Creates a custom role. Audited as `access.role.created`. `422 VALIDATION_FAILED`
162
+ * (name, description), `422 UNKNOWN_PERMISSION` (`details.permissions`),
163
+ * `422 UNKNOWN_ROLE` (`details.roleIds`), `409 ROLE_NAME_TAKEN`.
164
+ */
165
+ createRole(input: CreateRoleInput): Promise<Role>;
166
+ /**
167
+ * Changes a custom role's fields. Audited as `access.role.updated` with a diff
168
+ * (nothing is written or audited when nothing changes). As {@link createRole},
169
+ * plus `404 ROLE_NOT_FOUND`, `409 SYSTEM_ROLE_IMMUTABLE` and
170
+ * `422 ROLE_INHERITANCE_CYCLE` (`details.cycle`, role IDs from this role back to it).
171
+ */
172
+ updateRole(input: UpdateRoleInput): Promise<Role>;
173
+ /**
174
+ * Deletes a custom role and its assignments. Audited as `access.role.deleted`.
175
+ * `404 ROLE_NOT_FOUND`, `409 SYSTEM_ROLE_IMMUTABLE`, `409 ROLE_INHERITED`
176
+ * (`details.roleIds`: roles that inherit it).
177
+ */
178
+ deleteRole(input: DeleteRoleInput): Promise<void>;
179
+ /** The user's directly assigned roles. `404 USER_NOT_FOUND`. */
180
+ userRoles(userId: string): Promise<UserRole[]>;
181
+ }
182
+ interface AccessControlOptions {
183
+ /** Receives role-assignment events (e.g. `@awsmbiz/inbis-audit`'s sink). Without it, nothing is audited. */
184
+ audit?: AuditSink;
185
+ /** Stamped on audit events. */
186
+ requestId?: string;
187
+ /** Receives `access.audit.record_failed`. Defaults to a console JSON logger. */
188
+ logger?: Logger;
189
+ clock?: Clock;
190
+ }
191
+ /**
192
+ * Access control over the database holding the access-control tables (the
193
+ * app's access-control binding), for one request.
194
+ */
195
+ declare function createAccessControl<P extends string>(db: D1Database, definition: AccessControlDefinition<P>, options?: AccessControlOptions): AccessControl<P>;
196
+ export { SystemRoleDefinition as _, createAccessControl as a, DeleteRoleInput as c, UserRole as d, RoleFields as f, RoleDeclaration as g, PermissionDefinition as h, RoleAssignmentInput as i, Role as l, AccessControlInput as m, AccessControlOptions as n, SeedResult as o, AccessControlDefinition as p, AuthorizeInput as r, CreateRoleInput as s, AccessControl as t, UpdateRoleInput as u, defineAccessControl as v };
197
+ //# sourceMappingURL=access-control-eE6LZrpn.d.ts.map
package/dist/hono.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { d as UserRole, l as Role, t as AccessControl } from "./access-control-eE6LZrpn.js";
2
+ import { Context, Env, Hono, MiddlewareHandler } from "hono";
3
+ /** A role as the admin API returns it: {@link Role} with ISO 8601 timestamps. */
4
+ interface RoleResponse extends Omit<Role, "createdAt" | "updatedAt"> {
5
+ createdAt: string;
6
+ updatedAt: string;
7
+ }
8
+ /** A user's role as the admin API returns it: {@link UserRole} with an ISO 8601 `assignedAt`. */
9
+ interface UserRoleResponse extends Omit<UserRole, "assignedAt"> {
10
+ assignedAt: string;
11
+ }
12
+ interface MountRoleAdminOptions<E extends Env = Env> {
13
+ /** The request's access control, e.g. `accessControlOf` from {@link honoAccessControl} (seeds the catalogue). */
14
+ accessControl: (c: Context<E>) => Promise<AccessControl>;
15
+ /**
16
+ * Admits the caller to every route, or throws: pass the app's admin policy
17
+ * (`requireAdmin()` from `@awsmbiz/inbis-auth/hono`). Any env: guards are typed for their own variables.
18
+ */
19
+ guard: MiddlewareHandler;
20
+ /**
21
+ * Runs after `guard` on the routes that change something: pass the admin
22
+ * recent-sign-in check (`requireRecentAdminSignIn()` from `@awsmbiz/inbis-auth/hono`).
23
+ */
24
+ changeGuard: MiddlewareHandler;
25
+ /**
26
+ * The signed-in admin's user ID, recorded as the actor of each change. Default `c.var.user?.id`.
27
+ * A change without one is refused with `401 UNAUTHENTICATED`.
28
+ */
29
+ actorIdOf?: (c: Context<E>) => string | null | undefined;
30
+ /** Path prefix. Default `/api/admin`. */
31
+ basePath?: string;
32
+ }
33
+ /**
34
+ * Mounts the role admin API (under `/api/admin` by default). Every response is
35
+ * `Cache-Control: no-store`; errors are `AppError`s, rendered as the standard
36
+ * envelope by `@awsmbiz/inbis-api`.
37
+ *
38
+ * Reads (`guard`):
39
+ * - `GET /permissions` → `{ items: [{ name, description }] }`
40
+ * - `GET /roles` → `{ items: RoleResponse[] }` (system roles first, then by name)
41
+ * - `GET /roles/:roleId` → `RoleResponse` (`404 ROLE_NOT_FOUND`)
42
+ * - `GET /users/:userId/roles` → `{ items: UserRoleResponse[] }` (`404 USER_NOT_FOUND`)
43
+ *
44
+ * Changes (`guard`, then `changeGuard`), each audited:
45
+ * - `POST /roles` `{ name, description?, permissions, inherits? }` → `201 RoleResponse`
46
+ * - `PATCH /roles/:roleId` (any of those fields) → `RoleResponse`
47
+ * - `DELETE /roles/:roleId` → `204` (its assignments go with it)
48
+ * - `PUT /users/:userId/roles/:roleId` → the user's roles (idempotent)
49
+ * - `DELETE /users/:userId/roles/:roleId` → the user's roles (idempotent)
50
+ *
51
+ * Refusals: `422 VALIDATION_FAILED` (`details.issues[{ path, message }]`),
52
+ * `422 UNKNOWN_PERMISSION` (`details.permissions`), `422 UNKNOWN_ROLE`
53
+ * (`details.roleIds`), `422 ROLE_INHERITANCE_CYCLE` (`details.cycle`),
54
+ * `409 ROLE_NAME_TAKEN` (`details.name`), `409 SYSTEM_ROLE_IMMUTABLE`,
55
+ * `409 ROLE_INHERITED` (`details.roleIds`), `404 ROLE_NOT_FOUND`,
56
+ * `404 USER_NOT_FOUND`, plus whatever the guards refuse with.
57
+ */
58
+ export declare function mountRoleAdmin<E extends Env>(app: Hono<E>, options: MountRoleAdminOptions<E>): void;
59
+ /** Builds the access control for a request, from the app's bindings. */
60
+ export type AccessControlResolver<P extends string, E extends Env = Env> = (c: Context<E>) => AccessControl<P>;
61
+ /** Values put on the Hono context (`c.var`). */
62
+ export interface AccessControlVariables {
63
+ /** The request's access control (one per request: its answers are memoised for the request only). */
64
+ accessControl: AccessControl;
65
+ }
66
+ export interface HonoAccessControlOptions<E extends Env> {
67
+ /**
68
+ * The signed-in user's ID. Defaults to `c.var.user?.id`, as
69
+ * `@awsmbiz/inbis-auth/hono`'s `sessionMiddleware` sets it.
70
+ */
71
+ userId?: (c: Context<E>) => string | null | undefined;
72
+ }
73
+ export interface HonoAccessControl<P extends string, E extends Env> {
74
+ /**
75
+ * Middleware: `401 UNAUTHENTICATED` without a signed-in user, `403
76
+ * FORBIDDEN` (`details.permission`) unless `authorize()` allows the
77
+ * permission; both `AppError`s, rendered as the error envelope by
78
+ * `@awsmbiz/inbis-api`. Put it after `requireAuth()`, which applies the session policy.
79
+ */
80
+ requirePermission: (permission: P) => MiddlewareHandler<E>;
81
+ /** The request's access control (created, and the catalogue seeded, on first use). */
82
+ accessControlOf: (c: Context<E>) => Promise<AccessControl<P>>;
83
+ }
84
+ /**
85
+ * Hono helpers over one resolver. Build once per Worker isolate: the
86
+ * catalogue is seeded on the first use (and again after a failed attempt).
87
+ */
88
+ export declare function honoAccessControl<P extends string, E extends Env = Env>(resolve: AccessControlResolver<P, E>, options?: HonoAccessControlOptions<E>): HonoAccessControl<P, E>;
89
+ export type { MountRoleAdminOptions, RoleResponse, UserRoleResponse };
90
+ //# sourceMappingURL=hono.d.ts.map
package/dist/hono.js ADDED
@@ -0,0 +1,172 @@
1
+ import { i as parseRoleFields } from "./role-fields-ClqODoYn.js";
2
+ import { AppError, validationFailed } from "@awsmbiz/inbis-core";
3
+ // src/role-routes.ts
4
+ const toRoleResponse = (role) => ({
5
+ ...role,
6
+ createdAt: role.createdAt.toISOString(),
7
+ updatedAt: role.updatedAt.toISOString()
8
+ });
9
+ const toUserRoles = (roles) => ({ items: roles.map((role) => ({
10
+ ...role,
11
+ assignedAt: role.assignedAt.toISOString()
12
+ })) });
13
+ async function bodyOf(c) {
14
+ try {
15
+ return await c.req.json();
16
+ } catch {
17
+ throw validationFailed([{
18
+ path: [],
19
+ message: "Must be a JSON object."
20
+ }]);
21
+ }
22
+ }
23
+ /**
24
+ * Mounts the role admin API (under `/api/admin` by default). Every response is
25
+ * `Cache-Control: no-store`; errors are `AppError`s, rendered as the standard
26
+ * envelope by `@awsmbiz/inbis-api`.
27
+ *
28
+ * Reads (`guard`):
29
+ * - `GET /permissions` → `{ items: [{ name, description }] }`
30
+ * - `GET /roles` → `{ items: RoleResponse[] }` (system roles first, then by name)
31
+ * - `GET /roles/:roleId` → `RoleResponse` (`404 ROLE_NOT_FOUND`)
32
+ * - `GET /users/:userId/roles` → `{ items: UserRoleResponse[] }` (`404 USER_NOT_FOUND`)
33
+ *
34
+ * Changes (`guard`, then `changeGuard`), each audited:
35
+ * - `POST /roles` `{ name, description?, permissions, inherits? }` → `201 RoleResponse`
36
+ * - `PATCH /roles/:roleId` (any of those fields) → `RoleResponse`
37
+ * - `DELETE /roles/:roleId` → `204` (its assignments go with it)
38
+ * - `PUT /users/:userId/roles/:roleId` → the user's roles (idempotent)
39
+ * - `DELETE /users/:userId/roles/:roleId` → the user's roles (idempotent)
40
+ *
41
+ * Refusals: `422 VALIDATION_FAILED` (`details.issues[{ path, message }]`),
42
+ * `422 UNKNOWN_PERMISSION` (`details.permissions`), `422 UNKNOWN_ROLE`
43
+ * (`details.roleIds`), `422 ROLE_INHERITANCE_CYCLE` (`details.cycle`),
44
+ * `409 ROLE_NAME_TAKEN` (`details.name`), `409 SYSTEM_ROLE_IMMUTABLE`,
45
+ * `409 ROLE_INHERITED` (`details.roleIds`), `404 ROLE_NOT_FOUND`,
46
+ * `404 USER_NOT_FOUND`, plus whatever the guards refuse with.
47
+ */
48
+ function mountRoleAdmin(app, options) {
49
+ const base = options.basePath ?? "/api/admin";
50
+ const { guard, changeGuard } = options;
51
+ const own = (c) => c;
52
+ const access = (c) => options.accessControl(own(c));
53
+ const actorIdOf = options.actorIdOf ?? ((c) => c.get("user")?.id);
54
+ const actorOf = (c) => {
55
+ const id = actorIdOf(own(c));
56
+ if (id === null || id === void 0 || id === "") throw new AppError({
57
+ code: "UNAUTHENTICATED",
58
+ status: 401,
59
+ message: "Sign in to continue."
60
+ });
61
+ return id;
62
+ };
63
+ const hono = app;
64
+ const noStore = async (c, next) => {
65
+ c.header("cache-control", "no-store");
66
+ await next();
67
+ };
68
+ hono.get(`${base}/permissions`, guard, noStore, async (c) => c.json({ items: (await access(c)).listPermissions() }));
69
+ hono.get(`${base}/roles`, guard, noStore, async (c) => c.json({ items: (await (await access(c)).listRoles()).map(toRoleResponse) }));
70
+ hono.get(`${base}/roles/:roleId`, guard, noStore, async (c) => c.json(toRoleResponse(await (await access(c)).getRole(c.req.param("roleId")))));
71
+ hono.get(`${base}/users/:userId/roles`, guard, noStore, async (c) => c.json(toUserRoles(await (await access(c)).userRoles(c.req.param("userId")))));
72
+ hono.post(`${base}/roles`, guard, changeGuard, noStore, async (c) => {
73
+ const fields = parseRoleFields(await bodyOf(c));
74
+ const role = await (await access(c)).createRole({
75
+ ...fields,
76
+ actorId: actorOf(c)
77
+ });
78
+ return c.json(toRoleResponse(role), 201);
79
+ });
80
+ hono.patch(`${base}/roles/:roleId`, guard, changeGuard, noStore, async (c) => {
81
+ const changes = parseRoleFields(await bodyOf(c), { partial: true });
82
+ const role = await (await access(c)).updateRole({
83
+ roleId: c.req.param("roleId"),
84
+ changes,
85
+ actorId: actorOf(c)
86
+ });
87
+ return c.json(toRoleResponse(role));
88
+ });
89
+ hono.delete(`${base}/roles/:roleId`, guard, changeGuard, noStore, async (c) => {
90
+ await (await access(c)).deleteRole({
91
+ roleId: c.req.param("roleId"),
92
+ actorId: actorOf(c)
93
+ });
94
+ return c.body(null, 204);
95
+ });
96
+ hono.put(`${base}/users/:userId/roles/:roleId`, guard, changeGuard, noStore, async (c) => {
97
+ const control = await access(c);
98
+ const userId = c.req.param("userId");
99
+ await control.assignRole({
100
+ userId,
101
+ roleId: c.req.param("roleId"),
102
+ actorId: actorOf(c)
103
+ });
104
+ return c.json(toUserRoles(await control.userRoles(userId)));
105
+ });
106
+ hono.delete(`${base}/users/:userId/roles/:roleId`, guard, changeGuard, noStore, async (c) => {
107
+ const control = await access(c);
108
+ const userId = c.req.param("userId");
109
+ await control.unassignRole({
110
+ userId,
111
+ roleId: c.req.param("roleId"),
112
+ actorId: actorOf(c)
113
+ });
114
+ return c.json(toUserRoles(await control.userRoles(userId)));
115
+ });
116
+ }
117
+ // src/hono.ts
118
+ /**
119
+ * Hono integration: `@awsmbiz/inbis-access-control/hono`.
120
+ *
121
+ * ```ts
122
+ * const { requirePermission } = honoAccessControl((c) =>
123
+ * createAccessControl(c.env.ACCESS_CONTROL_DB, access, { audit, requestId: c.var.requestId }),
124
+ * );
125
+ * app.get("/v1/notes", requireAuth(), requirePermission("notes.read"), handler);
126
+ * ```
127
+ */
128
+ /**
129
+ * Hono helpers over one resolver. Build once per Worker isolate: the
130
+ * catalogue is seeded on the first use (and again after a failed attempt).
131
+ */
132
+ function honoAccessControl(resolve, options = {}) {
133
+ let seeded;
134
+ const userIdOf = options.userId ?? ((c) => c.get("user")?.id);
135
+ async function accessControlOf(c) {
136
+ const ctx = c;
137
+ const existing = ctx.get("accessControl");
138
+ if (existing !== void 0) return existing;
139
+ const access = resolve(c);
140
+ seeded ??= access.seed().catch((error) => {
141
+ seeded = void 0;
142
+ throw error;
143
+ });
144
+ await seeded;
145
+ ctx.set("accessControl", access);
146
+ return access;
147
+ }
148
+ return {
149
+ accessControlOf,
150
+ requirePermission: (permission) => async (c, next) => {
151
+ const userId = userIdOf(c);
152
+ if (userId === null || userId === void 0) throw new AppError({
153
+ code: "UNAUTHENTICATED",
154
+ status: 401,
155
+ message: "Sign in to continue."
156
+ });
157
+ if (!await (await accessControlOf(c)).authorize({
158
+ userId,
159
+ permission
160
+ })) throw new AppError({
161
+ code: "FORBIDDEN",
162
+ status: 403,
163
+ message: "You do not have permission to do this.",
164
+ details: { permission }
165
+ });
166
+ await next();
167
+ }
168
+ };
169
+ }
170
+ export { honoAccessControl, mountRoleAdmin };
171
+
172
+ //# sourceMappingURL=hono.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.js","names":[],"sources":["../src/role-routes.ts","../src/hono.ts"],"sourcesContent":["import { AppError, validationFailed } from \"@awsmbiz/inbis-core\";\nimport type { Context, Env, Hono, MiddlewareHandler } from \"hono\";\nimport type { AccessControl } from \"./access-control.js\";\nimport { parseRoleFields } from \"./role-fields.js\";\nimport type { Role, UserRole } from \"./roles.js\";\n\n/** A role as the admin API returns it: {@link Role} with ISO 8601 timestamps. */\nexport interface RoleResponse extends Omit<Role, \"createdAt\" | \"updatedAt\"> {\n createdAt: string;\n updatedAt: string;\n}\n\n/** A user's role as the admin API returns it: {@link UserRole} with an ISO 8601 `assignedAt`. */\nexport interface UserRoleResponse extends Omit<UserRole, \"assignedAt\"> {\n assignedAt: string;\n}\n\nexport interface MountRoleAdminOptions<E extends Env = Env> {\n /** The request's access control, e.g. `accessControlOf` from {@link honoAccessControl} (seeds the catalogue). */\n accessControl: (c: Context<E>) => Promise<AccessControl>;\n /**\n * Admits the caller to every route, or throws: pass the app's admin policy\n * (`requireAdmin()` from `@awsmbiz/inbis-auth/hono`). Any env: guards are typed for their own variables.\n */\n guard: MiddlewareHandler;\n /**\n * Runs after `guard` on the routes that change something: pass the admin\n * recent-sign-in check (`requireRecentAdminSignIn()` from `@awsmbiz/inbis-auth/hono`).\n */\n changeGuard: MiddlewareHandler;\n /**\n * The signed-in admin's user ID, recorded as the actor of each change. Default `c.var.user?.id`.\n * A change without one is refused with `401 UNAUTHENTICATED`.\n */\n actorIdOf?: (c: Context<E>) => string | null | undefined;\n /** Path prefix. Default `/api/admin`. */\n basePath?: string;\n}\n\nconst toRoleResponse = (role: Role): RoleResponse => ({\n ...role,\n createdAt: role.createdAt.toISOString(),\n updatedAt: role.updatedAt.toISOString(),\n});\n\nconst toUserRoles = (roles: UserRole[]) => ({\n items: roles.map((role): UserRoleResponse => ({ ...role, assignedAt: role.assignedAt.toISOString() })),\n});\n\nasync function bodyOf(c: Context): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n throw validationFailed([{ path: [], message: \"Must be a JSON object.\" }]);\n }\n}\n\n/**\n * Mounts the role admin API (under `/api/admin` by default). Every response is\n * `Cache-Control: no-store`; errors are `AppError`s, rendered as the standard\n * envelope by `@awsmbiz/inbis-api`.\n *\n * Reads (`guard`):\n * - `GET /permissions` → `{ items: [{ name, description }] }`\n * - `GET /roles` → `{ items: RoleResponse[] }` (system roles first, then by name)\n * - `GET /roles/:roleId` → `RoleResponse` (`404 ROLE_NOT_FOUND`)\n * - `GET /users/:userId/roles` → `{ items: UserRoleResponse[] }` (`404 USER_NOT_FOUND`)\n *\n * Changes (`guard`, then `changeGuard`), each audited:\n * - `POST /roles` `{ name, description?, permissions, inherits? }` → `201 RoleResponse`\n * - `PATCH /roles/:roleId` (any of those fields) → `RoleResponse`\n * - `DELETE /roles/:roleId` → `204` (its assignments go with it)\n * - `PUT /users/:userId/roles/:roleId` → the user's roles (idempotent)\n * - `DELETE /users/:userId/roles/:roleId` → the user's roles (idempotent)\n *\n * Refusals: `422 VALIDATION_FAILED` (`details.issues[{ path, message }]`),\n * `422 UNKNOWN_PERMISSION` (`details.permissions`), `422 UNKNOWN_ROLE`\n * (`details.roleIds`), `422 ROLE_INHERITANCE_CYCLE` (`details.cycle`),\n * `409 ROLE_NAME_TAKEN` (`details.name`), `409 SYSTEM_ROLE_IMMUTABLE`,\n * `409 ROLE_INHERITED` (`details.roleIds`), `404 ROLE_NOT_FOUND`,\n * `404 USER_NOT_FOUND`, plus whatever the guards refuse with.\n */\nexport function mountRoleAdmin<E extends Env>(app: Hono<E>, options: MountRoleAdminOptions<E>): void {\n const base = options.basePath ?? \"/api/admin\";\n const { guard, changeGuard } = options;\n // Routes run with the app's env E; Hono types the plain app's handlers with `any`.\n const own = (c: Context): Context<E> => c as Context<E>;\n const access = (c: Context) => options.accessControl(own(c));\n const actorIdOf =\n options.actorIdOf ??\n ((c: Context<E>) => (c as unknown as Context<{ Variables: { user?: { id: string } | null } }>).get(\"user\")?.id);\n // Fails closed: a change without a signed-in admin is refused, never audited without an actor.\n const actorOf = (c: Context): string => {\n const id = actorIdOf(own(c));\n if (id === null || id === undefined || id === \"\") {\n throw new AppError({ code: \"UNAUTHENTICATED\", status: 401, message: \"Sign in to continue.\" });\n }\n return id;\n };\n // Hono's handler types do not compose over a generic env: register on the plain app.\n const hono = app as unknown as Hono;\n const noStore: MiddlewareHandler = async (c, next) => {\n c.header(\"cache-control\", \"no-store\");\n await next();\n };\n\n hono.get(`${base}/permissions`, guard, noStore, async (c) => c.json({ items: (await access(c)).listPermissions() }));\n hono.get(`${base}/roles`, guard, noStore, async (c) => c.json({ items: (await (await access(c)).listRoles()).map(toRoleResponse) }));\n hono.get(`${base}/roles/:roleId`, guard, noStore, async (c) => c.json(toRoleResponse(await (await access(c)).getRole(c.req.param(\"roleId\")))));\n hono.get(`${base}/users/:userId/roles`, guard, noStore, async (c) =>\n c.json(toUserRoles(await (await access(c)).userRoles(c.req.param(\"userId\")))),\n );\n\n hono.post(`${base}/roles`, guard, changeGuard, noStore, async (c) => {\n const fields = parseRoleFields(await bodyOf(c));\n const role = await (await access(c)).createRole({ ...fields, actorId: actorOf(c) });\n return c.json(toRoleResponse(role), 201);\n });\n hono.patch(`${base}/roles/:roleId`, guard, changeGuard, noStore, async (c) => {\n const changes = parseRoleFields(await bodyOf(c), { partial: true });\n const role = await (await access(c)).updateRole({ roleId: c.req.param(\"roleId\"), changes, actorId: actorOf(c) });\n return c.json(toRoleResponse(role));\n });\n hono.delete(`${base}/roles/:roleId`, guard, changeGuard, noStore, async (c) => {\n await (await access(c)).deleteRole({ roleId: c.req.param(\"roleId\"), actorId: actorOf(c) });\n return c.body(null, 204);\n });\n hono.put(`${base}/users/:userId/roles/:roleId`, guard, changeGuard, noStore, async (c) => {\n const control = await access(c);\n const userId = c.req.param(\"userId\");\n await control.assignRole({ userId, roleId: c.req.param(\"roleId\"), actorId: actorOf(c) });\n return c.json(toUserRoles(await control.userRoles(userId)));\n });\n hono.delete(`${base}/users/:userId/roles/:roleId`, guard, changeGuard, noStore, async (c) => {\n const control = await access(c);\n const userId = c.req.param(\"userId\");\n await control.unassignRole({ userId, roleId: c.req.param(\"roleId\"), actorId: actorOf(c) });\n return c.json(toUserRoles(await control.userRoles(userId)));\n });\n}\n","/**\n * Hono integration: `@awsmbiz/inbis-access-control/hono`.\n *\n * ```ts\n * const { requirePermission } = honoAccessControl((c) =>\n * createAccessControl(c.env.ACCESS_CONTROL_DB, access, { audit, requestId: c.var.requestId }),\n * );\n * app.get(\"/v1/notes\", requireAuth(), requirePermission(\"notes.read\"), handler);\n * ```\n */\nimport { AppError } from \"@awsmbiz/inbis-core\";\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\nimport type { AccessControl } from \"./access-control.js\";\n\n/** Builds the access control for a request, from the app's bindings. */\nexport type AccessControlResolver<P extends string, E extends Env = Env> = (c: Context<E>) => AccessControl<P>;\n\n/** Values put on the Hono context (`c.var`). */\nexport interface AccessControlVariables {\n /** The request's access control (one per request: its answers are memoised for the request only). */\n accessControl: AccessControl;\n}\n\nexport interface HonoAccessControlOptions<E extends Env> {\n /**\n * The signed-in user's ID. Defaults to `c.var.user?.id`, as\n * `@awsmbiz/inbis-auth/hono`'s `sessionMiddleware` sets it.\n */\n userId?: (c: Context<E>) => string | null | undefined;\n}\n\nexport interface HonoAccessControl<P extends string, E extends Env> {\n /**\n * Middleware: `401 UNAUTHENTICATED` without a signed-in user, `403\n * FORBIDDEN` (`details.permission`) unless `authorize()` allows the\n * permission; both `AppError`s, rendered as the error envelope by\n * `@awsmbiz/inbis-api`. Put it after `requireAuth()`, which applies the session policy.\n */\n requirePermission: (permission: P) => MiddlewareHandler<E>;\n /** The request's access control (created, and the catalogue seeded, on first use). */\n accessControlOf: (c: Context<E>) => Promise<AccessControl<P>>;\n}\n\n/**\n * Hono helpers over one resolver. Build once per Worker isolate: the\n * catalogue is seeded on the first use (and again after a failed attempt).\n */\nexport function honoAccessControl<P extends string, E extends Env = Env>(\n resolve: AccessControlResolver<P, E>,\n options: HonoAccessControlOptions<E> = {},\n): HonoAccessControl<P, E> {\n let seeded: Promise<unknown> | undefined;\n const userIdOf =\n options.userId ?? ((c: Context<E>) => (c as unknown as Context<{ Variables: { user?: { id: string } | null } }>).get(\"user\")?.id);\n\n async function accessControlOf(c: Context<E>): Promise<AccessControl<P>> {\n const ctx = c as unknown as Context<{ Variables: AccessControlVariables }>;\n const existing = ctx.get(\"accessControl\") as AccessControl<P> | undefined;\n if (existing !== undefined) return existing;\n const access = resolve(c);\n seeded ??= access.seed().catch((error: unknown) => {\n seeded = undefined;\n throw error;\n });\n await seeded;\n ctx.set(\"accessControl\", access as AccessControl);\n return access;\n }\n\n return {\n accessControlOf,\n requirePermission: (permission) => async (c, next) => {\n const userId = userIdOf(c);\n if (userId === null || userId === undefined) {\n throw new AppError({ code: \"UNAUTHENTICATED\", status: 401, message: \"Sign in to continue.\" });\n }\n const access = await accessControlOf(c);\n if (!(await access.authorize({ userId, permission }))) {\n throw new AppError({\n code: \"FORBIDDEN\",\n status: 403,\n message: \"You do not have permission to do this.\",\n details: { permission },\n });\n }\n await next();\n },\n };\n}\nexport { mountRoleAdmin, type MountRoleAdminOptions, type RoleResponse, type UserRoleResponse } from \"./role-routes.js\";\n"],"mappings":"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"}