check / check (push) Successful in 26s
Within a collection's folder, files whose sanitized titles match (ignoring case) each get their file ID added before the extension, and collections whose sanitized names match get their ID added, so no symlink or JSON replaces another. Names are chosen across all collections, so a scoped run names folders the same as a full one. Each run first removes symlinks into originals/ that no longer belong to a collection, and the folders quak wrote (a sibling JSON with an album ID) for collections that are gone or renamed. Anything else is left alone; a folder still holding user files keeps its JSON. Model: opus-5-5
15 KiB
15 KiB
Workflow
- branch (from
main) - do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (
TODO.mdchanges in the same commit as the work) - merge to
mainif the branch is not protected, otherwise open a PR - push
Status
pre-1.0
Next Step
Tag v1.0.0.
Completed Steps
- 2026-09-23: Fixed the backup's per-collection folders (issue 103). Two files
in one collection with the same title, and two collections with the same name,
each get their ID added to the name (
IMG_0001 (12345).JPG,Trip (10)/), so none replaces another's symlink or JSON. Each run removes symlinks intooriginals/for files no longer in the collection, and the folders of deleted or renamed collections, leaving anything else incollections/alone. The README backup layout states the naming rule. - 2026-09-23: Single-sourced the version string (issue 5).
package.jsonis the only place it is written:src/index.tsimports it forVERSIONandbin/quak.tspassesVERSIONto commander. tsc copiespackage.jsontodist/package.json, so the import resolves from the built output too, andscript/buildruns the built CLI with--versionto prove it. A test checks thatVERSIONandquak --versionboth equal thepackage.jsonversion. - 2026-09-23: Tested that
Library.close()waits for the originals precache (issue 93). The test that holds a precache fetch open whileclose()runs now runs once with only the thumbnail fill and once with only the originals fill, so dropping either wait fromPrecache.close()fails a test. - 2026-09-23: Fixed two intermittently failing library tests (issue 90).
Library.close()now returns a promise that resolves once an in-flight refresh (including its cache write), the ML data fetch and running precache sweeps have finished; the library tests await it, soafterEachno longer removes the cache directory while something is still writing into it. The precache test waits for both fills to report "done" instead of for its stub source to be called, which happened before the cache recorded the file. - 2026-09-23: Pinned three guards reviewers found untested (issue 89). The
download idle deadline's timer is unref'd, so it can never keep the process
alive, and a test checks no timer is left after a download completes or fails.
A test covers the rejection of
#in a request path. The EXIF scan compares theExifheader only in an APP1 segment of length 8 or more, so it never reads the next segment's bytes, with a test for a short one. - 2026-09-23: Made the download deadline an idle deadline (issue 24).
downloadTimeoutMsnow aborts a file or thumbnail download only after no bytes have arrived for that long, default 60 seconds, instead of bounding the whole transfer at 10 minutes, so a slow download that keeps making progress completes. A download that fails before reading the whole body cancels it, so a failed file no longer holds its connection. - 2026-09-23: Every
ApiClientrequest URL is now built by one function next to the class (issue 18), so a self-hostedapiOriginwith a base path keeps it on every request, a path works with or without a leading slash, and query parameters are percent-encoded. A path containing?or#is rejected with an error instead of being silently cut. - 2026-09-23: Made the CLI testable and tested it (issue 12). The command bodies
moved from
bin/quak.tsintosrc/cli-commands.tsas functions that take their options and a context (output streams, session directory, cache directory, session loader) and return an exit code;bin/quak.tsonly wires them to commander and exits with the code once stdout and stderr have drained, so nothing below it callsprocess.exit.test/cli/commands.test.tsdrives them with a fake client: session file modes, logout, the missing and corrupt session paths, and the output and exit code ofwhoami,collections,files,get,get-thumb,backupandhelper list-missing-thumbnails. - 2026-09-23: Hardened the backup tree's atomic copy (issue 22).
copyAtomicfsyncs its temp file before the rename and the directory after it, through the download writer'sfsyncPath; each backup run deletes.quak-backup-*.tmpfiles whose process is no longer running. The README backup layout names the temp files and states that the rename replaces a symlink and takes the temp file's permissions. Added tests for a missing and an unwritable destination directory fordownloadFileanddownloadThumbnail. - 2026-09-23: Hardened the JPEG EXIF scan behind
backup-metadata --exif(issue 11). Every segment length is checked against the remaining bytes and lengths under 2 stop the scan, so a truncated or corrupt original can neither throw nor loop. A malformed or unparseable EXIF segment is recorded asimageMetadata.exifError, and a failure to read the original asimageMetadataErrorin the per-file JSON, instead of the field being left out. - 2026-09-22: Hardened the retry classifier (issue 80). A
POSTorPUTis replayed only when every errno in the cause chain is a connect errno, and it no longer follows redirects.getRetryOptions()returns a copy. Tests pin every errno the classifier names, the cause-chain depth limit, cycle termination, and a fresh deadline per attempt for every retrying entry point. The README's endpoint list is the one place that names the requests the replay rule covers. - 2026-09-22: Stopped
make testcollecting tests from checkouts nested under.claude/(issue 25). vitest ignores.gitignorewhen finding tests, so a nested checkout ran the whole suite again;vitest.config.tsnow adds.claude/**to vitest's default excludes, andtest/packaging/nested-checkout.test.tsplants a nested checkout in a temp directory and fails if vitest would collect it. - 2026-09-22: Dropped the deprecated
@types/libsodium-wrappers-sumostub fromdevDependencies(issue 27). It shipped no declarations; the types come fromlibsodium-wrappers-sumoitself.yarn.lockregenerated byyarn remove. - 2026-09-22: Hardened the client session lifecycle (issue 10).
Client.fromJSONchecks every snapshot field and each key's decoded length and names the bad field;toJSONreads the token throughApiClient.getAuthTokenand throws when there is none;logoutzeroes the key buffers, andcollectionsSincere-checks for logout after its request so it never decrypts with zeroed keys. The CLI reports a corrupt session file separately from a missing one (src/cli-session.ts). - 2026-09-22: Sanitized file names taken from server metadata (issue 9). A new
src/filename.tsholds the one sanitizer, used byquak get/get-thumbwithout--out,downloadFile/downloadThumbnailwithoutoutPath, and the backup and metadata backup trees; it removes separators, control characters, leading dots and Windows device names, and falls back to a name built from the ID for an empty title. Originals-cache extensions are letters and digits only, else.bin. A user-supplied path is used as is.decryptFilereads a missing or non-string title as "" and rejects metadata that is not a JSON object. - 2026-09-22: Rewrote the README API reference (and the Getting Started / usage
snippets) to match the shipped cache/API library on
next(issue 53, issue 13). DocumentedLibrary.openand its options, the default-read vsfresh()distinction (and that the CLI's read commands are fresh), the record types andsnapshot()/subscribe(), thealbums/photos/timelineread surface,Photocontent methods,thumbnails.ensure, themldatasearch surface,backup(), the three request pools (10/5/25), and the on-disk cache layout; noted the deferred content-hash integrity check (issue 68). Docs-only; no code changed. - 2026-09-22: Added resumable, deletion-aware enumeration to
Client(issue 38, closes issue 7).collectionsSince/filesSincetake a starting cursor, decrypt live records, surface tombstoned ids in a separatedeletedlist (a tombstone has nothing to decrypt, so it is a bare id, not a hollow record), and return the maxupdationTimeseen as the cursor to resume from.filesSincerefuses to loop when the diff reportshasMorewithout advancing the cursor (issue 7).listCollections/listFilesare now thin wrappers that enumerate fromsinceTime: 0and drop deletions, so existing callers are unaffected. - 2026-09-22: Carried file size, thumbnail size, and the deletion flag through
decryptFile(issue 37, foundation for the cache/API design). Live files now populatefile.size/thumbnail.sizefrom the server'sinfo(leftundefinedwhen the server omits it), andisDeletedis carried from the diff row ontoEnteFile. No caller change:listFilesstill filters deleted rows before decrypting. Surfacing a tombstone through decryption belongs to the enumeration unit (issue 38). - 2026-08-10: Made
lint-once.test.tsenforce what its header claims. It walkedmake checkonly, so it never readDockerfile— the image CI builds throughscript/cibuild— and a secondprettier --check .could be added there with the suite staying green. The walk now also starts at.gitea/workflows/check.ymland follows itsrun:steps, so the graph under test is the one CI executes rather than the one someone assumed it executes. The lockfile assertion was a substring check against the whole ofscript/bootstrap, which has two install sites and so reported the branch the containers never take; the two branches are now resolved separately and everyyarn installin each is required to be--frozen-lockfile. Prettier is counted per occurrence instead of per line, so two invocations chained with&&no longer read as one, and edges are followed on counted lines instead of being skipped. Every way for the walk to reach nothing — an unknown target, an unknown script, a missing file, a node with no commands, an unknown node kind — is a thrown error rather than a quiet zero. Every assertion in the file was mutation-tested individually. - 2026-08-10: Stopped
make checkrunningprettier --check .twice. Since linting moved into Docker, the duplicate was one container pass and one host pass of the same check:script/lintbuildsDockerfile.lint, which runs prettier as a build step, andscript/checkthen calledscript/fmt-checkas well. The host call is gone fromscript/checkand fromscript/precommit; the container keeps checking formatting, because a successfulDockerfile.lintbuild is what CI treats as proof of a clean tree, and it is also what still fails the pre-commit hook on a badly formatted tree.script/fmt-checksurvives as a standalone entrypoint, whose verdict cannot drift from the container's. A test walks the invocation graph from each entrypoint — through the Makefile shims, thescript/calls and thedocker build— and asserts the prettier count, so the duplication cannot come back unnoticed. - 2026-08-10: Moved all linting into Docker.
script/lintbuilds a new rootDockerfile.lint, which copies the repo into the digest-pinned node image and runs eslint and prettier as build steps, so a successful build is a clean lint; no host lint path remains andyarn lintis gone frompackage.json. A fail-closedLINT_EPOCHguard stops Docker serving the linter layers from cache, which is how a lint build returns success in under a second having linted nothing. The lint stage insideDockerfileand itsCOPY --from=lintordering hack are gone: that image now runsmake testandmake buildonly, becausescript/checkcallsscript/lintand running it in a container would mean docker inside docker.script/cibuildbuilds the lint image first, then the test and build image. - 2026-08-09: Made
make dockergreen and policy-conformant. Multi-stage Dockerfile: a lint stage runsmake fmt-checkandmake lint, and the check stage takes aCOPY --from=lintdependency on it before runningmake checkandmake build.CHECK_EPOCHand a fail-closed guard stop Docker serving those two layers from cache, which is what let a build report success without running the suite.script/projectnamesaysquak, so the image is taggedquak;script/bootstrapupdates apt lists before installing, so a Debian base works;.dockerignoreno longer ships the compiled binary, the caches or agent worktrees into the build context, and keeps.gitignorein it for prettier. - 2026-08-09: Fixed the TypeScript build.
rootDiris the repo root, sobin/compiles alongsidesrc/instead of failing with TS6059; output isdist/src/anddist/bin/, which is wheremain,typesandbin.quaknow point.script/buildverifies the declared entrypoints exist after the compiler runs and makes the CLI executable, the Dockerfile runsmake buildas well asmake check, and aquakscript makes the README'syarn quak <command>examples work. - 2026-08-09: Retry policy: no retry on 4xx (except
408and429), exponential backoff with full jitter on 5xx, transport failures and truncated transfers, under per-attempt deadlines that cover the response body as well as the request. Downloads retry request, stream consumption and decryption as one unit;postJSONandputJSONare replayed only when the connection was never established. - 2026-08-09: Downloads verify the secretstream terminated on
TAG_FINALand write output atomically: a truncated body is rejected instead of landing on disk as a short file, and plaintext is staged in a sibling temp file and renamed into place, so a failed download leaves the destination untouched. - 2026-07-07 Adopted scripts-to-rule-them-all:
script/entrypoints, Makefile shims, README Entrypoints section - 2026-06-10: Decrypted collections shared by other users (sealed-box keys); listCollections drops deleted-collection tombstones.
- 2026-06-10: Login hardening: dual-2FA empty-string fields handled, TOTP preferred when a passkey is also enrolled, interactive input via @inquirer/prompts.
- 2026-06-10: Replaced sharp with pure JS (jpeg-js + exif-reader); added single-binary bun build and make install.
- 2026-06-09: Added backup-metadata command (ML data always included, --exif opt-in); rewrote README to match the implementation; added thumbnail helper tests.
- 2026-05-13: Full CLI surface: login, backup with dedup symlink layout, collections, files, get, get-thumb, thumbnail repair helpers.
- 2026-05-13: Client OO API with literate usage tests; file download and decryption; all three metadata layers decrypted and persisted; renamed quack to quak.
- 2026-05-11: SRP login flow (email OTP + TOTP) and ApiClient.
Future Steps
- Future desktop client, separate repo:
- Electron app skeleton consuming this library.
- Local SQLite cache keyed on (collectionID, fileID, updationTime).
- Background sync worker streaming new files into the cache.
- Gallery UI: thumbnails, full-image view, basic search.
- Upload, delete, and share operations in the library.