@checkstack/backend 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9faec1f: # Unified AccessRule Terminology Refactoring
8
+
9
+ This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
10
+
11
+ ## Changes
12
+
13
+ ### Core Infrastructure (`@checkstack/common`)
14
+
15
+ - Introduced `AccessRule` interface as the primary access control type
16
+ - Added `accessPair()` helper for creating read/manage access rule pairs
17
+ - Added `access()` builder for individual access rules
18
+ - Replaced `Permission` type with `AccessRule` throughout
19
+
20
+ ### API Changes
21
+
22
+ - `env.registerPermissions()` → `env.registerAccessRules()`
23
+ - `meta.permissions` → `meta.access` in RPC contracts
24
+ - `usePermission()` → `useAccess()` in frontend hooks
25
+ - Route `permission:` field → `accessRule:` field
26
+
27
+ ### UI Changes
28
+
29
+ - "Roles & Permissions" tab → "Roles & Access Rules"
30
+ - "You don't have permission..." → "You don't have access..."
31
+ - All permission-related UI text updated
32
+
33
+ ### Documentation & Templates
34
+
35
+ - Updated 18 documentation files with AccessRule terminology
36
+ - Updated 7 scaffolding templates with `accessPair()` pattern
37
+ - All code examples use new AccessRule API
38
+
39
+ ## Migration Guide
40
+
41
+ ### Backend Plugins
42
+
43
+ ```diff
44
+ - import { permissionList } from "./permissions";
45
+ - env.registerPermissions(permissionList);
46
+ + import { accessRules } from "./access";
47
+ + env.registerAccessRules(accessRules);
48
+ ```
49
+
50
+ ### RPC Contracts
51
+
52
+ ```diff
53
+ - .meta({ userType: "user", permissions: [permissions.read.id] })
54
+ + .meta({ userType: "user", access: [access.read] })
55
+ ```
56
+
57
+ ### Frontend Hooks
58
+
59
+ ```diff
60
+ - const canRead = accessApi.usePermission(permissions.read.id);
61
+ + const canRead = accessApi.useAccess(access.read);
62
+ ```
63
+
64
+ ### Routes
65
+
66
+ ```diff
67
+ - permission: permissions.entityRead.id,
68
+ + accessRule: access.read,
69
+ ```
70
+
71
+ ### Patch Changes
72
+
73
+ - Updated dependencies [9faec1f]
74
+ - Updated dependencies [827b286]
75
+ - Updated dependencies [f533141]
76
+ - Updated dependencies [aa4a8ab]
77
+ - @checkstack/api-docs-common@0.1.0
78
+ - @checkstack/auth-common@0.2.0
79
+ - @checkstack/backend-api@0.3.0
80
+ - @checkstack/common@0.2.0
81
+ - @checkstack/signal-backend@0.1.0
82
+ - @checkstack/signal-common@0.1.0
83
+ - @checkstack/queue-api@0.0.5
84
+
3
85
  ## 0.2.0
4
86
 
5
87
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "bun --env-file=../../.env --watch src/index.ts",
package/src/index.ts CHANGED
@@ -25,9 +25,8 @@ import {
25
25
  import { createPluginAdminRouter } from "./plugin-manager/plugin-admin-router";
26
26
  import {
27
27
  pluginMetadata as apiDocsMetadata,
28
- permissions as apiDocsPermissions,
28
+ apiDocsAccess,
29
29
  } from "@checkstack/api-docs-common";
30
- import { qualifyPermissionId } from "@checkstack/common";
31
30
 
32
31
  import { cors } from "hono/cors";
33
32
 
@@ -241,10 +240,7 @@ const init = async () => {
241
240
  pluginManager,
242
241
  authService,
243
242
  baseUrl,
244
- requiredPermission: qualifyPermissionId(
245
- apiDocsMetadata,
246
- apiDocsPermissions.apiDocsView
247
- ),
243
+ requiredAccessRule: `${apiDocsMetadata.pluginId}.${apiDocsAccess.view.id}`,
248
244
  });
