@checkstack/common 0.0.3 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,150 @@
1
1
  # @checkstack/common
2
2
 
3
+ ## 0.2.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
+ - f533141: Enforce health result factory function usage via branded types
72
+
73
+ - Added `healthResultSchema()` builder that enforces the use of factory functions at compile-time
74
+ - Added `healthResultArray()` factory for array fields (e.g., DNS resolved values)
75
+ - Added branded `HealthResultField<T>` type to mark schemas created by factory functions
76
+ - Consolidated `ChartType` and `HealthResultMeta` into `@checkstack/common` as single source of truth
77
+ - Updated all 12 health check strategies and 11 collectors to use `healthResultSchema()`
78
+ - Using raw `z.number()` etc. inside `healthResultSchema()` now causes a TypeScript error
79
+
80
+ ## 0.1.0
81
+
82
+ ### Minor Changes
83
+
84
+ - 8e43507: # Teams and Resource-Level Access Control
85
+
86
+ This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
87
+
88
+ ## Features
89
+
90
+ ### Team Management
91
+
92
+ - Create, update, and delete teams with name and description
93
+ - Add/remove users from teams
94
+ - Designate team managers with elevated privileges
95
+ - View team membership and manager status
96
+
97
+ ### Resource-Level Access Control
98
+
99
+ - Grant teams access to specific resources (systems, health checks, incidents, maintenances)
100
+ - Configure read-only or manage permissions per team
101
+ - Resource-level "Team Only" mode that restricts access exclusively to team members
102
+ - Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
103
+ - Automatic cleanup of grants when teams are deleted (database cascade)
104
+
105
+ ### Middleware Integration
106
+
107
+ - Extended `autoAuthMiddleware` to support resource access checks
108
+ - Single-resource pre-handler validation for detail endpoints
109
+ - Automatic list filtering for collection endpoints
110
+ - S2S endpoints for access verification
111
+
112
+ ### Frontend Components
113
+
114
+ - `TeamsTab` component for managing teams in Auth Settings
115
+ - `TeamAccessEditor` component for assigning team access to resources
116
+ - Resource-level "Team Only" toggle in `TeamAccessEditor`
117
+ - Integration into System, Health Check, Incident, and Maintenance editors
118
+
119
+ ## Breaking Changes
120
+
121
+ ### API Response Format Changes
122
+
123
+ List endpoints now return objects with named keys instead of arrays directly:
124
+
125
+ ```typescript
126
+ // Before
127
+ const systems = await catalogApi.getSystems();
128
+
129
+ // After
130
+ const { systems } = await catalogApi.getSystems();
131
+ ```
132
+
133
+ Affected endpoints:
134
+
135
+ - `catalog.getSystems` → `{ systems: [...] }`
136
+ - `healthcheck.getConfigurations` → `{ configurations: [...] }`
137
+ - `incident.listIncidents` → `{ incidents: [...] }`
138
+ - `maintenance.listMaintenances` → `{ maintenances: [...] }`
139
+
140
+ ### User Identity Enrichment
141
+
142
+ `RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
143
+
144
+ ## Documentation
145
+
146
+ See `docs/backend/teams.md` for complete API reference and integration guide.
147
+
3
148
  ## 0.0.3
4
149
 
5
150
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/common",
3
- "version": "0.0.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,228 @@
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
+ /**
32
+ * An Access Rule defines WHO can do WHAT to WHICH resources.
33
+ * This is the fundamental building block of the unified access control system.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * // Simple access rule (no instance-level checks)
38
+ * const teamsRead = access("teams", "read", "View teams");
39
+ *
40
+ * // With instance-level access (single resource)
41
+ * const systemRead = access("system", "read", "View systems", {
42
+ * idParam: "systemId",
43
+ * });
44
+ *
45
+ * // With instance-level access (list filtering)
46
+ * const systemListRead = access("system", "read", "View systems", {
47
+ * listKey: "systems",
48
+ * });
49
+ * ```
50
+ */
51
+ export interface AccessRule {
52
+ /**
53
+ * Unique identifier for this rule (e.g., "system.read").
54
+ * Auto-generated from resource + level.
55
+ * This is used as the access rule ID when checking user access rules.
56
+ */
57
+ readonly id: string;
58
+
59
+ /**
60
+ * The resource domain (e.g., "system", "healthcheck", "incident").
61
+ * Combined with pluginId to create fully-qualified type for team access checks.
62
+ */
63
+ readonly resource: string;
64
+
65
+ /**
66
+ * The access level this rule grants: "read" or "manage".
67
+ * Directly maps to canRead/canManage in team grants.
68
+ */
69
+ readonly level: AccessLevel;
70
+
71
+ /**
72
+ * Human-readable description of what this rule allows.
73
+ */
74
+ readonly description: string;
75
+
76
+ /**
77
+ * Whether this rule is granted by default to authenticated users ("users" role).
78
+ */
79
+ readonly isDefault?: boolean;
80
+
81
+ /**
82
+ * Whether this rule is granted to anonymous/public users ("anonymous" role).
83
+ */
84
+ readonly isPublic?: boolean;
85
+
86
+ /**
87
+ * Instance-level (team-based) access configuration.
88
+ * If defined, the middleware will check team grants for specific resource instances.
89
+ * If undefined, only global access is checked.
90
+ */
91
+ readonly instanceAccess?: InstanceAccessConfig;
92
+ }
93
+
94
+ /**
95
+ * Creates a fully-qualified access rule ID by prefixing the rule's ID with the plugin ID.
96
+ *
97
+ * This is the canonical way to construct namespaced access rule IDs for authorization checks.
98
+ * The function ensures consistent formatting across all access-related operations.
99
+ *
100
+ * @param pluginMetadata - The plugin metadata containing the pluginId
101
+ * @param rule - The access rule object containing the local access rule ID
102
+ * @returns The fully-qualified access rule ID (e.g., "catalog.system.read")
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * import { qualifyAccessRuleId } from "@checkstack/common";
107
+ * import { pluginMetadata } from "./plugin-metadata";
108
+ * import { catalogAccess } from "./access";
109
+ *
110
+ * const qualifiedId = qualifyAccessRuleId(pluginMetadata, catalogAccess.systems.read);
111
+ * // Returns: "catalog.system.read"
112
+ * ```
113
+ */
114
+ export function qualifyAccessRuleId(
115
+ pluginMetadata: Pick<PluginMetadata, "pluginId">,
116
+ rule: Pick<AccessRule, "id">
117
+ ): string {
118
+ return `${pluginMetadata.pluginId}.${rule.id}`;
119
+ }
120
+
121
+ /**
122
+ * Creates an access rule for a resource.
123
+ *
124
+ * @param resource - The resource name (e.g., "system", "incident")
125
+ * @param level - The access level ("read" or "manage")
126
+ * @param description - Human-readable description
127
+ * @param options - Optional configuration for defaults and instance-level access
128
+ * @returns An AccessRule object
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * // Simple access rule
133
+ * const teamsRead = access("teams", "read", "View teams");
134
+ *
135
+ * // With defaults for public access
136
+ * const statusRead = access("status", "read", "View status", {
137
+ * isDefault: true,
138
+ * isPublic: true,
139
+ * });
140
+ *
141
+ * // With instance-level access
142
+ * const systemRead = access("system", "read", "View systems", {
143
+ * idParam: "systemId",
144
+ * listKey: "systems",
145
+ * isDefault: true,
146
+ * isPublic: true,
147
+ * });
148
+ * ```
149
+ */
150
+ export function access(
151
+ resource: string,
152
+ level: AccessLevel,
153
+ description: string,
154
+ options?: {
155
+ idParam?: string;
156
+ listKey?: string;
157
+ isDefault?: boolean;
158
+ isPublic?: boolean;
159
+ }
160
+ ): AccessRule {
161
+ const hasInstanceAccess = options?.idParam || options?.listKey;
162
+
163
+ return {
164
+ id: `${resource}.${level}`,
165
+ resource,
166
+ level,
167
+ description,
168
+ isDefault: options?.isDefault,
169
+ isPublic: options?.isPublic,
170
+ instanceAccess: hasInstanceAccess
171
+ ? {
172
+ idParam: options?.idParam,
173
+ listKey: options?.listKey,
174
+ }
175
+ : undefined,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Creates a read/manage pair for a resource.
181
+ * Most resources need both access levels, so this reduces boilerplate.
182
+ *
183
+ * @param resource - The resource name (e.g., "system", "incident")
184
+ * @param descriptions - Descriptions for read and manage levels
185
+ * @param options - Optional configuration for defaults and instance-level access
186
+ * @returns An object with `read` and `manage` AccessRule properties
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const systemAccess = accessPair("system", {
191
+ * read: "View systems",
192
+ * manage: "Create, update, and delete systems",
193
+ * }, {
194
+ * idParam: "systemId",
195
+ * listKey: "systems",
196
+ * readIsDefault: true,
197
+ * readIsPublic: true,
198
+ * });
199
+ *
200
+ * // Usage in contract:
201
+ * getSystem: _base.meta({ access: [systemAccess.read] }),
202
+ * updateSystem: _base.meta({ access: [systemAccess.manage] }),
203
+ * ```
204
+ */
205
+ export function accessPair(
206
+ resource: string,
207
+ descriptions: { read: string; manage: string },
208
+ options?: {
209
+ idParam?: string;
210
+ listKey?: string;
211
+ readIsDefault?: boolean;
212
+ readIsPublic?: boolean;
213
+ }
214
+ ): { read: AccessRule; manage: AccessRule } {
215
+ return {
216
+ read: access(resource, "read", descriptions.read, {
217
+ idParam: options?.idParam,
218
+ listKey: options?.listKey,
219
+ isDefault: options?.readIsDefault,
220
+ isPublic: options?.readIsPublic,
221
+ }),
222
+ manage: access(resource, "manage", descriptions.manage, {
223
+ idParam: options?.idParam,
224
+ // Note: manage doesn't typically use listKey (you don't "manage" a list in bulk)
225
+ // but we include idParam for single-resource manage checks
226
+ }),
227
+ };
228
+ }
@@ -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/index.ts CHANGED
@@ -3,7 +3,8 @@ export * from "./pagination";
3
3
  export * from "./routes";
4
4
  export * from "./plugin-metadata";
5
5
  export * from "./client-definition";
6
- export * from "./permission-utils";
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";
package/src/types.ts CHANGED
@@ -2,6 +2,23 @@
2
2
  // RPC PROCEDURE METADATA
3
3
  // =============================================================================
4
4
 
5
+ import type { AccessRule } from "./access-utils";
6
+
7
+ /**
8
+ * Qualifies a resource type with the plugin namespace.
9
+ * @param pluginId - The plugin identifier
10
+ * @param resourceType - The unqualified resource type
11
+ * @returns Qualified resource type (e.g., "catalog.system")
12
+ */
13
+ export function qualifyResourceType(
14
+ pluginId: string,
15
+ resourceType: string
16
+ ): string {
17
+ // If already qualified (contains a dot), return as-is
18
+ if (resourceType.includes(".")) return resourceType;
19
+ return `${pluginId}.${resourceType}`;
20
+ }
21
+
5
22
  /**
6
23
  * Metadata interface for RPC procedures.
7
24
  * Used by contracts to define auth requirements and by backend middleware to enforce them.
@@ -11,7 +28,7 @@
11
28
  * getItems: baseContractBuilder
12
29
  * .meta({
13
30
  * userType: "user",
14
- * permissions: [permissions.myPluginRead.id]
31
+ * access: [myPluginAccess.items.read]
15
32
  * })
16
33
  * .output(z.array(ItemSchema)),
17
34
  * };
@@ -19,8 +36,8 @@
19
36
  export interface ProcedureMetadata {
20
37
  /**
21
38
  * Which type of caller can access this endpoint.
22
- * - "anonymous": No authentication required, no permission checks (fully public)
23
- * - "public": Anyone can attempt, but permissions are checked (uses anonymous role for guests)
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)
24
41
  * - "user": Only real users (frontend authenticated)
25
42
  * - "service": Only services (backend-to-backend)
26
43
  * - "authenticated": Either users or services, but must be authenticated (default)
@@ -28,10 +45,17 @@ export interface ProcedureMetadata {
28
45
  userType?: "anonymous" | "public" | "user" | "service" | "authenticated";
29
46
 
30
47
  /**
31
- * Permissions required to access this endpoint.
32
- * Only enforced for real users - services are trusted.
33
- * For "public" userType, permissions are checked against the anonymous role if not authenticated.
34
- * User must have at least one of the listed permissions, or "*" (wildcard).
48
+ * Unified access rules combining access rules and resource-level access control.
49
+ * Each rule specifies the access rule ID, access level, and optional instance-level checks.
50
+ *
51
+ * User must satisfy ALL rules (AND logic).
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * access: [catalogAccess.systems.read]
56
+ * access: [catalogAccess.systems.manage]
57
+ * access: [catalogAccess.groups.manage, catalogAccess.systems.manage] // Both required
58
+ * ```
35
59
  */
36
- permissions?: string[];
60
+ access?: AccessRule[];
37
61
  }
@@ -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
- }