@checkstack/common 0.1.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 +131 -0
- package/package.json +1 -1
- package/src/access-utils.ts +240 -0
- package/src/chart-types.ts +38 -0
- package/src/client-definition.ts +39 -12
- package/src/index.ts +3 -1
- package/src/procedure-builder.ts +52 -0
- package/src/types.ts +23 -70
- package/src/permission-utils.ts +0 -81
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,136 @@
|
|
|
1
1
|
# @checkstack/common
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7a23261: ## TanStack Query Integration
|
|
8
|
+
|
|
9
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
10
|
+
|
|
11
|
+
### New Features
|
|
12
|
+
|
|
13
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
14
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
15
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
16
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
17
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
18
|
+
|
|
19
|
+
### Contract Changes
|
|
20
|
+
|
|
21
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const getItems = proc()
|
|
25
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
26
|
+
.output(z.array(itemSchema))
|
|
27
|
+
.query();
|
|
28
|
+
|
|
29
|
+
const createItem = proc()
|
|
30
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
31
|
+
.input(createItemSchema)
|
|
32
|
+
.output(itemSchema)
|
|
33
|
+
.mutation();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Before (forPlugin pattern)
|
|
40
|
+
const api = useApi(myPluginApiRef);
|
|
41
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
api.getItems().then(setItems);
|
|
44
|
+
}, [api]);
|
|
45
|
+
|
|
46
|
+
// After (usePluginClient pattern)
|
|
47
|
+
const client = usePluginClient(MyPluginApi);
|
|
48
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
54
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
55
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
56
|
+
|
|
57
|
+
## 0.2.0
|
|
58
|
+
|
|
59
|
+
### Minor Changes
|
|
60
|
+
|
|
61
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
62
|
+
|
|
63
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
64
|
+
|
|
65
|
+
## Changes
|
|
66
|
+
|
|
67
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
68
|
+
|
|
69
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
70
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
71
|
+
- Added `access()` builder for individual access rules
|
|
72
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
73
|
+
|
|
74
|
+
### API Changes
|
|
75
|
+
|
|
76
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
77
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
78
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
79
|
+
- Route `permission:` field → `accessRule:` field
|
|
80
|
+
|
|
81
|
+
### UI Changes
|
|
82
|
+
|
|
83
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
84
|
+
- "You don't have permission..." → "You don't have access..."
|
|
85
|
+
- All permission-related UI text updated
|
|
86
|
+
|
|
87
|
+
### Documentation & Templates
|
|
88
|
+
|
|
89
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
90
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
91
|
+
- All code examples use new AccessRule API
|
|
92
|
+
|
|
93
|
+
## Migration Guide
|
|
94
|
+
|
|
95
|
+
### Backend Plugins
|
|
96
|
+
|
|
97
|
+
```diff
|
|
98
|
+
- import { permissionList } from "./permissions";
|
|
99
|
+
- env.registerPermissions(permissionList);
|
|
100
|
+
+ import { accessRules } from "./access";
|
|
101
|
+
+ env.registerAccessRules(accessRules);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### RPC Contracts
|
|
105
|
+
|
|
106
|
+
```diff
|
|
107
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
108
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Frontend Hooks
|
|
112
|
+
|
|
113
|
+
```diff
|
|
114
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
115
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Routes
|
|
119
|
+
|
|
120
|
+
```diff
|
|
121
|
+
- permission: permissions.entityRead.id,
|
|
122
|
+
+ accessRule: access.read,
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- f533141: Enforce health result factory function usage via branded types
|
|
126
|
+
|
|
127
|
+
- Added `healthResultSchema()` builder that enforces the use of factory functions at compile-time
|
|
128
|
+
- Added `healthResultArray()` factory for array fields (e.g., DNS resolved values)
|
|
129
|
+
- Added branded `HealthResultField<T>` type to mark schemas created by factory functions
|
|
130
|
+
- Consolidated `ChartType` and `HealthResultMeta` into `@checkstack/common` as single source of truth
|
|
131
|
+
- Updated all 12 health check strategies and 11 collectors to use `healthResultSchema()`
|
|
132
|
+
- Using raw `z.number()` etc. inside `healthResultSchema()` now causes a TypeScript error
|
|
133
|
+
|
|
3
134
|
## 0.1.0
|
|
4
135
|
|
|
5
136
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { PluginMetadata } from "./plugin-metadata";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// UNIFIED ACCESS CONTROL
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The two fundamental access levels.
|
|
9
|
+
*/
|
|
10
|
+
export type AccessLevel = "read" | "manage";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for instance-level (team-based) access control.
|
|
14
|
+
* Specifies how to extract resource IDs from requests or responses.
|
|
15
|
+
*/
|
|
16
|
+
export interface InstanceAccessConfig {
|
|
17
|
+
/**
|
|
18
|
+
* For single-resource endpoints: parameter path in request input to extract resource ID.
|
|
19
|
+
* Uses dot notation for nested params (e.g., "params.systemId").
|
|
20
|
+
*/
|
|
21
|
+
idParam?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* For list endpoints: key in response object containing the array to filter.
|
|
25
|
+
* When set, post-filters results based on team grants.
|
|
26
|
+
* Example: "systems" for response { systems: [...] }
|
|
27
|
+
*/
|
|
28
|
+
listKey?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* For bulk record endpoints: key in response containing a Record<resourceId, data>.
|
|
32
|
+
* When set, post-filters the record keys based on team grants.
|
|
33
|
+
* Example: "statuses" for response { statuses: { [systemId]: {...} } }
|
|
34
|
+
*/
|
|
35
|
+
recordKey?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An Access Rule defines WHO can do WHAT to WHICH resources.
|
|
40
|
+
* This is the fundamental building block of the unified access control system.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // Simple access rule (no instance-level checks)
|
|
45
|
+
* const teamsRead = access("teams", "read", "View teams");
|
|
46
|
+
*
|
|
47
|
+
* // With instance-level access (single resource)
|
|
48
|
+
* const systemRead = access("system", "read", "View systems", {
|
|
49
|
+
* idParam: "systemId",
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* // With instance-level access (list filtering)
|
|
53
|
+
* const systemListRead = access("system", "read", "View systems", {
|
|
54
|
+
* listKey: "systems",
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export interface AccessRule {
|
|
59
|
+
/**
|
|
60
|
+
* Unique identifier for this rule (e.g., "system.read").
|
|
61
|
+
* Auto-generated from resource + level.
|
|
62
|
+
* This is used as the access rule ID when checking user access rules.
|
|
63
|
+
*/
|
|
64
|
+
readonly id: string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The resource domain (e.g., "system", "healthcheck", "incident").
|
|
68
|
+
* Combined with pluginId to create fully-qualified type for team access checks.
|
|
69
|
+
*/
|
|
70
|
+
readonly resource: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The access level this rule grants: "read" or "manage".
|
|
74
|
+
* Directly maps to canRead/canManage in team grants.
|
|
75
|
+
*/
|
|
76
|
+
readonly level: AccessLevel;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Human-readable description of what this rule allows.
|
|
80
|
+
*/
|
|
81
|
+
readonly description: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether this rule is granted by default to authenticated users ("users" role).
|
|
85
|
+
*/
|
|
86
|
+
readonly isDefault?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether this rule is granted to anonymous/public users ("anonymous" role).
|
|
90
|
+
*/
|
|
91
|
+
readonly isPublic?: boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Instance-level (team-based) access configuration.
|
|
95
|
+
* If defined, the middleware will check team grants for specific resource instances.
|
|
96
|
+
* If undefined, only global access is checked.
|
|
97
|
+
*/
|
|
98
|
+
readonly instanceAccess?: InstanceAccessConfig;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a fully-qualified access rule ID by prefixing the rule's ID with the plugin ID.
|
|
103
|
+
*
|
|
104
|
+
* This is the canonical way to construct namespaced access rule IDs for authorization checks.
|
|
105
|
+
* The function ensures consistent formatting across all access-related operations.
|
|
106
|
+
*
|
|
107
|
+
* @param pluginMetadata - The plugin metadata containing the pluginId
|
|
108
|
+
* @param rule - The access rule object containing the local access rule ID
|
|
109
|
+
* @returns The fully-qualified access rule ID (e.g., "catalog.system.read")
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* import { qualifyAccessRuleId } from "@checkstack/common";
|
|
114
|
+
* import { pluginMetadata } from "./plugin-metadata";
|
|
115
|
+
* import { catalogAccess } from "./access";
|
|
116
|
+
*
|
|
117
|
+
* const qualifiedId = qualifyAccessRuleId(pluginMetadata, catalogAccess.systems.read);
|
|
118
|
+
* // Returns: "catalog.system.read"
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function qualifyAccessRuleId(
|
|
122
|
+
pluginMetadata: Pick<PluginMetadata, "pluginId">,
|
|
123
|
+
rule: Pick<AccessRule, "id">
|
|
124
|
+
): string {
|
|
125
|
+
return `${pluginMetadata.pluginId}.${rule.id}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates an access rule for a resource.
|
|
130
|
+
*
|
|
131
|
+
* @param resource - The resource name (e.g., "system", "incident")
|
|
132
|
+
* @param level - The access level ("read" or "manage")
|
|
133
|
+
* @param description - Human-readable description
|
|
134
|
+
* @param options - Optional configuration for defaults and instance-level access
|
|
135
|
+
* @returns An AccessRule object
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Simple access rule
|
|
140
|
+
* const teamsRead = access("teams", "read", "View teams");
|
|
141
|
+
*
|
|
142
|
+
* // With defaults for public access
|
|
143
|
+
* const statusRead = access("status", "read", "View status", {
|
|
144
|
+
* isDefault: true,
|
|
145
|
+
* isPublic: true,
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* // With instance-level access
|
|
149
|
+
* const systemRead = access("system", "read", "View systems", {
|
|
150
|
+
* idParam: "systemId",
|
|
151
|
+
* listKey: "systems",
|
|
152
|
+
* isDefault: true,
|
|
153
|
+
* isPublic: true,
|
|
154
|
+
* });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function access(
|
|
158
|
+
resource: string,
|
|
159
|
+
level: AccessLevel,
|
|
160
|
+
description: string,
|
|
161
|
+
options?: {
|
|
162
|
+
idParam?: string;
|
|
163
|
+
listKey?: string;
|
|
164
|
+
recordKey?: string;
|
|
165
|
+
isDefault?: boolean;
|
|
166
|
+
isPublic?: boolean;
|
|
167
|
+
}
|
|
168
|
+
): AccessRule {
|
|
169
|
+
const hasInstanceAccess =
|
|
170
|
+
options?.idParam || options?.listKey || options?.recordKey;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id: `${resource}.${level}`,
|
|
174
|
+
resource,
|
|
175
|
+
level,
|
|
176
|
+
description,
|
|
177
|
+
isDefault: options?.isDefault,
|
|
178
|
+
isPublic: options?.isPublic,
|
|
179
|
+
instanceAccess: hasInstanceAccess
|
|
180
|
+
? {
|
|
181
|
+
idParam: options?.idParam,
|
|
182
|
+
listKey: options?.listKey,
|
|
183
|
+
recordKey: options?.recordKey,
|
|
184
|
+
}
|
|
185
|
+
: undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates a read/manage pair for a resource.
|
|
191
|
+
* Most resources need both access levels, so this reduces boilerplate.
|
|
192
|
+
*
|
|
193
|
+
* @param resource - The resource name (e.g., "system", "incident")
|
|
194
|
+
* @param descriptions - Descriptions for read and manage levels
|
|
195
|
+
* @param options - Optional configuration for defaults and instance-level access
|
|
196
|
+
* @returns An object with `read` and `manage` AccessRule properties
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* const systemAccess = accessPair("system", {
|
|
201
|
+
* read: "View systems",
|
|
202
|
+
* manage: "Create, update, and delete systems",
|
|
203
|
+
* }, {
|
|
204
|
+
* idParam: "systemId",
|
|
205
|
+
* listKey: "systems",
|
|
206
|
+
* readIsDefault: true,
|
|
207
|
+
* readIsPublic: true,
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* // Usage in contract:
|
|
211
|
+
* getSystem: _base.meta({ access: [systemAccess.read] }),
|
|
212
|
+
* updateSystem: _base.meta({ access: [systemAccess.manage] }),
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export function accessPair(
|
|
216
|
+
resource: string,
|
|
217
|
+
descriptions: { read: string; manage: string },
|
|
218
|
+
options?: {
|
|
219
|
+
idParam?: string;
|
|
220
|
+
listKey?: string;
|
|
221
|
+
recordKey?: string;
|
|
222
|
+
readIsDefault?: boolean;
|
|
223
|
+
readIsPublic?: boolean;
|
|
224
|
+
}
|
|
225
|
+
): { read: AccessRule; manage: AccessRule } {
|
|
226
|
+
return {
|
|
227
|
+
read: access(resource, "read", descriptions.read, {
|
|
228
|
+
idParam: options?.idParam,
|
|
229
|
+
listKey: options?.listKey,
|
|
230
|
+
recordKey: options?.recordKey,
|
|
231
|
+
isDefault: options?.readIsDefault,
|
|
232
|
+
isPublic: options?.readIsPublic,
|
|
233
|
+
}),
|
|
234
|
+
manage: access(resource, "manage", descriptions.manage, {
|
|
235
|
+
idParam: options?.idParam,
|
|
236
|
+
// Note: manage doesn't typically use listKey (you don't "manage" a list in bulk)
|
|
237
|
+
// but we include idParam for single-resource manage checks
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart types for auto-generated health check visualizations.
|
|
3
|
+
*
|
|
4
|
+
* Numeric types:
|
|
5
|
+
* - line: Time series line chart for numeric metrics over time
|
|
6
|
+
* - bar: Bar chart for distributions (record of string to number)
|
|
7
|
+
* - counter: Simple count display with trend indicator
|
|
8
|
+
* - gauge: Percentage gauge for rates/percentages (0-100)
|
|
9
|
+
*
|
|
10
|
+
* Non-numeric types:
|
|
11
|
+
* - boolean: Boolean indicator (success/failure, connected/disconnected)
|
|
12
|
+
* - text: Text display for string values
|
|
13
|
+
* - status: Status badge for error/warning states
|
|
14
|
+
*/
|
|
15
|
+
export type ChartType =
|
|
16
|
+
| "line"
|
|
17
|
+
| "bar"
|
|
18
|
+
| "pie"
|
|
19
|
+
| "counter"
|
|
20
|
+
| "gauge"
|
|
21
|
+
| "boolean"
|
|
22
|
+
| "text"
|
|
23
|
+
| "status";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Metadata type for health check result schemas.
|
|
27
|
+
* Provides autocompletion for chart-related metadata on result fields.
|
|
28
|
+
*/
|
|
29
|
+
export interface HealthResultMeta {
|
|
30
|
+
/** The type of chart to render for this field */
|
|
31
|
+
"x-chart-type"?: ChartType;
|
|
32
|
+
/** Human-readable label for the chart (defaults to field name) */
|
|
33
|
+
"x-chart-label"?: string;
|
|
34
|
+
/** Unit suffix for values (e.g., 'ms', '%', 'req/s') */
|
|
35
|
+
"x-chart-unit"?: string;
|
|
36
|
+
/** Whether this field supports JSONPath assertions */
|
|
37
|
+
"x-jsonpath"?: boolean;
|
|
38
|
+
}
|
package/src/client-definition.ts
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import type { ContractRouterClient, AnyContractRouter } from "@orpc/contract";
|
|
2
2
|
import type { PluginMetadata } from "./plugin-metadata";
|
|
3
|
+
import type { ProcedureMetadata } from "./types";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* A client definition that bundles an RPC contract
|
|
6
|
-
* Used for type-safe plugin RPC consumption.
|
|
6
|
+
* A client definition that bundles an RPC contract with its plugin metadata.
|
|
7
|
+
* Used for type-safe plugin RPC consumption and TanStack Query integration.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```typescript
|
|
10
11
|
* // Define in auth-common
|
|
11
12
|
* export const AuthApi = createClientDefinition(authContract, pluginMetadata);
|
|
12
13
|
*
|
|
13
|
-
* // Use in frontend
|
|
14
|
-
* const authClient =
|
|
14
|
+
* // Use in frontend with TanStack Query hooks
|
|
15
|
+
* const authClient = usePluginClient(AuthApi);
|
|
16
|
+
* const { data } = authClient.getUser.useQuery({ id });
|
|
15
17
|
* ```
|
|
16
18
|
*/
|
|
17
19
|
export interface ClientDefinition<
|
|
18
20
|
TContract extends AnyContractRouter = AnyContractRouter
|
|
19
21
|
> {
|
|
20
22
|
readonly pluginId: string;
|
|
23
|
+
readonly contract: TContract;
|
|
21
24
|
/**
|
|
22
25
|
* Phantom type for contract type inference.
|
|
23
26
|
* This property doesn't exist at runtime - it's only used by TypeScript
|
|
@@ -38,15 +41,37 @@ export interface ClientDefinition<
|
|
|
38
41
|
export type InferClient<T extends ClientDefinition> =
|
|
39
42
|
T extends ClientDefinition<infer C> ? ContractRouterClient<C> : never;
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Type helper to extract operationType from a procedure's metadata.
|
|
46
|
+
*/
|
|
47
|
+
export type ExtractOperationType<TProcedure> = TProcedure extends {
|
|
48
|
+
"~orpc": { meta: { operationType: infer T } };
|
|
49
|
+
}
|
|
50
|
+
? T
|
|
51
|
+
: "query"; // Default to query if not specified
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extracts the contract's metadata for a specific procedure path.
|
|
55
|
+
*/
|
|
56
|
+
export type InferProcedureMetadata<
|
|
57
|
+
TContract extends AnyContractRouter,
|
|
58
|
+
TPath extends keyof TContract
|
|
59
|
+
> = TContract[TPath] extends { "~orpc": { meta: infer M } }
|
|
60
|
+
? M extends ProcedureMetadata
|
|
61
|
+
? M
|
|
62
|
+
: never
|
|
63
|
+
: never;
|
|
64
|
+
|
|
41
65
|
/**
|
|
42
66
|
* Create a typed client definition for a plugin's RPC contract.
|
|
43
67
|
*
|
|
44
|
-
* This bundles the contract
|
|
45
|
-
*
|
|
68
|
+
* This bundles the contract with the plugin metadata, enabling type-safe
|
|
69
|
+
* TanStack Query hooks that expose only .useQuery() or .useMutation() based
|
|
70
|
+
* on the procedure's operationType.
|
|
46
71
|
*
|
|
47
|
-
* @param
|
|
72
|
+
* @param contract - The RPC contract object
|
|
48
73
|
* @param metadata - The plugin metadata from the plugin's plugin-metadata.ts
|
|
49
|
-
* @returns A ClientDefinition object that can be passed to
|
|
74
|
+
* @returns A ClientDefinition object that can be passed to usePluginClient()
|
|
50
75
|
*
|
|
51
76
|
* @example
|
|
52
77
|
* ```typescript
|
|
@@ -56,16 +81,18 @@ export type InferClient<T extends ClientDefinition> =
|
|
|
56
81
|
*
|
|
57
82
|
* export const AuthApi = createClientDefinition(authContract, pluginMetadata);
|
|
58
83
|
*
|
|
59
|
-
* // In consumer (frontend
|
|
60
|
-
* const authClient =
|
|
61
|
-
*
|
|
84
|
+
* // In consumer (frontend)
|
|
85
|
+
* const authClient = usePluginClient(AuthApi);
|
|
86
|
+
* const { data } = authClient.getUsers.useQuery({}); // Type-safe!
|
|
87
|
+
* const mutation = authClient.deleteUser.useMutation(); // Type-safe!
|
|
62
88
|
* ```
|
|
63
89
|
*/
|
|
64
90
|
export function createClientDefinition<TContract extends AnyContractRouter>(
|
|
65
|
-
|
|
91
|
+
contract: TContract,
|
|
66
92
|
metadata: PluginMetadata
|
|
67
93
|
): ClientDefinition<TContract> {
|
|
68
94
|
return {
|
|
69
95
|
pluginId: metadata.pluginId,
|
|
96
|
+
contract,
|
|
70
97
|
} as ClientDefinition<TContract>;
|
|
71
98
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,9 @@ export * from "./pagination";
|
|
|
3
3
|
export * from "./routes";
|
|
4
4
|
export * from "./plugin-metadata";
|
|
5
5
|
export * from "./client-definition";
|
|
6
|
-
export * from "./
|
|
6
|
+
export * from "./access-utils";
|
|
7
7
|
export * from "./icons";
|
|
8
8
|
export * from "./transport-client";
|
|
9
9
|
export * from "./json-schema";
|
|
10
|
+
export * from "./chart-types";
|
|
11
|
+
export * from "./procedure-builder";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import type { ContractProcedureBuilder } from "@orpc/contract";
|
|
3
|
+
import type { Schema, ErrorMap } from "@orpc/contract";
|
|
4
|
+
import type { ProcedureMetadata } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Return type of proc() that preserves the operationType in the type system.
|
|
8
|
+
* This extends ContractProcedureBuilder but with our specific ProcedureMetadata.
|
|
9
|
+
*/
|
|
10
|
+
export type TypedContractProcedureBuilder<TMeta extends ProcedureMetadata> =
|
|
11
|
+
ContractProcedureBuilder<
|
|
12
|
+
Schema<unknown, unknown>,
|
|
13
|
+
Schema<unknown, unknown>,
|
|
14
|
+
ErrorMap,
|
|
15
|
+
TMeta
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates an oRPC procedure builder with required metadata.
|
|
20
|
+
* All procedures MUST provide userType, operationType, and access.
|
|
21
|
+
*
|
|
22
|
+
* The return type preserves the operationType in the generic parameter,
|
|
23
|
+
* enabling type-safe hook selection (useQuery vs useMutation) in usePluginClient.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { proc } from "@checkstack/common";
|
|
28
|
+
*
|
|
29
|
+
* export const myContract = {
|
|
30
|
+
* // Returns TypedContractProcedureBuilder<{ operationType: "query", ... }>
|
|
31
|
+
* getItems: proc({
|
|
32
|
+
* operationType: "query",
|
|
33
|
+
* userType: "public",
|
|
34
|
+
* access: [myAccess.items.read],
|
|
35
|
+
* }).output(z.array(ItemSchema)),
|
|
36
|
+
*
|
|
37
|
+
* // Returns TypedContractProcedureBuilder<{ operationType: "mutation", ... }>
|
|
38
|
+
* createItem: proc({
|
|
39
|
+
* operationType: "mutation",
|
|
40
|
+
* userType: "authenticated",
|
|
41
|
+
* access: [myAccess.items.manage],
|
|
42
|
+
* })
|
|
43
|
+
* .input(CreateItemSchema)
|
|
44
|
+
* .output(ItemSchema),
|
|
45
|
+
* };
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function proc<TMeta extends ProcedureMetadata>(
|
|
49
|
+
meta: TMeta
|
|
50
|
+
): TypedContractProcedureBuilder<TMeta> {
|
|
51
|
+
return oc.$meta<TMeta>(meta) as TypedContractProcedureBuilder<TMeta>;
|
|
52
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -2,64 +2,7 @@
|
|
|
2
2
|
// RPC PROCEDURE METADATA
|
|
3
3
|
// =============================================================================
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* Configuration for resource-level access control.
|
|
7
|
-
* Used to enforce fine-grained permissions based on team grants.
|
|
8
|
-
*/
|
|
9
|
-
export interface ResourceAccessConfig {
|
|
10
|
-
/**
|
|
11
|
-
* The resource type identifier (e.g., "system", "healthcheck").
|
|
12
|
-
* Will be auto-prefixed with pluginId in middleware (e.g., "catalog.system").
|
|
13
|
-
*/
|
|
14
|
-
resourceType: string;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parameter name in request input to extract resource ID from.
|
|
18
|
-
* Uses dot notation for nested params (e.g., "params.id").
|
|
19
|
-
* Required for single-resource access checks.
|
|
20
|
-
*/
|
|
21
|
-
idParam?: string;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Filter mode for list endpoints.
|
|
25
|
-
* - "single": Check access to a single resource (default)
|
|
26
|
-
* - "list": Post-filter result array based on team access
|
|
27
|
-
*/
|
|
28
|
-
filterMode?: "single" | "list";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Key in response object containing the array to filter.
|
|
32
|
-
* Required when filterMode is "list".
|
|
33
|
-
* Example: "systems" for response { systems: [...] }
|
|
34
|
-
*/
|
|
35
|
-
outputKey?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Creates a ResourceAccessConfig for single-resource access checks.
|
|
40
|
-
*
|
|
41
|
-
* @param resourceType - The resource type (auto-prefixed with pluginId)
|
|
42
|
-
* @param idParam - Parameter path in input to extract resource ID from
|
|
43
|
-
*/
|
|
44
|
-
export function createResourceAccess(
|
|
45
|
-
resourceType: string,
|
|
46
|
-
idParam: string
|
|
47
|
-
): ResourceAccessConfig {
|
|
48
|
-
return { resourceType, idParam, filterMode: "single" };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Creates a ResourceAccessConfig for list filtering.
|
|
53
|
-
*
|
|
54
|
-
* @param resourceType - The resource type (auto-prefixed with pluginId)
|
|
55
|
-
* @param outputKey - Key in response object containing the array to filter
|
|
56
|
-
*/
|
|
57
|
-
export function createResourceAccessList(
|
|
58
|
-
resourceType: string,
|
|
59
|
-
outputKey: string
|
|
60
|
-
): ResourceAccessConfig {
|
|
61
|
-
return { resourceType, filterMode: "list", outputKey };
|
|
62
|
-
}
|
|
5
|
+
import type { AccessRule } from "./access-utils";
|
|
63
6
|
|
|
64
7
|
/**
|
|
65
8
|
* Qualifies a resource type with the plugin namespace.
|
|
@@ -85,7 +28,7 @@ export function qualifyResourceType(
|
|
|
85
28
|
* getItems: baseContractBuilder
|
|
86
29
|
* .meta({
|
|
87
30
|
* userType: "user",
|
|
88
|
-
*
|
|
31
|
+
* access: [myPluginAccess.items.read]
|
|
89
32
|
* })
|
|
90
33
|
* .output(z.array(ItemSchema)),
|
|
91
34
|
* };
|
|
@@ -93,25 +36,35 @@ export function qualifyResourceType(
|
|
|
93
36
|
export interface ProcedureMetadata {
|
|
94
37
|
/**
|
|
95
38
|
* Which type of caller can access this endpoint.
|
|
96
|
-
* - "anonymous": No authentication required, no
|
|
97
|
-
* - "public": Anyone can attempt, but
|
|
39
|
+
* - "anonymous": No authentication required, no access checks (fully public)
|
|
40
|
+
* - "public": Anyone can attempt, but access rules are checked (uses anonymous role for guests)
|
|
98
41
|
* - "user": Only real users (frontend authenticated)
|
|
99
42
|
* - "service": Only services (backend-to-backend)
|
|
100
43
|
* - "authenticated": Either users or services, but must be authenticated (default)
|
|
101
44
|
*/
|
|
102
|
-
userType
|
|
45
|
+
userType: "anonymous" | "public" | "user" | "service" | "authenticated";
|
|
103
46
|
|
|
104
47
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
48
|
+
* Operation type for TanStack Query integration.
|
|
49
|
+
* - "query": Read-only operation, uses useQuery hook
|
|
50
|
+
* - "mutation": Write operation, uses useMutation hook
|
|
51
|
+
*
|
|
52
|
+
* This is REQUIRED for all procedures to enable type-safe frontend hooks.
|
|
109
53
|
*/
|
|
110
|
-
|
|
54
|
+
operationType: "query" | "mutation";
|
|
111
55
|
|
|
112
56
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
57
|
+
* Unified access rules combining access rules and resource-level access control.
|
|
58
|
+
* Each rule specifies the access rule ID, access level, and optional instance-level checks.
|
|
59
|
+
*
|
|
60
|
+
* User must satisfy ALL rules (AND logic).
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* access: [catalogAccess.systems.read]
|
|
65
|
+
* access: [catalogAccess.systems.manage]
|
|
66
|
+
* access: [catalogAccess.groups.manage, catalogAccess.systems.manage] // Both required
|
|
67
|
+
* ```
|
|
115
68
|
*/
|
|
116
|
-
|
|
69
|
+
access: AccessRule[];
|
|
117
70
|
}
|
package/src/permission-utils.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { PluginMetadata } from "./plugin-metadata";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Supported actions for permissions.
|
|
5
|
-
*/
|
|
6
|
-
export type PermissionAction = "read" | "manage";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Represents a permission that can be assigned to roles.
|
|
10
|
-
*/
|
|
11
|
-
export interface Permission {
|
|
12
|
-
/** Permission identifier (e.g., "catalog.read") */
|
|
13
|
-
id: string;
|
|
14
|
-
/** Human-readable description of what this permission allows */
|
|
15
|
-
description?: string;
|
|
16
|
-
/** Whether this permission is assigned to the default "users" role (authenticated users) */
|
|
17
|
-
isAuthenticatedDefault?: boolean;
|
|
18
|
-
/** Whether this permission is assigned to the "anonymous" role (public access) */
|
|
19
|
-
isPublicDefault?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Represents a permission tied to a specific resource and action.
|
|
24
|
-
*/
|
|
25
|
-
export interface ResourcePermission extends Permission {
|
|
26
|
-
/** The resource this permission applies to */
|
|
27
|
-
resource: string;
|
|
28
|
-
/** The action allowed on the resource */
|
|
29
|
-
action: PermissionAction;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Helper to create a standardized resource permission.
|
|
34
|
-
*
|
|
35
|
-
* @param resource The resource name (e.g., "catalog", "healthcheck")
|
|
36
|
-
* @param action The action (e.g., "read", "manage")
|
|
37
|
-
* @param description Optional human-readable description
|
|
38
|
-
* @param options Additional options like isAuthenticatedDefault and isPublicDefault
|
|
39
|
-
*/
|
|
40
|
-
export function createPermission(
|
|
41
|
-
resource: string,
|
|
42
|
-
action: PermissionAction,
|
|
43
|
-
description?: string,
|
|
44
|
-
options?: { isAuthenticatedDefault?: boolean; isPublicDefault?: boolean }
|
|
45
|
-
): ResourcePermission {
|
|
46
|
-
return {
|
|
47
|
-
id: `${resource}.${action}`,
|
|
48
|
-
resource,
|
|
49
|
-
action,
|
|
50
|
-
description,
|
|
51
|
-
isAuthenticatedDefault: options?.isAuthenticatedDefault,
|
|
52
|
-
isPublicDefault: options?.isPublicDefault,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Creates a fully-qualified permission ID by prefixing the permission's ID with the plugin ID.
|
|
58
|
-
*
|
|
59
|
-
* This is the canonical way to construct namespaced permission IDs for authorization checks.
|
|
60
|
-
* The function ensures consistent formatting across all permission-related operations.
|
|
61
|
-
*
|
|
62
|
-
* @param pluginMetadata - The plugin metadata containing the pluginId
|
|
63
|
-
* @param permission - The permission object containing the local permission ID
|
|
64
|
-
* @returns The fully-qualified permission ID (e.g., "catalog.catalog.read")
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```typescript
|
|
68
|
-
* import { qualifyPermissionId } from "@checkstack/common";
|
|
69
|
-
* import { pluginMetadata } from "./plugin-metadata";
|
|
70
|
-
* import { permissions } from "./permissions";
|
|
71
|
-
*
|
|
72
|
-
* const qualifiedId = qualifyPermissionId(pluginMetadata, permissions.catalogRead);
|
|
73
|
-
* // Returns: "catalog.catalog.read"
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export function qualifyPermissionId(
|
|
77
|
-
pluginMetadata: Pick<PluginMetadata, "pluginId">,
|
|
78
|
-
permission: Pick<Permission, "id">
|
|
79
|
-
): string {
|
|
80
|
-
return `${pluginMetadata.pluginId}.${permission.id}`;
|
|
81
|
-
}
|