@checkstack/incident-common 1.6.4 → 1.7.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,142 @@
1
1
  # @checkstack/incident-common
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e430fbe: Add "Mass delete" and "Mass resolve" to the Incidents and Maintenances lists,
8
+ authorized per item (RLAC).
9
+
10
+ The incidents and maintenances list pages now support multi-select with a bulk
11
+ action bar. A user may only select and act on entries they are allowed to
12
+ MANAGE: a row's checkbox appears only when the caller can manage it (the same
13
+ `canAccess(id)` gate as the per-row actions), so a team-scoped member sees
14
+ checkboxes only for their team's entries. Mass delete confirms before running;
15
+ mass resolve (incidents) and mass complete (maintenances, the "resolve"
16
+ equivalent = close, status -> completed) skip entries that are already
17
+ resolved/completed. Each action reports a per-id partial-success summary
18
+ (e.g. "3 deleted, 1 skipped").
19
+
20
+ New backend procedures: `incident.bulkDeleteIncidents`,
21
+ `incident.bulkResolveIncidents`, `maintenance.bulkDeleteMaintenances`, and
22
+ `maintenance.bulkCloseMaintenances`. Each authorizes EACH id against the
23
+ caller's manage grant and never fails open: unauthorized ids are filtered out
24
+ before the handler runs and returned as `forbidden`; missing ids as `notFound`;
25
+ a per-id failure is isolated as `error` without aborting the batch. Per-id cache
26
+ invalidation, realtime signals, and subscriber notifications run for every
27
+ success so dashboards and status pages stay consistent.
28
+
29
+ Platform: a new `instanceAccess` mode `bulkManage: { idsParam }` is the
30
+ enforcement point for bulk writes. Before the handler runs, `autoAuthMiddleware`
31
+ partitions the input id array into the caller's manageable subset and the denied
32
+ remainder and exposes both on `context.bulkAccess` (fail-closed on an S2S
33
+ error). The boot-time contract validator (`validateContractInstanceAccess`)
34
+ accepts `bulkManage` as one of the mutually-exclusive scoping modes, marks its
35
+ type team-scopable, and cross-checks `idsParam` against the input schema.
36
+
37
+ State and scale: authorization is derived per request from the shared team-grant
38
+ store via the existing auth S2S path (no process-local state); the read returns
39
+ the same answer on every pod. No database migration.
40
+
41
+ - 0d912a3: Make the frontend fully RLAC-aware so team-scoped users see and can use exactly
42
+ what the backend already authorises - no more, no less. Previously every nav
43
+ entry, route, management page, create button, per-row action, and resource
44
+ picker gated purely on a user's GLOBAL access rule, so a user whose team manages
45
+ a system saw none of the surfaces the backend would happily let them use, and
46
+ (where a page did render) could select systems they don't manage and only fail
47
+ after submit.
48
+
49
+ Platform primitives (on `AccessApi`, from `@checkstack/frontend-api`, implemented
50
+ in `@checkstack/auth-frontend`). Each ORs the global RBAC rule with team-derived
51
+ (ReBAC) grants, so a global-rule holder always sees everything:
52
+
53
+ - `useCanCreate({ accessRule, objectType, parentType? })` - may the user create
54
+ this type (global rule, a team `creator` grant, or managing a parent resource).
55
+ - `useCanAccessType({ accessRule, objectType, parentType? })` - may the user
56
+ reach a management SURFACE for this type at all (create capability OR managing
57
+ any existing object of the type / its parent). Powers route guards, sidebar
58
+ entries, and a management page's top-level `allowed`.
59
+ - `useResourceAccess({ accessRule, objectType, resourceIds })` - a `canAccess(id)`
60
+ predicate for per-row controls and for filtering resource pickers.
61
+
62
+ Backed by three authenticated `auth` RPC procedures - `canCreate`,
63
+ `myManageableTypes`, and `listMyAccessibleResources` - the frontend-facing
64
+ mirrors of the existing S2S authorization endpoints, resolved against the
65
+ caller's own team grants.
66
+
67
+ Route/nav gating is now capability-aware: a route may declare
68
+ `manageCapability: { objectType, parentType? }`; the route guard and sidebar then
69
+ show/allow it for team-scoped users via `myManageableTypes`. Applied to the
70
+ catalog, incident, maintenance, SLO, healthcheck, automation, and status-page
71
+ management routes. The route guard resolves this through a single
72
+ `useRouteAccess` hook with a constant hook count, since the guard is reconciled
73
+ in place as the URL changes (a conditional hook there would trip the rules of
74
+ hooks).
75
+
76
+ Resource types are now typed, plugin-qualified constants. A new
77
+ `resourceType(pluginMetadata, localType)` factory in `@checkstack/common` mints a
78
+ nominal `ResourceType`, and each `*-common` package exports its constants (e.g.
79
+ `catalogResourceTypes.system`, `incidentResourceTypes.incident`). The capability
80
+ APIs accept `ResourceType`, so a mistyped `"catalog.system"` string now fails
81
+ typecheck instead of silently breaking a gate.
82
+
83
+ Resource pickers now offer only what the backend will accept:
84
+
85
+ - Incident and maintenance "Affected Systems" pickers show only systems the user
86
+ manages (or all with the global rule), matching the backend's requirement of
87
+ MANAGE on every referenced system.
88
+ - SLO creation is now system-scoped end to end: `createObjective` gains a
89
+ `catalog.system` parent gate (managing the target system authorises creating an
90
+ SLO for it, like incident/maintenance), and the SLO editor's system picker is
91
+ filtered to manageable systems.
92
+ - Catalog group and environment membership (add-to-group / add-to-environment,
93
+ per-row and bulk) is gated on managing the system being (re)assigned.
94
+ - The health-check assignment surface (Assignment IDE + the system-detail
95
+ "Health Checks" action) requires MANAGE on the target system.
96
+
97
+ Catalog membership chips only render a removable "x" for systems the user
98
+ manages (removing a group/environment membership requires managing the system),
99
+ and the Dependency Map only lets a user originate an edge from a system they
100
+ manage (the source is access-checked; the target is not).
101
+
102
+ Owning-team correctness: a parent-gated creator (team member, no global rule)
103
+ who left the owning team unset previously created an object with no team grant -
104
+ which they then could not edit. The `authorizeCreate` parent-gate path now
105
+ resolves an owning team instead of silently orphaning the object (auto-assigns
106
+ when the caller belongs to exactly one team, requires an explicit choice when
107
+ several), and the `TeamOwnershipPicker` marks the field required and
108
+ auto-selects the sole eligible team.
109
+
110
+ Dependency writes are fixed to authorize on the SOURCE system. `createDependency`
111
+ / `updateDependency` / `deleteDependency` previously used `instanceAccess:
112
+ { idParam: "systemId" }`, which made the middleware look for a `dependency` grant
113
+ keyed by the system id - a grant that never exists - so every team-scoped source
114
+ manager was denied ("Access denied to resource dependency:<systemId>"). They now
115
+ `parentScope` on `catalog.system` manage, so managing the source system
116
+ authorises editing its dependencies (the target is not access-checked), matching
117
+ health-check assignment.
118
+
119
+ The backend authorization changes are limited to: the new read-only capability
120
+ procedures (`canCreate` / `myManageableTypes` / `listMyAccessibleResources`), the
121
+ SLO create parent gate, the `authorizeCreate` owning-team resolution, and the
122
+ dependency source-scope fix. Everything else only aligns the UI with
123
+ authorization the backend already enforced.
124
+
125
+ ### Patch Changes
126
+
127
+ - Updated dependencies [d1b71b6]
128
+ - Updated dependencies [d9f4654]
129
+ - Updated dependencies [e430fbe]
130
+ - Updated dependencies [eab80e3]
131
+ - Updated dependencies [259b93c]
132
+ - Updated dependencies [53666a7]
133
+ - Updated dependencies [0d912a3]
134
+ - @checkstack/notification-common@1.5.0
135
+ - @checkstack/frontend-api@0.13.0
136
+ - @checkstack/common@0.19.0
137
+ - @checkstack/catalog-common@2.6.0
138
+ - @checkstack/signal-common@0.2.14
139
+
3
140
  ## 1.6.4
