@checkstack/backend-api 0.21.3 → 0.21.5
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 +95 -0
- package/package.json +8 -8
- package/src/plugin-system.ts +6 -0
- package/src/rpc.test.ts +5 -4
- package/src/schema-utils.test.ts +33 -0
- package/src/schema-utils.ts +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# @checkstack/backend-api
|
|
2
2
|
|
|
3
|
+
## 0.21.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0626782: Guard the role editor against granting inert (and misleading) permissions to the
|
|
8
|
+
anonymous role.
|
|
9
|
+
|
|
10
|
+
RPC procedures carry two independent axes: `userType` (the hard authentication
|
|
11
|
+
gate) and `access` rules (authorization). An admin can grant the anonymous role
|
|
12
|
+
any access rule, but if the procedures needing that rule are `userType:
|
|
13
|
+
"authenticated"`/`"user"`, the grant does nothing - the auth middleware rejects
|
|
14
|
+
unauthenticated callers BEFORE access rules are checked (so there is no security
|
|
15
|
+
hole; the grant is simply inert). After anonymous users started seeing
|
|
16
|
+
permission-gated UI, such a grant would surface as visible-but-broken controls.
|
|
17
|
+
|
|
18
|
+
- The backend now computes, from contract metadata, the access rules an anonymous
|
|
19
|
+
caller can actually use (a rule is "usable" iff at least one `public` procedure
|
|
20
|
+
requires it) via `pluginManager.getAnonymousUsableAccessRuleIds()`, exposed to
|
|
21
|
+
plugins through the plugin environment.
|
|
22
|
+
- `auth.getAccessRules` annotates each rule with `anonymousUsable`.
|
|
23
|
+
- `auth.updateRole` REFUSES to ADD a non-usable rule to the anonymous role
|
|
24
|
+
(existing grants are untouched, so no configuration can be wedged). This is a
|
|
25
|
+
guardrail, not an enforcement change - RPC authorization is unchanged.
|
|
26
|
+
- The role editor disables non-usable rules (with an explanation) when editing
|
|
27
|
+
the anonymous role.
|
|
28
|
+
|
|
29
|
+
Verified live: `getAccessRules` reports 11 anonymous-usable vs 58 not; granting
|
|
30
|
+
`incident.incident.manage` to the anonymous role returns HTTP 400 with a clear
|
|
31
|
+
message.
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [56e7c75]
|
|
34
|
+
- @checkstack/common@0.15.0
|
|
35
|
+
- @checkstack/healthcheck-common@1.5.4
|
|
36
|
+
- @checkstack/cache-api@0.3.12
|
|
37
|
+
- @checkstack/queue-api@0.3.12
|
|
38
|
+
- @checkstack/signal-common@0.2.9
|
|
39
|
+
- @checkstack/template-engine@0.4.3
|
|
40
|
+
|
|
41
|
+
## 0.21.4
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- b50916d: Fix "Date cannot be represented in JSON Schema" crashing the AI chat. Zod v4's
|
|
46
|
+
`toJSONSchema()` throws on `z.date()` (and even `z.coerce.date()`) by default,
|
|
47
|
+
and the chat hit this in TWO places:
|
|
48
|
+
|
|
49
|
+
- **`@checkstack/backend-api`** `toJsonSchema()` (the OpenAPI generator and AI
|
|
50
|
+
tool-introspection / MCP substrate) called it with no options.
|
|
51
|
+
- **`@checkstack/ai-backend`** the agent loop hands the Vercel AI SDK the raw
|
|
52
|
+
Zod tool input, and the SDK runs its OWN `toJSONSchema()` (throwing) to build
|
|
53
|
+
the model-facing tool schema - so a single date field in any tool input
|
|
54
|
+
crashed every chat turn (the whole tool list is projected before the model is
|
|
55
|
+
called).
|
|
56
|
+
|
|
57
|
+
Both now render dates as `{ type: "string", format: "date-time" }` (their wire
|
|
58
|
+
shape) and degrade other unrepresentable types to `{}` instead of throwing.
|
|
59
|
+
|
|
60
|
+
For the model boundary, a single `dateSafeModelSchema()` helper hands the SDK a
|
|
61
|
+
ready-made date-safe schema plus a validator that COERCES the ISO strings the
|
|
62
|
+
model emits back into real `Date`s before parsing with the original schema
|
|
63
|
+
(refinements and the downstream RPC client, which expects `Date`s, keep
|
|
64
|
+
working). A single `toModelSchema()` entry point applies this at EVERY point a
|
|
65
|
+
schema is handed to the model - chat tool inputs, the headless agent runner's
|
|
66
|
+
tool inputs (the automation "AI Action"), and `generateObject` structured
|
|
67
|
+
output - gated so non-date schemas are untouched, so individual tool / agent
|
|
68
|
+
definitions never special-case dates. Regression tests cover the converter, the
|
|
69
|
+
AI tool serializer, and the model-schema generation + coercion helper, including
|
|
70
|
+
the full inbound round-trip with the exact ISO shape a live model emits
|
|
71
|
+
(`...T22:00:00Z`, no milliseconds).
|
|
72
|
+
|
|
73
|
+
**Timezone correctness.** Because the model produces dates as text, the chat now
|
|
74
|
+
enforces an unambiguous wire contract: a date-time tool argument MUST be RFC 3339
|
|
75
|
+
with an explicit timezone offset. Zone-less (`2026-07-01T22:00:00`) and date-only
|
|
76
|
+
(`2026-07-01`) values are rejected with a model-readable error (the model
|
|
77
|
+
self-repairs), instead of being silently interpreted in the pod's local zone -
|
|
78
|
+
which would resolve the same string to different instants across pods. To resolve
|
|
79
|
+
an operator's bare "22:00", the browser's IANA timezone is sent with every chat
|
|
80
|
+
turn and folded into the system prompt, so each operator's times are interpreted
|
|
81
|
+
in their own zone by default. When no browser zone is available (a headless
|
|
82
|
+
automation AI Action), the reference zone falls back to the host/container
|
|
83
|
+
timezone (`TZ`), not UTC. A format-matrix test covers every common shape a model
|
|
84
|
+
might emit. The chat UI shows the operator which timezone is in use, and the
|
|
85
|
+
`TZ` override is documented for operators.
|
|
86
|
+
|
|
87
|
+
**Current time in context.** The model has no clock, so the system prompt now
|
|
88
|
+
includes the current instant (UTC plus the reference-zone wall clock), letting it
|
|
89
|
+
resolve relative dates like "today at 10:00" without asking. Applied to both the
|
|
90
|
+
chat and the headless agent runner, computed per turn/run so it is never stale.
|
|
91
|
+
|
|
92
|
+
**Less-strict topic classifier.** The chat's off-topic pre-classifier was
|
|
93
|
+
refusing legitimate requests like "create a maintenance" because maintenances
|
|
94
|
+
(and several other domains) were not listed. The classifier now enumerates the
|
|
95
|
+
full domain set and treats any create/list/update/delete action on a platform
|
|
96
|
+
resource as on-topic by default.
|
|
97
|
+
|
|
3
98
|
## 0.21.3
|
|
4
99
|
|
|
5
100
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend-api",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.5",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"lint:code": "eslint . --max-warnings 0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@checkstack/cache-api": "0.3.
|
|
14
|
-
"@checkstack/common": "0.
|
|
15
|
-
"@checkstack/healthcheck-common": "1.5.
|
|
16
|
-
"@checkstack/queue-api": "0.3.
|
|
17
|
-
"@checkstack/signal-common": "0.2.
|
|
18
|
-
"@checkstack/template-engine": "0.4.
|
|
13
|
+
"@checkstack/cache-api": "0.3.12",
|
|
14
|
+
"@checkstack/common": "0.15.0",
|
|
15
|
+
"@checkstack/healthcheck-common": "1.5.4",
|
|
16
|
+
"@checkstack/queue-api": "0.3.12",
|
|
17
|
+
"@checkstack/signal-common": "0.2.9",
|
|
18
|
+
"@checkstack/template-engine": "0.4.3",
|
|
19
19
|
"@orpc/client": "^1.14.4",
|
|
20
20
|
"@orpc/contract": "^1.14.4",
|
|
21
21
|
"@orpc/openapi": "^1.14.4",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"zod": "^4.2.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@checkstack/scripts": "0.6.
|
|
30
|
+
"@checkstack/scripts": "0.6.1",
|
|
31
31
|
"@checkstack/tsconfig": "0.0.7",
|
|
32
32
|
"@types/bun": "latest",
|
|
33
33
|
"@types/pg": "^8.20.0",
|
package/src/plugin-system.ts
CHANGED
|
@@ -139,6 +139,12 @@ export type BackendPluginRegistry = {
|
|
|
139
139
|
) => void;
|
|
140
140
|
pluginManager: {
|
|
141
141
|
getAllAccessRules: () => { id: string; description?: string }[];
|
|
142
|
+
/**
|
|
143
|
+
* Qualified ids of access rules an anonymous caller can actually use (rules
|
|
144
|
+
* required by at least one `public` procedure). Used to guard the role
|
|
145
|
+
* editor against granting inert permissions to the anonymous role.
|
|
146
|
+
*/
|
|
147
|
+
getAnonymousUsableAccessRuleIds: () => string[];
|
|
142
148
|
};
|
|
143
149
|
};
|
|
144
150
|
|
package/src/rpc.test.ts
CHANGED
|
@@ -25,7 +25,7 @@ const testContracts = {
|
|
|
25
25
|
publicGlobalEndpoint: proc({
|
|
26
26
|
userType: "public",
|
|
27
27
|
operationType: "query",
|
|
28
|
-
access: [access("resource", "read", "Test access")],
|
|
28
|
+
access: [access("resource", "read", "Test access", { pluginId: "test" })],
|
|
29
29
|
}).output(z.object({ message: z.string() })),
|
|
30
30
|
|
|
31
31
|
// Public endpoint with list filtering
|
|
@@ -39,7 +39,7 @@ const testContracts = {
|
|
|
39
39
|
read: { description: "View systems", isPublic: true },
|
|
40
40
|
manage: { description: "Manage systems" },
|
|
41
41
|
},
|
|
42
|
-
{ listKey: "systems" },
|
|
42
|
+
{ listKey: "systems", pluginId: "test" },
|
|
43
43
|
).read,
|
|
44
44
|
],
|
|
45
45
|
}).output(
|
|
@@ -80,7 +80,7 @@ const testContracts = {
|
|
|
80
80
|
read: { description: "View systems", isPublic: true },
|
|
81
81
|
manage: { description: "Manage systems" },
|
|
82
82
|
},
|
|
83
|
-
{ idParam: "systemId" },
|
|
83
|
+
{ idParam: "systemId", pluginId: "test" },
|
|
84
84
|
).read,
|
|
85
85
|
],
|
|
86
86
|
})
|
|
@@ -95,6 +95,7 @@ const testContracts = {
|
|
|
95
95
|
access("bulk", "read", "Bulk read", {
|
|
96
96
|
recordKey: "statuses",
|
|
97
97
|
isPublic: true,
|
|
98
|
+
pluginId: "test",
|
|
98
99
|
}),
|
|
99
100
|
],
|
|
100
101
|
})
|
|
@@ -129,7 +130,7 @@ const testContracts = {
|
|
|
129
130
|
read: { description: "View systems", isPublic: true },
|
|
130
131
|
manage: { description: "Manage systems" },
|
|
131
132
|
},
|
|
132
|
-
{ idParam: "systemId" },
|
|
133
|
+
{ idParam: "systemId", pluginId: "test" },
|
|
133
134
|
).read,
|
|
134
135
|
],
|
|
135
136
|
instanceAccess: { recordKey: "statuses" },
|
package/src/schema-utils.test.ts
CHANGED
|
@@ -42,3 +42,36 @@ describe("toJsonSchema x-* metadata", () => {
|
|
|
42
42
|
expect(json.properties.secretEnv?.["x-secret-env"]).toBe(true);
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
describe("toJsonSchema unrepresentable types", () => {
|
|
47
|
+
// Regression: Zod v4 throws "Date cannot be represented in JSON Schema" for
|
|
48
|
+
// `z.date()` by default. This converter feeds the OpenAPI generator AND the
|
|
49
|
+
// AI tool projection, so a single date field (timestamps are everywhere)
|
|
50
|
+
// would crash the whole AI chat. Dates must serialize as date-time strings.
|
|
51
|
+
test("represents z.date() as a date-time string instead of throwing", () => {
|
|
52
|
+
const schema = z.object({
|
|
53
|
+
createdAt: z.date(),
|
|
54
|
+
note: z.string(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const json = toJsonSchema(schema) as {
|
|
58
|
+
properties: Record<string, Record<string, unknown>>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
expect(json.properties.createdAt?.type).toBe("string");
|
|
62
|
+
expect(json.properties.createdAt?.format).toBe("date-time");
|
|
63
|
+
expect(json.properties.note?.type).toBe("string");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("handles a top-level / optional / array date without throwing", () => {
|
|
67
|
+
expect(() => toJsonSchema(z.date())).not.toThrow();
|
|
68
|
+
const json = toJsonSchema(
|
|
69
|
+
z.object({ seen: z.array(z.date()), at: z.date().optional() }),
|
|
70
|
+
) as { properties: Record<string, Record<string, unknown>> };
|
|
71
|
+
expect(
|
|
72
|
+
(json.properties.seen?.items as Record<string, unknown> | undefined)
|
|
73
|
+
?.format,
|
|
74
|
+
).toBe("date-time");
|
|
75
|
+
expect(json.properties.at?.format).toBe("date-time");
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/schema-utils.ts
CHANGED
|
@@ -101,8 +101,25 @@ function addSchemaMetadata(
|
|
|
101
101
|
* dropdowns for optionsResolver fields, hidden for auto-populated fields).
|
|
102
102
|
*/
|
|
103
103
|
export function toJsonSchema(zodSchema: z.ZodTypeAny): Record<string, unknown> {
|
|
104
|
-
// Use Zod's native JSON Schema conversion
|
|
105
|
-
|
|
104
|
+
// Use Zod's native JSON Schema conversion.
|
|
105
|
+
//
|
|
106
|
+
// Zod v4 throws "Date cannot be represented in JSON Schema" for `z.date()`
|
|
107
|
+
// by default (`unrepresentable: "throw"`). Many platform contracts carry
|
|
108
|
+
// date fields (timestamps), and this converter is the substrate for the
|
|
109
|
+
// OpenAPI generator AND the AI tool projection - so a single date field
|
|
110
|
+
// would crash the whole AI chat (every turn projects the full tool list)
|
|
111
|
+
// and break OpenAPI. Dates serialize as ISO strings over the wire, so we
|
|
112
|
+
// represent them as `{ type: "string", format: "date-time" }` and let any
|
|
113
|
+
// other unrepresentable type degrade to `{}` (any) rather than throw.
|
|
114
|
+
const jsonSchema = z.toJSONSchema(zodSchema, {
|
|
115
|
+
unrepresentable: "any",
|
|
116
|
+
override: (ctx) => {
|
|
117
|
+
if (ctx.zodSchema instanceof z.ZodDate) {
|
|
118
|
+
ctx.jsonSchema.type = "string";
|
|
119
|
+
ctx.jsonSchema.format = "date-time";
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}) as Record<string, unknown>;
|
|
106
123
|
addSchemaMetadata(zodSchema, jsonSchema);
|
|
107
124
|
return jsonSchema;
|
|
108
125
|
}
|