@checkstack/incident-common 1.6.4 → 1.7.1

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,153 @@
1
1
  # @checkstack/incident-common
2
2
 
3
+ ## 1.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [faf98f5]
8
+ - @checkstack/common@0.20.0
9
+ - @checkstack/catalog-common@2.6.1
10
+ - @checkstack/frontend-api@0.13.1
11
+ - @checkstack/notification-common@1.5.1
12
+ - @checkstack/signal-common@0.2.15
13
+
14
+ ## 1.7.0
15
+
16
+ ### Minor Changes
17
+
18
+ - e430fbe: Add "Mass delete" and "Mass resolve" to the Incidents and Maintenances lists,
19
+ authorized per item (RLAC).
20
+
21
+ The incidents and maintenances list pages now support multi-select with a bulk
22
+ action bar. A user may only select and act on entries they are allowed to
23
+ MANAGE: a row's checkbox appears only when the caller can manage it (the same
24
+ `canAccess(id)` gate as the per-row actions), so a team-scoped member sees
25
+ checkboxes only for their team's entries. Mass delete confirms before running;
26
+ mass resolve (incidents) and mass complete (maintenances, the "resolve"
27
+ equivalent = close, status -> completed) skip entries that are already
28
+ resolved/completed. Each action reports a per-id partial-success summary
29
+ (e.g. "3 deleted, 1 skipped").
30
+
31
+ New backend procedures: `incident.bulkDeleteIncidents`,
32
+ `incident.bulkResolveIncidents`, `maintenance.bulkDeleteMaintenances`, and
33
+ `maintenance.bulkCloseMaintenances`. Each authorizes EACH id against the
34
+ caller's manage grant and never fails open: unauthorized ids are filtered out
35
+ before the handler runs and returned as `forbidden`; missing ids as `notFound`;
36
+ a per-id failure is isolated as `error` without aborting the batch. Per-id cache
37
+ invalidation, realtime signals, and subscriber notifications run for every
38
+ success so dashboards and status pages stay consistent.
39
+
40
+ Platform: a new `instanceAccess` mode `bulkManage: { idsParam }` is the
41
+ enforcement point for bulk writes. Before the handler runs, `autoAuthMiddleware`
42
+ partitions the input id array into the caller's manageable subset and the denied
43
+ remainder and exposes both on `context.bulkAccess` (fail-closed on an S2S
44
+ error). The boot-time contract validator (`validateContractInstanceAccess`)
45
+ accepts `bulkManage` as one of the mutually-exclusive scoping modes, marks its
46
+ type team-scopable, and cross-checks `idsParam` against the input schema.
47
+
48
+ State and scale: authorization is derived per request from the shared team-grant
49
+ store via the existing auth S2S path (no process-local state); the read returns
50
+ the same answer on every pod. No database migration.
51
+
52
+ - 0d912a3: Make the frontend fully RLAC-aware so team-scoped users see and can use exactly
53
+ what the backend already authorises - no more, no less. Previously every nav
54
+ entry, route, management page, create button, per-row action, and resource
55
+ picker gated purely on a user's GLOBAL access rule, so a user whose team manages
56
+ a system saw none of the surfaces the backend would happily let them use, and
57
+ (where a page did render) could select systems they don't manage and only fail
58
+ after submit.
59
+
60
+ Platform primitives (on `AccessApi`, from `@checkstack/frontend-api`, implemented
61
+ in `@checkstack/auth-frontend`). Each ORs the global RBAC rule with team-derived
62
+ (ReBAC) grants, so a global-rule holder always sees everything:
63
+
64
+ - `useCanCreate({ accessRule, objectType, parentType? })` - may the user create
65
+ this type (global rule, a team `creator` grant, or managing a parent resource).
66
+ - `useCanAccessType({ accessRule, objectType, parentType? })` - may the user
67
+ reach a management SURFACE for this type at all (create capability OR managing
68
+ any existing object of the type / its parent). Powers route guards, sidebar
69
+ entries, and a management page's top-level `allowed`.
70
+ - `useResourceAccess({ accessRule, objectType, resourceIds })` - a `canAccess(id)`
71
+ predicate for per-row controls and for filtering resource pickers.
72
+
73
+ Backed by three authenticated `auth` RPC procedures - `canCreate`,
74
+ `myManageableTypes`, and `listMyAccessibleResources` - the frontend-facing
75
+ mirrors of the existing S2S authorization endpoints, resolved against the
76
+ caller's own team grants.
77
+
78
+ Route/nav gating is now capability-aware: a route may declare
79
+ `manageCapability: { objectType, parentType? }`; the route guard and sidebar then
80
+ show/allow it for team-scoped users via `myManageableTypes`. Applied to the
81
+ catalog, incident, maintenance, SLO, healthcheck, automation, and status-page
82
+ management routes. The route guard resolves this through a single
83
+ `useRouteAccess` hook with a constant hook count, since the guard is reconciled
84
+ in place as the URL changes (a conditional hook there would trip the rules of
85
+ hooks).
86
+
87
+ Resource types are now typed, plugin-qualified constants. A new
88
+ `resourceType(pluginMetadata, localType)` factory in `@checkstack/common` mints a
89
+ nominal `ResourceType`, and each `*-common` package exports its constants (e.g.
90
+ `catalogResourceTypes.system`, `incidentResourceTypes.incident`). The capability
91
+ APIs accept `ResourceType`, so a mistyped `"catalog.system"` string now fails
92
+ typecheck instead of silently breaking a gate.
93
+
94
+ Resource pickers now offer only what the backend will accept:
95
+
96
+ - Incident and maintenance "Affected Systems" pickers show only systems the user
97
+ manages (or all with the global rule), matching the backend's requirement of
98
+ MANAGE on every referenced system.
99
+ - SLO creation is now system-scoped end to end: `createObjective` gains a
100
+ `catalog.system` parent gate (managing the target system authorises creating an
101
+ SLO for it, like incident/maintenance), and the SLO editor's system picker is
102
+ filtered to manageable systems.
103
+ - Catalog group and environment membership (add-to-group / add-to-environment,
104
+ per-row and bulk) is gated on managing the system being (re)assigned.
105
+ - The health-check assignment surface (Assignment IDE + the system-detail
106
+ "Health Checks" action) requires MANAGE on the target system.
107
+
108
+ Catalog membership chips only render a removable "x" for systems the user
109
+ manages (removing a group/environment membership requires managing the system),
110
+ and the Dependency Map only lets a user originate an edge from a system they
111
+ manage (the source is access-checked; the target is not).
112
+
113
+ Owning-team correctness: a parent-gated creator (team member, no global rule)
114
+ who left the owning team unset previously created an object with no team grant -
115
+ which they then could not edit. The `authorizeCreate` parent-gate path now
116
+ resolves an owning team instead of silently orphaning the object (auto-assigns
117
+ when the caller belongs to exactly one team, requires an explicit choice when
118
+ several), and the `TeamOwnershipPicker` marks the field required and
119
+ auto-selects the sole eligible team.
120
+
121
+ Dependency writes are fixed to authorize on the SOURCE system. `createDependency`
122
+ / `updateDependency` / `deleteDependency` previously used `instanceAccess:
123
+ { idParam: "systemId" }`, which made the middleware look for a `dependency` grant
124
+ keyed by the system id - a grant that never exists - so every team-scoped source
125
+ manager was denied ("Access denied to resource dependency:<systemId>"). They now
126
+ `parentScope` on `catalog.system` manage, so managing the source system
127
+ authorises editing its dependencies (the target is not access-checked), matching
128
+ health-check assignment.
129
+
130
+ The backend authorization changes are limited to: the new read-only capability
131
+ procedures (`canCreate` / `myManageableTypes` / `listMyAccessibleResources`), the
132
+ SLO create parent gate, the `authorizeCreate` owning-team resolution, and the
133
+ dependency source-scope fix. Everything else only aligns the UI with
134
+ authorization the backend already enforced.
135
+
136
+ ### Patch Changes
137
+
138
+ - Updated dependencies [d1b71b6]
139
+ - Updated dependencies [d9f4654]
140
+ - Updated dependencies [e430fbe]
141
+ - Updated dependencies [eab80e3]
142
+ - Updated dependencies [259b93c]
143
+ - Updated dependencies [53666a7]
144
+ - Updated dependencies [0d912a3]
145
+ - @checkstack/notification-common@1.5.0
146
+ - @checkstack/frontend-api@0.13.0
147
+ - @checkstack/common@0.19.0
148
+ - @checkstack/catalog-common@2.6.0
149
+ - @checkstack/signal-common@0.2.14
150
+
3
151
  ## 1.6.4
4
152
 
5
153
  ### 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.1",
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.20.0",
16
+ "@checkstack/catalog-common": "2.6.1",
17
+ "@checkstack/frontend-api": "0.13.1",
18
+ "@checkstack/notification-common": "1.5.1",
19
+ "@checkstack/signal-common": "0.2.15",
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.1"
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
+ >;