4
141
 
5
142
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/incident-common",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -12,18 +12,18 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@checkstack/common": "0.18.0",
16
- "@checkstack/catalog-common": "2.5.0",
17
- "@checkstack/frontend-api": "0.12.1",
18
- "@checkstack/notification-common": "1.4.2",
19
- "@checkstack/signal-common": "0.2.13",
15
+ "@checkstack/common": "0.19.0",
16
+ "@checkstack/catalog-common": "2.6.0",
17
+ "@checkstack/frontend-api": "0.13.0",
18
+ "@checkstack/notification-common": "1.5.0",
19
+ "@checkstack/signal-common": "0.2.14",
20
20
  "@orpc/contract": "^1.14.4",
21
21
  "zod": "^4.2.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "^5.7.2",
25
25
  "@checkstack/tsconfig": "0.0.7",
26
- "@checkstack/scripts": "0.6.5"
26
+ "@checkstack/scripts": "0.7.0"
27
27
  },
28
28
  "scripts": {
29
29
  "typecheck": "tsgo -b",
package/src/access.ts CHANGED
@@ -1,6 +1,15 @@
1
- import { accessPair } from "@checkstack/common";
1
+ import { accessPair, resourceType } from "@checkstack/common";
2
2
  import { pluginMetadata } from "./plugin-metadata";
3
3
 
4
+ /**
5
+ * Plugin-qualified resource type ids for the incident plugin's resources.
6
+ * Reference these (never a raw `"incident.incident"` string) at capability call
7
+ * sites so a typo fails typecheck.
8
+ */
9
+ export const incidentResourceTypes = {
10
+ incident: resourceType(pluginMetadata, "incident"),
11
+ };
12
+
4
13
  /**
5
14
  * Access rules for the Incident plugin.
6
15
  */
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- export { incidentAccess, incidentAccessRules } from "./access";
1
+ export {
2
+ incidentAccess,
3
+ incidentAccessRules,
4
+ incidentResourceTypes,
5
+ } from "./access";
2
6
  export {
3
7
  incidentContract,
4
8
  IncidentApi,
@@ -16,6 +20,10 @@ export {
16
20
  CreateIncidentInputSchema,
17
21
  UpdateIncidentInputSchema,
18
22
  AddIncidentUpdateInputSchema,
23
+ BulkIncidentActionStatusEnum,
24
+ BulkIncidentActionResultSchema,
25
+ BulkIncidentIdsInputSchema,
26
+ BulkResolveIncidentsInputSchema,
19
27
  type IncidentStatus,
20
28
  type IncidentSeverity,
21
29
  type Incident,
@@ -27,6 +35,10 @@ export {
27
35
  type CreateIncidentInput,
28
36
  type UpdateIncidentInput,
29
37
  type AddIncidentUpdateInput,
38
+ type BulkIncidentActionStatus,
39
+ type BulkIncidentActionResult,
40
+ type BulkIncidentIdsInput,
41
+ type BulkResolveIncidentsInput,
30
42
  } from "./schemas";
31
43
  export { IncidentDetailsSlot, IncidentStatusSlot } from "./slots";
32
44
  export {
@@ -39,6 +39,7 @@ interface InstanceAccess {
39
39
  recordKey?: string;
40
40
  create?: InstanceAccessCreate;
41
41
  parentScope?: InstanceAccessParentScope;
42
+ bulkManage?: { idsParam?: string };
42
43
  }
43
44
 
44
45
  function instanceAccessFor(
@@ -146,3 +147,40 @@ describe("incident contract createIncident carries create-mode team ownership",
146
147
  expect(access?.create).toBeUndefined();
147
148
  });
148
149
  });
150
+
151
+ describe("incident contract bulk actions gate per-id via bulkManage", () => {
152
+ // Both bulk mutations authorize EACH id against the caller's manage grant
153
+ // through the platform `bulkManage` instance-access mode, keyed on the `ids`
154
+ // input array (RLAC: never fail open on a bulk WRITE).
155
+ const bulkProcs: Array<keyof typeof incidentContract> = [
156
+ "bulkDeleteIncidents",
157
+ "bulkResolveIncidents",
158
+ ];
159
+
160
+ for (const proc of bulkProcs) {
161
+ test(`${proc} declares instanceAccess.bulkManage.idsParam = "ids"`, () => {
162
+ const access = instanceAccessFor(proc);
163
+ expect(access).toBeDefined();
164
+ expect(access?.bulkManage?.idsParam).toBe("ids");
165
+ // Exactly one mode: no object/list/record/create scoping alongside it.
166
+ expect(access?.idParam).toBeUndefined();
167
+ expect(access?.listKey).toBeUndefined();
168
+ expect(access?.recordKey).toBeUndefined();
169
+ expect(access?.create).toBeUndefined();
170
+ expect(access?.global).toBeUndefined();
171
+ });
172
+
173
+ test(`${proc} is gated on the incident.manage access rule`, () => {
174
+ const procDef = incidentContract[proc] as unknown as Record<
175
+ string,
176
+ unknown
177
+ >;
178
+ const orpc = procDef["~orpc"] as {
179
+ meta?: { access?: Array<{ id: string; resource: string; level: string }> };
180
+ };
181
+ const rule = orpc.meta?.access?.[0];
182
+ expect(rule?.resource).toBe("incident");
183
+ expect(rule?.level).toBe("manage");
184
+ });
185
+ }
186
+ });
@@ -12,6 +12,9 @@ import {
12
12
  UpdateIncidentInputSchema,
13
13
  AddIncidentUpdateInputSchema,
14
14
  IncidentStatusEnum,
15
+ BulkIncidentActionResultSchema,
16
+ BulkIncidentIdsInputSchema,
17
+ BulkResolveIncidentsInputSchema,
15
18
  } from "./schemas";
16
19
 
17
20
  export const incidentContract = {
@@ -193,6 +196,41 @@ export const incidentContract = {
193
196
  .input(z.object({ id: z.string() }))
194
197
  .output(z.object({ success: z.boolean() })),
195
198
 
199
+ /**
200
+ * Mass-delete incidents, authorizing EACH id against the caller's manage
201
+ * grant. The `bulkManage` instance-access mode pre-partitions `input.ids`
202
+ * into the caller's manageable subset (global rule OR per-incident team
203
+ * grant) and the denied remainder BEFORE the handler runs, so an
204
+ * unauthorized id is never deleted. Returns a per-id result so the frontend
205
+ * can report partial success; a denied id is reported as `forbidden`, never a
206
+ * hard failure of the whole batch.
207
+ */
208
+ bulkDeleteIncidents: proc({
209
+ operationType: "mutation",
210
+ userType: "authenticated",
211
+ access: [incidentAccess.incident.manage],
212
+ instanceAccess: { bulkManage: { idsParam: "ids" } },
213
+ })
214
+ .route({ method: "POST" })
215
+ .input(BulkIncidentIdsInputSchema)
216
+ .output(z.object({ results: z.array(BulkIncidentActionResultSchema) })),
217
+
218
+ /**
219
+ * Mass-resolve incidents (mirrors the single-item `resolveIncident`: status →
220
+ * resolved). Authorizes EACH id against the caller's manage grant via the
221
+ * `bulkManage` mode; only authorized ids are resolved and a per-id result is
222
+ * returned. Already-resolved or missing ids are reported, never fatal.
223
+ */
224
+ bulkResolveIncidents: proc({
225
+ operationType: "mutation",
226
+ userType: "authenticated",
227
+ access: [incidentAccess.incident.manage],
228
+ instanceAccess: { bulkManage: { idsParam: "ids" } },
229
+ })
230
+ .route({ method: "POST" })
231
+ .input(BulkResolveIncidentsInputSchema)
232
+ .output(z.object({ results: z.array(BulkIncidentActionResultSchema) })),
233
+
196
234
  /**
197
235
  * Check if a system has an active incident with notification suppression enabled.
198
236
  * Used by the health check system to suppress notifications during acknowledged incidents.
package/src/schemas.ts CHANGED
@@ -126,3 +126,46 @@ export const AddIncidentUpdateInputSchema = z.object({
126
126
  export type AddIncidentUpdateInput = z.infer<
127
127
  typeof AddIncidentUpdateInputSchema
128
128
  >;
129
+
130
+ /**
131
+ * Per-id outcome of a bulk incident action (mass delete / mass resolve).
132
+ *
133
+ * - `deleted` / `resolved`: the action succeeded for this incident.
134
+ * - `notFound`: no incident with this id exists (or it vanished mid-batch).
135
+ * - `forbidden`: the caller cannot MANAGE this incident (filtered out by the
136
+ * `bulkManage` instance-access mode before the handler ran).
137
+ * - `error`: the per-id operation threw; the batch continued past it.
138
+ */
139
+ export const BulkIncidentActionStatusEnum = z.enum([
140
+ "deleted",
141
+ "resolved",
142
+ "notFound",
143
+ "forbidden",
144
+ "error",
145
+ ]);
146
+ export type BulkIncidentActionStatus = z.infer<
147
+ typeof BulkIncidentActionStatusEnum
148
+ >;
149
+
150
+ export const BulkIncidentActionResultSchema = z.object({
151
+ id: z.string(),
152
+ status: BulkIncidentActionStatusEnum,
153
+ });
154
+ export type BulkIncidentActionResult = z.infer<
155
+ typeof BulkIncidentActionResultSchema
156
+ >;
157
+
158
+ /** Input for a bulk incident action: a non-empty set of incident ids. */
159
+ export const BulkIncidentIdsInputSchema = z.object({
160
+ ids: z.array(z.string()).min(1, "At least one incident is required"),
161
+ });
162
+ export type BulkIncidentIdsInput = z.infer<typeof BulkIncidentIdsInputSchema>;
163
+
164
+ /** Input for bulk resolve: incident ids plus an optional shared message. */
165
+ export const BulkResolveIncidentsInputSchema = z.object({
166
+ ids: z.array(z.string()).min(1, "At least one incident is required"),
167
+ message: z.string().optional(),
168
+ });
169
+ export type BulkResolveIncidentsInput = z.infer<
170
+ typeof BulkResolveIncidentsInputSchema
171
+ >;