diff --git a/apps/mobile/src/features/threads/SecretRequestCard.tsx b/apps/mobile/src/features/threads/SecretRequestCard.tsx index bdcef6f48ef7f..c93cd1e4fdfa2 100644 --- a/apps/mobile/src/features/threads/SecretRequestCard.tsx +++ b/apps/mobile/src/features/threads/SecretRequestCard.tsx @@ -120,7 +120,7 @@ function PendingSecretRequestForm(props: { void send({ type: "reference", source, reference }); } }; - const draft = source === null ? secret : reference; + const draft = source === null ? secret : reference.trim(); const link = secretRequestLink(item); return ( @@ -219,7 +219,7 @@ function PendingSecretRequestForm(props: { label="Save securely" tone="primary" fullWidth - disabled={submitting || draft.trim().length === 0} + disabled={submitting || draft.length === 0} onPress={save} /> diff --git a/apps/web/src/components/chat/SecretRequestCard.tsx b/apps/web/src/components/chat/SecretRequestCard.tsx index b6cdabb8a7b69..8010b79131508 100644 --- a/apps/web/src/components/chat/SecretRequestCard.tsx +++ b/apps/web/src/components/chat/SecretRequestCard.tsx @@ -125,7 +125,7 @@ function PendingSecretRequestForm(props: { void send({ type: "reference", source, reference }); } }; - const draft = source === null ? secret : reference; + const draft = source === null ? secret : reference.trim(); const link = secretRequestLink(item); const describedBy = error !== null ? `${privacyId} ${errorId}` : privacyId; @@ -222,7 +222,7 @@ function PendingSecretRequestForm(props: { /> )} - diff --git a/packages/client-runtime/src/secretRequest.test.ts b/packages/client-runtime/src/secretRequest.test.ts index 547ebfe8599b5..cc9e215acd0fb 100644 --- a/packages/client-runtime/src/secretRequest.test.ts +++ b/packages/client-runtime/src/secretRequest.test.ts @@ -1,10 +1,12 @@ import { + SecretRequestAnswerInput, SecretProviderId, SecretProviderKind, SecretRequestError, ThreadId, TurnItemId, } from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; import { describe, expect, it } from "vite-plus/test"; import { @@ -16,6 +18,8 @@ import { secretRequestSources, } 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({ @@ -35,6 +39,29 @@ 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(); + expect(() => + decodeAnswerInput({ + threadId: item.threadId, + turnItemId: item.id, + answer: { type: "save", secret: "" }, + }), + ).toThrow(); + }); +}); + describe("secretRequestLink", () => { it.each([ [ diff --git a/packages/client-runtime/src/secretRequest.ts b/packages/client-runtime/src/secretRequest.ts index cd7427049f5f3..c0fa346c38719 100644 --- a/packages/client-runtime/src/secretRequest.ts +++ b/packages/client-runtime/src/secretRequest.ts @@ -209,7 +209,7 @@ export function secretRequestAnswerInput( ? null : { threadId: item.threadId, turnItemId: item.id, answer: { type: "reference", reference } }; } - 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 ccd114c397e38..b75e649862ec1 100644 --- a/packages/contracts/src/secretRequest.ts +++ b/packages/contracts/src/secretRequest.ts @@ -1,6 +1,6 @@ import * as Schema from "effect/Schema"; -import { ThreadId, TrimmedNonEmptyString, TurnItemId } from "./baseSchemas.ts"; +import { ThreadId, TurnItemId } from "./baseSchemas.ts"; import { SecretReference } from "./secretReference.ts"; /** The http(s) page a secret card may open, or null for anything else. */ @@ -19,7 +19,7 @@ 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") }), /** Read once through a configured secret provider; only servers advertising `secretRequestReferences` accept it. */ Schema.Struct({ type: Schema.Literal("reference"), reference: SecretReference }),