Skip to content

Commit caa2a75

Browse files
Brooooooklynclaude
andcommitted
fix(cloudflare): reject restore times finer than a millisecond
`new Date` keeps only milliseconds, so `--timestamp 2026-10-16T09:30:00.123456Z` restored to .123, an earlier instant than the one written, which could cross a write. `void db restore` now accepts at most three fractional digits and says so in its error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent e7eea9c commit caa2a75

3 files changed

Lines changed: 9 additions & 5 deletions

File tree

‎docs/reference/cli/database.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ void db restore --bookmark <bookmark> [--platform cloudflare] [--yes]
9696
void db restore --timestamp <iso-time> [--platform cloudflare] [--yes]
9797
```
9898

99-
Restore the deployed D1 database of a direct Cloudflare deploy with D1 [Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/). Before `void deploy --platform cloudflare` applies migrations, it prints the `--bookmark` that undoes them. `--timestamp` restores the state at an ISO 8601 time with `Z` or an offset instead, such as `2026-10-16T09:30:00Z`. Time Travel keeps 7 days of history on Workers Free and 30 days on Workers Paid. An older bookmark or time can no longer be restored: Cloudflare rejects it and the database stays unchanged. To undo an older migration, write a new migration that reverses it.
99+
Restore the deployed D1 database of a direct Cloudflare deploy with D1 [Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/). Before `void deploy --platform cloudflare` applies migrations, it prints the `--bookmark` that undoes them. `--timestamp` restores the state at an ISO 8601 time with `Z` or an offset instead, such as `2026-10-16T09:30:00Z`, with at most millisecond precision. Time Travel keeps 7 days of history on Workers Free and 30 days on Workers Paid. An older bookmark or time can no longer be restored: Cloudflare rejects it and the database stays unchanged. To undo an older migration, write a new migration that reverses it.
100100

101101
If the project's saved destination is not Cloudflare and you deployed once with `void deploy --platform cloudflare`, pass `--platform cloudflare`; deploy prints the command with it.
102102

‎packages/void/src/cli/db-args.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,12 @@ const ACTION_FLAGS: Record<DbArgs['action'], Array<string>> = {
146146

147147
/**
148148
* Read a restore time as a UTC ISO string. A time without a zone would depend on where the
149-
* command runs, and `new Date` rolls impossible dates over (Feb 30 becomes Mar 2), so both are
150-
* rejected instead of restoring to a moment the user did not write.
149+
* command runs, `new Date` rolls impossible dates over (Feb 30 becomes Mar 2), and it keeps only
150+
* milliseconds, so all three are rejected instead of restoring to a moment the user did not write.
151151
*/
152152
function parseRestoreTime(value: string): string | undefined {
153153
const match =
154-
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|([+-])(\d{2}):(\d{2}))$/i.exec(
154+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(?:Z|([+-])(\d{2}):(\d{2}))$/i.exec(
155155
value,
156156
);
157157
const time = new Date(value);
@@ -273,7 +273,7 @@ export function parseDbArgs(args: Array<string>): DbArgs {
273273
const time = parseRestoreTime(timestamp);
274274
if (!time) {
275275
throw new Error(
276-
'db: --timestamp must be a valid ISO 8601 time with Z or an offset, for example 2026-10-16T09:30:00Z.',
276+
'db: --timestamp must be a valid ISO 8601 time with Z or an offset and at most millisecond precision, for example 2026-10-16T09:30:00Z.',
277277
);
278278
}
279279
return { action, target: { timestamp: time }, platform, yes: values.yes };

‎packages/void/test/unit/db-args.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ describe('parseDbArgs', () => {
216216
['2024-02-29T23:59:59Z', '2024-02-29T23:59:59.000Z'],
217217
['2026-10-16T09:30:00-05:30', '2026-10-16T15:00:00.000Z'],
218218
['2026-01-01T00:30:00+01:00', '2025-12-31T23:30:00.000Z'],
219+
['2026-10-16T09:30:00.5Z', '2026-10-16T09:30:00.500Z'],
220+
['2026-10-16T09:30:00.123+08:00', '2026-10-16T01:30:00.123Z'],
219221
])('reads the restore time %s as %s', (input, utc) => {
220222
expect(parseDbArgs(['restore', '--timestamp', input])).toMatchObject({
221223
target: { timestamp: utc },
@@ -236,6 +238,8 @@ describe('parseDbArgs', () => {
236238
[['--timestamp', '2026-10-16T09:30:00'], 'with Z or an offset'],
237239
[['--timestamp', '2026-02-30T09:30:00Z'], 'must be a valid ISO 8601 time'],
238240
[['--timestamp', '2026-10-16T24:00:00Z'], 'must be a valid ISO 8601 time'],
241+
// `new Date` would truncate this to .123, an earlier instant than the user wrote.
242+
[['--timestamp', '2026-10-16T09:30:00.123456Z'], 'at most millisecond precision'],
239243
[['--platform', 'void', '--bookmark', '00000085-0000024c'], "accepts only 'cloudflare'"],
240244
[['--bookmark=--dry-run'], '--bookmark must be a D1 restore point'],
241245
[['--bookmark', 'not-hex'], '--bookmark must be a D1 restore point'],

0 commit comments

Comments
 (0)