Skip to content

Commit 979a6b1

Browse files
justinhelmerJustin Helmerclaude
authored
fix(auth): send a JSON body on the bind-run POST so the API edge lets it through (#83)
The API edge answers a body-less POST without a JSON content type with a bare 403 before the worker runs (nominal#1575), and `request` only sets Content-Type when a body is present. `auth bind-run` therefore failed on every real call — invisible to the fetch-mocked unit tests and swallowed by the installer's `|| true`. Send `{}`, and pin the wire shape in a test. Co-authored-by: Justin Helmer <justin@coreplane.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1a3a7b8 commit 979a6b1

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

‎src/commands/auth/bind-run.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ export async function bindOnboardingRun(
4040
process.stderr.write(`Not signed in; onboarding run ${runId} left unbound.\n`);
4141
return { bound: false, runId };
4242
}
43+
// The route takes no input, but the API edge answers a body-less POST without
44+
// a JSON content type with a bare 403 before the worker sees it (nominal#1575).
45+
// `request` sets Content-Type only when a body is present, so send `{}`.
4346
const result = await requestJson<{ bound: boolean }>(config, {
4447
method: 'POST',
4548
url: `/v1/auth/onboarding_runs/${runId}/bind`,
49+
body: {},
4650
...(apiKey ? { headers: { 'x-api-key': apiKey }, noAuth: true } : {}),
4751
});
4852
const bound = result.bound === true;

‎test/auth-bind-run.test.ts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('auth bind-run', () => {
5252
const originalStderrWrite = process.stderr.write.bind(process.stderr);
5353
let stdout = '';
5454
let stderr = '';
55-
let bindCalls: { url: string; auth: string | undefined }[] = [];
55+
let bindCalls: { url: string; auth: string | undefined; contentType: string | null; body: string }[] = [];
5656
let bindStatus = 200;
5757

5858
before(() => {
@@ -84,7 +84,12 @@ describe('auth bind-run', () => {
8484
}
8585
if (url.includes('/v1/auth/onboarding_runs/')) {
8686
const headers = new Headers(init?.headers);
87-
bindCalls.push({ url, auth: headers.get('authorization') ?? headers.get('x-api-key') ?? undefined });
87+
bindCalls.push({
88+
url,
89+
auth: headers.get('authorization') ?? headers.get('x-api-key') ?? undefined,
90+
contentType: headers.get('content-type'),
91+
body: String(init?.body ?? ''),
92+
});
8893
if (bindStatus === 401) {
8994
return Response.json(
9095
{ success: false, error: { message: 'Unauthorized', detail: 'Session expired' }, result: null },
@@ -120,6 +125,17 @@ describe('auth bind-run', () => {
120125
assert.deepEqual(JSON.parse(stdout), { bound: true, runId: ENV_RUN });
121126
});
122127

128+
// The API edge rejects a body-less POST without a JSON content type with a
129+
// bare 403 before the worker runs (nominal#1575), which a fetch mock cannot
130+
// reproduce — so pin the wire shape the edge needs.
131+
it('sends a JSON content type and an empty object body so the API edge lets it through', async () => {
132+
writeCredentialsFile();
133+
process.env.POLYLANE_ONBOARDING_RUN = ENV_RUN;
134+
await run();
135+
assert.equal(bindCalls[0]!.contentType, 'application/json');
136+
assert.equal(bindCalls[0]!.body, '{}');
137+
});
138+
123139
it('binds the file run id when the env var is unset', async () => {
124140
writeCredentialsFile();
125141
writeFileSync(RUN_FILE, `${FILE_RUN}\n`);

0 commit comments

Comments
 (0)