@checkstack/backend-api 0.0.2

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.
@@ -0,0 +1,371 @@
1
+ /* eslint-disable unicorn/no-null */
2
+ import { z } from "zod";
3
+
4
+ // ============================================================================
5
+ // OPERATOR ENUMS
6
+ // ============================================================================
7
+
8
+ /**
9
+ * Operators for numeric comparisons.
10
+ */
11
+ export const NumericOperators = z.enum([
12
+ "equals",
13
+ "notEquals",
14
+ "lessThan",
15
+ "lessThanOrEqual",
16
+ "greaterThan",
17
+ "greaterThanOrEqual",
18
+ ]);
19
+
20
+ /**
21
+ * Operators for time thresholds (typically only "less than" makes sense).
22
+ */
23
+ export const TimeThresholdOperators = z.enum(["lessThan", "lessThanOrEqual"]);
24
+
25
+ /**
26
+ * Operators for string matching.
27
+ */
28
+ export const StringOperators = z.enum([
29
+ "equals",
30
+ "notEquals",
31
+ "contains",
32
+ "startsWith",
33
+ "endsWith",
34
+ "matches",
35
+ "isEmpty",
36
+ ]);
37
+
38
+ /**
39
+ * Operators for boolean checks.
40
+ */
41
+ export const BooleanOperators = z.enum(["isTrue", "isFalse"]);
42
+
43
+ /**
44
+ * Universal operators for dynamic/unknown types (JSONPath values).
45
+ * Works via runtime type coercion.
46
+ */
47
+ export const DynamicOperators = z.enum([
48
+ // String operators (always work)
49
+ "equals",
50
+ "notEquals",
51
+ "contains",
52
+ "startsWith",
53
+ "endsWith",
54
+ "matches",
55
+ // Existence check
56
+ "exists",
57
+ "notExists",
58
+ // Numeric operators (value coerced to number at runtime)
59
+ "lessThan",
60
+ "lessThanOrEqual",
61
+ "greaterThan",
62
+ "greaterThanOrEqual",
63
+ ]);
64
+
65
+ // ============================================================================
66
+ // SCHEMA FACTORIES
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Creates an assertion schema for numeric fields with full comparison operators.
71
+ */
72
+ export function numericField(
73
+ name: string,
74
+ config?: { min?: number; max?: number }
75
+ ) {
76
+ return z.object({
77
+ field: z.literal(name),
78
+ operator: NumericOperators,
79
+ value: z
80
+ .number()
81
+ .min(config?.min ?? -Infinity)
82
+ .max(config?.max ?? Infinity),
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Creates an assertion schema for time threshold fields (latency, query time, etc).
88
+ * Only supports "less than" operators since higher is typically worse.
89
+ */
90
+ export function timeThresholdField(name: string) {
91
+ return z.object({
92
+ field: z.literal(name),
93
+ operator: TimeThresholdOperators,
94
+ value: z.number().min(0).describe("Threshold in milliseconds"),
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Creates an assertion schema for string matching fields.
100
+ */
101
+ export function stringField(name: string) {
102
+ return z.object({
103
+ field: z.literal(name),
104
+ operator: StringOperators,
105
+ value: z.string().optional().describe("Pattern (optional for isEmpty)"),
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Creates an assertion schema for boolean fields.
111
+ * Uses isTrue/isFalse operators to explicitly check expected value.
112
+ */
113
+ export function booleanField(name: string) {
114
+ return z.object({
115
+ field: z.literal(name),
116
+ operator: BooleanOperators,
117
+ // No value needed - operator determines expected boolean
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Creates an assertion schema for enum fields (e.g., status codes).
123
+ */
124
+ export function enumField<T extends string>(
125
+ name: string,
126
+ values: readonly T[]
127
+ ) {
128
+ return z.object({
129
+ field: z.literal(name),
130
+ operator: z.literal("equals"),
131
+ value: z.enum(values as [T, ...T[]]),
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Creates an assertion schema for JSONPath fields with dynamic typing.
137
+ * Type is unknown at config time, so we accept string values and coerce at runtime.
138
+ */
139
+ export function jsonPathField() {
140
+ return z.object({
141
+ path: z
142
+ .string()
143
+ .describe("JSONPath expression (e.g. $.status, $.data[0].id)"),
144
+ operator: DynamicOperators,
145
+ value: z
146
+ .string()
147
+ .optional()
148
+ .describe("Expected value (not needed for exists/notExists)"),
149
+ });
150
+ }
151
+
152
+ // ============================================================================
153
+ // EVALUATION ENGINE
154
+ // ============================================================================
155
+
156
+ /**
157
+ * Result of evaluating a single assertion.
158
+ */
159
+ export interface AssertionResult {
160
+ passed: boolean;
161
+ field: string;
162
+ operator?: string;
163
+ expected?: unknown;
164
+ actual?: unknown;
165
+ message?: string;
166
+ }
167
+
168
+ /**
169
+ * Evaluate a single assertion against actual values.
170
+ */
171
+ export function evaluateAssertion<T extends { field: string }>(
172
+ assertion: T,
173
+ values: Record<string, unknown>
174
+ ): AssertionResult {
175
+ const field = assertion.field;
176
+ const actual = values[field];
177
+
178
+ const { operator, value: expected } = assertion as T & {
179
+ operator: string;
180
+ value?: unknown;
181
+ };
182
+
183
+ const passed = evaluateOperator(operator, actual, expected);
184
+
185
+ return {
186
+ passed,
187
+ field,
188
+ operator,
189
+ expected,
190
+ actual,
191
+ message: passed
192
+ ? undefined
193
+ : formatFailureMessage(field, operator, expected, actual),
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Evaluate all assertions, returning the first failure or null if all pass.
199
+ */
200
+ export function evaluateAssertions<T extends { field: string }>(
201
+ assertions: T[] | undefined,
202
+ values: Record<string, unknown>
203
+ ): T | null {
204
+ if (!assertions?.length) return null;
205
+
206
+ for (const assertion of assertions) {
207
+ const result = evaluateAssertion(assertion, values);
208
+ if (!result.passed) {
209
+ return assertion;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ /**
216
+ * Evaluate JSONPath assertions against a JSON object.
217
+ * Requires a JSONPath extraction function to be passed in to avoid bundling jsonpath-plus.
218
+ */
219
+ export function evaluateJsonPathAssertions<
220
+ T extends { path: string; operator: string; value?: string }
221
+ >(
222
+ assertions: T[] | undefined,
223
+ json: unknown,
224
+ extractPath: (path: string, json: unknown) => unknown
225
+ ): T | null {
226
+ if (!assertions?.length) return null;
227
+
228
+ for (const assertion of assertions) {
229
+ const extractedValue = extractPath(assertion.path, json);
230
+ const passed = evaluateOperator(
231
+ assertion.operator,
232
+ extractedValue,
233
+ assertion.value
234
+ );
235
+
236
+ if (!passed) return assertion;
237
+ }
238
+ return null;
239
+ }
240
+
241
+ // ============================================================================
242
+ // INTERNAL HELPERS
243
+ // ============================================================================
244
+
245
+ /**
246
+ * Evaluate an operator against actual and expected values.
247
+ */
248
+ function evaluateOperator(
249
+ op: string,
250
+ actual: unknown,
251
+ expected: unknown
252
+ ): boolean {
253
+ // Existence checks
254
+ if (op === "exists") return actual !== undefined && actual !== null;
255
+ if (op === "notExists") return actual === undefined || actual === null;
256
+
257
+ // Boolean operators
258
+ if (op === "isTrue") return actual === true;
259
+ if (op === "isFalse") return actual === false;
260
+
261
+ // Empty check
262
+ if (op === "isEmpty") return !actual || String(actual).trim() === "";
263
+
264
+ // For numeric operators, try to coerce to numbers
265
+ if (
266
+ [
267
+ "lessThan",
268
+ "lessThanOrEqual",
269
+ "greaterThan",
270
+ "greaterThanOrEqual",
271
+ ].includes(op)
272
+ ) {
273
+ const numActual = Number(actual);
274
+ const numExpected = Number(expected);
275
+ if (Number.isNaN(numActual) || Number.isNaN(numExpected)) return false;
276
+
277
+ switch (op) {
278
+ case "lessThan": {
279
+ return numActual < numExpected;
280
+ }
281
+ case "lessThanOrEqual": {
282
+ return numActual <= numExpected;
283
+ }
284
+ case "greaterThan": {
285
+ return numActual > numExpected;
286
+ }
287
+ case "greaterThanOrEqual": {
288
+ return numActual >= numExpected;
289
+ }
290
+ }
291
+ }
292
+
293
+ // String operators (coerce to string)
294
+ const strActual = String(actual ?? "");
295
+ const strExpected = String(expected ?? "");
296
+
297
+ switch (op) {
298
+ case "equals": {
299
+ // Try numeric equality first, then strict equality, then string equality
300
+ if (typeof actual === "number" && typeof expected === "number") {
301
+ return actual === expected;
302
+ }
303
+ if (actual === expected) return true;
304
+ return strActual === strExpected;
305
+ }
306
+ case "notEquals": {
307
+ if (actual === expected) return false;
308
+ return strActual !== strExpected;
309
+ }
310
+ case "contains": {
311
+ return strActual.includes(strExpected);
312
+ }
313
+ case "startsWith": {
314
+ return strActual.startsWith(strExpected);
315
+ }
316
+ case "endsWith": {
317
+ return strActual.endsWith(strExpected);
318
+ }
319
+ case "matches": {
320
+ try {
321
+ return new RegExp(strExpected).test(strActual);
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+ default: {
327
+ return false;
328
+ }
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Format a human-readable failure message.
334
+ */
335
+ function formatFailureMessage(
336
+ field: string,
337
+ operator: string,
338
+ expected: unknown,
339
+ actual: unknown
340
+ ): string {
341
+ const opLabels: Record<string, string> = {
342
+ equals: "to equal",
343
+ notEquals: "not to equal",
344
+ lessThan: "to be less than",
345
+ lessThanOrEqual: "to be at most",
346
+ greaterThan: "to be greater than",
347
+ greaterThanOrEqual: "to be at least",
348
+ contains: "to contain",
349
+ startsWith: "to start with",
350
+ endsWith: "to end with",
351
+ matches: "to match pattern",
352
+ isEmpty: "to be empty",
353
+ exists: "to exist",
354
+ notExists: "not to exist",
355
+ isTrue: "to be true",
356
+ isFalse: "to be false",
357
+ };
358
+
359
+ const opLabel = opLabels[operator] || operator;
360
+
361
+ // For operators without expected values
362
+ if (
363
+ ["isEmpty", "exists", "notExists", "isTrue", "isFalse"].includes(operator)
364
+ ) {
365
+ return `${field}: expected ${opLabel}, got ${JSON.stringify(actual)}`;
366
+ }
367
+
368
+ return `${field}: expected ${opLabel} ${JSON.stringify(
369
+ expected
370
+ )}, got ${JSON.stringify(actual)}`;
371
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import type { Migration } from "./config-versioning";
3
+ import type { LucideIconName } from "@checkstack/common";
4
+
5
+ /**
6
+ * Migration chain for auth strategy configurations.
7
+ */
8
+ export type AuthStrategyMigrationChain<_T> = Migration<unknown, unknown>[];
9
+
10
+ /**
11
+ * Defines an authentication strategy for better-auth integration.
12
+ * Strategies provide configuration schemas for OAuth providers and other auth methods.
13
+ */
14
+ export interface AuthStrategy<Config = unknown> {
15
+ /** Unique identifier for the strategy (e.g., "github", "google") */
16
+ id: string;
17
+
18
+ /** Display name shown in UI */
19
+ displayName: string;
20
+
21
+ /** Optional description of the strategy */
22
+ description?: string;
23
+
24
+ /** Lucide icon name in PascalCase (e.g., 'Github', 'Chrome', 'Mail') */
25
+ icon?: LucideIconName;
26
+
27
+ /** Current version of the configuration schema */
28
+ configVersion: number;
29
+
30
+ /** Zod validation schema for the strategy-specific config */
31
+ configSchema: z.ZodType<Config>;
32
+
33
+ /** Optional migrations for backward compatibility */
34
+ migrations?: AuthStrategyMigrationChain<Config>;
35
+
36
+ /**
37
+ * Whether this strategy requires manual user registration via a signup form.
38
+ * - `true` for strategies like credentials where users explicitly register
39
+ * - `false` for strategies like social providers or LDAP where users are auto-registered on first login
40
+ */
41
+ requiresManualRegistration: boolean;
42
+
43
+ /**
44
+ * Markdown instructions shown when admins configure the strategy settings.
45
+ * Displayed in the StrategyConfigCard before the configuration form.
46
+ */
47
+ adminInstructions?: string;
48
+ }
49
+
50
+ /**
51
+ * Registry for authentication strategies.
52
+ * Allows plugins to register custom auth strategies.
53
+ */
54
+ export interface AuthStrategyRegistry {
55
+ register(strategy: AuthStrategy<unknown>): void;
56
+ getStrategy(id: string): AuthStrategy<unknown> | undefined;
57
+ getStrategies(): AuthStrategy<unknown>[];
58
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Chart metadata types for auto-generated health check visualizations.
3
+ *
4
+ * Use Zod's .meta() to annotate result schema fields with chart information.
5
+ * The metadata flows through toJSONSchema() and is rendered by auto-chart components.
6
+ *
7
+ * Uses x- prefixed keys for consistency with platform patterns (x-secret, x-color, x-hidden).
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const aggregatedSchema = z.object({
12
+ * avgResponseTime: z.number().meta({
13
+ * "x-chart-type": "line",
14
+ * "x-chart-label": "Avg Response Time",
15
+ * "x-chart-unit": "ms"
16
+ * }),
17
+ * statusCodeCounts: z.record(z.string(), z.number()).meta({
18
+ * "x-chart-type": "bar",
19
+ * "x-chart-label": "Status Codes"
20
+ * }),
21
+ * connected: z.boolean().meta({
22
+ * "x-chart-type": "boolean",
23
+ * "x-chart-label": "Connected"
24
+ * }),
25
+ * });
26
+ * ```
27
+ */
28
+
29
+ /**
30
+ * Available chart types for auto-generated visualizations.
31
+ *
32
+ * Numeric types:
33
+ * - line: Time series line chart for numeric metrics over time
34
+ * - bar: Bar chart for distributions (record of string to number)
35
+ * - counter: Simple count display with trend indicator
36
+ * - gauge: Percentage gauge for rates/percentages (0-100)
37
+ *
38
+ * Non-numeric types:
39
+ * - boolean: Boolean indicator (success/failure, connected/disconnected)
40
+ * - text: Text display for string values
41
+ * - status: Status badge for error/warning states
42
+ *
43
+ * Note: Fields without chart annotations simply won't render - no "hidden" type needed.
44
+ */
45
+ export type ChartType =
46
+ | "line"
47
+ | "bar"
48
+ | "counter"
49
+ | "gauge"
50
+ | "boolean"
51
+ | "text"
52
+ | "status";
53
+
54
+ /**
55
+ * Chart metadata to attach to Zod schema fields via .meta().
56
+ * Uses x- prefixed keys for consistency with platform patterns.
57
+ */
58
+ export interface ChartMeta {
59
+ /** The type of chart to render for this field */
60
+ "x-chart-type": ChartType;
61
+ /** Human-readable label for the chart (defaults to field name) */
62
+ "x-chart-label"?: string;
63
+ /** Unit suffix for values (e.g., 'ms', '%', 'req/s') */
64
+ "x-chart-unit"?: string;
65
+ }
66
+
67
+ /**
68
+ * Type guard to check if metadata contains chart information.
69
+ */
70
+ export function isChartMeta(meta: unknown): meta is ChartMeta {
71
+ return (
72
+ typeof meta === "object" &&
73
+ meta !== null &&
74
+ "x-chart-type" in meta &&
75
+ typeof (meta as ChartMeta)["x-chart-type"] === "string"
76
+ );
77
+ }
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ import type { Migration } from "./config-versioning";
3
+
4
+ /**
5
+ * Service for managing plugin configurations with automatic secret handling.
6
+ * Each plugin gets its own scoped instance that can only access its own configs.
7
+ */
8
+ export interface ConfigService {
9
+ /**
10
+ * Store a configuration with automatic secret encryption and migration support.
11
+ *
12
+ * @param configId - Unique identifier for this config (e.g., "github-strategy", "smtp-settings")
13
+ * @param schema - Zod schema that defines the config structure and marks secret fields
14
+ * @param version - Current schema version
15
+ * @param data - The configuration data to store
16
+ * @param migrations - Optional migration chain for backward compatibility
17
+ */
18
+ set<T>(
19
+ configId: string,
20
+ schema: z.ZodType<T>,
21
+ version: number,
22
+ data: T,
23
+ migrations?: Migration<unknown, unknown>[]
24
+ ): Promise<void>;
25
+
26
+ /**
27
+ * Load a configuration with automatic secret decryption and migration.
28
+ * Returns undefined if config doesn't exist.
29
+ *
30
+ * @param configId - Unique identifier for the config
31
+ * @param schema - Zod schema for validation and secret detection
32
+ * @param version - Expected schema version
33
+ * @param migrations - Optional migration chain
34
+ */
35
+ get<T>(
36
+ configId: string,
37
+ schema: z.ZodType<T>,
38
+ version: number,
39
+ migrations?: Migration<unknown, unknown>[]
40
+ ): Promise<T | undefined>;
41
+
42
+ /**
43
+ * Load a configuration without decrypting secrets (safe for frontend).
44
+ * Returns the data with secret fields removed.
45
+ *
46
+ * @param configId - Unique identifier for the config
47
+ * @param schema - Zod schema for secret detection
48
+ * @param version - Expected schema version
49
+ * @param migrations - Optional migration chain
50
+ */
51
+ getRedacted<T>(
52
+ configId: string,
53
+ schema: z.ZodType<T>,
54
+ version: number,
55
+ migrations?: Migration<unknown, unknown>[]
56
+ ): Promise<Partial<T> | undefined>;
57
+
58
+ /**
59
+ * Delete a configuration.
60
+ *
61
+ * @param configId - Unique identifier for the config to delete
62
+ */
63
+ delete(configId: string): Promise<void>;
64
+
65
+ /**
66
+ * List all config IDs for this plugin.
67
+ *
68
+ * @returns Array of config IDs belonging to this plugin
69
+ */
70
+ list(): Promise<string[]>;
71
+ }