249
245
  app.get("/api/openapi.json", async (c) => {
250
246
  const response = await openApiHandler(c.req.raw);
@@ -260,7 +256,7 @@ const init = async () => {
260
256
  // 3. Load Plugins
261
257
  await pluginManager.loadPlugins(app);
262
258
 
263
- // 4. Wire up auth client for permission-based signal filtering
259
+ // 4. Wire up auth client for access-based signal filtering
264
260
  // This must happen AFTER plugins load so auth-backend is available
265
261
  const rpcClient = await pluginManager.getService(coreServices.rpcClient);
266
262
  if (rpcClient) {
@@ -268,7 +264,7 @@ const init = async () => {
268
264
  const authClient = rpcClient.forPlugin(AuthApi);
269
265
  signalService.setAuthClient(authClient);
270
266
  rootLogger.debug(
271
- "SignalService: Auth client configured for permission filtering"
267
+ "SignalService: Auth client configured for access filtering"
272
268
  );
273
269
  } else {
274
270
  rootLogger.warn(
@@ -19,43 +19,43 @@ describe("EventBus Integration Tests", () => {
19
19
  eventBus = new EventBus(mockQueueManager, mockLogger);
20
20
  });
21
21
 
22
- describe("Permission Sync Scenario", () => {
23
- it("should sync permissions across plugins using work-queue mode", async () => {
24
- // Simulate the permissionsRegistered hook
25
- const permissionsRegistered = createHook<{
22
+ describe("Access Rule Sync Scenario", () => {
23
+ it("should sync access rules across plugins using work-queue mode", async () => {
24
+ // Simulate the accessRulesRegistered hook
25
+ const accessRulesRegistered = createHook<{
26
26
  pluginId: string;
27
- permissions: Array<{ id: string; description?: string }>;
28
- }>("core.permissionsRegistered");
27
+ accessRules: Array<{ id: string; description?: string }>;
28
+ }>("core.accessRulesRegistered");
29
29
 
30
- const syncedPermissions: Array<{ id: string; description?: string }> = [];
30
+ const syncedAccessRules: Array<{ id: string; description?: string }> = [];
31
31
 
32
- // Auth-backend subscribes to sync permissions (work-queue mode)
32
+ // Auth-backend subscribes to sync access rules (work-queue mode)
33
33
  await eventBus.subscribe(
34
34
  "auth-backend",
35
- permissionsRegistered,
36
- async ({ permissions }) => {
35
+ accessRulesRegistered,
36
+ async ({ accessRules }) => {
37
37
  // Simulate DB sync
38
- syncedPermissions.push(...permissions);
38
+ syncedAccessRules.push(...accessRules);
39
39
  },
40
40
  {
41
41
  mode: "work-queue",
42
- workerGroup: "permission-db-sync",
42
+ workerGroup: "access-rule-db-sync",
43
43
  maxRetries: 3,
44
44
  }
45
45
  );
46
46
 
47
- // Emit permission registration events from different plugins
48
- await eventBus.emit(permissionsRegistered, {
47
+ // Emit access rule registration events from different plugins
48
+ await eventBus.emit(accessRulesRegistered, {
49
49
  pluginId: "catalog",
50
- permissions: [
50
+ accessRules: [
51
51
  { id: "catalog-backend.read", description: "Read catalog" },
52
52
  { id: "catalog-backend.manage", description: "Manage catalog" },
53
53
  ],
54
54
  });
55
55
 
56
- await eventBus.emit(permissionsRegistered, {
56
+ await eventBus.emit(accessRulesRegistered, {
57
57
  pluginId: "queue",
58
- permissions: [
58
+ accessRules: [
59
59
  { id: "queue-backend.read", description: "Read queue" },
60
60
  { id: "queue-backend.manage", description: "Manage queue" },
61
61
  ],
@@ -64,18 +64,18 @@ describe("EventBus Integration Tests", () => {
64
64
  // Wait for async processing
65
65
  await new Promise((resolve) => setTimeout(resolve, 100));
66
66
 
67
- // All permissions should be synced
68
- expect(syncedPermissions.length).toBe(4);
69
- expect(syncedPermissions.map((p) => p.id)).toContain(
67
+ // All access rules should be synced
68
+ expect(syncedAccessRules.length).toBe(4);
69
+ expect(syncedAccessRules.map((p) => p.id)).toContain(
70
70
  "catalog-backend.read"
71
71
  );
72
- expect(syncedPermissions.map((p) => p.id)).toContain(
72
+ expect(syncedAccessRules.map((p) => p.id)).toContain(
73
73
  "catalog-backend.manage"
74
74
  );
75
- expect(syncedPermissions.map((p) => p.id)).toContain(
75
+ expect(syncedAccessRules.map((p) => p.id)).toContain(
76
76
  "queue-backend.read"
77
77
  );
78
- expect(syncedPermissions.map((p) => p.id)).toContain(
78
+ expect(syncedAccessRules.map((p) => p.id)).toContain(
79
79
  "queue-backend.manage"
80
80
  );
81
81
  });
@@ -12,16 +12,16 @@ import type { PluginManager } from "./plugin-manager";
12
12
  import type { AuthService } from "@checkstack/backend-api";
13
13
 
14
14
  /**
15
- * Check if a user has a specific permission.
15
+ * Check if a user has a specific access rule.
16
16
  * Supports wildcard (*) for admin access.
17
17
  */
18
- function hasPermission(
19
- user: { permissions?: string[] },
20
- permission: string
18
+ function hasAccess(
19
+ user: { accessRules?: string[] },
20
+ accessRule: string
21
21
  ): boolean {
22
- if (!user.permissions) return false;
22
+ if (!user.accessRules) return false;
23
23
  return (
24
- user.permissions.includes("*") || user.permissions.includes(permission)
24
+ user.accessRules.includes("*") || user.accessRules.includes(accessRule)
25
25
  );
26
26
  }
27
27
 
@@ -30,9 +30,9 @@ function hasPermission(
30
30
  */
31
31
  function extractProcedureMetadata(
32
32
  contract: unknown
33
- ): { userType?: string; permissions?: string[] } | undefined {
33
+ ): { userType?: string; accessRules?: string[] } | undefined {
34
34
  const orpcData = (contract as Record<string, unknown>)?.["~orpc"] as
35
- | { meta?: { userType?: string; permissions?: string[] } }
35
+ | { meta?: { userType?: string; accessRules?: string[] } }
36
36
  | undefined;
37
37
  return orpcData?.meta;
38
38
  }
@@ -43,10 +43,10 @@ function extractProcedureMetadata(
43
43
  */
44
44
  function buildMetadataLookup(
45
45
  contracts: Map<string, AnyContractRouter>
46
- ): Map<string, { userType?: string; permissions?: string[] }> {
46
+ ): Map<string, { userType?: string; accessRules?: string[] }> {
47
47
  const lookup = new Map<
48
48
  string,
49
- { userType?: string; permissions?: string[] }
49
+ { userType?: string; accessRules?: string[] }
50
50
  >();
51
51
 
52
52
  for (const [pluginId, contract] of contracts) {
@@ -141,12 +141,12 @@ export function createOpenApiHandler({
141
141
  pluginManager,
142
142
  authService,
143
143
  baseUrl,
144
- requiredPermission,
144
+ requiredAccessRule,
145
145
  }: {
146
146
  pluginManager: PluginManager;
147
147
  authService: AuthService;
148
148
  baseUrl: string;
149
- requiredPermission: string;
149
+ requiredAccessRule: string;
150
150
  }): (req: Request) => Promise<Response> {
151
151
  return async (req: Request) => {
152
152
  // Authenticate request
@@ -156,9 +156,9 @@ export function createOpenApiHandler({
156
156
  return Response.json({ error: "Unauthorized" }, { status: 401 });
157
157
  }
158
158
 
159
- // Check permission (applications.manage from auth plugin)
160
- // Services don't have permissions, so deny them access to docs
161
- if (user.type === "service" || !hasPermission(user, requiredPermission)) {
159
+ // Check access rule (applications.manage from auth plugin)
160
+ // Services don't have accesss, so deny them access to docs
161
+ if (user.type === "service" || !hasAccess(user, requiredAccessRule)) {
162
162
  return Response.json({ error: "Forbidden" }, { status: 403 });
163
163
  }
164
164
 
@@ -233,18 +233,18 @@ describe("Plugin Lifecycle", () => {
233
233
  expect(pluginRpcRouters.has("test-plugin")).toBe(false);
234
234
  });
235
235
 
236
- it("should clear permissions for plugin", async () => {
237
- const registeredPermissions = (pluginManager as never)[
238
- "registeredPermissions"
236
+ it("should clear access rules for plugin", async () => {
237
+ const registeredAccessRules = (pluginManager as never)[
238
+ "registeredAccessRules"
239
239
  ] as { pluginId: string; id: string }[];
240
240
 
241
- // Clear existing permissions first
242
- while (registeredPermissions.length > 0) {
243
- registeredPermissions.pop();
241
+ // Clear existing access rules first
242
+ while (registeredAccessRules.length > 0) {
243
+ registeredAccessRules.pop();
244
244
  }
245
245
 
246
- // Add test permissions
247
- registeredPermissions.push(
246
+ // Add test access rules
247
+ registeredAccessRules.push(
248
248
  { pluginId: "test-plugin", id: "test-plugin.perm1" },
249
249
  { pluginId: "test-plugin", id: "test-plugin.perm2" },
250
250
  { pluginId: "other-plugin", id: "other-plugin.perm1" }
@@ -254,8 +254,8 @@ describe("Plugin Lifecycle", () => {
254
254
  deleteSchema: false,
255
255
  });
256
256
 
257
- // Use getAllPermissions() which returns the current array
258
- const remaining = pluginManager.getAllPermissions();
257
+ // Use getAllAccessRules() which returns the current array
258
+ const remaining = pluginManager.getAllAccessRules();
259
259
  expect(remaining).toHaveLength(1);
260
260
  expect(remaining[0].id).toBe("other-plugin.perm1");
261
261
  });
@@ -100,8 +100,8 @@ export function registerCoreServices({
100
100
  });
101
101
 
102
102
  // 3. Auth Factory (Scoped)
103
- // Cache for anonymous permissions to avoid repeated DB queries
104
- let anonymousPermissionsCache: string[] | undefined;
103
+ // Cache for anonymous access rules to avoid repeated DB queries
104
+ let anonymousAccessRulesCache: string[] | undefined;
105
105
  let anonymousCacheTime = 0;
106
106
  const CACHE_TTL_MS = 60_000; // 1 minute cache
107
107
 
@@ -146,33 +146,33 @@ export function registerCoreServices({
146
146
  return { headers: { Authorization: `Bearer ${token}` } };
147
147
  },
148
148
 
149
- getAnonymousPermissions: async (): Promise<string[]> => {
149
+ getAnonymousAccessRules: async (): Promise<string[]> => {
150
150
  const now = Date.now();
151
151
  // Return cached value if still valid
152
152
  if (
153
- anonymousPermissionsCache !== undefined &&
153
+ anonymousAccessRulesCache !== undefined &&
154
154
  now - anonymousCacheTime < CACHE_TTL_MS
155
155
  ) {
156
- return anonymousPermissionsCache;
156
+ return anonymousAccessRulesCache;
157
157
  }
158
158
 
159
- // Use RPC client to call auth-backend's getAnonymousPermissions endpoint
159
+ // Use RPC client to call auth-backend's getAnonymousAccessRules endpoint
160
160
  try {
161
161
  const rpcClient = await registry.get(coreServices.rpcClient, {
162
162
  pluginId: "core",
163
163
  });
164
164
  const authClient = rpcClient.forPlugin(AuthApi);
165
- const permissions = await authClient.getAnonymousPermissions();
165
+ const accessRulesResult = await authClient.getAnonymousAccessRules();
166
166
 
167
167
  // Update cache
168
- anonymousPermissionsCache = permissions;
168
+ anonymousAccessRulesCache = accessRulesResult;
169
169
  anonymousCacheTime = now;
170
170
 
171
- return permissions;
171
+ return accessRulesResult;
172
172
  } catch (error) {
173
173
  // RPC client not available yet (during startup), return empty
174
174
  rootLogger.warn(
175
- `[auth] getAnonymousPermissions: RPC failed, returning empty array. Error: ${error}`
175
+ `[auth] getAnonymousAccessRules: RPC failed, returning empty array. Error: ${error}`
176
176
  );
177
177
  return [];
178
178
  }
@@ -186,8 +186,8 @@ export function registerCoreServices({
186
186
  const authClient = rpcClient.forPlugin(AuthApi);
187
187
  return await authClient.checkResourceTeamAccess(params);
188
188
  } catch {
189
- // Fall back to global permission on error
190
- return { hasAccess: params.hasGlobalPermission };
189
+ // Fall back to global access on error
190
+ return { hasAccess: params.hasGlobalAccess };
191
191
  }
192
192
  },
193
193
 
@@ -199,8 +199,8 @@ export function registerCoreServices({
199
199
  const authClient = rpcClient.forPlugin(AuthApi);
200
200
  return await authClient.getAccessibleResourceIds(params);
201
201
  } catch {
202
- // Fall back to global permission on error
203
- return params.hasGlobalPermission ? params.resourceIds : [];
202
+ // Fall back to global access on error
203
+ return params.hasGlobalAccess ? params.resourceIds : [];
204
204
  }
205
205
  },
206
206
  };
@@ -18,7 +18,7 @@ import {
18
18
  HookSubscribeOptions,
19
19
  RpcContext,
20
20
  } from "@checkstack/backend-api";
21
- import type { Permission } from "@checkstack/common";
21
+ import type { AccessRule } from "@checkstack/common";
22
22
  import { getPluginSchemaName } from "@checkstack/drizzle-helper";
23
23
  import { rootLogger } from "../logger";
24
24
  import type { ServiceRegistry } from "../services/service-registry";
@@ -41,8 +41,8 @@ export interface PluginLoaderDeps {
41
41
  pluginRpcRouters: Map<string, unknown>;
42
42
  pluginHttpHandlers: Map<string, (req: Request) => Promise<Response>>;
43
43
  extensionPointManager: ExtensionPointManager;
44
- registeredPermissions: (Permission & { pluginId: string })[];
45
- getAllPermissions: () => Permission[];
44
+ registeredAccessRules: (AccessRule & { pluginId: string })[];
45
+ getAllAccessRules: () => AccessRule[];
46
46
  db: NodePgDatabase<Record<string, unknown>>;
47
47
  /**
48
48
  * Map of pluginId -> PluginMetadata for request-time context injection.
@@ -121,18 +121,16 @@ export function registerPlugin({
121
121
  getExtensionPoint: (ref) => {
122
122
  return deps.extensionPointManager.getExtensionPoint(ref);
123
123
  },
124
- registerPermissions: (permissions: Permission[]) => {
125
- // Store permissions with pluginId prefix to namespace them
126
- const prefixed = permissions.map((p) => ({
124
+ registerAccessRules: (accessRules: AccessRule[]) => {
125
+ // Store access rules with pluginId prefix to namespace them
126
+ const prefixed = accessRules.map((rule) => ({
127
+ ...rule,
127
128
  pluginId: pluginId,
128
- id: `${pluginId}.${p.id}`,
129
- description: p.description,
130
- isAuthenticatedDefault: p.isAuthenticatedDefault,
131
- isPublicDefault: p.isPublicDefault,
129
+ id: `${pluginId}.${rule.id}`,
132
130
  }));
133
- deps.registeredPermissions.push(...prefixed);
131
+ deps.registeredAccessRules.push(...prefixed);
134
132
  rootLogger.debug(
135
- ` -> Registered ${prefixed.length} permissions for ${pluginId}`
133
+ ` -> Registered ${prefixed.length} access rules for ${pluginId}`
136
134
  );
137
135
  },
138
136
  registerRouter: (
@@ -150,7 +148,7 @@ export function registerPlugin({
150
148
  rootLogger.debug(` -> Registered cleanup handler for ${pluginId}`);
151
149
  },
152
150
  pluginManager: {
153
- getAllPermissions: () => deps.getAllPermissions(),
151
+ getAllAccessRules: () => deps.getAllAccessRules(),
154
152
  },
155
153
  });
156
154
  }
@@ -374,29 +372,24 @@ export async function loadPlugins({
374
372
  // Phase 3: Run afterPluginsReady callbacks
375
373
  rootLogger.debug("🔄 Running afterPluginsReady callbacks...");
376
374
 
377
- // Emit permission registration hooks at start of Phase 3
375
+ // Emit access rule registration hooks at start of Phase 3
378
376
  // (EventBus already retrieved above, all plugins can receive notifications)
379
- const permissionsByPlugin = new Map<string, Permission[]>();
380
- for (const perm of deps.registeredPermissions) {
381
- if (!permissionsByPlugin.has(perm.pluginId)) {
382
- permissionsByPlugin.set(perm.pluginId, []);
377
+ const accessRulesByPlugin = new Map<string, AccessRule[]>();
378
+ for (const { pluginId, ...rule } of deps.registeredAccessRules) {
379
+ if (!accessRulesByPlugin.has(pluginId)) {
380
+ accessRulesByPlugin.set(pluginId, []);
383
381
  }
384
- permissionsByPlugin.get(perm.pluginId)!.push({
385
- id: perm.id,
386
- description: perm.description,
387
- isAuthenticatedDefault: perm.isAuthenticatedDefault,
388
- isPublicDefault: perm.isPublicDefault,
389
- });
382
+ accessRulesByPlugin.get(pluginId)!.push(rule);
390
383
  }
391
- for (const [pluginId, permissions] of permissionsByPlugin) {
384
+ for (const [pluginId, accessRules] of accessRulesByPlugin) {
392
385
  try {
393
- await eventBus.emit(coreHooks.permissionsRegistered, {
386
+ await eventBus.emit(coreHooks.accessRulesRegistered, {
394
387
  pluginId,
395
- permissions,
388
+ accessRules,
396
389
  });
397
390
  } catch (error) {
398
391
  rootLogger.error(
399
- `Failed to emit permissionsRegistered hook for ${pluginId}:`,
392
+ `Failed to emit accessRulesRegistered hook for ${pluginId}:`,
400
393
  error
401
394
  );
402
395
  }
@@ -375,54 +375,76 @@ describe("PluginManager", () => {
375
375
  });
376
376
  });
377
377
 
378
- describe("Permission Registration", () => {
379
- it("should store permissions in the registry", () => {
380
- // Permissions are now stored directly via the registeredPermissions array
378
+ describe("Access Rule Registration", () => {
379
+ it("should store access rules in the registry", () => {
380
+ // Access rules are now stored directly via the registeredAccessRules array
381
381
  // and hooks are emitted in Phase 3 (afterPluginsReady)
382
382
  const perms = (
383
383
  pluginManager as unknown as {
384
- registeredPermissions: {
384
+ registeredAccessRules: {
385
385
  pluginId: string;
386
386
  id: string;
387
- description?: string;
387
+ resource: string;
388
+ level: string;
389
+ description: string;
388
390
  }[];
389
391
  }
390
- ).registeredPermissions;
392
+ ).registeredAccessRules;
391
393
 
392
- // Add permissions directly (simulating what plugin-loader does)
394
+ // Add access rules directly (simulating what plugin-loader does)
393
395
  perms.push({
394
396
  pluginId: "test-plugin",
395
- id: "test-plugin.test.permission",
396
- description: "Test permission",
397
+ id: "test-plugin.test.accessRule",
398
+ resource: "test",
399
+ level: "read",
400
+ description: "Test access rule",
397
401
  });
398
402
 
399
- // getAllPermissions should return them (without pluginId in the output)
400
- const all = pluginManager.getAllPermissions();
403
+ // getAllAccessRules should return them (without pluginId in the output)
404
+ const all = pluginManager.getAllAccessRules();
401
405
  expect(all.length).toBe(1);
402
- expect(all[0]).toEqual({
403
- id: "test-plugin.test.permission",
404
- description: "Test permission",
405
- });
406
+ expect(all[0].id).toBe("test-plugin.test.accessRule");
407
+ expect(all[0].description).toBe("Test access rule");
406
408
  });
407
409
 
408
- it("should aggregate permissions from multiple plugins", () => {
410
+ it("should aggregate access rules from multiple plugins", () => {
409
411
  const perms = (
410
412
  pluginManager as unknown as {
411
- registeredPermissions: {
413
+ registeredAccessRules: {
412
414
  pluginId: string;
413
415
  id: string;
414
- description?: string;
416
+ resource: string;
417
+ level: string;
418
+ description: string;
415
419
  }[];
416
420
  }
417
- ).registeredPermissions;
421
+ ).registeredAccessRules;
418
422
 
419
423
  perms.push(
420
- { pluginId: "plugin-1", id: "plugin-1.perm.1", description: undefined },
421
- { pluginId: "plugin-1", id: "plugin-1.perm.2", description: undefined },
422
- { pluginId: "plugin-2", id: "plugin-2.perm.3", description: undefined }
424
+ {
425
+ pluginId: "plugin-1",
426
+ id: "plugin-1.perm.1",
427
+ resource: "perm",
428
+ level: "read",
429
+ description: "Access Rule 1",
430
+ },
431
+ {
432
+ pluginId: "plugin-1",
433
+ id: "plugin-1.perm.2",
434
+ resource: "perm",
435
+ level: "manage",
436
+ description: "Access Rule 2",
437
+ },
438
+ {
439
+ pluginId: "plugin-2",
440
+ id: "plugin-2.perm.3",
441
+ resource: "perm",
442
+ level: "read",
443
+ description: "Access Rule 3",
444
+ }
423
445
  );
424
446
 
425
- const all = pluginManager.getAllPermissions();
447
+ const all = pluginManager.getAllAccessRules();
426
448
  expect(all.length).toBe(3);
427
449
  });
428
450
  });
@@ -11,7 +11,7 @@ import {
11
11
  HookUnsubscribe,
12
12
  } from "@checkstack/backend-api";
13
13
  import type { AnyContractRouter } from "@orpc/contract";
14
- import type { Permission, PluginMetadata } from "@checkstack/common";
14
+ import type { AccessRule, PluginMetadata } from "@checkstack/common";
15
15
 
16
16
  // Extracted modules
17
17
  import { registerCoreServices } from "./plugin-manager/core-services";
@@ -32,8 +32,8 @@ export class PluginManager {
32
32
  >();
33
33
  private extensionPointManager = createExtensionPointManager();
34
34
 
35
- // Permission registry - stores all registered permissions with pluginId for hook emission
36
- private registeredPermissions: (Permission & { pluginId: string })[] = [];
35
+ // Access rule registry - stores all registered access rules with pluginId for hook emission
36
+ private registeredAccessRules: (AccessRule & { pluginId: string })[] = [];
37
37
 
38
38
  // Plugin metadata registry - stores PluginMetadata for request-time context injection
39
39
  private pluginMetadataRegistry = new Map<string, PluginMetadata>();
@@ -77,14 +77,9 @@ export class PluginManager {
77
77
  this.pluginRpcRouters.set(routerId, router);
78
78
  }
79
79
 
80
- getAllPermissions(): Permission[] {
81
- return this.registeredPermissions.map(
82
- ({ id, description, isAuthenticatedDefault, isPublicDefault }) => ({
83
- id,
84
- description,
85
- isAuthenticatedDefault,
86
- isPublicDefault,
87
- })
80
+ getAllAccessRules(): AccessRule[] {
81
+ return this.registeredAccessRules.map(
82
+ ({ pluginId: _pluginId, ...rule }) => rule
88
83
  );
89
84
  }
90
85
 
@@ -110,8 +105,8 @@ export class PluginManager {
110
105
  pluginRpcRouters: this.pluginRpcRouters,
111
106
  pluginHttpHandlers: this.pluginHttpHandlers,
112
107
  extensionPointManager: this.extensionPointManager,
113
- registeredPermissions: this.registeredPermissions,
114
- getAllPermissions: () => this.getAllPermissions(),
108
+ registeredAccessRules: this.registeredAccessRules,
109
+ getAllAccessRules: () => this.getAllAccessRules(),
115
110
  db,
116
111
  pluginMetadataRegistry: this.pluginMetadataRegistry,
117
112
  cleanupHandlers: this.cleanupHandlers,
@@ -178,15 +173,15 @@ export class PluginManager {
178
173
  );
179
174
  this.collectorRegistry.unregisterByMissingStrategies(loadedPluginIds);
180
175
 
181
- // 5. Remove permissions from registry
182
- const beforeCount = this.registeredPermissions.length;
183
- this.registeredPermissions = this.registeredPermissions.filter(
176
+ // 5. Remove access rules from registry
177
+ const beforeCount = this.registeredAccessRules.length;
178
+ this.registeredAccessRules = this.registeredAccessRules.filter(
184
179
  (p) => p.pluginId !== pluginId
185
180
  );
186
181
  rootLogger.debug(
187
182
  ` -> Removed ${
188
- beforeCount - this.registeredPermissions.length
189
- } permissions`
183
+ beforeCount - this.registeredAccessRules.length
184
+ } access rules`
190
185
  );
191
186
 
192
187
  // 6. Drop schema if requested
@@ -200,7 +195,7 @@ export class PluginManager {
200
195
  }
201
196
  }
202
197
 
203
- // 7. Emit pluginDeregistered hook (for permission cleanup in auth-backend)
198
+ // 7. Emit pluginDeregistered hook (for access rule cleanup in auth-backend)
204
199
  await eventBus.emit(coreHooks.pluginDeregistered, { pluginId });
205
200
 
206
201
  rootLogger.info(`✅ Plugin deregistered: ${pluginId}`);
@@ -390,18 +385,18 @@ export class PluginManager {
390
385
  },
391
386
  });
392
387
  },
393
- registerPermissions: (permissions) => {
394
- const prefixed = permissions.map((p) => ({
388
+ registerAccessRules: (accessRules) => {
389
+ const prefixed = accessRules.map((p) => ({
395
390
  ...p,
396
391
  id: `${metaPluginId}.${p.id}`,
397
392
  pluginId: metaPluginId,
398
393
  }));
399
- this.registeredPermissions.push(...prefixed);
394
+ this.registeredAccessRules.push(...prefixed);
400
395
 
401
- // Emit permission hook
402
- eventBus.emit(coreHooks.permissionsRegistered, {
396
+ // Emit access rule hook
397
+ eventBus.emit(coreHooks.accessRulesRegistered, {
403
398
  pluginId: metaPluginId,
404
- permissions: prefixed,
399
+ accessRules: prefixed,
405
400
  });
406
401
  },
407
402
  registerService: (ref, impl) => {
@@ -422,7 +417,7 @@ export class PluginManager {
422
417
  this.pluginContractRegistry.set(metaPluginId, contract);
423
418
  },
424
419
  pluginManager: {
425
- getAllPermissions: () => this.getAllPermissions(),
420
+ getAllAccessRules: () => this.getAllAccessRules(),
426
421
  },
427
422
  });
428
423
 
@@ -19,11 +19,11 @@ describe("RPC REST Compatibility", () => {
19
19
  app = new Hono();
20
20
  });
21
21
 
22
- it("should handle GET /api/auth/permissions via oRPC router", async () => {
22
+ it("should handle GET /api/auth/accessRules via oRPC router", async () => {
23
23
  // 1. Setup a mock auth router
24
24
  const authRouter = os.router({
25
- permissions: os.handler(async () => {
26
- return { permissions: ["test-perm"] };
25
+ accessRules: os.handler(async () => {
26
+ return { accessRules: ["test-perm"] };
27
27
  }),
28
28
  });
29
29
 
@@ -33,11 +33,11 @@ describe("RPC REST Compatibility", () => {
33
33
  // The new API auto-prefixes based on pluginId, but for test we need to manually set the map key
34
34
  // Since we're testing the router handler directly, we use the derived name "auth"
35
35
  // Second argument is the contract (for OpenAPI generation) - using a mock object for test
36
- rpcService?.registerRouter(authRouter, { permissions: {} });
36
+ rpcService?.registerRouter(authRouter, { accessRules: {} });
37
37
 
38
38
  // 3. Mock the auth service to skip real authentication
39
39
  const mockAuth: any = {
40
- authenticate: mock(async () => ({ id: "user-1", permissions: ["*"] })),
40
+ authenticate: mock(async () => ({ id: "user-1", accessRules: ["*"] })),
41
41
  };
42
42
  pluginManager.registerService(coreServices.auth, mockAuth);
43
43
 
@@ -63,7 +63,7 @@ describe("RPC REST Compatibility", () => {
63
63
  await pluginManager.loadPlugins(app);
64
64
 
65
65
  // 5. Simulate the request that frontend makes (now /api/auth instead of /api/auth-backend)
66
- const res = await app.request("/api/auth/permissions", {
66
+ const res = await app.request("/api/auth/accessRules", {
67
67
  method: "GET",
68
68
  });
69
69
 
@@ -72,7 +72,7 @@ describe("RPC REST Compatibility", () => {
72
72
  if (res.status === 200) {
73
73
  const body = await res.json();
74
74
  console.log("Response body:", JSON.stringify(body));
75
- expect(body.permissions).toContain("test-perm");
75
+ expect(body.accessRules).toContain("test-perm");
76
76
  } else {
77
77
  console.log("Response text:", await res.text());
78
78
  }