Skip to content

Commit 091c584

Browse files
andreaanezclaudesigmachirality
authored
fix: [PR-1680] parse correct API field names for v2/images (#251)
* fix: correct API field names and resolve is-fullwidth-code-point ESM conflict in images upload Three bugs fixed in `sf images upload`: 1. `partResponse.data.upload_url` → `.url`: the v2/images/{id}/parts endpoint returns `url`, not `upload_url` as the schema declared. 2. `sha256_hash` → `sha256`: the v2/images/{id}/complete endpoint expects the field named `sha256`, not `sha256_hash` as the schema declared. Schema updated to match both. 3. isFullwidthCodePoint runtime crash: bun's symlink layout caused Node.js (v24) to resolve `is-fullwidth-code-point` to v5.1.0 (ESM-only) from within `cli-progress`'s nested `string-width@4.2.3` (CJS). When CJS require()s an ESM module in Node 24 it gets the module namespace object, not the default export, so `isFullwidthCodePoint(code)` threw "not a function". Fixed by pinning `is-fullwidth-code-point` to 3.0.0 via package.json overrides and a clean bun install. Also improves the complete-upload error message to use `completeResponse.error` (already parsed by openapi-fetch) instead of re-reading the consumed response body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert: undo schema.ts field name changes The v2 API field names (sha256, url) differ from the generated schema (sha256_hash, upload_url). Rather than modifying the generated schema, the next commit switches sf images upload to raw fetch with hardcoded types so it is not coupled to schema.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: use raw fetch for v2/images upload to decouple from schema types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Daniel Tao <danieltaox@gmail.com>
1 parent 4650ad7 commit 091c584

3 files changed

Lines changed: 55 additions & 38 deletions

File tree

bun.lock

Lines changed: 3 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,8 @@
7575
"peerDependencies": {
7676
"typescript": "^5.6.2"
7777
},
78-
"version": "0.30.0"
78+
"version": "0.30.0",
79+
"overrides": {
80+
"is-fullwidth-code-point": "3.0.0"
81+
}
7982
}

src/lib/images/upload.ts

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import chalk from "chalk";
1010
import cliProgress from "cli-progress";
1111
import cliSpinners from "cli-spinners";
1212
import ora, { type Ora } from "ora";
13-
import { apiClient } from "../../apiClient.ts";
13+
import { getAuthToken, loadConfig } from "../../helpers/config.ts";
1414
import { logAndQuit } from "../../helpers/errors.ts";
1515

1616
async function readChunk(
@@ -75,23 +75,31 @@ const upload = new Command("upload")
7575
let progressBar: cliProgress.SingleBar | undefined;
7676

7777
try {
78-
const client = await apiClient();
78+
const config = await loadConfig();
79+
const token = await getAuthToken();
80+
const apiHeaders = {
81+
Authorization: `Bearer ${token}`,
82+
"Content-Type": "application/json",
83+
};
7984

8085
preparingSpinner = ora(`Preparing upload for ${name}...`).start();
8186

8287
// Create image via v2 API
83-
const startResponse = await client.POST("/v2/images", {
84-
body: { name },
88+
const startResponse = await fetch(`${config.api_url}/v2/images`, {
89+
method: "POST",
90+
headers: apiHeaders,
91+
body: JSON.stringify({ name }),
8592
});
8693

87-
if (!startResponse.response.ok || !startResponse.data) {
88-
const errorText = await startResponse.response.text().catch(() => "");
94+
if (!startResponse.ok) {
8995
throw new Error(
90-
`Failed to start upload: ${startResponse.response.status} ${startResponse.response.statusText}${errorText ? ` - ${errorText}` : ""}`,
96+
`Failed to start upload: ${startResponse.status} ${startResponse.statusText}`,
9197
);
9298
}
9399

94-
const imageId = startResponse.data.id;
100+
const startData: { object: "image"; id: string; upload_status: string } =
101+
await startResponse.json();
102+
const imageId = startData.id;
95103

96104
preparingSpinner.succeed(
97105
`Started upload for image ${chalk.cyan(name)} (${chalk.blackBright(
@@ -231,16 +239,17 @@ const upload = new Command("upload")
231239
}
232240

233241
// Get presigned URL via v2 API
234-
const partResponse = await client.POST("/v2/images/{id}/parts", {
235-
params: { path: { id: imageId } },
236-
body: { part_id: part },
237-
});
242+
const partResponse = await fetch(
243+
`${config.api_url}/v2/images/${imageId}/parts`,
244+
{
245+
method: "POST",
246+
headers: apiHeaders,
247+
body: JSON.stringify({ part_id: part }),
248+
},
249+
);
238250

239-
if (!partResponse.response.ok || !partResponse.data) {
240-
const status = partResponse.response.status;
241-
const errorText = await partResponse.response
242-
.text()
243-
.catch(() => "");
251+
if (!partResponse.ok) {
252+
const status = partResponse.status;
244253

245254
if (
246255
status >= 400 &&
@@ -250,18 +259,20 @@ const upload = new Command("upload")
250259
) {
251260
bail(
252261
new Error(
253-
`Failed to get upload URL for part ${part}: ${status} ${partResponse.response.statusText} - ${errorText}`,
262+
`Failed to get upload URL for part ${part}: ${status} ${partResponse.statusText}`,
254263
),
255264
);
256265
return;
257266
}
258267

259268
throw new Error(
260-
`Failed to get upload URL for part ${part}: ${status} ${partResponse.response.statusText} - ${errorText}`,
269+
`Failed to get upload URL for part ${part}: ${status} ${partResponse.statusText}`,
261270
);
262271
}
263272

264-
const url = partResponse.data.upload_url;
273+
const partData: { url: string; expires_at: string } =
274+
await partResponse.json();
275+
const url = partData.url;
265276

266277
// Read chunk from disk with progress tracking
267278
const payload = await readChunk(
@@ -353,24 +364,31 @@ const upload = new Command("upload")
353364
const sha256Hash = hash.digest("hex");
354365

355366
// Complete upload via v2 API
356-
const completeResponse = await client.POST("/v2/images/{id}/complete", {
357-
params: { path: { id: imageId } },
358-
body: { sha256_hash: sha256Hash },
359-
});
367+
const completeResponse = await fetch(
368+
`${config.api_url}/v2/images/${imageId}/complete`,
369+
{
370+
method: "POST",
371+
headers: apiHeaders,
372+
body: JSON.stringify({ sha256: sha256Hash }),
373+
},
374+
);
360375

361-
if (!completeResponse.response.ok || !completeResponse.data) {
362-
const errorText = await completeResponse.response
363-
.text()
364-
.catch(() => "");
376+
if (!completeResponse.ok) {
365377
throw new Error(
366-
`Failed to complete upload: ${completeResponse.response.status} ${completeResponse.response.statusText}${errorText ? ` - ${errorText}` : ""}`,
378+
`Failed to complete upload: ${completeResponse.status} ${completeResponse.statusText}`,
367379
);
368380
}
369381

382+
const completeData: {
383+
object: "image";
384+
upload_status: string;
385+
id: string;
386+
} = await completeResponse.json();
387+
370388
finalizingSpinner.succeed("Image uploaded and verified");
371389

372390
console.log(chalk.gray("\nNext steps:"));
373-
console.log(` sf images get ${chalk.cyan(completeResponse.data.id)}`);
391+
console.log(` sf images get ${chalk.cyan(completeData.id)}`);
374392
} catch (err) {
375393
if (spinnerTimer) {
376394
clearInterval(spinnerTimer);

0 commit comments

Comments
 (0)