@checkstack/backend-api 0.17.1 → 0.18.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,103 @@
1
1
  # @checkstack/backend-api
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6d52276: feat(automation): expose `trigger.actor` so automations can filter on who/what caused an event
8
+
9
+ Every platform event now carries an **actor** - the user, application (API
10
+ client), service (backend-to-backend), or `system` (background /
11
+ unauthenticated) that caused it - and the automation engine surfaces it to
12
+ automations as `trigger.actor`. This lets a trigger filter gate on the
13
+ origin of the event it reacts to:
14
+
15
+ ```text
16
+ {{ trigger.actor.type == "system" }} # auto-created by the platform
17
+ {{ trigger.actor.type == "user" }} # a human
18
+ {{ trigger.actor.id == "app-deploybot" }} # a specific application
19
+ ```
20
+
21
+ `trigger.actor` is available on **every** trigger - it is injected by the
22
+ platform, not declared per trigger - and editor autocomplete + Run Script
23
+ context types include `trigger.actor.{type,id,name}`.
24
+
25
+ How it works:
26
+
27
+ - **`@checkstack/common`** adds the canonical `Actor` type / `ActorSchema`
28
+ and `SYSTEM_ACTOR`.
29
+ - **`@checkstack/backend-api`** adds `resolveActor(user)` and a
30
+ `HookEventMeta` envelope. The hook listener / `onHook` signature gains an
31
+ optional second `meta` argument (additive, backward compatible).
32
+ - **`@checkstack/backend`** wraps emitted hooks in an envelope so the actor
33
+ travels with the payload through the distributed queue, unwrapping it
34
+ before delivery. The RPC emit path captures the authenticated caller;
35
+ background emits default to the system actor. Raw/legacy queue data is
36
+ treated as a system-actor payload, so delivery stays backward compatible.
37
+ - **`@checkstack/automation-backend`** threads the actor into the dispatch
38
+ scope (`trigger.actor`), available to trigger filters, top-level
39
+ conditions, and all run templates, and persisted in the run's scope
40
+ snapshot. Manual runs are attributed to the invoking user.
41
+ - **`@checkstack/automation-common`** / **`@checkstack/automation-frontend`**
42
+ expose `trigger.actor` in the editor variable scope and the generated
43
+ Run Script `context.trigger.actor` types.
44
+
45
+ No database migration and no per-trigger schema changes: the actor rides as
46
+ event-envelope metadata and in the run scope snapshot.
47
+
48
+ - 35bc682: feat(healthcheck): expose check + system run-context to script collectors
49
+
50
+ Script health checks can now read which check and system a run is for.
51
+ Previously shell scripts got only a curated env whitelist and inline
52
+ scripts only `context.config`, so a script had no built-in way to know
53
+ its own check name or the system it was checking.
54
+
55
+ - `@checkstack/backend-api`: new `CollectorRunContext` type
56
+ (`{ check: { id, name, intervalSeconds }, system: { id, name } }`) and
57
+ an optional `runContext` param on `CollectorStrategy.execute`. Optional,
58
+ so existing collector implementations are unaffected.
59
+ - Shell-script collector: injects reserved `CHECKSTACK_CHECK_ID`,
60
+ `CHECKSTACK_CHECK_NAME`, `CHECKSTACK_CHECK_INTERVAL_SECONDS`,
61
+ `CHECKSTACK_SYSTEM_ID`, `CHECKSTACK_SYSTEM_NAME` env vars (user-supplied
62
+ `env` still wins on collision).
63
+ - Inline-script collector: exposes `context.check` and `context.system`
64
+ alongside `context.config`; the inline-script editor now types them for
65
+ autocomplete.
66
+ - Shell editors (health-check collectors and automation shell actions) now
67
+ also suggest the user's own `env` (JSON) keys as `$NAME` completions, via
68
+ the new exported `customShellEnvVars` helper. Keys that aren't valid shell
69
+ identifiers are omitted.
70
+ - Fix: the Typefox `CodeEditor` captured a stale `onChange` at editor start,
71
+ so editing one `DynamicForm` field reverted sibling fields changed since
72
+ mount (e.g. typing in a shell `script` field wiped an unsaved `env` value,
73
+ or deleted a sibling automation action added after mount). The change
74
+ handler now routes through a ref to the current `onChange`.
75
+ - Fix: focusing a JSON editor threw "LanguageStatusService.addStatus is not
76
+ supported" because the standalone service set omitted `ILanguageStatusService`.
77
+ That one service is now registered via `serviceOverrides`.
78
+ - Fix: the automation trigger card nested a `<Badge>` (a `<div>`) inside a
79
+ `<p>`, producing a `validateDOMNesting` warning. Switched the wrapper to a
80
+ `<div>`.
81
+ - Local runs (`queue-executor`) and satellite runs both populate the
82
+ context. `SatelliteAssignment` (and the `getAssignmentsForSatellite`
83
+ RPC output) gained optional `configName` / `systemName` so the metadata
84
+ reaches satellite-side execution; `HealthCheckService` resolves the
85
+ system name via the catalog client.
86
+
87
+ BREAKING CHANGE: `createHealthCheckRouter` now requires a `catalogClient`
88
+ option (used to resolve system names for satellite assignments). Update
89
+ call sites to pass the catalog RPC client.
90
+
91
+ ### Patch Changes
92
+
93
+ - Updated dependencies [6d52276]
94
+ - Updated dependencies [35bc682]
95
+ - @checkstack/common@0.12.0
96
+ - @checkstack/healthcheck-common@1.3.0
97
+ - @checkstack/signal-common@0.2.5
98
+ - @checkstack/cache-api@0.3.6
99
+ - @checkstack/queue-api@0.3.6
100
+
3
101
  ## 0.17.1
