From e3b3e30fe4083eb211aa84c4d1c3196abb5e9c8e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 21:31:51 +0000 Subject: [PATCH 1/3] test(client-runtime): reproduce secret whitespace being trimmed (#31690) These tests reproduce pingdotgg/t3code#31690: a saved secret should be delivered exactly as typed. Today the client trims it (and refuses a whitespace-only value) and the wire contract trims it again. Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/secretRequest.test.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/secretRequest.test.ts b/packages/client-runtime/src/secretRequest.test.ts index 57d4c4f49a8cb..d33a66a61913c 100644 --- a/packages/client-runtime/src/secretRequest.test.ts +++ b/packages/client-runtime/src/secretRequest.test.ts @@ -1,12 +1,21 @@ -import { SecretRequestError } from "@t3tools/contracts"; +import { + SecretRequestAnswerInput, + SecretRequestError, + ThreadId, + TurnItemId, +} from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; import { describe, expect, it } from "vite-plus/test"; import { + secretRequestAnswerInput, secretRequestDisplay, secretRequestFailureMessage, secretRequestPrivacyNote, } from "./secretRequest.ts"; +const decodeAnswerInput = Schema.decodeUnknownSync(SecretRequestAnswerInput); + describe("secretRequestDisplay", () => { it("never offers the form for a request inherited from another thread", () => { expect(secretRequestDisplay({ secretStatus: "pending" }, "inherited")).toEqual({ @@ -26,6 +35,22 @@ describe("secretRequestPrivacyNote", () => { }); }); +describe("secretRequestAnswerInput", () => { + const item = { id: TurnItemId.make("item-1"), threadId: ThreadId.make("thread-1") }; + const savedSecret = (secret: string) => { + const input = secretRequestAnswerInput(item, { type: "save", secret }); + if (input === null) return null; + const delivered = decodeAnswerInput(input).answer; + return delivered.type === "save" ? delivered.secret : null; + }; + + it("delivers the secret exactly as typed, whitespace included", () => { + expect(savedSecret(" pass word ")).toBe(" pass word "); + expect(savedSecret(" ")).toBe(" "); + expect(savedSecret("")).toBeNull(); + }); +}); + describe("secretRequestFailureMessage", () => { it("passes through server errors but hides anything that could echo the payload", () => { expect( From 326d5d08bb5d9120cc61e6c8d39e3ad00f3d9b2f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 21:36:04 +0000 Subject: [PATCH 2/3] fix: deliver private secrets exactly as typed (#31690) The secret card trimmed the value in the client and again in the wire contract, so leading/trailing whitespace was lost and a whitespace-only secret could not be saved. Only an empty value now counts as empty: the contract uses Schema.NonEmptyString, the client stops trimming, and the web and mobile "Save securely" buttons enable for any non-empty value. Co-Authored-By: Claude Opus 5.5 --- apps/mobile/src/features/threads/SecretRequestCard.tsx | 2 +- apps/web/src/components/chat/SecretRequestCard.tsx | 2 +- packages/client-runtime/src/secretRequest.ts | 2 +- packages/contracts/src/secretRequest.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/features/threads/SecretRequestCard.tsx b/apps/mobile/src/features/threads/SecretRequestCard.tsx index 19bf52ccd48c7..f70b06059a4e7 100644 --- a/apps/mobile/src/features/threads/SecretRequestCard.tsx +++ b/apps/mobile/src/features/threads/SecretRequestCard.tsx @@ -135,7 +135,7 @@ function PendingSecretRequestForm(props: { label="Save securely" tone="primary" fullWidth - disabled={submitting || secret.trim().length === 0} + disabled={submitting || secret.length === 0} onPress={() => void send({ type: "save", secret })} /> diff --git a/apps/web/src/components/chat/SecretRequestCard.tsx b/apps/web/src/components/chat/SecretRequestCard.tsx index 2ec2923b88bbd..86d88ce5f7f86 100644 --- a/apps/web/src/components/chat/SecretRequestCard.tsx +++ b/apps/web/src/components/chat/SecretRequestCard.tsx @@ -142,7 +142,7 @@ function PendingSecretRequestForm(props: { onChange={(event) => setSecret(event.currentTarget.value)} /> - diff --git a/packages/client-runtime/src/secretRequest.ts b/packages/client-runtime/src/secretRequest.ts index c6ec7ce95f2c9..196dda80dc40c 100644 --- a/packages/client-runtime/src/secretRequest.ts +++ b/packages/client-runtime/src/secretRequest.ts @@ -69,7 +69,7 @@ export function secretRequestAnswerInput( if (answer.type === "decline") { return { threadId: item.threadId, turnItemId: item.id, answer: { type: "decline" } }; } - const secret = answer.secret.trim(); + const { secret } = answer; if (secret.length === 0) return null; return { threadId: item.threadId, turnItemId: item.id, answer: { type: "save", secret } }; } diff --git a/packages/contracts/src/secretRequest.ts b/packages/contracts/src/secretRequest.ts index b449a17643d40..cf51d42e2fc01 100644 --- a/packages/contracts/src/secretRequest.ts +++ b/packages/contracts/src/secretRequest.ts @@ -1,13 +1,13 @@ import * as Schema from "effect/Schema"; -import { ThreadId, TrimmedNonEmptyString, TurnItemId } from "./baseSchemas.ts"; +import { ThreadId, TurnItemId } from "./baseSchemas.ts"; /** Server stores the value under a one-use SecretRef; the thread learns only status. */ export const SecretRequestAnswerInput = Schema.Struct({ threadId: ThreadId, turnItemId: TurnItemId, answer: Schema.Union([ - Schema.Struct({ type: Schema.Literal("save"), secret: TrimmedNonEmptyString }), + Schema.Struct({ type: Schema.Literal("save"), secret: Schema.NonEmptyString }), Schema.Struct({ type: Schema.Literal("decline") }), ]), }); From b075a426f278837291f20f76e2e509cc04478565 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Nov 2026 21:41:39 +0000 Subject: [PATCH 3/3] test(client-runtime): the secret answer contract still rejects an empty secret Co-Authored-By: Claude Opus 5.5 --- packages/client-runtime/src/secretRequest.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/client-runtime/src/secretRequest.test.ts b/packages/client-runtime/src/secretRequest.test.ts index d33a66a61913c..0bf5ed6a2a961 100644 --- a/packages/client-runtime/src/secretRequest.test.ts +++ b/packages/client-runtime/src/secretRequest.test.ts @@ -48,6 +48,13 @@ describe("secretRequestAnswerInput", () => { expect(savedSecret(" pass word ")).toBe(" pass word "); expect(savedSecret(" ")).toBe(" "); expect(savedSecret("")).toBeNull(); + expect(() => + decodeAnswerInput({ + threadId: item.threadId, + turnItemId: item.id, + answer: { type: "save", secret: "" }, + }), + ).toThrow(); }); });