@checkstack/integration-backend 0.3.1 → 0.4.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,88 @@
|
|
|
1
1
|
# @checkstack/integration-backend
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [13373ce]
|
|
8
|
+
- @checkstack/common@0.14.0
|
|
9
|
+
- @checkstack/backend-api@0.21.1
|
|
10
|
+
- @checkstack/queue-api@0.3.10
|
|
11
|
+
- @checkstack/command-backend@0.2.1
|
|
12
|
+
- @checkstack/integration-common@0.7.1
|
|
13
|
+
- @checkstack/secrets-backend@0.2.1
|
|
14
|
+
- @checkstack/secrets-common@0.2.1
|
|
15
|
+
- @checkstack/signal-common@0.2.7
|
|
16
|
+
|
|
17
|
+
## 0.4.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 9dcc848: Harden config-versioning so stored configs always migrate-then-validate and broken migration chains fail fast at boot.
|
|
22
|
+
|
|
23
|
+
- `@checkstack/backend-api` `Versioned<T>` gains `parseAssumingV1` (migrate-from-v1 then validate leniently, runtime path), `parseStrictAssumingV1` (migrate then validate strictly, editor path), and `validateMigrationChainFromV1()`. A standalone pure helper `assertMigrationChainFromV1({ version, migrations })` is the single shared implementation behind the constructor guard and `validateMigrationChainFromV1`.
|
|
24
|
+
- `Versioned` now validates its own v1 -> `version` chain in the constructor, which runs at module import / plugin registration. A new `no-restricted-syntax` ESLint rule bans calling `parse` / `safeParse` / `parseAsync` / `strict` directly on a `Versioned`'s `.schema` member.
|
|
25
|
+
- Auth strategy migration chains are validated at the `betterAuthExtensionPoint.addStrategy` chokepoint (`@checkstack/auth-backend`).
|
|
26
|
+
- Automation action AND trigger configs migrate-then-validate (lenient at dispatch, strict in the editor validator, recursing into `choose`/`parallel`/`repeat`/`sequence` blocks). The `run_script` / `run_shell` action configs bump to `version: 2` dropping the removed `sandbox` key, fixing the editor's `Unrecognized key: sandbox` error.
|
|
27
|
+
- Anomaly read path now validates: `getAnomalyConfig` / `getAnomalyAssignmentConfig` run stored records through `Versioned.parseRecord`; `PartialAnomalySettingsSchema` moved to `@checkstack/anomaly-common`. Notification ConfigService reads thread the migrations argument, and per-strategy `userConfig` is migrate-then-validated before `send()`.
|
|
28
|
+
- gitops-apply migrate-then-validates authored health-check config; integration connection validation routes through `safeValidate`. The latent HTTP health-check `result` schema (at `version: 3` with no migrations) now ships a pass-through v1 -> v2 -> v3 chain.
|
|
29
|
+
|
|
30
|
+
BREAKING CHANGES (fail-fast at boot, intended):
|
|
31
|
+
|
|
32
|
+
- Any `Versioned` config with `version > 1` and an incomplete or non-contiguous migration chain now throws at construction (boot) instead of failing lazily on first read. This covers every `Versioned` instance repo-wide, including future plugin types. Out-of-tree plugins shipping such a config must add the missing migration step(s); all in-repo strategies already have complete chains.
|
|
33
|
+
- An auth strategy declaring `configVersion > 1` without a complete chain throws at registration.
|
|
34
|
+
- A trigger's per-automation config is now a versioned `config: Versioned<TConfig>` instead of a bare `configSchema?`. Plugins registering triggers with `configSchema:` must wrap it: `config: new Versioned({ version: 1, schema })`. The underlying schema stays reachable via `config.schema`; triggers without per-automation config are unaffected.
|
|
35
|
+
|
|
36
|
+
State and scale: all affected reads resolve from shared Postgres / in-process registries, so every pod sees the same migrated answer. No new framework-owned current-state store.
|
|
37
|
+
|
|
38
|
+
This is a beta minor.
|
|
39
|
+
|
|
40
|
+
- 9dcc848: Surface integration connection validation errors inline, and fix blank secrets clearing stored credentials on edit.
|
|
41
|
+
|
|
42
|
+
`@checkstack/ui` `DynamicForm` gains opt-in, backward-compatible props plus pure DOM-free helpers:
|
|
43
|
+
|
|
44
|
+
- `showInlineErrors` (default `false`): renders a concise per-field error under each touched required field; the `onValidChange` validity boolean derives from the same per-field error map.
|
|
45
|
+
- `fieldErrors`: an externally-supplied `{ [fieldPath]: message }` map (dot-joined for nested fields) for surfacing SERVER validation inline; nested paths flag their parent.
|
|
46
|
+
- `keepExistingSecretFields`: in EDIT mode, lists `x-secret` keys already stored server-side - a blank input means "keep existing" and is treated as VALID (CREATE mode omits it). New exported helpers: `deriveClientFieldErrors`, `deriveServerFieldErrors`, `parseServerValidationData`, `omitKeepExistingSecrets`, `listSecretFieldKeys`. `DynamicForm` also no longer shows a required (`*`) marker on the child fields of an OPTIONAL nested object while that object is empty (e.g. the OpenAI-compatible `spendCap`); required nested objects are unchanged.
|
|
47
|
+
|
|
48
|
+
`@checkstack/integration-backend`: connection-config validation failures attach the structured zod issues to `ORPCError.data` under a `CONFIG_VALIDATION` discriminator; the human-readable message is unchanged.
|
|
49
|
+
|
|
50
|
+
`@checkstack/integration-frontend` `ProviderConnectionsPage`: validation failures appear inline on the offending fields (the toast remains a fallback); Create/Save stays disabled while invalid; on edit a blank `x-secret` field is treated as "keep existing" (no required error, omitted from the update so the stored secret is not cleared).
|
|
51
|
+
|
|
52
|
+
BREAKING CHANGES: none. The new `DynamicForm` props are optional and default to previous behavior.
|
|
53
|
+
|
|
54
|
+
This is a beta minor.
|
|
55
|
+
|
|
56
|
+
- 9dcc848: Align workspace dependency versions and migrate React Router to v7.
|
|
57
|
+
|
|
58
|
+
BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
|
|
59
|
+
|
|
60
|
+
Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
|
|
61
|
+
|
|
62
|
+
Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
|
|
63
|
+
|
|
64
|
+
Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- Updated dependencies [9dcc848]
|
|
69
|
+
- Updated dependencies [9dcc848]
|
|
70
|
+
- Updated dependencies [9dcc848]
|
|
71
|
+
- Updated dependencies [9dcc848]
|
|
72
|
+
- Updated dependencies [9dcc848]
|
|
73
|
+
- Updated dependencies [9dcc848]
|
|
74
|
+
- Updated dependencies [9dcc848]
|
|
75
|
+
- Updated dependencies [9dcc848]
|
|
76
|
+
- Updated dependencies [9dcc848]
|
|
77
|
+
- @checkstack/backend-api@0.21.0
|
|
78
|
+
- @checkstack/common@0.13.0
|
|
79
|
+
- @checkstack/command-backend@0.2.0
|
|
80
|
+
- @checkstack/integration-common@0.7.0
|
|
81
|
+
- @checkstack/secrets-backend@0.2.0
|
|
82
|
+
- @checkstack/secrets-common@0.2.0
|
|
83
|
+
- @checkstack/queue-api@0.3.9
|
|
84
|
+
- @checkstack/signal-common@0.2.6
|
|
85
|
+
|
|
3
86
|
## 0.3.1
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/integration-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -15,23 +15,23 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@checkstack/integration-common": "0.
|
|
19
|
-
"@checkstack/backend-api": "0.
|
|
20
|
-
"@checkstack/signal-common": "0.2.
|
|
21
|
-
"@checkstack/queue-api": "0.3.
|
|
22
|
-
"@checkstack/common": "0.
|
|
23
|
-
"@checkstack/command-backend": "0.
|
|
24
|
-
"@checkstack/secrets-common": "0.0
|
|
25
|
-
"@checkstack/secrets-backend": "0.0
|
|
18
|
+
"@checkstack/integration-common": "0.7.0",
|
|
19
|
+
"@checkstack/backend-api": "0.21.0",
|
|
20
|
+
"@checkstack/signal-common": "0.2.6",
|
|
21
|
+
"@checkstack/queue-api": "0.3.9",
|
|
22
|
+
"@checkstack/common": "0.13.0",
|
|
23
|
+
"@checkstack/command-backend": "0.2.0",
|
|
24
|
+
"@checkstack/secrets-common": "0.2.0",
|
|
25
|
+
"@checkstack/secrets-backend": "0.2.0",
|
|
26
26
|
"drizzle-orm": "^0.45.0",
|
|
27
27
|
"zod": "^4.2.1",
|
|
28
|
-
"@orpc/server": "^1.
|
|
28
|
+
"@orpc/server": "^1.14.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@checkstack/drizzle-helper": "0.0.5",
|
|
32
|
-
"@checkstack/scripts": "0.
|
|
32
|
+
"@checkstack/scripts": "0.4.0",
|
|
33
33
|
"@checkstack/tsconfig": "0.0.7",
|
|
34
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
34
|
+
"@checkstack/test-utils-backend": "0.1.34",
|
|
35
35
|
"@types/node": "^20.0.0",
|
|
36
36
|
"drizzle-kit": "^0.31.10",
|
|
37
37
|
"typescript": "^5.0.0"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildConfigValidationErrorData,
|
|
6
|
+
CONFIG_VALIDATION_ERROR_CODE,
|
|
7
|
+
} from "./connection-validation-error";
|
|
8
|
+
|
|
9
|
+
const connectionSchema = z.object({
|
|
10
|
+
apiKey: z.string().min(1),
|
|
11
|
+
baseUrl: z.string().url(),
|
|
12
|
+
spendCap: z.object({ tokenBudget: z.number().positive() }).optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function errorFor(input: unknown): z.ZodError {
|
|
16
|
+
const result = connectionSchema.safeParse(input);
|
|
17
|
+
if (result.success) throw new Error("expected validation to fail");
|
|
18
|
+
return result.error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("buildConfigValidationErrorData", () => {
|
|
22
|
+
it("tags the payload with the config-validation discriminator", () => {
|
|
23
|
+
const data = buildConfigValidationErrorData(
|
|
24
|
+
errorFor({ apiKey: "", baseUrl: "not-a-url" }),
|
|
25
|
+
);
|
|
26
|
+
expect(data.code).toBe(CONFIG_VALIDATION_ERROR_CODE);
|
|
27
|
+
expect(data.issues.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("carries top-level field paths and messages", () => {
|
|
31
|
+
const data = buildConfigValidationErrorData(
|
|
32
|
+
errorFor({ apiKey: "", baseUrl: "https://x.test" }),
|
|
33
|
+
);
|
|
34
|
+
const apiKeyIssue = data.issues.find((i) => i.path[0] === "apiKey");
|
|
35
|
+
expect(apiKeyIssue).toBeDefined();
|
|
36
|
+
expect(typeof apiKeyIssue?.message).toBe("string");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("carries nested field paths", () => {
|
|
40
|
+
const data = buildConfigValidationErrorData(
|
|
41
|
+
errorFor({
|
|
42
|
+
apiKey: "sk",
|
|
43
|
+
baseUrl: "https://x.test",
|
|
44
|
+
spendCap: { tokenBudget: -1 },
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
const nested = data.issues.find(
|
|
48
|
+
(i) => i.path[0] === "spendCap" && i.path[1] === "tokenBudget",
|
|
49
|
+
);
|
|
50
|
+
expect(nested).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("yields a JSON-roundtrippable payload (string/number paths only)", () => {
|
|
54
|
+
const data = buildConfigValidationErrorData(
|
|
55
|
+
errorFor({ apiKey: "", baseUrl: "bad" }),
|
|
56
|
+
);
|
|
57
|
+
const roundtripped: unknown = JSON.parse(JSON.stringify(data));
|
|
58
|
+
expect(roundtripped).toEqual(data);
|
|
59
|
+
for (const issue of data.issues) {
|
|
60
|
+
for (const segment of issue.path) {
|
|
61
|
+
expect(["string", "number"]).toContain(typeof segment);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discriminator placed on the `ORPCError.data` payload so the frontend can
|
|
5
|
+
* tell a connection-config validation failure apart from any other
|
|
6
|
+
* structured error data and map each issue onto a form field.
|
|
7
|
+
*/
|
|
8
|
+
export const CONFIG_VALIDATION_ERROR_CODE = "CONFIG_VALIDATION";
|
|
9
|
+
|
|
10
|
+
/** A single field-scoped validation problem crossing the oRPC boundary. */
|
|
11
|
+
export interface ConfigValidationIssue {
|
|
12
|
+
/** Field path (string/number segments only; symbols are dropped). */
|
|
13
|
+
path: (string | number)[];
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Structured payload attached to `ORPCError.data` for config validation. */
|
|
18
|
+
export interface ConfigValidationErrorData {
|
|
19
|
+
code: typeof CONFIG_VALIDATION_ERROR_CODE;
|
|
20
|
+
issues: ConfigValidationIssue[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the structured `data` payload for a connection-config validation
|
|
25
|
+
* failure from a zod error. Symbol path segments cannot survive JSON
|
|
26
|
+
* serialization and never name a form field, so they are filtered out. Kept
|
|
27
|
+
* pure (no oRPC) so the wire shape can be unit-tested against the frontend
|
|
28
|
+
* parser without standing up the router.
|
|
29
|
+
*/
|
|
30
|
+
export function buildConfigValidationErrorData(
|
|
31
|
+
error: z.ZodError,
|
|
32
|
+
): ConfigValidationErrorData {
|
|
33
|
+
return {
|
|
34
|
+
code: CONFIG_VALIDATION_ERROR_CODE,
|
|
35
|
+
issues: error.issues.map((issue) => ({
|
|
36
|
+
path: issue.path.filter(
|
|
37
|
+
(segment): segment is string | number =>
|
|
38
|
+
typeof segment === "string" || typeof segment === "number",
|
|
39
|
+
),
|
|
40
|
+
message: issue.message,
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test: every integration provider `connectionSchema` (a Versioned)
|
|
3
|
+
* that is stored UNVERSIONED and read back via assume-v1-on-read MUST have a
|
|
4
|
+
* COMPLETE, contiguous migration chain from version 1 to its current
|
|
5
|
+
* `version`. Pure STRUCTURAL check (`validateMigrationChainFromV1` — no
|
|
6
|
+
* `migrate()` is run), so it carries zero per-config upkeep: the day someone
|
|
7
|
+
* bumps a connection schema's `version` without shipping a covering migration,
|
|
8
|
+
* the connection read path would silently fail at runtime on a genuinely-v1
|
|
9
|
+
* stored connection — this test turns that into a CI failure instead. See the
|
|
10
|
+
* HTTP plugin's equivalent test for the full rationale.
|
|
11
|
+
*
|
|
12
|
+
* Providers are registered by integration plugins (e.g. integration-jira),
|
|
13
|
+
* NOT by this core package, so there are no built-in providers to enumerate.
|
|
14
|
+
* This test asserts the enumeration contract holds for whatever a registry is
|
|
15
|
+
* given (so any future provider's `connectionSchema` is covered automatically)
|
|
16
|
+
* and exercises it with a representative fixture provider.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, expect, it } from "bun:test";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { Versioned } from "@checkstack/backend-api";
|
|
21
|
+
import { definePluginMetadata } from "@checkstack/common";
|
|
22
|
+
import { createIntegrationProviderRegistry } from "./provider-registry";
|
|
23
|
+
import type { IntegrationProvider } from "./provider-types";
|
|
24
|
+
|
|
25
|
+
const meta = definePluginMetadata({ pluginId: "integration-fixture" });
|
|
26
|
+
|
|
27
|
+
const fixtureProvider: IntegrationProvider<{ baseUrl: string }> = {
|
|
28
|
+
id: "fixture",
|
|
29
|
+
displayName: "Fixture Provider",
|
|
30
|
+
connectionSchema: new Versioned({
|
|
31
|
+
version: 1,
|
|
32
|
+
schema: z.object({ baseUrl: z.string() }),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("integration connectionSchema migration-chain contract", () => {
|
|
37
|
+
it("every registered provider connectionSchema has a complete v1->version chain", () => {
|
|
38
|
+
const registry = createIntegrationProviderRegistry();
|
|
39
|
+
registry.register(fixtureProvider, meta);
|
|
40
|
+
|
|
41
|
+
const providers = registry.getProviders();
|
|
42
|
+
expect(providers.length).toBeGreaterThan(0);
|
|
43
|
+
|
|
44
|
+
for (const provider of providers) {
|
|
45
|
+
if (!provider.connectionSchema) continue;
|
|
46
|
+
const problem = provider.connectionSchema.validateMigrationChainFromV1();
|
|
47
|
+
expect(
|
|
48
|
+
problem,
|
|
49
|
+
`Provider "${provider.qualifiedId}" connectionSchema (version ${provider.connectionSchema.version}) has a broken migration chain: ${problem}`,
|
|
50
|
+
).toBeUndefined();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
package/src/router.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { integrationContract } from "@checkstack/integration-common";
|
|
|
12
12
|
|
|
13
13
|
import type { IntegrationProviderRegistry } from "./provider-registry";
|
|
14
14
|
import type { ConnectionStore } from "./connection-store";
|
|
15
|
+
import { buildConfigValidationErrorData } from "./connection-validation-error";
|
|
15
16
|
import * as schema from "./schema";
|
|
16
17
|
|
|
17
18
|
interface RouterDeps {
|
|
@@ -147,17 +148,24 @@ export function createIntegrationRouter(deps: RouterDeps) {
|
|
|
147
148
|
});
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
const parseResult = provider.connectionSchema.
|
|
151
|
+
const parseResult = provider.connectionSchema.safeValidate(config);
|
|
151
152
|
if (!parseResult.success) {
|
|
153
|
+
// Attach the structured zod issues (field path + message) on the
|
|
154
|
+
// error `data` so the frontend can surface each failure INLINE on the
|
|
155
|
+
// offending connection field. The `code` discriminator lets the
|
|
156
|
+
// client distinguish this from any other structured error payload.
|
|
152
157
|
throw new ORPCError("BAD_REQUEST", {
|
|
153
158
|
message: `Invalid connection config: ${parseResult.error.message}`,
|
|
159
|
+
data: buildConfigValidationErrorData(parseResult.error),
|
|
154
160
|
});
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
// `connectionSchema` is `Versioned<unknown>` once the provider leaves
|
|
164
|
+
// the registry (the per-provider `TConnection` generic is erased), so
|
|
165
|
+
// `safeValidate` yields `unknown`. The connection store stores configs
|
|
166
|
+
// as `Record<string, unknown>`; this validated value is the provider's
|
|
167
|
+
// own object config, so the narrowing cast is safe.
|
|
168
|
+
const validatedConfig = parseResult.data as Record<string, unknown>;
|
|
161
169
|
|
|
162
170
|
const connection = await connectionStore.createConnection({
|
|
163
171
|
providerId,
|