@checkstack/secrets-backend 0.1.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 +148 -0
- package/package.json +48 -0
- package/src/active-backend.test.ts +46 -0
- package/src/active-backend.ts +28 -0
- package/src/admin-service.test.ts +77 -0
- package/src/admin-service.ts +91 -0
- package/src/backend-config-store.ts +76 -0
- package/src/hooks.ts +14 -0
- package/src/index.ts +236 -0
- package/src/internal-secrets-service.test.ts +85 -0
- package/src/internal-secrets-service.ts +54 -0
- package/src/leak-guard.test.ts +194 -0
- package/src/masking-context.test.ts +31 -0
- package/src/masking-context.ts +50 -0
- package/src/resolver-service.test.ts +87 -0
- package/src/resolver-service.ts +122 -0
- package/src/router.test.ts +76 -0
- package/src/router.ts +167 -0
- package/src/secret-backend-registry.ts +45 -0
- package/src/secret-backend.test.ts +21 -0
- package/src/secret-backend.ts +94 -0
- package/src/secret-resolver.test.ts +112 -0
- package/src/secret-resolver.ts +250 -0
- package/src/walk-secret-fields.ts +140 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { configString } from "@checkstack/backend-api";
|
|
4
|
+
import { resolveSecretsBySchema, type SecretStore } from "./secret-resolver";
|
|
5
|
+
|
|
6
|
+
const mockSecretStore: SecretStore = {
|
|
7
|
+
resolve: async (name: string): Promise<string> => {
|
|
8
|
+
const secrets: Record<string, string> = {
|
|
9
|
+
DB_PASS: "s3cret!",
|
|
10
|
+
API_KEY: "key-12345",
|
|
11
|
+
DB_USER: "admin",
|
|
12
|
+
DB_HOST: "db.production.internal",
|
|
13
|
+
};
|
|
14
|
+
const value = secrets[name];
|
|
15
|
+
if (!value) throw new Error(`Secret not found: ${name}`);
|
|
16
|
+
return value;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("resolveSecretsBySchema (promoted from gitops)", () => {
|
|
21
|
+
it("resolves a field marked with x-secret", async () => {
|
|
22
|
+
const schema = z.object({
|
|
23
|
+
host: z.string(),
|
|
24
|
+
password: configString({ "x-secret": true }),
|
|
25
|
+
});
|
|
26
|
+
const { resolved, warnings } = await resolveSecretsBySchema({
|
|
27
|
+
value: { host: "localhost", password: "${{ secrets.DB_PASS }}" },
|
|
28
|
+
schema,
|
|
29
|
+
secretStore: mockSecretStore,
|
|
30
|
+
});
|
|
31
|
+
expect(resolved).toEqual({ host: "localhost", password: "s3cret!" });
|
|
32
|
+
expect(warnings).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("leaves non-secret fields untouched and emits warnings", async () => {
|
|
36
|
+
const schema = z.object({
|
|
37
|
+
description: z.string(),
|
|
38
|
+
password: configString({ "x-secret": true }),
|
|
39
|
+
});
|
|
40
|
+
const { resolved, warnings } = await resolveSecretsBySchema({
|
|
41
|
+
value: {
|
|
42
|
+
description: "Contains ${{ secrets.DB_PASS }} but should NOT resolve",
|
|
43
|
+
password: "${{ secrets.DB_PASS }}",
|
|
44
|
+
},
|
|
45
|
+
schema,
|
|
46
|
+
secretStore: mockSecretStore,
|
|
47
|
+
});
|
|
48
|
+
expect(resolved).toEqual({
|
|
49
|
+
description: "Contains ${{ secrets.DB_PASS }} but should NOT resolve",
|
|
50
|
+
password: "s3cret!",
|
|
51
|
+
});
|
|
52
|
+
expect(warnings).toHaveLength(1);
|
|
53
|
+
expect(warnings[0]).toContain("description");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("resolves inline interpolation in secret fields", async () => {
|
|
57
|
+
const schema = z.object({
|
|
58
|
+
connectionString: configString({ "x-secret": true }),
|
|
59
|
+
});
|
|
60
|
+
const { resolved } = await resolveSecretsBySchema({
|
|
61
|
+
value: {
|
|
62
|
+
connectionString:
|
|
63
|
+
"postgres://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@${{ secrets.DB_HOST }}/mydb",
|
|
64
|
+
},
|
|
65
|
+
schema,
|
|
66
|
+
secretStore: mockSecretStore,
|
|
67
|
+
});
|
|
68
|
+
expect(resolved).toEqual({
|
|
69
|
+
connectionString: "postgres://admin:s3cret!@db.production.internal/mydb",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("resolves secrets in nested objects and arrays", async () => {
|
|
74
|
+
const schema = z.object({
|
|
75
|
+
credentials: z.array(
|
|
76
|
+
z.object({
|
|
77
|
+
name: z.string(),
|
|
78
|
+
secret: configString({ "x-secret": true }),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
});
|
|
82
|
+
const { resolved } = await resolveSecretsBySchema({
|
|
83
|
+
value: {
|
|
84
|
+
credentials: [
|
|
85
|
+
{ name: "db", secret: "${{ secrets.DB_PASS }}" },
|
|
86
|
+
{ name: "api", secret: "${{ secrets.API_KEY }}" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
schema,
|
|
90
|
+
secretStore: mockSecretStore,
|
|
91
|
+
});
|
|
92
|
+
expect(resolved).toEqual({
|
|
93
|
+
credentials: [
|
|
94
|
+
{ name: "db", secret: "s3cret!" },
|
|
95
|
+
{ name: "api", secret: "key-12345" },
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("throws when a referenced secret is not found", async () => {
|
|
101
|
+
const schema = z.object({
|
|
102
|
+
password: configString({ "x-secret": true }),
|
|
103
|
+
});
|
|
104
|
+
await expect(
|
|
105
|
+
resolveSecretsBySchema({
|
|
106
|
+
value: { password: "${{ secrets.NONEXISTENT }}" },
|
|
107
|
+
schema,
|
|
108
|
+
secretStore: mockSecretStore,
|
|
109
|
+
}),
|
|
110
|
+
).rejects.toThrow("Secret not found: NONEXISTENT");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SECRET_TEMPLATE_REGEX } from "@checkstack/secrets-common";
|
|
3
|
+
import { isSecretSchema } from "@checkstack/backend-api";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for resolving secret names to their decrypted values.
|
|
7
|
+
* Promoted out of `@checkstack/gitops-backend`.
|
|
8
|
+
*/
|
|
9
|
+
export interface SecretStore {
|
|
10
|
+
resolve: (name: string) => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Result of schema-driven secret resolution.
|
|
15
|
+
*/
|
|
16
|
+
export interface SecretResolutionResult<T> {
|
|
17
|
+
/** The value with x-secret fields resolved. */
|
|
18
|
+
resolved: T;
|
|
19
|
+
/**
|
|
20
|
+
* Warnings for `${{ secrets.NAME }}` templates found in non-secret
|
|
21
|
+
* fields. These templates are NOT resolved — the field must be
|
|
22
|
+
* annotated with `configString({ "x-secret": true })` for resolution.
|
|
23
|
+
*/
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Schema-driven secret resolver.
|
|
29
|
+
*
|
|
30
|
+
* Walks a Zod schema to find fields annotated with
|
|
31
|
+
* `configString({ "x-secret": true })`, then resolves
|
|
32
|
+
* `${{ secrets.NAME }}` templates **only** in those fields. Non-secret
|
|
33
|
+
* fields are returned as-is, preventing accidental leaks into display
|
|
34
|
+
* fields. Templates in non-secret fields are reported as warnings.
|
|
35
|
+
*
|
|
36
|
+
* Supports full-value templates and inline interpolation:
|
|
37
|
+
* - `"${{ secrets.DB_PASS }}"` → `"actual-password"`
|
|
38
|
+
* - `"postgres://u:${{ secrets.PASS }}@host/db"` → interpolated
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveSecretsBySchema<T = unknown>(params: {
|
|
41
|
+
value: T;
|
|
42
|
+
schema: z.ZodTypeAny;
|
|
43
|
+
secretStore: SecretStore;
|
|
44
|
+
}): Promise<SecretResolutionResult<T>> {
|
|
45
|
+
const { value, schema, secretStore } = params;
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
const resolved = await walkAndResolve({
|
|
48
|
+
value,
|
|
49
|
+
schema,
|
|
50
|
+
secretStore,
|
|
51
|
+
warnings,
|
|
52
|
+
path: "",
|
|
53
|
+
});
|
|
54
|
+
return { resolved: resolved as T, warnings };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Recursive Walker ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async function walkAndResolve(params: {
|
|
60
|
+
value: unknown;
|
|
61
|
+
schema: z.ZodTypeAny;
|
|
62
|
+
secretStore: SecretStore;
|
|
63
|
+
warnings: string[];
|
|
64
|
+
path: string;
|
|
65
|
+
}): Promise<unknown> {
|
|
66
|
+
const { value, secretStore, warnings, path } = params;
|
|
67
|
+
const schema = unwrapZod(params.schema);
|
|
68
|
+
|
|
69
|
+
if (value === null || value === undefined) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Leaf node: if this schema is marked x-secret, resolve templates.
|
|
74
|
+
if (isSecretSchema(schema)) {
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
return resolveTemplateString({ value, secretStore });
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Non-secret string leaf: check for unresolved templates and warn.
|
|
82
|
+
if (typeof value === "string" && containsSecretTemplate(value)) {
|
|
83
|
+
const fieldPath = path || "(root)";
|
|
84
|
+
warnings.push(
|
|
85
|
+
`Field "${fieldPath}" contains a secret template but is not marked as a secret field. ` +
|
|
86
|
+
`Add configString({ "x-secret": true }) to the schema for this field to enable resolution.`,
|
|
87
|
+
);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Object: recurse into each property using the schema's shape.
|
|
92
|
+
if (
|
|
93
|
+
schema instanceof z.ZodObject &&
|
|
94
|
+
typeof value === "object" &&
|
|
95
|
+
!Array.isArray(value)
|
|
96
|
+
) {
|
|
97
|
+
const shape = schema.shape as Record<string, z.ZodTypeAny>;
|
|
98
|
+
const result: Record<string, unknown> = {
|
|
99
|
+
...(value as Record<string, unknown>),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
103
|
+
if (key in result) {
|
|
104
|
+
result[key] = await walkAndResolve({
|
|
105
|
+
value: result[key],
|
|
106
|
+
schema: fieldSchema,
|
|
107
|
+
secretStore,
|
|
108
|
+
warnings,
|
|
109
|
+
path: path ? `${path}.${key}` : key,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Array: recurse into each element using the element schema.
|
|
118
|
+
if (schema instanceof z.ZodArray && Array.isArray(value)) {
|
|
119
|
+
const elementSchema = schema.element as z.ZodTypeAny;
|
|
120
|
+
return Promise.all(
|
|
121
|
+
value.map((item, index) =>
|
|
122
|
+
walkAndResolve({
|
|
123
|
+
value: item,
|
|
124
|
+
schema: elementSchema,
|
|
125
|
+
secretStore,
|
|
126
|
+
warnings,
|
|
127
|
+
path: `${path}[${index}]`,
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Discriminated union: find the matching variant and recurse.
|
|
134
|
+
if (
|
|
135
|
+
schema instanceof z.ZodDiscriminatedUnion &&
|
|
136
|
+
typeof value === "object" &&
|
|
137
|
+
value !== null
|
|
138
|
+
) {
|
|
139
|
+
const discriminator = (schema.def as { discriminator: string })
|
|
140
|
+
.discriminator;
|
|
141
|
+
const discriminatorValue = (value as Record<string, unknown>)[
|
|
142
|
+
discriminator
|
|
143
|
+
];
|
|
144
|
+
const options = schema.options as z.ZodObject<z.ZodRawShape>[];
|
|
145
|
+
const matchedVariant = options.find((option) => {
|
|
146
|
+
const discField = option.shape[discriminator];
|
|
147
|
+
if (discField instanceof z.ZodLiteral) {
|
|
148
|
+
return discField.value === discriminatorValue;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (matchedVariant) {
|
|
154
|
+
return walkAndResolve({
|
|
155
|
+
value,
|
|
156
|
+
schema: matchedVariant,
|
|
157
|
+
secretStore,
|
|
158
|
+
warnings,
|
|
159
|
+
path,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Union (oneOf/anyOf without discriminator): try each variant.
|
|
165
|
+
if (
|
|
166
|
+
schema instanceof z.ZodUnion &&
|
|
167
|
+
typeof value === "object" &&
|
|
168
|
+
value !== null
|
|
169
|
+
) {
|
|
170
|
+
const options = schema.options as z.ZodTypeAny[];
|
|
171
|
+
for (const option of options) {
|
|
172
|
+
const parsed = option.safeParse(value);
|
|
173
|
+
if (parsed.success) {
|
|
174
|
+
return walkAndResolve({
|
|
175
|
+
value,
|
|
176
|
+
schema: option,
|
|
177
|
+
secretStore,
|
|
178
|
+
warnings,
|
|
179
|
+
path,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// No schema match — return value unchanged.
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Quick check whether a string contains any `${{ secrets.NAME }}` pattern. */
|
|
190
|
+
function containsSecretTemplate(value: string): boolean {
|
|
191
|
+
SECRET_TEMPLATE_REGEX.lastIndex = 0;
|
|
192
|
+
return SECRET_TEMPLATE_REGEX.test(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Zod Unwrapping ────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/** Unwraps Optional, Default, and Nullable wrappers to get the inner schema. */
|
|
198
|
+
function unwrapZod(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
199
|
+
let unwrapped = schema;
|
|
200
|
+
|
|
201
|
+
if (unwrapped instanceof z.ZodOptional) {
|
|
202
|
+
unwrapped = unwrapped.unwrap() as z.ZodTypeAny;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (unwrapped instanceof z.ZodDefault) {
|
|
206
|
+
unwrapped = unwrapped.def.innerType as z.ZodTypeAny;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (unwrapped instanceof z.ZodNullable) {
|
|
210
|
+
unwrapped = unwrapped.unwrap() as z.ZodTypeAny;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return unwrapped;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Template Resolution ───────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolves all `${{ secrets.NAME }}` patterns in a string. Each unique
|
|
220
|
+
* secret name is resolved once (cached per call) to avoid duplicate
|
|
221
|
+
* lookups.
|
|
222
|
+
*/
|
|
223
|
+
async function resolveTemplateString(params: {
|
|
224
|
+
value: string;
|
|
225
|
+
secretStore: SecretStore;
|
|
226
|
+
}): Promise<string> {
|
|
227
|
+
const { value, secretStore } = params;
|
|
228
|
+
|
|
229
|
+
const secretNames = new Set<string>();
|
|
230
|
+
SECRET_TEMPLATE_REGEX.lastIndex = 0;
|
|
231
|
+
let match: RegExpExecArray | null;
|
|
232
|
+
while ((match = SECRET_TEMPLATE_REGEX.exec(value)) !== null) {
|
|
233
|
+
secretNames.add(match[1]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (secretNames.size === 0) {
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const resolvedValues = new Map<string, string>();
|
|
241
|
+
for (const name of secretNames) {
|
|
242
|
+
resolvedValues.set(name, await secretStore.resolve(name));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
SECRET_TEMPLATE_REGEX.lastIndex = 0;
|
|
246
|
+
return value.replaceAll(
|
|
247
|
+
SECRET_TEMPLATE_REGEX,
|
|
248
|
+
(_fullMatch, secretName: string) => resolvedValues.get(secretName)!,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isSecretSchema } from "@checkstack/backend-api";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic schema-driven walk over `x-secret`-annotated string fields.
|
|
6
|
+
*
|
|
7
|
+
* This is the shared machinery behind both the runtime secret resolution
|
|
8
|
+
* (`resolveSecretsBySchema`) and the connection-credential consolidation
|
|
9
|
+
* (extract inline values to internal secrets / inflate references). It
|
|
10
|
+
* walks a value against its Zod schema exactly like the resolver does
|
|
11
|
+
* (objects, arrays, optional/default/nullable, discriminated + plain
|
|
12
|
+
* unions) and invokes `visit` for every `x-secret` STRING leaf, replacing
|
|
13
|
+
* that leaf with whatever `visit` returns.
|
|
14
|
+
*
|
|
15
|
+
* `visit` is async and receives the field path (for diagnostics) and the
|
|
16
|
+
* current string value. Non-secret fields are returned unchanged — so this
|
|
17
|
+
* never touches non-credential config, which is the whole point of acting
|
|
18
|
+
* only on `x-secret` fields.
|
|
19
|
+
*/
|
|
20
|
+
export async function walkSecretFields(params: {
|
|
21
|
+
value: unknown;
|
|
22
|
+
schema: z.ZodTypeAny;
|
|
23
|
+
visit: (input: { path: string; value: string }) => Promise<string>;
|
|
24
|
+
}): Promise<unknown> {
|
|
25
|
+
return walk({
|
|
26
|
+
value: params.value,
|
|
27
|
+
schema: params.schema,
|
|
28
|
+
visit: params.visit,
|
|
29
|
+
path: "",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function walk(params: {
|
|
34
|
+
value: unknown;
|
|
35
|
+
schema: z.ZodTypeAny;
|
|
36
|
+
visit: (input: { path: string; value: string }) => Promise<string>;
|
|
37
|
+
path: string;
|
|
38
|
+
}): Promise<unknown> {
|
|
39
|
+
const { value, visit, path } = params;
|
|
40
|
+
const schema = unwrapZod(params.schema);
|
|
41
|
+
|
|
42
|
+
if (value === null || value === undefined) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// x-secret string leaf: hand it to the visitor.
|
|
47
|
+
if (isSecretSchema(schema)) {
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
return visit({ path: path || "(root)", value });
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
schema instanceof z.ZodObject &&
|
|
56
|
+
typeof value === "object" &&
|
|
57
|
+
!Array.isArray(value)
|
|
58
|
+
) {
|
|
59
|
+
const shape = schema.shape as Record<string, z.ZodTypeAny>;
|
|
60
|
+
const result: Record<string, unknown> = {
|
|
61
|
+
...(value as Record<string, unknown>),
|
|
62
|
+
};
|
|
63
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
64
|
+
if (key in result) {
|
|
65
|
+
result[key] = await walk({
|
|
66
|
+
value: result[key],
|
|
67
|
+
schema: fieldSchema,
|
|
68
|
+
visit,
|
|
69
|
+
path: path ? `${path}.${key}` : key,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (schema instanceof z.ZodArray && Array.isArray(value)) {
|
|
77
|
+
const elementSchema = schema.element as z.ZodTypeAny;
|
|
78
|
+
return Promise.all(
|
|
79
|
+
value.map((item, index) =>
|
|
80
|
+
walk({
|
|
81
|
+
value: item,
|
|
82
|
+
schema: elementSchema,
|
|
83
|
+
visit,
|
|
84
|
+
path: `${path}[${index}]`,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
schema instanceof z.ZodDiscriminatedUnion &&
|
|
92
|
+
typeof value === "object" &&
|
|
93
|
+
value !== null
|
|
94
|
+
) {
|
|
95
|
+
const discriminator = (schema.def as { discriminator: string })
|
|
96
|
+
.discriminator;
|
|
97
|
+
const discriminatorValue = (value as Record<string, unknown>)[
|
|
98
|
+
discriminator
|
|
99
|
+
];
|
|
100
|
+
const options = schema.options as z.ZodObject<z.ZodRawShape>[];
|
|
101
|
+
const matched = options.find((option) => {
|
|
102
|
+
const discField = option.shape[discriminator];
|
|
103
|
+
return discField instanceof z.ZodLiteral
|
|
104
|
+
? discField.value === discriminatorValue
|
|
105
|
+
: false;
|
|
106
|
+
});
|
|
107
|
+
if (matched) {
|
|
108
|
+
return walk({ value, schema: matched, visit, path });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
schema instanceof z.ZodUnion &&
|
|
114
|
+
typeof value === "object" &&
|
|
115
|
+
value !== null
|
|
116
|
+
) {
|
|
117
|
+
const options = schema.options as z.ZodTypeAny[];
|
|
118
|
+
for (const option of options) {
|
|
119
|
+
if (option.safeParse(value).success) {
|
|
120
|
+
return walk({ value, schema: option, visit, path });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function unwrapZod(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
129
|
+
let unwrapped = schema;
|
|
130
|
+
if (unwrapped instanceof z.ZodOptional) {
|
|
131
|
+
unwrapped = unwrapped.unwrap() as z.ZodTypeAny;
|
|
132
|
+
}
|
|
133
|
+
if (unwrapped instanceof z.ZodDefault) {
|
|
134
|
+
unwrapped = unwrapped.def.innerType as z.ZodTypeAny;
|
|
135
|
+
}
|
|
136
|
+
if (unwrapped instanceof z.ZodNullable) {
|
|
137
|
+
unwrapped = unwrapped.unwrap() as z.ZodTypeAny;
|
|
138
|
+
}
|
|
139
|
+
return unwrapped;
|
|
140
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@checkstack/tsconfig/backend.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"src"
|
|
5
|
+
],
|
|
6
|
+
"references": [
|
|
7
|
+
{
|
|
8
|
+
"path": "../backend"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "../backend-api"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "../common"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "../dev-server"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "../drizzle-helper"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "../secrets-common"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "../test-utils-backend"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|