Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b95f986a36 |
No files matched your search
@@ -31,7 +31,8 @@ again on the next run. Every download is decrypted on its way to disk.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The commands below need `git`, Node 22 or later and `yarn`.
|
||||
The commands below need `git`, `yarn` and Node 22.13 or later. Node 23 needs
|
||||
23.5 or later.
|
||||
|
||||
```bash
|
||||
git clone https://git.eeqj.de/sneak/quak.git
|
||||
@@ -69,11 +70,26 @@ The library keeps a local copy of the account's album and file lists, the cache.
|
||||
It brings the cache up to date with the server in rounds called refreshes.
|
||||
|
||||
quak is not published to a package registry: `package.json` marks it `private`,
|
||||
and `dist/` is not committed. Clone and build it as above, then add the clone to
|
||||
your own program's `package.json` by its path, for example
|
||||
`yarn add file:../quak`. The package's `main`, `types` and `exports` fields
|
||||
point into `dist/`, so the clone must be built first. The package is an ES
|
||||
module only, so a CommonJS program cannot `require` it.
|
||||
and `dist/` is not committed. Clone and build it as above, then link the clone
|
||||
into your own program:
|
||||
|
||||
```bash
|
||||
# In the quak clone:
|
||||
yarn link
|
||||
|
||||
# In your program's directory:
|
||||
yarn link quak
|
||||
```
|
||||
|
||||
A linked quak uses the dependencies that `yarn install` put in the clone, at the
|
||||
versions in quak's `yarn.lock`. Avoid `yarn add file:../quak`: it resolves
|
||||
quak's dependencies again, and their newer releases can need a newer Node.
|
||||
`yarn link quak` does not add quak to your `package.json`, so run it again in
|
||||
each new checkout of your program.
|
||||
|
||||
The package's `main`, `types` and `exports` fields point into `dist/`, so the
|
||||
clone must be built first. The package is an ES module only, so a CommonJS
|
||||
program cannot `require` it.
|
||||
|
||||
```ts
|
||||
import { Client, Library } from "quak";
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": "^22.13.0 || >=23.5.0"
|
||||
},
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
|
||||
+1
-8
@@ -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
@@ -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
@@ -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
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user