From 09a7c694ef3e7dc06e4ff8139c1b34303ee77d90 Mon Sep 17 00:00:00 2001 From: Ben Limmer Date: Tue, 26 May 2026 10:50:03 -0600 Subject: [PATCH] fix: send owner and email with report uploads The presign endpoint dropped the legacy identifier/kind fields in favor of separate owner and email, and now rejects unknown keys (.strict()). Update the client to match: thread the scanned target through as owner, rename identifier to email everywhere, and stop sending kind:'html'. Without this, uploads from a fresh binary would 400 against the updated contract. See contextbridge/patchwave#8 for the server-side change. --- src/interactive/sharePrompt.test.ts | 7 ++++--- src/interactive/sharePrompt.ts | 19 ++++++++++--------- src/upload/Uploader.test.ts | 7 +++---- src/upload/Uploader.ts | 7 ++++--- src/upload/testFactories.ts | 3 ++- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/interactive/sharePrompt.test.ts b/src/interactive/sharePrompt.test.ts index 7d6c13a..c4e2e28 100644 --- a/src/interactive/sharePrompt.test.ts +++ b/src/interactive/sharePrompt.test.ts @@ -35,16 +35,17 @@ describe('runSharePrompt', () => { expect(handle.analytics.capturedEvents('share_choice')[0]?.properties).toMatchObject({ choice: 'declined' }); }); - test('html-only: uploads the raw html bytes with kind:html', async () => { + test('html-only: uploads the raw html bytes with owner and email', async () => { const handle = fakeContextHandle.build(); handle.prompter.scriptSelect('html').scriptText('ben@example.com'); const outcome = await runSharePrompt(sharePromptInputsFor(handle)); - expect(outcome).toMatchObject({ kind: 'shared', identifier: 'ben@example.com' }); + expect(outcome).toMatchObject({ kind: 'shared', email: 'ben@example.com' }); expect(handle.uploader.calls).toHaveLength(1); expect(handle.uploader.calls[0]).toMatchObject({ - identifier: 'ben@example.com', + owner: 'acme', + email: 'ben@example.com', appVersion: '0.0.1', timestamp: '2026-05-22T12:00:00Z', }); diff --git a/src/interactive/sharePrompt.ts b/src/interactive/sharePrompt.ts index bc424d8..d19e1cd 100644 --- a/src/interactive/sharePrompt.ts +++ b/src/interactive/sharePrompt.ts @@ -14,7 +14,7 @@ export interface SharePromptInputs { } export type ShareOutcome = - | { kind: 'shared'; uploadId: string; identifier: string } + | { kind: 'shared'; uploadId: string; email: string } | { kind: 'declined' } | { kind: 'cancelled' } | { kind: 'upload-failed'; message: string }; @@ -51,18 +51,19 @@ export async function runSharePrompt(inputs: SharePromptInputs): Promise { +async function askForEmail(prompter: Prompter): Promise<{ kind: 'ok'; email: string } | { kind: 'cancelled' }> { const result = await prompter.text({ message: 'Email:', placeholder: 'you@example.com', @@ -120,5 +121,5 @@ async function askForEmail(prompter: Prompter): Promise<{ kind: 'ok'; identifier } const trimmed = result.value.trim(); - return { kind: 'ok', identifier: trimmed }; + return { kind: 'ok', email: trimmed }; } diff --git a/src/upload/Uploader.test.ts b/src/upload/Uploader.test.ts index 109f216..cf15363 100644 --- a/src/upload/Uploader.test.ts +++ b/src/upload/Uploader.test.ts @@ -26,7 +26,7 @@ function presignResponse() { } describe('UploaderImpl', () => { - test("html kind: posts kind:'html' and PUTs raw bytes with text/html", async () => { + test('posts owner/email metadata and PUTs raw html bytes with text/html', async () => { const { fetch, calls } = recordFetch([presignResponse(), new Response('', { status: 200 })]); const bytes = htmlBytes.build(); @@ -39,13 +39,12 @@ describe('UploaderImpl', () => { expect(calls[0]?.init?.method).toBe('POST'); const postBody = JSON.parse(calls[0]?.init?.body as string) as Record; expect(postBody).toMatchObject({ - identifier: 'ben@example.com', + owner: 'acme', + email: 'ben@example.com', appVersion: '0.0.1', timestamp: '2026-05-22T12:00:00Z', - kind: 'html', sizeBytes: bytes.byteLength, }); - expect(postBody).not.toHaveProperty('contentType'); expect(calls[1]?.url).toBe(presignResponseBody.build().presignedUrl); expect(calls[1]?.init?.method).toBe('PUT'); diff --git a/src/upload/Uploader.ts b/src/upload/Uploader.ts index 85611fa..2fe0ca1 100644 --- a/src/upload/Uploader.ts +++ b/src/upload/Uploader.ts @@ -12,7 +12,8 @@ export type UploadError = export interface UploadInput { readonly bytes: Uint8Array; - readonly identifier: string; + readonly owner: string; + readonly email: string; readonly appVersion: string; readonly timestamp: string; } @@ -55,10 +56,10 @@ export class UploaderImpl implements Uploader { #requestPresign(input: UploadInput): ResultAsync { const body = JSON.stringify({ - identifier: input.identifier, + owner: input.owner, + email: input.email, appVersion: input.appVersion, timestamp: input.timestamp, - kind: 'html', sizeBytes: input.bytes.byteLength, }); return ResultAsync.fromPromise( diff --git a/src/upload/testFactories.ts b/src/upload/testFactories.ts index 4ec0058..5201d89 100644 --- a/src/upload/testFactories.ts +++ b/src/upload/testFactories.ts @@ -5,7 +5,8 @@ export const htmlBytes = Factory.define(() => new TextEncoder().enco export const uploadInput = Factory.define(() => ({ bytes: htmlBytes.build(), - identifier: 'ben@example.com', + owner: 'acme', + email: 'ben@example.com', appVersion: '0.0.1', timestamp: '2026-05-22T12:00:00Z', }));