Compile bin/ alongside src/ so the package can be built (closes #3)
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
rootDir was ./src while include also matched bin/**/*, which is TS6059: tsc refuses to emit at all when a compiled file sits outside rootDir. rootDir is now the repository root, which is the smallest change that makes the two agree and leaves the source layout the README documents alone. Output keeps the shape of the source tree, so main and types move to dist/src/index.js and dist/src/index.d.ts while bin.quak stays at dist/bin/quak.js. The alternative, moving the CLI body into src/ behind a shim in bin/, would hold main at dist/index.js at the cost of churning the CLI and contradicting the layout diagram in the README. Clearing TS6059 exposed two type errors that had never been reached, because the config error aborts before checking: StateAddress was read as a namespace member off the default import, and the secretstream pull was called without the additional-data argument, which libsodium does not make optional. The type is now taken from the module's named export and the pull passes null for ad, matching the null already passed on the push side in encryptBlob. Neither changes what runs. noEmitOnError stops a failed build from leaving output behind. It emitted despite the error before, which is how a stale bin/quak.js came to sit next to bin/quak.ts in a working tree, where eslint then read it and failed make check on a generated file. script/build compiles and then checks that the files package.json advertises are among the ones the compiler wrote, since tsc knows nothing about the manifest and a green build could still ship a package whose main resolves to nothing. It also sets the executable bit on the bin entries, which tsc does not carry over from the source even though it does copy the shebang. The Makefile target is now a shim over it, as the other targets are, and package.json's build script points at it so yarn build gets the same checks. The Dockerfile runs make build after make check, so a branch that does not compile cannot reach main. What make check itself runs is unchanged. package.json gains a quak script, so the yarn quak commands the README's Getting Started block has always listed resolve to the built CLI.
This commit was merged in pull request #26.
This commit is contained in:
@@ -8,3 +8,4 @@ RUN script/bootstrap
|
||||
COPY . .
|
||||
|
||||
RUN make check
|
||||
RUN make build
|
||||
|
||||
2
Makefile
2
Makefile
@@ -24,7 +24,7 @@ check:
|
||||
@script/check
|
||||
|
||||
build:
|
||||
@$(YARN) tsc
|
||||
@script/build
|
||||
|
||||
build-bin:
|
||||
nix-shell -p bun --run "bun build bin/quak.ts --compile --outfile bin/quak"
|
||||
|
||||
15
README.md
15
README.md
@@ -81,6 +81,9 @@ alpine. We provide:
|
||||
`script/bootstrap`, then `script/install-precommit`
|
||||
- `script/projectname` — output the project name (our own extension); used by
|
||||
`script/docker` for the image tag
|
||||
- `script/build` — compile the TypeScript sources into `dist/`, then verify that
|
||||
the entrypoints `package.json` declares (`main`, `types`, `bin`) are among the
|
||||
files the compiler wrote, and make the CLI executable (our own extension)
|
||||
- `script/test` — run the test suite (vitest, hard-capped at 30s where `timeout`
|
||||
is available, verbose rerun on failure)
|
||||
- `script/lint` — run eslint and a prettier check
|
||||
@@ -91,7 +94,7 @@ alpine. We provide:
|
||||
- `script/docker` — build the Docker image, tagged via `script/projectname`
|
||||
(byte-identical across repos)
|
||||
- `script/cibuild` — cd to the repo root and `docker build .` (what CI runs; the
|
||||
image build runs `make check`)
|
||||
image build runs `make check` and `make build`)
|
||||
- `script/precommit` — run by the git pre-commit hook (our own extension); runs
|
||||
`script/lint` and `script/fmt-check` but deliberately not the tests, so the
|
||||
TDD red-phase commit can land
|
||||
@@ -133,8 +136,8 @@ All work on quak is test-driven. No exceptions.
|
||||
3. Subsequent commits add the implementation and any refactors needed to make
|
||||
the tests pass.
|
||||
4. A feature branch can only be merged into `main` when `make check` is green.
|
||||
`main` is always green. The Dockerfile runs `make check`, so a red branch
|
||||
cannot pass CI.
|
||||
`main` is always green. The Dockerfile runs `make check` and `make build`, so
|
||||
neither a red branch nor one that does not compile can pass CI.
|
||||
5. Tests are the canonical API documentation for this library. Every test file
|
||||
is commented thoroughly enough that a reader who has never seen quak can
|
||||
learn how to use it from the tests alone. Comments explain why a behavior
|
||||
@@ -183,6 +186,12 @@ quak/
|
||||
tsconfig.json
|
||||
```
|
||||
|
||||
`make build` compiles that tree into `dist/`, preserving its shape: the library
|
||||
lands in `dist/src/` and the CLI in `dist/bin/quak.js`, which is what
|
||||
`package.json` points `main`, `types` and `bin` at. The compiler's `rootDir` is
|
||||
the repository root rather than `src/`, because `bin/` is compiled too and
|
||||
`rootDir` has to contain everything that is compiled.
|
||||
|
||||
### Cryptography
|
||||
|
||||
All cryptography is done by `libsodium-wrappers-sumo` (the "sumo" build is
|
||||
|
||||
7
TODO.md
7
TODO.md
@@ -18,6 +18,13 @@ Update the README API reference section to match the current implementation.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-09: Fixed the TypeScript build. `rootDir` is the repo root, so `bin/`
|
||||
compiles alongside `src/` instead of failing with TS6059; output is
|
||||
`dist/src/` and `dist/bin/`, which is where `main`, `types` and `bin.quak` now
|
||||
point. `script/build` verifies the declared entrypoints exist after the
|
||||
compiler runs and makes the CLI executable, the Dockerfile runs `make build`
|
||||
as well as `make check`, and a `quak` script makes the README's
|
||||
`yarn quak <command>` examples work.
|
||||
- 2026-08-09: Retry policy: no retry on 4xx (except `408` and `429`),
|
||||
exponential backoff with full jitter on 5xx, transport failures and truncated
|
||||
transfers, under per-attempt deadlines that cover the response body as well as
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"url": "https://git.eeqj.de/sneak/quak.git"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"bin": {
|
||||
"quak": "./dist/bin/quak.js"
|
||||
},
|
||||
@@ -21,7 +21,8 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "script/build",
|
||||
"quak": "node ./dist/bin/quak.js",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"fmt": "prettier --write .",
|
||||
|
||||
54
script/build
Executable file
54
script/build
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
# script/build: compile the TypeScript sources into dist/, then verify that
|
||||
# the artifacts package.json advertises are among the files the compiler
|
||||
# actually wrote. tsc reports success by exit status alone and knows nothing
|
||||
# about the manifest, so without this step a green build can still ship a
|
||||
# package whose main, types or bin resolve to nothing. Our own extension to
|
||||
# scripts-to-rule-them-all.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# Reads package.json, requires every declared entrypoint to exist, requires
|
||||
# each bin entry to have kept its shebang, and makes the bin entries
|
||||
# executable: tsc copies the shebang through but not the mode bits, and an
|
||||
# installed CLI has to be runnable.
|
||||
verify_entrypoints() {
|
||||
node -e '
|
||||
const { readFileSync, statSync, chmodSync } = require("node:fs");
|
||||
|
||||
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
|
||||
const bins = Object.values(pkg.bin ?? {});
|
||||
const fail = (message) => {
|
||||
console.error("build: " + message);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
for (const declared of [pkg.main, pkg.types, ...bins]) {
|
||||
if (!declared) continue;
|
||||
try {
|
||||
statSync(declared);
|
||||
} catch {
|
||||
fail("package.json declares " + declared + ", which the build did not produce");
|
||||
}
|
||||
console.log("build: verified " + declared);
|
||||
}
|
||||
|
||||
for (const bin of bins) {
|
||||
const firstLine = readFileSync(bin, "utf-8").split("\n")[0];
|
||||
if (!firstLine.startsWith("#!")) {
|
||||
fail(bin + " lost its shebang, so it cannot be executed directly");
|
||||
}
|
||||
chmodSync(bin, 0o755);
|
||||
console.log("build: " + bin + " is executable (" + firstLine + ")");
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
yarn run tsc
|
||||
verify_entrypoints
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,4 +1,4 @@
|
||||
import sodium from "libsodium-wrappers-sumo";
|
||||
import sodium, { type StateAddress } from "libsodium-wrappers-sumo";
|
||||
|
||||
// Plaintext chunk size used by Ente for file content streams. Hard-coded by
|
||||
// the server; clients must match.
|
||||
@@ -47,8 +47,10 @@ export const encryptBlob = (
|
||||
};
|
||||
|
||||
// Opaque handle to libsodium's secretstream pull state. Threaded through
|
||||
// successive pullStreamChunk calls.
|
||||
export type StreamPullState = sodium.StateAddress;
|
||||
// successive pullStreamChunk calls. The type comes from the named export;
|
||||
// the default import is the module's value side and has no type namespace
|
||||
// under it.
|
||||
export type StreamPullState = StateAddress;
|
||||
|
||||
// Initialise a pull stream from the per-file decryption header and the
|
||||
// per-file key.
|
||||
@@ -84,9 +86,13 @@ export const pullStreamChunk = (
|
||||
state: StreamPullState,
|
||||
ciphertext: Uint8Array,
|
||||
): { plaintext: Uint8Array; tag: number } => {
|
||||
// The additional-data argument is not optional in libsodium's signature.
|
||||
// null is "no additional data", matching the null passed on the push side
|
||||
// in encryptBlob; Ente's file streams carry none.
|
||||
const result = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
state,
|
||||
ciphertext,
|
||||
null,
|
||||
);
|
||||
if (result === false) {
|
||||
throw new Error("secretstream chunk authentication failed");
|
||||
|
||||
@@ -49,18 +49,20 @@ const rootDir = clean(tsconfig.compilerOptions.rootDir);
|
||||
const globRoot = (pattern: string): string => {
|
||||
const segments = clean(pattern).split("/");
|
||||
const wildcard = segments.findIndex((s) => s.includes("*"));
|
||||
return segments.slice(0, wildcard === -1 ? segments.length : wildcard)
|
||||
.join("/") || ".";
|
||||
return (
|
||||
segments
|
||||
.slice(0, wildcard === -1 ? segments.length : wildcard)
|
||||
.join("/") || "."
|
||||
);
|
||||
};
|
||||
|
||||
// Where tsc will write the output for a source file: the path relative to
|
||||
// rootDir, re-rooted under outDir, with the extension swapped.
|
||||
const emitted = (source: string, extension: string): string =>
|
||||
"./" +
|
||||
posix.join(outDir, posix.relative(rootDir, clean(source))).replace(
|
||||
/\.ts$/,
|
||||
extension,
|
||||
);
|
||||
posix
|
||||
.join(outDir, posix.relative(rootDir, clean(source)))
|
||||
.replace(/\.ts$/, extension);
|
||||
|
||||
describe("tsconfig include and rootDir", () => {
|
||||
// TS6059 is not a style complaint: tsc refuses to emit anything at all
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"rootDir": ".",
|
||||
"noEmitOnError": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
Reference in New Issue
Block a user