diff --git a/packages/shared/src/schemaYaml.test.ts b/packages/shared/src/schemaYaml.test.ts index 7b5a23acbe1..c4eaab88b02 100644 --- a/packages/shared/src/schemaYaml.test.ts +++ b/packages/shared/src/schemaYaml.test.ts @@ -50,9 +50,45 @@ tags: expect(decodeYaml("answer: 42\n")).toEqual({ answer: 42 }); }); - it("rejects malformed YAML", () => { + it("reports malformed YAML with safe structural diagnostics", () => { const decodeYaml = Schema.decodeUnknownSync(fromYaml(Schema.Unknown)); + const secret = "credential=secret-value"; + let error: unknown; - expect(() => decodeYaml("name: ok\n bad-indent: nope\n")).toThrow(); + try { + decodeYaml(`name: ${secret}\n bad-indent: nope\n`); + } catch (cause) { + error = cause; + } + + expect(Schema.isSchemaError(error)).toBe(true); + if (!Schema.isSchemaError(error)) { + throw new Error("Expected a schema error"); + } + expect(error.message).toBe("Invalid YAML (code=BLOCK_AS_IMPLICIT_KEY, line=1, column=7)."); + expect(error.message).not.toContain(secret); + }); + + it("does not expose stringify failure details", () => { + const encodeYaml = Schema.encodeSync(fromYaml(Schema.Unknown)); + const secret = "credential=secret-value"; + let error: unknown; + + try { + encodeYaml({ + toJSON() { + throw new Error(secret); + }, + }); + } catch (cause) { + error = cause; + } + + expect(Schema.isSchemaError(error)).toBe(true); + if (!Schema.isSchemaError(error)) { + throw new Error("Expected a schema error"); + } + expect(error.message).toBe("Failed to stringify YAML."); + expect(error.message).not.toContain(secret); }); }); diff --git a/packages/shared/src/schemaYaml.ts b/packages/shared/src/schemaYaml.ts index 1b0d10fb888..ddb1b5c916c 100644 --- a/packages/shared/src/schemaYaml.ts +++ b/packages/shared/src/schemaYaml.ts @@ -5,6 +5,7 @@ import * as SchemaGetter from "effect/SchemaGetter"; import * as SchemaIssue from "effect/SchemaIssue"; import * as SchemaTransformation from "effect/SchemaTransformation"; import { + YAMLParseError, parse as parseYamlString, stringify as stringifyYamlValue, type CreateNodeOptions, @@ -22,8 +23,14 @@ export type YamlStringifyOptions = DocumentOptions & CreateNodeOptions & ToStringOptions; -function formatYamlError(error: unknown): string { - return error instanceof Error ? error.message : String(error); +function formatYamlParseError(error: unknown): string { + if (!(error instanceof YAMLParseError)) { + return "Invalid YAML."; + } + + const position = error.linePos?.[0]; + const location = position === undefined ? "" : `, line=${position.line}, column=${position.col}`; + return `Invalid YAML (code=${error.code}${location}).`; } /** @@ -56,7 +63,7 @@ export function parseYaml( Effect.try({ try: () => parseYamlString(input, options) as unknown, catch: (error) => - new SchemaIssue.InvalidValue(Option.some(input), { message: formatYamlError(error) }), + new SchemaIssue.InvalidValue(Option.none(), { message: formatYamlParseError(error) }), }), ); } @@ -90,8 +97,8 @@ export function stringifyYaml( return SchemaGetter.transformOrFail((input: unknown) => Effect.try({ try: () => stringifyYamlValue(input, options), - catch: (error) => - new SchemaIssue.InvalidValue(Option.some(input), { message: formatYamlError(error) }), + catch: () => + new SchemaIssue.InvalidValue(Option.none(), { message: "Failed to stringify YAML." }), }), ); }