Author SHA1 Message Date
sneak 9c4790d969 TODO.md: open work only (closes #235)
check / check (push) Successful in 3m25s
Removes the Completed Steps section: dated accounts of finished work,
review and merge history, and notes on past decisions. Git keeps the
history. Status, Next Step and Future Steps are unchanged.

The Workflow step "move Next Step to the top of Completed Steps" now
reads "delete Next Step from `TODO.md`", the standard wording. The
`README.md` TODO section no longer calls `TODO.md` the work log; it
says the file holds the next step and the steps planned.

Model: opus-5-5
2026-11-11 13:27:23 +00:00
5 changed files with 9 additions and 40 deletions

No files matched your search

+1 -8
View File
@@ -354,13 +354,7 @@ export class ApiClient {
});
}
// `contentMD5` is the value given to `getUploadURL`. The server signs it
// into the URL, and storage refuses a PUT without that `Content-MD5`.
async putFile(
presignedURL: string,
data: Uint8Array,
contentMD5: string,
): Promise<void> {
async putFile(presignedURL: string, data: Uint8Array): Promise<void> {
// Idempotent despite being a write: a presigned PUT stores one whole
// object at one key in one request, so replaying it either overwrites
// the same bytes or lands them for the first time. There is no partial
@@ -376,7 +370,6 @@ export class ApiClient {
headers: {
"Content-Type": "application/octet-stream",
"Content-Length": String(data.length),
"Content-MD5": contentMD5,
},
body: data,
signal: AbortSignal.timeout(this.requestTimeoutMs),
+1 -1
View File
@@ -325,7 +325,7 @@ export const fixMissingThumbnails = async (
ciphertext.length,
md5,
);
await api.putFile(url, ciphertext, md5);
await api.putFile(url, ciphertext);
await api.updateThumbnail(file.id, objectKey, toBase64(header));
log(`[${collectionName}] Thumbnail uploaded for ${title}`);
+4 -10
View File
@@ -780,7 +780,7 @@ describe("ApiClient timeouts", () => {
await client.getJSON("/a");
await client.postJSON("/b", {});
await client.putFile("https://s3.example/x", new Uint8Array([1]), "m");
await client.putFile("https://s3.example/x", new Uint8Array([1]));
await client.putJSON("/c", {});
for (const call of calls) {
@@ -825,8 +825,7 @@ describe("ApiClient timeouts", () => {
[
"putFile",
() => new Response(null, { status: 200 }),
(c) =>
c.putFile("https://s3.example/x", new Uint8Array([1]), "m"),
(c) => c.putFile("https://s3.example/x", new Uint8Array([1])),
],
[
"getFileStream",
@@ -1050,7 +1049,7 @@ describe("ApiClient error typing", () => {
const client = new ApiClient({ fetch, retry: noWait });
const err: unknown = await client
.putFile("https://s3.example/obj", new Uint8Array([1, 2]), "m")
.putFile("https://s3.example/obj", new Uint8Array([1, 2]))
.catch((e: unknown) => e);
expect(err).toBeInstanceOf(ApiError);
@@ -1068,11 +1067,7 @@ describe("ApiClient error typing", () => {
);
const client = new ApiClient({ fetch, retry: noWait });
await client.putFile(
"https://s3.example/obj",
new Uint8Array([1, 2]),
"m",
);
await client.putFile("https://s3.example/obj", new Uint8Array([1, 2]));
expect(calls).toHaveLength(2);
});
@@ -1282,7 +1277,6 @@ describe("ApiClient onRequest", () => {
await client.putFile(
"https://s3.example/obj?X-Amz-Signature=abc",
new Uint8Array([1, 2]),
"m",
);
expect(requests).toEqual([
+2 -13
View File
@@ -80,20 +80,13 @@ describe("ApiClient.putFile", () => {
const client = new ApiClient({ fetch, authToken: "secret-tok" });
const data = new Uint8Array([1, 2, 3, 4, 5]);
await client.putFile(
"https://s3.example.com/presigned?sig=abc",
data,
"md5base64==",
);
await client.putFile("https://s3.example.com/presigned?sig=abc", data);
expect(calls[0]!.url).toBe("https://s3.example.com/presigned?sig=abc");
expect(calls[0]!.init?.method).toBe("PUT");
const headers = new Headers(calls[0]!.init?.headers as HeadersInit);
expect(headers.get("Content-Type")).toBe("application/octet-stream");
expect(headers.get("Content-Length")).toBe("5");
// The server signs the MD5 into the URL; storage refuses the PUT
// without it.
expect(headers.get("Content-MD5")).toBe("md5base64==");
// Must NOT leak auth headers to S3
expect(headers.has("X-Auth-Token")).toBe(false);
expect(headers.has("X-Client-Package")).toBe(false);
@@ -106,11 +99,7 @@ describe("ApiClient.putFile", () => {
const client = new ApiClient({ fetch });
await expect(
client.putFile(
"https://s3.example.com/bad",
new Uint8Array(10),
"md5base64==",
),
client.putFile("https://s3.example.com/bad", new Uint8Array(10)),
).rejects.toThrow(/403/);
});
});
+1 -8
View File
@@ -273,7 +273,6 @@ const buildThumbMock = async (opts?: {
const buildThumbFetch = (m: ThumbMockState) => {
let srpServer: SrpServer;
let signedMD5: string | undefined;
return (async (
input: RequestInfo | URL,
@@ -366,9 +365,8 @@ const buildThumbFetch = (m: ThumbMockState) => {
return new Response("not found", { status: 404 });
}
// Upload URL minting. The real server signs the MD5 into the URL.
// Upload URL minting
if (path === "/files/upload-url") {
signedMD5 = JSON.parse(init?.body as string).contentMD5;
return json({
objectKey: `42/thumb-${Date.now()}`,
url: "https://s3.mock.test/presigned-put",
@@ -377,11 +375,6 @@ const buildThumbFetch = (m: ThumbMockState) => {
// Presigned PUT (S3 upload)
if (url.startsWith("https://s3.mock.test/")) {
// Storage refuses a PUT that lacks the signed `Content-MD5`.
const headers = new Headers(init?.headers);
if (headers.get("Content-MD5") !== signedMD5) {
return new Response("AccessDenied", { status: 400 });
}
const body = init?.body;
// Store the uploaded bytes for later inspection
if (body instanceof Uint8Array) {