The health endpoint was returning version 0.0.0 because readPackageVersion()
tried import.meta.require on a JSON path that Bun cannot bundle and the
package.json never shipped into the Nix store (installPhase only copied
dist/, home.html, schema.sql).
- flake.nix installPhase now copies package.json into $out/share/teleuploader/
- readPackageVersion() reads import.meta.dir/../package.json (bundle) or
process.cwd()/package.json (dev) via readFileSync at runtime
Verified local resolution to 1.2.1; after this fixes deploy it will expose
the live semver on GET /health.
dispatch.mjs dispatches deploy.yml via workflow_dispatch, which requires
actions: write. Without it the success hook hit HTTP 403 (Resource not
accessible by integration), same trap as the mytheclipse pipeline, and the
auto deploy never ran for v1.2.1.
prepare.mjs previously replaced the FIRST version="X" string in flake.nix,
which is the Bun overlay override (line ~17), not the TeleUploader package
version (pname = "teleuploader", line ~30). Store path derives from the
TeleUploader version, so the Bun override got clobbered to the app version
while the store path stayed at 1.1.1 -> deploy reused the old derivation and
the VPS profile never advanced.
Now prepare.mjs anchors on pname = "teleuploader" and bumps that block only,
leaving the Bun 1.3.14 override intact. Verified: flake.nix keeps
bun version=1.3.14, teleuploader version becomes the semver-version.
Also restores bun overlay version=1.3.14 (was overwritten to 1.2.0).
GitHub disables workflow re-triggering for GITHUB_TOKEN pushes (the release
commit), so the push trigger in deploy.yml never runs for release commits.
This adds @semantic-release/exec successCmd -> .semrel/dispatch.mjs which
dispatches deploy.yml for every released version, completing the auto loop:
fix/feat -> semantic-release bump -> release commit+tag -> deploy.
Reads application version from package.json via env.ts readPackageVersion()
and returns it alongside status so deploy verification is instant:
curl https://upload.asepharyana.my.id/health -> {status: ok, version: X.Y.Z}
- .releaserc.json: commit-analyzer + release-notes-generator + changelog
+ exec (prepare) + git + github plugins.
- .semrel/prepare.mjs: syncs next release version into package.json AND
flake.nix before the release commit — guarantees a fresh Nix store path
on every deploy (fixes the trap where a source change without a version
bump reused the same store path and CI silently deployed stale code).
- release.yml: runs semantic-release on main push (GITHUB_TOKEN,
isolated /tmp/sr-tools install so package.json/bun.lock stay clean).
- Release commit (no [skip ci]) triggers deploy.yml → auto build+deploy.
- Baseline tag v1.1.1 backfilled at current HEAD.
Force a fresh Nix store path so the Buffer-from-gzip fix actually reaches
the VPS. The 1.1.0 store path hash was reused by Nix (identical derivation
inputs from Nix's perspective), so a version bump guarantees the fixed
dist bundle is copied and the profile is switched.
Root cause of '400: Bad Request: there is no document in the request' on
chunked uploads of compressible files (MP4, zip, text, etc.):
Bun.gzipSync() returns a plain Uint8Array, NOT a Buffer. Telegraf
(sendDocument/sendVideo) only recognises Buffer/Blob/stream sources as
file uploads — a plain Uint8Array produces an empty multipart body and
Telegram rejects the request.
Verified via isolated forwardToStorage probes:
- raw Buffer chunk (.bin, MP4) -> OK
- plain Uint8Array chunk -> FAIL: there is no document
- Buffer.from(gzip) chunk -> OK (after fix)
- .gz extension on Uint8Array did NOT help (rules out MIME/extension)
This only affected chunked uploads of compressible files; incompressible
files (random .bin) worked by accident because gzip didn't shrink them so
the raw Buffer was forwarded unchanged.
Fixes browser CORS error when frontend fetches audio via Web Audio API
(decodeAudio): the 302 redirected to api.telegram.org which lacks
Access-Control-Allow-Origin, blocking fetch/XHR. Now the file bytes are
proxied server-side with CORS headers + proper Content-Type, so the
browser stays same-origin. Also adds CORS to chunked/archive streams.
Bun 1.4.0 (upgraded 0deb607) breaks Telegraf 4.15 sendDocument with
'The socket connection was closed unexpectedly' — reproduced with the same
libraries: Telegraf 4.15.0 on Bun 1.4.0 fails exactly like prod, on Bun 1.3.14
it returns a proper HTTP response. All uploads 500 since the upgrade deployed
(Aug 29 11:06). Pin flake nix override + CI bun-version to 1.3.14.
- Update Nix flake.nix overlay to use Bun 1.4.0 (overrideAttrs)
- Update CI (setup-bun) to pin bun-version: 1.4.0
- Update package.json packageManager/Dockerfile/vercel.json
- sha256: sha256:Poy0vf7yJ/hzk33QiQj5gnshI5Q7dfbaMD7xgwiyDKw=
Add comprehensive unit tests and repair stale tests that referenced the
old (pre-refactor) src/utils/* layout which no longer exists:
- s3-range: expand to 25 cases (suffix, clamping, malformed, zero-size,
invalid totals, content-range formatting)
- s3-object-stream: rewrite against the real interfaces/s3 module; add
multi-part ordering, ranges spanning parts, S3/CORS headers, fetch-error
propagation
- s3-helpers-edge (new): compress heuristics, virtual-host bucket parsing,
S3 route detection, client-IP/trustProxy, S3 response headers
- s3-auth-edge (new): verifyBodyHash, isS3Request, canonical-query-string
encoding/sorting
- chunked-storage: rewrite against the real ChunkedStorage class (was
importing deleted src/utils/chunked-storage) — chunk split, hashing,
compression, size-limit guards, forwarding
- zip: fix stale import + add path-traversal/duplicate sanitization,
locateZipEntry, empty-name fallback
- s3-docker-registry: fix stale src/config import; correct the rate-limit
test to assert S3 routes INTENTIONALLY bypass rate limiting
- temp-stream (new): streamToTemp hashing, MD5, signature bytes, empty and
oversized streams
- package.json: add the S3/unit files to test and test:s3 scripts
All new unit tests pass when run per-file (the project's documented mode to
avoid cross-file mock pollution). s3-sdk.test.ts (live E2E against a running
server) is deliberately excluded from test:s3.
Optimize the S3 GET path for chunked/multipart objects and reduce Telegram
API round-trips:
- object-stream: fetch object parts concurrently (bounded, in-order fan-in)
instead of serializing N sequential Telegram CDN fetches. Response latency
is now ~the slowest part fetch, not the sum of all part fetches.
- bot-pool.getFileInfo: cache file_id -> file_path in the existing in-memory
cache so repeated S3 GET/HEAD of the same object skip the Telegram API call
(file-controller had its own cache wrapper; the S3 path did not).
- chunked-storage + s3-controller: resolve multipart/chunked part CDN URLs
concurrently via Promise.all instead of sequentially.
- s3-controller multipart: await writer.end() before re-reading the temp part
file to avoid a flush race.
Adds object-stream-parallel.test.ts covering in-order fan-in, byte ranges,
and single-part responses even when the slowest part resolves out of order.
The 1GB suite downloads a 1GB fixture from Hetzner and uploads it to the
live deployment. Bun's Response-body write hangs against this source, so
the suite burned the full hook timeout on every routine run. Now:
- skipped by default (opt-in via RUN_LARGE_E2E=1)
- pre-flight Range probe (8s AbortSignal) fails fast when source is down
- 10min hook timeout retained for when the suite is actually opted in
The Login button (onclick=window.showAuthScreen()) was dead — showAuthScreen
was defined but never exported via Object.assign. Also drop dead code from
the e2e suite (unused createReadStream import, unused s3StreamRequest).
Build & Deploy (Nix) / build-and-deploy (push) Successful in 54s
handleHome looked up `${import.meta.dir}/home.html` which exists in neither
layout: dev (src/interfaces/http/controllers/) nor prod bundle
($out/share/teleuploader/dist/ — flake copies home.html beside dist/).
Add resolveHomeHtml(): walk up from import.meta.dir (bounded) to find
home.html. Works for dev (src/home.html, 4 levels up) and prod
(../home.html, 1 level up). Fails fast with a clear error instead of a
bare ENOENT 500. Tests cover both layouts + not-found fallback.
Build & Deploy (Nix) / build-and-deploy (push) Successful in 56s
Chunk parts > 19MB are stored to Telegram but getFile cannot resolve files over 20MB ('Bad Request: file is too big'), making every part undownloadable (prod bug 2026-08-01: 48MB chunk -> download 500).
- src/env.ts: reject TELEGRAM_CHUNK_SIZE_BYTES > 19922944 at startup (log error + throw), default changed 20MB -> 19MB
- src/shared/utils/validation.ts: TELEGRAM_CHUNK_SIZE_MAX_BYTES constant; asSafeChunkSize now enforces the max at runtime (covers S3 multipart parts too)
- test/env.test.ts: unit tests + subprocess fail-fast tests (48MB rejected, 19MB accepted)
- test/helpers/setup-env.ts: pin safe chunk size so a stale .env can't break the suite
- .env.example + CLAUDE.md: document the 20MB getFile limit