Found while implementing #8373 (export cell rendering); filed rather than fixed there, because it changes a different user-visible surface (the suggested download filename) and #8373's fix direction named only the cell formatter.
What
exportContentDisposition() in packages/rest/src/export-format.ts builds the download's filename timestamp with local-time getters:
conststamp=`${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}`+`-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;getFullYear() / getHours() read the processTZ, which is a deployment-host fact, not the caller's business timezone. The route resolves the business timezone one frame up (resolveExecCtx gives an ExecutionContext carrying timezone, the platform-default to global to tenant cascade) and does not pass it here.
Why it matters now
Before #8373 this was the quieter half of a self-contradiction the issue reported as evidence: the filename read the host clock while the file's contents read UTC. #8373 moves the contents onto the business timezone. The filename is now the only part of an export still on the host clock, so the two disagree exactly when TZ is not the business timezone — a container at TZ=UTC serving an Asia/Shanghai tenant downloads orders-20260731-220000.csv whose first row is 2026-08-01 06:00:00. The reported deployment happened to set TZ=Asia/Shanghai, which is why the filename looked right there.
Not a regression introduced by #8373 — the local-time stamp predates it — but #8373 is what makes it the odd one out.
Suggested direction
Thread the same ExecutionContext.timezone into exportContentDisposition() and take the stamp's calendar components from it, falling back to today's behaviour when no timezone is resolved. Note the fallback question is worth deciding explicitly rather than assuming: for the contents the compatible fallback is UTC, but this function has always used process-local, so "no timezone" here means keeping local, not switching to UTC — otherwise every deployment without a configured timezone sees its filenames change.
Repro
Unit level, no server needed:
TZ=UTC node -e "console.log(new Date('2026-08-01T02:00:00Z').getHours())" # 2
TZ=Asia/Shanghai node -e "console.log(new Date('2026-08-01T02:00:00Z').getHours())" # 10
exportContentDisposition('orders', undefined, 'csv', new Date('2026-08-01T02:00:00Z')) yields a different stamp per host TZ for the same instant and the same tenant.
Scope
packages/rest/src/export-format.ts — exportContentDisposition- its call site in the export route in
packages/rest/src/rest-server.ts packages/rest/src/export-format.test.ts pins the current local-time behaviour (const NOW = new Date(2026, 6, 14, 15, 30, 45)), so those expectations move with the fix.
Related: #8373.
Generated by Claude Code
Found while implementing #8373 (export cell rendering); filed rather than fixed there, because it changes a different user-visible surface (the suggested download filename) and #8373's fix direction named only the cell formatter.
What
exportContentDisposition()inpackages/rest/src/export-format.tsbuilds the download's filename timestamp with local-time getters:getFullYear()/getHours()read the processTZ, which is a deployment-host fact, not the caller's business timezone. The route resolves the business timezone one frame up (resolveExecCtxgives anExecutionContextcarryingtimezone, the platform-default to global to tenant cascade) and does not pass it here.Why it matters now
Before #8373 this was the quieter half of a self-contradiction the issue reported as evidence: the filename read the host clock while the file's contents read UTC. #8373 moves the contents onto the business timezone. The filename is now the only part of an export still on the host clock, so the two disagree exactly when
TZis not the business timezone — a container atTZ=UTCserving an Asia/Shanghai tenant downloadsorders-20260731-220000.csvwhose first row is2026-08-01 06:00:00. The reported deployment happened to setTZ=Asia/Shanghai, which is why the filename looked right there.Not a regression introduced by #8373 — the local-time stamp predates it — but #8373 is what makes it the odd one out.
Suggested direction
Thread the same
ExecutionContext.timezoneintoexportContentDisposition()and take the stamp's calendar components from it, falling back to today's behaviour when no timezone is resolved. Note the fallback question is worth deciding explicitly rather than assuming: for the contents the compatible fallback is UTC, but this function has always used process-local, so "no timezone" here means keeping local, not switching to UTC — otherwise every deployment without a configured timezone sees its filenames change.Repro
Unit level, no server needed:
exportContentDisposition('orders', undefined, 'csv', new Date('2026-08-01T02:00:00Z'))yields a different stamp per hostTZfor the same instant and the same tenant.Scope
packages/rest/src/export-format.ts—exportContentDispositionpackages/rest/src/rest-server.tspackages/rest/src/export-format.test.tspins the current local-time behaviour (const NOW = new Date(2026, 6, 14, 15, 30, 45)), so those expectations move with the fix.Related: #8373.
Generated by Claude Code