Skip to content

Commit 09703fb

Browse files
committed
Allow larger Worker source reads after secret changes, closes #459.
1 parent 1781bde commit 09703fb

7 files changed

Lines changed: 122 additions & 51 deletions

‎packages/void/src/cli/cf-command-values.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Leave room for a 64 MiB Worker, base64 expansion, and JSON metadata.
2+
export const CF_VERSION_SOURCE_MAX_OUTPUT_BYTES: number = 128 * 1024 * 1024;
3+
14
/** Accept a cf positional only when it cannot be read as a flag or carry control characters. */
25
export function safePositional(value: unknown): value is string {
36
return (

‎packages/void/src/cli/cf-version-runner.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import type {
1212
import { isRecord as record } from '@void/deploy-core';
1313
import { dirname, join } from 'pathslash';
1414
import type { CfBuildOutput } from './cf-build-output.ts';
15-
import { readCfUploadOutput, safePositional } from './cf-command-values.ts';
15+
import {
16+
CF_VERSION_SOURCE_MAX_OUTPUT_BYTES,
17+
readCfUploadOutput,
18+
safePositional,
19+
} from './cf-command-values.ts';
1620
import { migrateBuiltWranglerConfig } from './cf-config-migrate.ts';
1721
import { getCfCliVersion } from './cf-user-agent.ts';
1822
import {
@@ -577,7 +581,7 @@ export function createCfVersionRunner(options: {
577581
cwd: output.directory,
578582
capture: true,
579583
timeoutMs: 30_000,
580-
maxOutputBytes: 1024 * 1024,
584+
maxOutputBytes: CF_VERSION_SOURCE_MAX_OUTPUT_BYTES,
581585
})
582586
: await run(args, true);
583587
const result = { ...called, data: called.data ?? parseJson(called.stdout) };

‎packages/void/src/cli/cloudflare-operation-process.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import type {
1414
} from '@void/deploy-cloudflare';
1515
import { parse } from 'jsonc-parser';
1616
import { dirname, join, resolve } from 'pathslash';
17-
import { isD1Bookmark, readCfUploadOutput, safePositional } from './cf-command-values.ts';
17+
import {
18+
CF_VERSION_SOURCE_MAX_OUTPUT_BYTES,
19+
isD1Bookmark,
20+
readCfUploadOutput,
21+
safePositional,
22+
} from './cf-command-values.ts';
1823
import { persistCfKeyringPreference, readCfSessionCredential } from './cf-session.ts';
1924
import { getCfCliVersion, getVoidUserAgent } from './cf-user-agent.ts';
2025
import { publishCloudflareObservability } from './cf-worker-settings.ts';
@@ -962,7 +967,9 @@ async function workerHistory(): Promise<Result> {
962967
...(operation.includeSource ? ['--include', 'modules'] : []),
963968
],
964969
process.cwd(),
965-
operation.includeSource ? { timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 } : undefined,
970+
operation.includeSource
971+
? { timeoutMs: 30_000, maxOutputBytes: CF_VERSION_SOURCE_MAX_OUTPUT_BYTES }
972+
: undefined,
966973
);
967974
if (called.status !== 0) {
968975
return called;

‎packages/void/src/cli/cloudflare-process.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { spawnSync } from 'node:child_process';
22
import type { CloudflareCommandResult, CloudflareOperation } from '@void/deploy-cloudflare';
3+
import { CF_VERSION_SOURCE_MAX_OUTPUT_BYTES } from './cf-command-values.ts';
34
import { resolveCloudflareOperationArgv } from './preset.ts';
45

56
/** Operations contain public targets only. Secret values travel through stdin. */
@@ -133,7 +134,10 @@ export function createCloudflareProcessRunner(options: {
133134
encoding: 'utf8',
134135
shell: false,
135136
windowsHide: true,
136-
maxBuffer: 32 * 1024 * 1024,
137+
maxBuffer:
138+
operation.type === 'versions.view' && operation.includeSource
139+
? CF_VERSION_SOURCE_MAX_OUTPUT_BYTES
140+
: 32 * 1024 * 1024,
137141
timeout: options.timeoutMs,
138142
stdio: streaming ? ['inherit', 'inherit', 'inherit'] : ['pipe', 'pipe', 'pipe'],
139143
});

‎packages/void/test/unit/cf-version-runner.test.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,16 @@ function pinnedCf(apiBaseUrl: string, accountId = 'account-id', env: NodeJS.Proc
100100
});
101101
}
102102

103-
it(
104-
'reads the exact immutable SDK draft source through the real pinned cf HTTP transport',
105-
async () => {
103+
it.each([false, true])(
104+
'reads immutable version source through the real pinned cf HTTP transport (large=%s)',
105+
async (large) => {
106106
const prepared = await output();
107107
const draft = voidSecretDraftVersion();
108+
if (large) {
109+
draft.modules[0]!.content_base64 = Buffer.from(
110+
`/* ${'x'.repeat(1024 * 1024)} */\nexport default { fetch() { return new Response("ok"); } };`,
111+
).toString('base64');
112+
}
108113
const requests: Array<string> = [];
109114
const userAgents: Array<string | undefined> = [];
110115
const apiBaseUrl = await listen(
@@ -141,14 +146,18 @@ it(
141146
},
142147
{ capture: true },
143148
);
144-
expect(result.status).toBe(0);
145-
expect(isVoidSecretOnlyDraft(result.data as Record<string, unknown>)).toBe(true);
149+
expect(result.status, result.stderr).toBe(0);
150+
expect(result.data).toMatchObject(draft);
151+
expect(isVoidSecretOnlyDraft(result.data as Record<string, unknown>)).toBe(!large);
146152
expect(requests).toEqual([
147153
`/client/v4/accounts/test-account/workers/workers/void-test/versions/${draft.id}?include=modules`,
148154
]);
149155
expect(commands).toHaveLength(1);
150156
expect(userAgents).toEqual([cfUserAgent]);
151-
expect(commands[0]!.bounds).toMatchObject({ timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 });
157+
expect(commands[0]!.bounds).toMatchObject({
158+
timeoutMs: 30_000,
159+
maxOutputBytes: 128 * 1024 * 1024,
160+
});
152161
expect(JSON.stringify(result)).not.toContain('synthetic-token');
153162
} finally {
154163
await prepared.dispose();

‎packages/void/test/unit/cloudflare-operation-process.test.ts‎

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function runOperation(
9898
execFile(
9999
command!,
100100
[...(preload ? ['--import', pathToFileURL(preload).href] : []), ...prefix, ...args],
101-
{ cwd, encoding: 'utf8', timeout: 20_000, env },
101+
{ cwd, encoding: 'utf8', timeout: 20_000, maxBuffer: 128 * 1024 * 1024, env },
102102
(error, stdout, stderr) =>
103103
resolve({ status: error ? Number(error.code) || 1 : 0, stdout, stderr }),
104104
);
@@ -166,43 +166,56 @@ it.each([true, false])(
166166
15_000,
167167
);
168168

169-
it('requests immutable version source explicitly without loading or provisioning build output', async () => {
170-
const directory = temporaryDirectory('void-cf-secret-source-');
171-
const draft = voidSecretDraftVersion();
172-
const requests: Array<string> = [];
173-
const userAgents: Array<string | undefined> = [];
174-
const api = await listen(
175-
createServer((request, response) => {
176-
requests.push(request.url ?? '');
177-
userAgents.push(request.headers['user-agent']);
178-
request.resume();
179-
response.writeHead(200, { 'Content-Type': 'application/json' });
180-
response.end(JSON.stringify({ success: true, errors: [], messages: [], result: draft }));
181-
}),
182-
);
183-
const configPath = join(directory, 'wrangler.json');
184-
writeFileSync(configPath, JSON.stringify({ name: 'test-worker', account_id: 'test-account' }));
185-
const output = await runOperation(
186-
directory,
187-
{
188-
type: 'versions.view',
189-
versionId: draft.id,
190-
includeSource: true,
191-
configPath,
192-
workerName: 'test-worker',
193-
},
194-
{ ...tokenEnv(api), CLI_VERSION: 'inherited-version void/stale' },
195-
);
196-
expect(output.status, output.stderr).toBe(0);
197-
const result = decodeCloudflareOperationResult({ status: 0, stdout: output.stdout, stderr: '' });
198-
expect(result.status).toBe(0);
199-
expect(isVoidSecretOnlyDraft(result.data as Record<string, unknown>)).toBe(true);
200-
expect(requests).toEqual([
201-
`/client/v4/accounts/test-account/workers/workers/test-worker/versions/${draft.id}?include=modules`,
202-
]);
203-
expect(output.stdout).not.toContain('synthetic-token');
204-
expect(userAgents).toEqual([`cf-cli/${cfVersion} void/${voidVersion}`]);
205-
});
169+
it.each([false, true])(
170+
'requests immutable version source without provisioning (large=%s)',
171+
async (large) => {
172+
const directory = temporaryDirectory('void-cf-secret-source-');
173+
const draft = voidSecretDraftVersion();
174+
if (large) {
175+
draft.modules[0]!.content_base64 = Buffer.from(
176+
`/* ${'x'.repeat(1024 * 1024)} */\nexport default { fetch() { return new Response("ok"); } };`,
177+
).toString('base64');
178+
}
179+
const requests: Array<string> = [];
180+
const userAgents: Array<string | undefined> = [];
181+
const api = await listen(
182+
createServer((request, response) => {
183+
requests.push(request.url ?? '');
184+
userAgents.push(request.headers['user-agent']);
185+
request.resume();
186+
response.writeHead(200, { 'Content-Type': 'application/json' });
187+
response.end(JSON.stringify({ success: true, errors: [], messages: [], result: draft }));
188+
}),
189+
);
190+
const configPath = join(directory, 'wrangler.json');
191+
writeFileSync(configPath, JSON.stringify({ name: 'test-worker', account_id: 'test-account' }));
192+
const output = await runOperation(
193+
directory,
194+
{
195+
type: 'versions.view',
196+
versionId: draft.id,
197+
includeSource: true,
198+
configPath,
199+
workerName: 'test-worker',
200+
},
201+
{ ...tokenEnv(api), CLI_VERSION: 'inherited-version void/stale' },
202+
);
203+
expect(output.status, output.stderr).toBe(0);
204+
const result = decodeCloudflareOperationResult({
205+
status: 0,
206+
stdout: output.stdout,
207+
stderr: '',
208+
});
209+
expect(result.status, result.stderr).toBe(0);
210+
expect(result.data).toEqual(draft);
211+
expect(isVoidSecretOnlyDraft(result.data as Record<string, unknown>)).toBe(!large);
212+
expect(requests).toEqual([
213+
`/client/v4/accounts/test-account/workers/workers/test-worker/versions/${draft.id}?include=modules`,
214+
]);
215+
expect(output.stdout).not.toContain('synthetic-token');
216+
expect(userAgents).toEqual([`cf-cli/${cfVersion} void/${voidVersion}`]);
217+
},
218+
);
206219

207220
it.each([200, 503])(
208221
'reports HTTP %i without loading the build toolchain when domain cleanup returns non-JSON',

‎packages/void/test/unit/cloudflare-process.test.ts‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { describe, expect, it } from 'vite-plus/test';
2-
import { decodeCloudflareOperationResult } from '../../src/cli/cloudflare-process.ts';
1+
import childProcess from 'node:child_process';
2+
import { syncBuiltinESMExports } from 'node:module';
3+
import { describe, expect, it, vi } from 'vite-plus/test';
4+
import {
5+
createCloudflareProcessRunner,
6+
decodeCloudflareOperationResult,
7+
} from '../../src/cli/cloudflare-process.ts';
38

49
const processResult = (envelope: unknown) => ({
510
status: 0,
@@ -8,6 +13,32 @@ const processResult = (envelope: unknown) => ({
813
});
914

1015
describe('typed Cloudflare process protocol', () => {
16+
it('allows a full-size base64 Worker source through the parent capture without widening other reads', () => {
17+
const spawn = vi.spyOn(childProcess, 'spawnSync').mockReturnValue({
18+
pid: 1,
19+
output: [],
20+
status: 0,
21+
signal: null,
22+
stdout: JSON.stringify({ operationResult: 1, data: { id: 'version-id' } }),
23+
stderr: '',
24+
});
25+
syncBuiltinESMExports();
26+
try {
27+
const runner = createCloudflareProcessRunner({ root: process.cwd() });
28+
const read = { type: 'versions.view', versionId: 'version-id', workerName: 'app' } as const;
29+
expect(runner({ ...read, includeSource: true }, { capture: true }).status).toBe(0);
30+
// Workers allow 64 MiB of source, which grows by a third when base64-encoded.
31+
const encodedWorkerBytes = Math.ceil((64 * 1024 * 1024 * 4) / 3);
32+
expect(spawn.mock.calls[0]![2]!.maxBuffer).toBeGreaterThan(encodedWorkerBytes);
33+
34+
expect(runner(read, { capture: true }).status).toBe(0);
35+
expect(spawn.mock.calls[1]![2]!.maxBuffer).toBe(32 * 1024 * 1024);
36+
} finally {
37+
spawn.mockRestore();
38+
syncBuiltinESMExports();
39+
}
40+
});
41+
1142
it('keeps structured data separate from human output', () => {
1243
expect(
1344
decodeCloudflareOperationResult(

0 commit comments

Comments
 (0)