4
102
 
5
103
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend-api",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -11,9 +11,9 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@checkstack/common": "0.11.0",
14
- "@checkstack/healthcheck-common": "1.1.2",
15
- "@checkstack/cache-api": "0.3.4",
16
- "@checkstack/queue-api": "0.3.4",
14
+ "@checkstack/healthcheck-common": "1.2.0",
15
+ "@checkstack/cache-api": "0.3.5",
16
+ "@checkstack/queue-api": "0.3.5",
17
17
  "@checkstack/signal-common": "0.2.4",
18
18
  "@orpc/client": "^1.13.14",
19
19
  "@orpc/contract": "^1.13.14",
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { SYSTEM_ACTOR } from "@checkstack/common";
3
+ import { resolveActor } from "./actor";
4
+
5
+ describe("resolveActor", () => {
6
+ it("falls back to the system actor when there is no caller", () => {
7
+ expect(resolveActor(undefined)).toEqual(SYSTEM_ACTOR);
8
+ });
9
+
10
+ it("maps a real (human) user", () => {
11
+ expect(
12
+ resolveActor({ type: "user", id: "user-1", name: "Nico" }),
13
+ ).toEqual({ type: "user", id: "user-1", name: "Nico" });
14
+ });
15
+
16
+ it("maps an application (API client)", () => {
17
+ expect(
18
+ resolveActor({ type: "application", id: "app-deploybot", name: "Deploy Bot" }),
19
+ ).toEqual({ type: "application", id: "app-deploybot", name: "Deploy Bot" });
20
+ });
21
+
22
+ it("maps a service to its originating plugin id", () => {
23
+ expect(resolveActor({ type: "service", pluginId: "healthcheck" })).toEqual({
24
+ type: "service",
25
+ id: "healthcheck",
26
+ name: "healthcheck",
27
+ });
28
+ });
29
+ });
package/src/actor.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { SYSTEM_ACTOR, type Actor } from "@checkstack/common";
2
+ import type { AuthUser } from "./types";
3
+
4
+ /**
5
+ * Resolve the canonical platform {@link Actor} for an event from the
6
+ * authenticated caller. Background / unauthenticated emits (no `user`)
7
+ * resolve to the system actor, so every emitted event carries an actor.
8
+ *
9
+ * - {@link RealUser} -> `{ type: "user" }`
10
+ * - {@link ApplicationUser} -> `{ type: "application" }`
11
+ * - {@link ServiceUser} -> `{ type: "service", id: pluginId }`
12
+ * - `undefined` -> {@link SYSTEM_ACTOR}
13
+ */
14
+ export function resolveActor(user?: AuthUser): Actor {
15
+ if (!user) return SYSTEM_ACTOR;
16
+ switch (user.type) {
17
+ case "user": {
18
+ return { type: "user", id: user.id, name: user.name };
19
+ }
20
+ case "application": {
21
+ return { type: "application", id: user.id, name: user.name };
22
+ }
23
+ case "service": {
24
+ return { type: "service", id: user.pluginId, name: user.pluginId };
25
+ }
26
+ }
27
+ }
@@ -17,6 +17,15 @@ export interface CollectorResult<TResult> {
17
17
  error?: string;
18
18
  }
19
19
 
20
+ /**
21
+ * Curated, read-only metadata about the health check + system a collector
22
+ * run is for. Metadata only - never secrets/config.
23
+ */
24
+ export interface CollectorRunContext {
25
+ check: { id: string; name: string; intervalSeconds: number };
26
+ system: { id: string; name: string };
27
+ }
28
+
20
29
  /**
21
30
  * Generic collector strategy interface.
22
31
  *
@@ -71,12 +80,15 @@ export interface CollectorStrategy<
71
80
  * @param params.config - Validated collector configuration
72
81
  * @param params.client - Connected transport client
73
82
  * @param params.pluginId - ID of the transport strategy invoking this collector
83
+ * @param params.runContext - Curated, read-only metadata about the health
84
+ * check + system this run is for (metadata only, never secrets/config)
74
85
  * @returns Collector result with typed metadata
75
86
  */
