@checkstack/command-backend 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/package.json +1 -1
- package/src/registry.ts +23 -23
- package/src/router.ts +23 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# @checkstack/command-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.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/backend-api@0.3.0
|
|
78
|
+
- @checkstack/command-common@0.1.0
|
|
79
|
+
- @checkstack/common@0.2.0
|
|
80
|
+
|
|
81
|
+
## 0.0.4
|
|
82
|
+
|
|
83
|
+
### Patch Changes
|
|
84
|
+
|
|
85
|
+
- Updated dependencies [97c5a6b]
|
|
86
|
+
- Updated dependencies [8e43507]
|
|
87
|
+
- @checkstack/backend-api@0.2.0
|
|
88
|
+
- @checkstack/common@0.1.0
|
|
89
|
+
- @checkstack/command-common@0.0.4
|
|
90
|
+
|
|
3
91
|
## 0.0.3
|
|
4
92
|
|
|
5
93
|
### Patch Changes
|
package/package.json
CHANGED
package/src/registry.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SearchResult } from "@checkstack/command-common";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
qualifyAccessRuleId,
|
|
4
4
|
type PluginMetadata,
|
|
5
|
-
type
|
|
5
|
+
type AccessRule,
|
|
6
6
|
type LucideIconName,
|
|
7
7
|
} from "@checkstack/common";
|
|
8
8
|
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
* Context provided to search providers during search operations.
|
|
15
15
|
*/
|
|
16
16
|
export interface SearchContext {
|
|
17
|
-
/** The user's
|
|
18
|
-
|
|
17
|
+
/** The user's access rule IDs for filtering results */
|
|
18
|
+
userAccessRules: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* A command definition for simple registration.
|
|
23
|
-
*
|
|
23
|
+
* Access rules will be automatically qualified using the plugin's metadata.
|
|
24
24
|
*/
|
|
25
25
|
export interface CommandDefinition {
|
|
26
26
|
/** Unique command ID (will be prefixed with plugin ID) */
|
|
@@ -33,8 +33,8 @@ export interface CommandDefinition {
|
|
|
33
33
|
shortcuts?: string[];
|
|
34
34
|
/** Route to navigate to when executed */
|
|
35
35
|
route: string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/** Access rules required (will be auto-qualified with plugin ID) */
|
|
37
|
+
requiredAccessRules?: AccessRule[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -49,7 +49,7 @@ export interface BackendSearchProvider {
|
|
|
49
49
|
priority?: number;
|
|
50
50
|
/**
|
|
51
51
|
* Search function that returns results matching the query.
|
|
52
|
-
* Results should NOT be pre-filtered by
|
|
52
|
+
* Results should NOT be pre-filtered by access rules - the registry handles that.
|
|
53
53
|
*/
|
|
54
54
|
search: (
|
|
55
55
|
query: string,
|
|
@@ -61,13 +61,13 @@ export interface BackendSearchProvider {
|
|
|
61
61
|
* Options for registering a search provider.
|
|
62
62
|
*/
|
|
63
63
|
export interface RegisterSearchProviderOptions {
|
|
64
|
-
/** The plugin's metadata - used to qualify
|
|
64
|
+
/** The plugin's metadata - used to qualify access rule IDs */
|
|
65
65
|
pluginMetadata: PluginMetadata;
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Simple command definitions. These will be automatically:
|
|
69
69
|
* - Converted to a search provider
|
|
70
|
-
* - Have
|
|
70
|
+
* - Have access rules qualified with pluginId
|
|
71
71
|
* - Be searchable by title, subtitle, and category
|
|
72
72
|
*/
|
|
73
73
|
commands?: CommandDefinition[];
|
|
@@ -75,7 +75,7 @@ export interface RegisterSearchProviderOptions {
|
|
|
75
75
|
/**
|
|
76
76
|
* Custom search provider for more complex search logic.
|
|
77
77
|
* Use this for entity search (e.g., catalog systems).
|
|
78
|
-
*
|
|
78
|
+
* Access rules in returned results will be auto-qualified.
|
|
79
79
|
*/
|
|
80
80
|
provider?: Omit<BackendSearchProvider, "id"> & {
|
|
81
81
|
/** Provider ID (will be prefixed with plugin ID) */
|
|
@@ -93,7 +93,7 @@ const searchProviders = new Map<string, BackendSearchProvider>();
|
|
|
93
93
|
* Register a search provider with the command palette.
|
|
94
94
|
*
|
|
95
95
|
* This is the main API for plugins to contribute to command palette search.
|
|
96
|
-
*
|
|
96
|
+
* Access rules are automatically qualified with the plugin's ID.
|
|
97
97
|
*
|
|
98
98
|
* @example Simple command registration:
|
|
99
99
|
* ```ts
|
|
@@ -107,7 +107,7 @@ const searchProviders = new Map<string, BackendSearchProvider>();
|
|
|
107
107
|
* iconName: "AlertCircle",
|
|
108
108
|
* shortcuts: ["meta+shift+i", "ctrl+shift+i"],
|
|
109
109
|
* route: "/incidents?action=create",
|
|
110
|
-
*
|
|
110
|
+
* requiredAccessRules: [access.incidentManage],
|
|
111
111
|
* },
|
|
112
112
|
* ],
|
|
113
113
|
* });
|
|
@@ -146,7 +146,7 @@ export function registerSearchProvider(
|
|
|
146
146
|
searchProviders.set(commandProvider.id, commandProvider);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// Register custom provider with auto-qualified
|
|
149
|
+
// Register custom provider with auto-qualified access rules
|
|
150
150
|
if (provider) {
|
|
151
151
|
const qualifiedProvider = createQualifiedProvider(pluginMetadata, provider);
|
|
152
152
|
searchProviders.set(qualifiedProvider.id, qualifiedProvider);
|
|
@@ -160,7 +160,7 @@ function createCommandProvider(
|
|
|
160
160
|
pluginMetadata: PluginMetadata,
|
|
161
161
|
commands: CommandDefinition[]
|
|
162
162
|
): BackendSearchProvider {
|
|
163
|
-
// Pre-qualify all
|
|
163
|
+
// Pre-qualify all access rules and build SearchResult objects
|
|
164
164
|
const searchableCommands: SearchResult[] = commands.map((cmd) => ({
|
|
165
165
|
id: `${pluginMetadata.pluginId}.${cmd.id}`,
|
|
166
166
|
type: "command" as const,
|
|
@@ -170,8 +170,8 @@ function createCommandProvider(
|
|
|
170
170
|
category: capitalize(pluginMetadata.pluginId),
|
|
171
171
|
shortcuts: cmd.shortcuts,
|
|
172
172
|
route: cmd.route,
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
requiredAccessRules: cmd.requiredAccessRules?.map((rule) =>
|
|
174
|
+
qualifyAccessRuleId(pluginMetadata, rule)
|
|
175
175
|
),
|
|
176
176
|
}));
|
|
177
177
|
|
|
@@ -197,7 +197,7 @@ function createCommandProvider(
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
* Wrap a custom provider to auto-qualify
|
|
200
|
+
* Wrap a custom provider to auto-qualify access rules in results.
|
|
201
201
|
*/
|
|
202
202
|
function createQualifiedProvider(
|
|
203
203
|
pluginMetadata: PluginMetadata,
|
|
@@ -209,17 +209,17 @@ function createQualifiedProvider(
|
|
|
209
209
|
search: async (query, context) => {
|
|
210
210
|
const results = await provider.search(query, context);
|
|
211
211
|
|
|
212
|
-
// Auto-qualify
|
|
212
|
+
// Auto-qualify access rule IDs in results
|
|
213
213
|
return results.map((result) => ({
|
|
214
214
|
...result,
|
|
215
215
|
id: result.id.includes(".")
|
|
216
216
|
? result.id
|
|
217
217
|
: `${pluginMetadata.pluginId}.${result.id}`,
|
|
218
|
-
|
|
218
|
+
requiredAccessRules: result.requiredAccessRules?.map((ruleId) =>
|
|
219
219
|
// If already qualified (contains the plugin ID prefix), leave it
|
|
220
|
-
|
|
221
|
-
?
|
|
222
|
-
: `${pluginMetadata.pluginId}.${
|
|
220
|
+
ruleId.startsWith(`${pluginMetadata.pluginId}.`)
|
|
221
|
+
? ruleId
|
|
222
|
+
: `${pluginMetadata.pluginId}.${ruleId}`
|
|
223
223
|
),
|
|
224
224
|
}));
|
|
225
225
|
},
|
package/src/router.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { implement } from "@orpc/server";
|
|
2
|
-
import {
|
|
3
|
-
autoAuthMiddleware,
|
|
4
|
-
type RpcContext,
|
|
5
|
-
} from "@checkstack/backend-api";
|
|
2
|
+
import { autoAuthMiddleware, type RpcContext } from "@checkstack/backend-api";
|
|
6
3
|
import {
|
|
7
4
|
commandContract,
|
|
8
|
-
|
|
5
|
+
filterByAccessRules,
|
|
9
6
|
type SearchResult,
|
|
10
7
|
} from "@checkstack/command-common";
|
|
11
8
|
import { getSearchProviders } from "./registry";
|
|
@@ -13,24 +10,24 @@ import { getSearchProviders } from "./registry";
|
|
|
13
10
|
/**
|
|
14
11
|
* Creates the command router using contract-based implementation.
|
|
15
12
|
*
|
|
16
|
-
* Auth and
|
|
17
|
-
* based on the contract's meta.userType and meta.
|
|
13
|
+
* Auth and access rules are automatically enforced via autoAuthMiddleware
|
|
14
|
+
* based on the contract's meta.userType and meta.access.
|
|
18
15
|
*/
|
|
19
16
|
const os = implement(commandContract)
|
|
20
17
|
.$context<RpcContext>()
|
|
21
18
|
.use(autoAuthMiddleware);
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
|
-
* Extract
|
|
25
|
-
* Only RealUser and ApplicationUser have
|
|
21
|
+
* Extract access rules from the context user.
|
|
22
|
+
* Only RealUser and ApplicationUser have access rules; ServiceUser doesn't.
|
|
26
23
|
*/
|
|
27
|
-
function
|
|
24
|
+
function getUserAccessRules(context: RpcContext): string[] {
|
|
28
25
|
const user = context.user;
|
|
29
26
|
if (!user) return [];
|
|
30
27
|
if (user.type === "user" || user.type === "application") {
|
|
31
|
-
return user.
|
|
28
|
+
return user.accessRules ?? [];
|
|
32
29
|
}
|
|
33
|
-
// ServiceUser has no
|
|
30
|
+
// ServiceUser has no accesss array - treated as having all access
|
|
34
31
|
// but for search filtering, return empty (no filtering applied)
|
|
35
32
|
return [];
|
|
36
33
|
}
|
|
@@ -38,21 +35,23 @@ function getUserPermissions(context: RpcContext): string[] {
|
|
|
38
35
|
export const createCommandRouter = () => {
|
|
39
36
|
/**
|
|
40
37
|
* Search across all registered search providers.
|
|
41
|
-
* Results are aggregated from all providers, filtered by
|
|
38
|
+
* Results are aggregated from all providers, filtered by access rules,
|
|
42
39
|
* and returned in priority order.
|
|
43
40
|
*/
|
|
44
41
|
const search = os.search.handler(async ({ input, context }) => {
|
|
45
42
|
const providers = getSearchProviders();
|
|
46
43
|
const query = input.query.toLowerCase().trim();
|
|
47
44
|
|
|
48
|
-
// Get user
|
|
49
|
-
const
|
|
45
|
+
// Get user access rules for filtering
|
|
46
|
+
const userAccessRules = getUserAccessRules(context);
|
|
50
47
|
|
|
51
48
|
// Execute all provider searches in parallel
|
|
52
49
|
const providerResults = await Promise.all(
|
|
53
50
|
providers.map(async (provider) => {
|
|
54
51
|
try {
|
|
55
|
-
const results = await provider.search(query, {
|
|
52
|
+
const results = await provider.search(query, {
|
|
53
|
+
userAccessRules: userAccessRules,
|
|
54
|
+
});
|
|
56
55
|
return results;
|
|
57
56
|
} catch (error) {
|
|
58
57
|
// Log but don't fail - one failing provider shouldn't break search
|
|
@@ -62,25 +61,27 @@ export const createCommandRouter = () => {
|
|
|
62
61
|
})
|
|
63
62
|
);
|
|
64
63
|
|
|
65
|
-
// Flatten and filter by
|
|
64
|
+
// Flatten and filter by access rules
|
|
66
65
|
const allResults = providerResults.flat();
|
|
67
|
-
return
|
|
66
|
+
return filterByAccessRules(allResults, userAccessRules);
|
|
68
67
|
});
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* Get all registered commands for browsing.
|
|
72
|
-
* Returns commands filtered by user
|
|
71
|
+
* Returns commands filtered by user access rules.
|
|
73
72
|
*/
|
|
74
73
|
const getCommands = os.getCommands.handler(async ({ context }) => {
|
|
75
74
|
const providers = getSearchProviders();
|
|
76
|
-
const
|
|
75
|
+
const userAccessRules = getUserAccessRules(context);
|
|
77
76
|
|
|
78
77
|
// Get all results with empty query (commands return all when query is empty)
|
|
79
78
|
const providerResults = await Promise.all(
|
|
80
79
|
providers.map(async (provider) => {
|
|
81
80
|
try {
|
|
82
81
|
// Empty query = return all items
|
|
83
|
-
const results = await provider.search("", {
|
|
82
|
+
const results = await provider.search("", {
|
|
83
|
+
userAccessRules: userAccessRules,
|
|
84
|
+
});
|
|
84
85
|
// Filter to only commands for this endpoint
|
|
85
86
|
return results.filter(
|
|
86
87
|
(r): r is SearchResult & { type: "command" } => r.type === "command"
|
|
@@ -93,7 +94,7 @@ export const createCommandRouter = () => {
|
|
|
93
94
|
);
|
|
94
95
|
|
|
95
96
|
const allCommands = providerResults.flat();
|
|
96
|
-
return
|
|
97
|
+
return filterByAccessRules(allCommands, userAccessRules);
|
|
97
98
|
});
|
|
98
99
|
|
|
99
100
|
return os.router({
|