mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-16 14:40:49 +02:00
Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.4.3. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/colinhacks/zod/releases">zod's releases</a>.</em></p> <blockquote> <h2>v4.4.3</h2> <h2>Commits:</h2> <ul> <li>4c2fa95ce3f3390fbc522324e406b4e9e89b88f9 docs: use Zernio primary wordmark for gold sponsor logo</li> <li>2aeec83eb135e3a83756e973ef44845fc5a455d2 docs: prune lapsed gold sponsors and rebalance logo sizing</li> <li>7391be88ac1ee5cd02057f5ccc012a1f5df4efd0 docs: prune lapsed silver/bronze sponsors and add active ones</li> <li>2c703322a21b4e2b12f33f49ea8430c451a68b4f docs: normalize bronze sponsor logos to github avatar pattern</li> <li>9195250cab0e7950efe39c3926d6c203b4b0a170 docs: remove Mintlify from bronze sponsors (churned)</li> <li>b8dffe9e62f17e6571e6249d05cc5102b54d94e4 docs: remove Numeric and Speakeasy (2+ missed monthly cycles)</li> <li>1cab69383fcdeae2a366d5e2a2fc4d8fc765d168 fix(v4): restore catch handling for absent object keys (<a href="https://redirect.github.com/colinhacks/zod/issues/5937">#5937</a>) (<a href="https://redirect.github.com/colinhacks/zod/issues/5939">#5939</a>)</li> <li>c2be4f819064eed62c7c350a2d399b5faecd15f8 fix(v4): generalize optin/fallback to transform; restore preprocess on absent keys (<a href="https://redirect.github.com/colinhacks/zod/issues/5941">#5941</a>)</li> <li>f3c9ec03ba7a28ae72d25cc295f38674bee0f559 4.4.3</li> <li>1fb56a5c18c27102dbc92260a4007c7732a0ccca docs: document release procedure in AGENTS.md</li> </ul> <h2>v4.4.2</h2> <h2>Commits:</h2> <ul> <li>0c62df0ea19fd05abdf90473e9eef7eea530fab2 Clean up docs navigation and stale labels (<a href="https://redirect.github.com/colinhacks/zod/issues/5901">#5901</a>)</li> <li>20cc794895cc8604fe0c87d83a5d1c3f89fad0ac chore: add security policy and refresh tooling deps</li> <li>6fbe07b0177efdd1bf1c0b05160e70d7a0702337 fix(docs): heading anchor links now include the hash so it doesnt scoll all the way up, follows navbar logic (<a href="https://redirect.github.com/colinhacks/zod/issues/5791">#5791</a>)</li> <li>4bbed1b1c73eca4ce9e59b1189ed236aa6c8b5bd Tighten discriminated union option typing</li> <li>bbac3e567e7fccfaaf7cdc97f1ce30c295e2c908 Update PR guidance for agents</li> <li>cf0dc942a32805c292fff59ade20a7ace980735a Merge remote-tracking branch 'origin/main' into fix-discriminated-union-key-constraint</li> <li>292c894a5fd2aa42e527900b83d8d7a3009a709c docs: add Zernio gold sponsor</li> <li>1fc9f311c28dcf80d0bb5a36b177086cbc3d8eca docs: document codec inversion</li> <li>1373c85da9aeff704a9762d27bc58699618aefb7 docs: remove AI disclosure guidance</li> <li>e20d02b473c08e3a4e557bc610b1b5fac079b649 chore: ignore triage notes</li> <li>e58ea4d91b1dfe8194b73508203213cbc7e9c936 docs: test Zod Mini tab code heights</li> <li>905761a5d127e8d5dd2ebb3bc88c75cb0b8149ff docs: document preprocess input type narrowing</li> <li>bf64bac850d4dee2b7dde7e64909d5d796d32043 chore: tighten test guidance in AGENTS.md</li> <li>8ec4e73f4c4693b6361ad591be40fb41eb8a9f95 chore: update play.ts scratch</li> <li>02c2baf7d0d615872fa4528a8020603b71211702 Make z.preprocess defer optionality to inner schema (<a href="https://redirect.github.com/colinhacks/zod/issues/5929">#5929</a>)</li> <li>88015df8e25c44fb5385eb3ef28935119cd5edea fix(docs): drop deprecated <code>baseUrl</code> from tsconfig</li> <li>c59d4474e3b4cad1b323462186cf607178ce8267 4.4.2</li> </ul> <h2>v4.4.1</h2> <h2>Commits:</h2> <ul> <li>481f7be4238c83ed58183f921b2646f340a91c6a ci: gate release publishing on full test workflow</li> <li>95ccab423aec720b2523c3a64cdc7e3204537cc7 test(v3): restore optional undefined expectations</li> <li>cede2c63739a5823d6aa5093d291e9a111da943d fix(v4): reject tuple holes before required defaults (<a href="https://redirect.github.com/colinhacks/zod/issues/5900">#5900</a>)</li> <li>edd0bf0f5ada4a8dc581c259407d7bbad0a71ea7 release: 4.4.1</li> <li>180d83d1dbe6a59260710cc8637a3dea2281ee56 docs: remove Jazz featured sponsor</li> </ul> <h2>v4.4.0</h2> <h2>4.4.0</h2> <p>This is a minor release with a wide set of correctness and soundness fixes. Some fixes intentionally make Zod stricter, so code that depended on previously accepted invalid or ambiguous inputs may need small updates.</p> <h2>Potentially breaking bug fixes</h2> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/colinhacks/zod/commit/1fb56a5c18c27102dbc92260a4007c7732a0ccca"><code>1fb56a5</code></a> docs: document release procedure in AGENTS.md</li> <li><a href="https://github.com/colinhacks/zod/commit/f3c9ec03ba7a28ae72d25cc295f38674bee0f559"><code>f3c9ec0</code></a> 4.4.3</li> <li><a href="https://github.com/colinhacks/zod/commit/c2be4f819064eed62c7c350a2d399b5faecd15f8"><code>c2be4f8</code></a> fix(v4): generalize optin/fallback to transform; restore preprocess on absent...</li> <li><a href="https://github.com/colinhacks/zod/commit/1cab69383fcdeae2a366d5e2a2fc4d8fc765d168"><code>1cab693</code></a> fix(v4): restore catch handling for absent object keys (<a href="https://redirect.github.com/colinhacks/zod/issues/5937">#5937</a>) (<a href="https://redirect.github.com/colinhacks/zod/issues/5939">#5939</a>)</li> <li><a href="https://github.com/colinhacks/zod/commit/b8dffe9e62f17e6571e6249d05cc5102b54d94e4"><code>b8dffe9</code></a> docs: remove Numeric and Speakeasy (2+ missed monthly cycles)</li> <li><a href="https://github.com/colinhacks/zod/commit/9195250cab0e7950efe39c3926d6c203b4b0a170"><code>9195250</code></a> docs: remove Mintlify from bronze sponsors (churned)</li> <li><a href="https://github.com/colinhacks/zod/commit/2c703322a21b4e2b12f33f49ea8430c451a68b4f"><code>2c70332</code></a> docs: normalize bronze sponsor logos to github avatar pattern</li> <li><a href="https://github.com/colinhacks/zod/commit/7391be88ac1ee5cd02057f5ccc012a1f5df4efd0"><code>7391be8</code></a> docs: prune lapsed silver/bronze sponsors and add active ones</li> <li><a href="https://github.com/colinhacks/zod/commit/2aeec83eb135e3a83756e973ef44845fc5a455d2"><code>2aeec83</code></a> docs: prune lapsed gold sponsors and rebalance logo sizing</li> <li><a href="https://github.com/colinhacks/zod/commit/4c2fa95ce3f3390fbc522324e406b4e9e89b88f9"><code>4c2fa95</code></a> docs: use Zernio primary wordmark for gold sponsor logo</li> <li>Additional commits viewable in <a href="https://github.com/colinhacks/zod/compare/v3.25.76...v4.4.3">compare view</a></li> </ul> </details> <details> <summary>Maintainer changes</summary> <p>This version was pushed to npm by <a href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new releaser for zod since your current version.</p> </details> <br /> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Priya Raman <priya@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing>
350 lines
12 KiB
TypeScript
350 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
AUTH_BASE_URL_MODES,
|
|
BIND_MODES,
|
|
DEPLOYMENT_EXPOSURES,
|
|
DEPLOYMENT_MODES,
|
|
SECRET_PROVIDERS,
|
|
STORAGE_PROVIDERS,
|
|
} from "./constants.js";
|
|
import { validateConfiguredBindMode } from "./network-bind.js";
|
|
|
|
export const configMetaSchema = z.object({
|
|
version: z.literal(1),
|
|
updatedAt: z.string(),
|
|
source: z.enum(["onboard", "configure", "doctor"]),
|
|
}).passthrough();
|
|
|
|
export const llmConfigSchema = z.object({
|
|
provider: z.enum(["claude", "openai"]),
|
|
apiKey: z.string().optional(),
|
|
}).passthrough();
|
|
|
|
export const databaseBackupConfigSchema = z.object({
|
|
enabled: z.boolean().default(true),
|
|
intervalMinutes: z.number().int().min(1).max(7 * 24 * 60).default(60),
|
|
retentionDays: z.number().int().min(1).max(3650).default(7),
|
|
dir: z.string().default("~/.paperclip/instances/default/data/backups"),
|
|
}).passthrough();
|
|
|
|
export const databaseConfigSchema = z.object({
|
|
mode: z.enum(["embedded-postgres", "postgres"]).default("embedded-postgres"),
|
|
connectionString: z.string().optional(),
|
|
embeddedPostgresDataDir: z.string().default("~/.paperclip/instances/default/db"),
|
|
embeddedPostgresPort: z.number().int().min(1).max(65535).default(54329),
|
|
backup: databaseBackupConfigSchema.default({
|
|
enabled: true,
|
|
intervalMinutes: 60,
|
|
retentionDays: 7,
|
|
dir: "~/.paperclip/instances/default/data/backups",
|
|
}),
|
|
}).passthrough();
|
|
|
|
export const loggingConfigSchema = z.object({
|
|
mode: z.enum(["file", "cloud"]),
|
|
logDir: z.string().default("~/.paperclip/instances/default/logs"),
|
|
}).passthrough();
|
|
|
|
export const serverConfigSchema = z.object({
|
|
deploymentMode: z.enum(DEPLOYMENT_MODES).default("local_trusted"),
|
|
exposure: z.enum(DEPLOYMENT_EXPOSURES).default("private"),
|
|
bind: z.enum(BIND_MODES).optional(),
|
|
customBindHost: z.string().optional(),
|
|
host: z.string().default("127.0.0.1"),
|
|
port: z.number().int().min(1).max(65535).default(3100),
|
|
allowedHostnames: z.array(z.string().min(1)).default([]),
|
|
serveUi: z.boolean().default(true),
|
|
}).passthrough();
|
|
|
|
export const authConfigSchema = z.object({
|
|
baseUrlMode: z.enum(AUTH_BASE_URL_MODES).default("auto"),
|
|
publicBaseUrl: z.string().url().optional(),
|
|
disableSignUp: z.boolean().default(false),
|
|
}).passthrough();
|
|
|
|
export const storageLocalDiskConfigSchema = z.object({
|
|
baseDir: z.string().default("~/.paperclip/instances/default/data/storage"),
|
|
}).passthrough();
|
|
|
|
export const storageS3ConfigSchema = z.object({
|
|
bucket: z.string().min(1).default("paperclip"),
|
|
region: z.string().min(1).default("us-east-1"),
|
|
endpoint: z.string().optional(),
|
|
prefix: z.string().default(""),
|
|
forcePathStyle: z.boolean().default(false),
|
|
}).passthrough();
|
|
|
|
export const storageConfigSchema = z.object({
|
|
provider: z.enum(STORAGE_PROVIDERS).default("local_disk"),
|
|
localDisk: storageLocalDiskConfigSchema.default({
|
|
baseDir: "~/.paperclip/instances/default/data/storage",
|
|
}),
|
|
s3: storageS3ConfigSchema.default({
|
|
bucket: "paperclip",
|
|
region: "us-east-1",
|
|
prefix: "",
|
|
forcePathStyle: false,
|
|
}),
|
|
}).passthrough();
|
|
|
|
export const secretsLocalEncryptedConfigSchema = z.object({
|
|
keyFilePath: z.string().default("~/.paperclip/instances/default/secrets/master.key"),
|
|
}).passthrough();
|
|
|
|
export const secretsConfigSchema = z.object({
|
|
provider: z.enum(SECRET_PROVIDERS).default("local_encrypted"),
|
|
strictMode: z.boolean().default(false),
|
|
localEncrypted: secretsLocalEncryptedConfigSchema.default({
|
|
keyFilePath: "~/.paperclip/instances/default/secrets/master.key",
|
|
}),
|
|
}).passthrough();
|
|
|
|
export const telemetryConfigSchema = z.object({
|
|
enabled: z.boolean().default(true),
|
|
}).passthrough().prefault({});
|
|
|
|
export const updatesConfigSchema = z.object({
|
|
checkEnabled: z.boolean().default(true),
|
|
}).passthrough().prefault({});
|
|
|
|
export const paperclipConfigSchema = z
|
|
.object({
|
|
$meta: configMetaSchema,
|
|
llm: llmConfigSchema.optional(),
|
|
database: databaseConfigSchema,
|
|
logging: loggingConfigSchema,
|
|
server: serverConfigSchema,
|
|
telemetry: telemetryConfigSchema,
|
|
updates: updatesConfigSchema.optional(),
|
|
auth: authConfigSchema.default({
|
|
baseUrlMode: "auto",
|
|
disableSignUp: false,
|
|
}),
|
|
storage: storageConfigSchema.default({
|
|
provider: "local_disk",
|
|
localDisk: {
|
|
baseDir: "~/.paperclip/instances/default/data/storage",
|
|
},
|
|
s3: {
|
|
bucket: "paperclip",
|
|
region: "us-east-1",
|
|
prefix: "",
|
|
forcePathStyle: false,
|
|
},
|
|
}),
|
|
secrets: secretsConfigSchema.default({
|
|
provider: "local_encrypted",
|
|
strictMode: false,
|
|
localEncrypted: {
|
|
keyFilePath: "~/.paperclip/instances/default/secrets/master.key",
|
|
},
|
|
}),
|
|
})
|
|
.passthrough()
|
|
.superRefine((value, ctx) => {
|
|
if (value.server.deploymentMode === "local_trusted" && value.server.exposure !== "private") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "server.exposure must be private when deploymentMode is local_trusted",
|
|
path: ["server", "exposure"],
|
|
});
|
|
}
|
|
|
|
for (const message of validateConfiguredBindMode({
|
|
deploymentMode: value.server.deploymentMode,
|
|
deploymentExposure: value.server.exposure,
|
|
bind: value.server.bind,
|
|
host: value.server.host,
|
|
customBindHost: value.server.customBindHost,
|
|
})) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message,
|
|
path: message.includes("customBindHost") ? ["server", "customBindHost"] : ["server", "bind"],
|
|
});
|
|
}
|
|
|
|
if (value.auth.baseUrlMode === "explicit" && !value.auth.publicBaseUrl) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "auth.publicBaseUrl is required when auth.baseUrlMode is explicit",
|
|
path: ["auth", "publicBaseUrl"],
|
|
});
|
|
}
|
|
|
|
if (value.server.exposure === "public" && value.auth.baseUrlMode !== "explicit") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "auth.baseUrlMode must be explicit when deploymentMode=authenticated and exposure=public",
|
|
path: ["auth", "baseUrlMode"],
|
|
});
|
|
}
|
|
|
|
if (value.server.exposure === "public" && !value.auth.publicBaseUrl) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "auth.publicBaseUrl is required when deploymentMode=authenticated and exposure=public",
|
|
path: ["auth", "publicBaseUrl"],
|
|
});
|
|
}
|
|
});
|
|
|
|
export type PaperclipConfig = z.infer<typeof paperclipConfigSchema>;
|
|
export type LlmConfig = z.infer<typeof llmConfigSchema>;
|
|
export type DatabaseConfig = z.infer<typeof databaseConfigSchema>;
|
|
export type LoggingConfig = z.infer<typeof loggingConfigSchema>;
|
|
export type ServerConfig = z.infer<typeof serverConfigSchema>;
|
|
export type StorageConfig = z.infer<typeof storageConfigSchema>;
|
|
export type StorageLocalDiskConfig = z.infer<typeof storageLocalDiskConfigSchema>;
|
|
export type StorageS3Config = z.infer<typeof storageS3ConfigSchema>;
|
|
export type SecretsConfig = z.infer<typeof secretsConfigSchema>;
|
|
export type SecretsLocalEncryptedConfig = z.infer<typeof secretsLocalEncryptedConfigSchema>;
|
|
export type AuthConfig = z.infer<typeof authConfigSchema>;
|
|
export type TelemetryConfig = z.infer<typeof telemetryConfigSchema>;
|
|
export type UpdatesConfig = z.infer<typeof updatesConfigSchema>;
|
|
export type ConfigMeta = z.infer<typeof configMetaSchema>;
|
|
export type DatabaseBackupConfig = z.infer<typeof databaseBackupConfigSchema>;
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function unwrapConfigSchema(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
let current: z.ZodTypeAny = schema;
|
|
while (true) {
|
|
if (current instanceof z.ZodOptional) {
|
|
current = current.unwrap() as z.ZodTypeAny;
|
|
continue;
|
|
}
|
|
if (current instanceof z.ZodDefault) {
|
|
current = current.unwrap() as z.ZodTypeAny;
|
|
continue;
|
|
}
|
|
return current;
|
|
}
|
|
}
|
|
|
|
function mergeUnknownConfigKeys(
|
|
source: Record<string, unknown>,
|
|
update: Record<string, unknown>,
|
|
schema: z.ZodTypeAny,
|
|
): Record<string, unknown> {
|
|
const objectSchema = unwrapConfigSchema(schema);
|
|
if (!(objectSchema instanceof z.ZodObject)) return { ...update };
|
|
|
|
const shape = objectSchema.shape as Record<string, z.ZodTypeAny>;
|
|
const merged = { ...update };
|
|
|
|
for (const [key, sourceValue] of Object.entries(source)) {
|
|
const childSchema = shape[key];
|
|
if (childSchema === undefined) {
|
|
merged[key] = sourceValue;
|
|
continue;
|
|
}
|
|
|
|
const updateValue = update[key];
|
|
if (isRecord(sourceValue) && isRecord(updateValue)) {
|
|
merged[key] = mergeUnknownConfigKeys(sourceValue, updateValue, childSchema);
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
/**
|
|
* Applies a config update while retaining extension keys from the parsed source.
|
|
* Known optional keys that are absent from the update stay absent, so callers can
|
|
* intentionally clear values such as llm.apiKey or auth.publicBaseUrl.
|
|
*/
|
|
export function mergePaperclipConfig(
|
|
source: PaperclipConfig,
|
|
update: PaperclipConfig,
|
|
): PaperclipConfig {
|
|
return mergeUnknownConfigKeys(
|
|
source as Record<string, unknown>,
|
|
update as Record<string, unknown>,
|
|
paperclipConfigSchema,
|
|
) as PaperclipConfig;
|
|
}
|
|
|
|
export type ConfigKeyWarning = {
|
|
path: string;
|
|
suggestion: string;
|
|
};
|
|
|
|
function editDistance(left: string, right: string): number {
|
|
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
|
|
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
const current = [leftIndex];
|
|
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
current[rightIndex] = Math.min(
|
|
current[rightIndex - 1] + 1,
|
|
previous[rightIndex] + 1,
|
|
previous[rightIndex - 1] + substitutionCost,
|
|
);
|
|
}
|
|
previous.splice(0, previous.length, ...current);
|
|
}
|
|
|
|
return previous[right.length];
|
|
}
|
|
|
|
function nearMatch(key: string, candidates: string[]): string | null {
|
|
const normalizedKey = key.toLowerCase();
|
|
let best: { candidate: string; distance: number } | null = null;
|
|
|
|
for (const candidate of candidates) {
|
|
const normalizedCandidate = candidate.toLowerCase();
|
|
if (normalizedKey === normalizedCandidate && key !== candidate) return candidate;
|
|
|
|
const distance = editDistance(normalizedKey, normalizedCandidate);
|
|
const threshold = Math.max(normalizedKey.length, normalizedCandidate.length) >= 8 ? 2 : 1;
|
|
if (distance > threshold || (best && distance >= best.distance)) continue;
|
|
best = { candidate, distance };
|
|
}
|
|
|
|
return best?.candidate ?? null;
|
|
}
|
|
|
|
function collectConfigKeyWarnings(
|
|
value: Record<string, unknown>,
|
|
schema: z.ZodTypeAny,
|
|
prefix: string,
|
|
warnings: ConfigKeyWarning[],
|
|
): void {
|
|
const objectSchema = unwrapConfigSchema(schema);
|
|
if (!(objectSchema instanceof z.ZodObject)) return;
|
|
|
|
const shape = objectSchema.shape as Record<string, z.ZodTypeAny>;
|
|
const knownKeys = Object.keys(shape);
|
|
|
|
for (const [key, childValue] of Object.entries(value)) {
|
|
const childSchema = shape[key];
|
|
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
if (childSchema === undefined) {
|
|
const suggestion = nearMatch(key, knownKeys);
|
|
if (suggestion) {
|
|
warnings.push({
|
|
path: childPath,
|
|
suggestion: prefix ? `${prefix}.${suggestion}` : suggestion,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (isRecord(childValue)) {
|
|
collectConfigKeyWarnings(childValue, childSchema, childPath, warnings);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns likely misspellings among retained extension keys without modifying them. */
|
|
export function findPaperclipConfigKeyWarnings(config: unknown): ConfigKeyWarning[] {
|
|
if (!isRecord(config)) return [];
|
|
const warnings: ConfigKeyWarning[] = [];
|
|
collectConfigKeyWarnings(config, paperclipConfigSchema, "", warnings);
|
|
return warnings;
|
|
}
|