76
87
  execute(params: {
77
88
  config: TConfig;
78
89
  client: TClient;
79
90
  pluginId: string;
91
+ runContext?: CollectorRunContext;
80
92
  }): Promise<CollectorResult<TResult>>;
81
93
 
82
94
  /**
@@ -1,4 +1,9 @@
1
- import type { Hook, HookSubscribeOptions, HookUnsubscribe } from "./hooks";
1
+ import type {
2
+ Hook,
3
+ HookEventMeta,
4
+ HookSubscribeOptions,
5
+ HookUnsubscribe,
6
+ } from "./hooks";
2
7
 
3
8
  /**
4
9
  * EventBus interface for dependency injection
@@ -7,22 +12,26 @@ export interface EventBus {
7
12
  subscribe<T>(
8
13
  pluginId: string,
9
14
  hook: Hook<T>,
10
- listener: (payload: T) => Promise<void>,
15
+ listener: (payload: T, meta?: HookEventMeta) => Promise<void>,
11
16
  options?: HookSubscribeOptions
12
17
  ): Promise<HookUnsubscribe>;
13
18
 
14
19
  /**
15
20
  * Emit a hook through the distributed queue system.
16
21
  * All instances receive broadcast hooks; one instance handles work-queue hooks.
22
+ *
23
+ * `meta` carries event-envelope metadata (the acting `actor`). When omitted,
24
+ * the bus defaults to the system actor, so every emitted hook carries an
25
+ * actor even when emitted from a background/unauthenticated context.
17
26
  */
18
- emit<T>(hook: Hook<T>, payload: T): Promise<void>;
27
+ emit<T>(hook: Hook<T>, payload: T, meta?: HookEventMeta): Promise<void>;
19
28
 
20
29
  /**
21
30
  * Emit a hook locally only (not distributed).
22
31
  * Use for instance-local hooks that should only run on THIS instance.
23
32
  * Uses Promise.allSettled to ensure one listener error doesn't block others.
24
33
  */
25
- emitLocal<T>(hook: Hook<T>, payload: T): Promise<void>;
34
+ emitLocal<T>(hook: Hook<T>, payload: T, meta?: HookEventMeta): Promise<void>;
26
35
 
27
36
  shutdown(): Promise<void>;
28
37
  }
package/src/hooks.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccessRule } from "@checkstack/common";
1
+ import type { AccessRule, Actor } from "@checkstack/common";
2
2
 
3
3
  /**
4
4
  * Hook definition for type-safe event emission and subscription
@@ -8,6 +8,19 @@ export interface Hook<T = unknown> {
8
8
  _type?: T; // Phantom type for TypeScript inference
9
9
  }
10
10
 
11
+ /**
12
+ * Envelope metadata that travels alongside every emitted hook payload,
13
+ * independent of the hook's typed payload. Injected centrally at emit time
14
+ * (from the request context, defaulting to the system actor) and delivered to
15
+ * subscribers as the optional second listener argument.
16
+ *
17
+ * The automation engine reads `actor` and exposes it to automations as
18
+ * `trigger.actor`, so a trigger filter can gate on who/what caused the event.
19
+ */
20
+ export interface HookEventMeta {
21
+ actor: Actor;
22
+ }
23
+
11
24
  /**
12
25
  * Create a typed hook
13
26
  */
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export * from "./rpc";
17
17
  export * from "./test-utils";
18
18
  export * from "./hooks";
19
19
  export * from "./event-bus-types";
20
+ export * from "./actor";
20
21
  export * from "./plugin-source";
21
22
  export * from "./plugin-artifact-store";
22
23
  export * from "./notification-strategy";
@@ -2,7 +2,12 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import { ServiceRef } from "./service-ref";
3
3
  import { ExtensionPoint } from "./extension-point";
4
4
  import type { AccessRule, PluginMetadata } from "@checkstack/common";
5
- import type { Hook, HookSubscribeOptions, HookUnsubscribe } from "./hooks";
5
+ import type {
6
+ Hook,
7
+ HookEventMeta,
8
+ HookSubscribeOptions,
9
+ HookUnsubscribe,
10
+ } from "./hooks";
6
11
  import { Router } from "@orpc/server";
7
12
  import { RpcContext } from "./rpc";
8
13
  import { AnyContractRouter } from "@orpc/contract";
@@ -48,7 +53,7 @@ export type AfterPluginsReadyContext = {
48
53
  */
49
54
  onHook: <T>(
50
55
  hook: Hook<T>,
51
- listener: (payload: T) => Promise<void>,
56
+ listener: (payload: T, meta?: HookEventMeta) => Promise<void>,
52
57
  options?: HookSubscribeOptions,
53
58
  ) => HookUnsubscribe;
54
59
  /**