instant-cli 1.0.64 → 1.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
 
2
- > instant-cli@1.0.64 build /home/runner/work/instant/instant/client/packages/cli
2
+ > instant-cli@1.0.65 build /home/runner/work/instant/instant/client/packages/cli
3
3
  > rm -rf dist; tsc -p tsconfig.build.json
4
4
 
@@ -1 +1 @@
1
- {"version":3,"file":"backupDownload.d.ts","sourceRoot":"","sources":["../../src/lib/backupDownload.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,SAAS,EAET,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACf,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EACV,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAgE7B;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACxD,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA4ChC"}
1
+ {"version":3,"file":"backupDownload.d.ts","sourceRoot":"","sources":["../../src/lib/backupDownload.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,SAAS,EAET,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACf,MAAM,qBAAqB,CAAC;AAM7B,YAAY,EACV,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAuG7B;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACxD,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA4ChC"}
@@ -6,6 +6,9 @@ import { get as httpGet } from 'node:http';
6
6
  import { get as httpsGet } from 'node:https';
7
7
  import { Readable, Writable } from 'node:stream';
8
8
  import zlib from 'node:zlib';
9
+ import version from '../version.js';
10
+ // Set a user-agent or else cloudfront might block the request
11
+ const userAgent = `instant-cli/${version}`;
9
12
  // zstd landed in node:zlib in 22.15 / 23.8; on older Nodes the property is
10
13
  // absent, so feature-detect instead of assuming the type declarations match
11
14
  // the runtime.
@@ -13,7 +16,7 @@ const createZstdDecompress = zlib.createZstdDecompress;
13
16
  function fetchStream(url, signal) {
14
17
  return new Promise((resolve, reject) => {
15
18
  const get = url.startsWith('https:') ? httpsGet : httpGet;
16
- const req = get(url, { signal }, resolve);
19
+ const req = get(url, { signal, headers: { 'user-agent': userAgent } }, resolve);
17
20
  req.on('error', reject);
18
21
  });
19
22
  }
@@ -23,6 +26,41 @@ function pipe(src, dst) {
23
26
  src.on('error', (e) => dst.destroy(e));
24
27
  return src.pipe(dst);
25
28
  }
29
+ // S3/CloudFront answer a rejected presigned URL with a short XML body naming
30
+ // the actual cause (SignatureDoesNotMatch, AccessDenied, expired, …). Read a
31
+ // bounded prefix so the error surfaces the reason instead of a bare status.
32
+ async function readErrorBody(res, signal) {
33
+ // A stalled or dribbling error body must not hang the download; destroying
34
+ // res ends the read below. The request's own signal already tears res down
35
+ // on a caller abort, so this only guards the no-abort stall.
36
+ const timer = setTimeout(() => res.destroy(), 5000);
37
+ try {
38
+ const chunks = [];
39
+ let total = 0;
40
+ for await (const chunk of res) {
41
+ chunks.push(chunk);
42
+ total += chunk.length;
43
+ if (total >= 2048)
44
+ break;
45
+ }
46
+ return Buffer.concat(chunks)
47
+ .toString('utf8')
48
+ .replace(/\s+/g, ' ')
49
+ .trim()
50
+ .slice(0, 500);
51
+ }
52
+ catch (e) {
53
+ // A caller-initiated abort must propagate, not be masked as an empty body;
54
+ // only other read failures fall back to no reason.
55
+ if (signal.aborted)
56
+ throw e;
57
+ return '';
58
+ }
59
+ finally {
60
+ clearTimeout(timer);
61
+ res.destroy();
62
+ }
63
+ }
26
64
  // Fetches a presigned URL with node:http(s), decompressing explicitly: the
27
65
  // entity shards are served with `Content-Encoding: zstd` and Node doesn't
28
66
  // auto-decompress that. downloadBackupToFile refuses to run without zstd
@@ -30,8 +68,8 @@ function pipe(src, dst) {
30
68
  async function fetchBody(url, signal) {
31
69
  const res = await fetchStream(url, signal);
32
70
  if (res.statusCode !== 200) {
33
- res.resume();
34
- throw new Error(`HTTP ${res.statusCode}`);
71
+ const body = await readErrorBody(res, signal);
72
+ throw new Error(`HTTP ${res.statusCode}${body ? ` — ${body}` : ''}`);
35
73
  }
36
74
  const encoding = res.headers['content-encoding'];
37
75
  let stream = res;
@@ -1 +1 @@
1
- {"version":3,"file":"backupDownload.js","sourceRoot":"","sources":["../../src/lib/backupDownload.ts"],"names":[],"mappings":"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","sourcesContent":["import { createWriteStream } from 'node:fs';\nimport { open, rename, unlink } from 'node:fs/promises';\nimport { randomBytes } from 'node:crypto';\nimport { once } from 'node:events';\nimport { get as httpGet, type IncomingMessage } from 'node:http';\nimport { get as httpsGet } from 'node:https';\nimport { Readable, Writable, type Duplex } from 'node:stream';\nimport zlib from 'node:zlib';\nimport type {\n AppBackup,\n BackupArchiveWriter,\n BackupDownloadProgress,\n BackupDownloadResult,\n BackupsManager,\n} from '@instantdb/platform';\n\nexport type {\n BackupDownloadProgress,\n BackupDownloadResult,\n} from '@instantdb/platform';\n\n// zstd landed in node:zlib in 22.15 / 23.8; on older Nodes the property is\n// absent, so feature-detect instead of assuming the type declarations match\n// the runtime.\nconst createZstdDecompress: (() => Duplex) | undefined = (\n zlib as { createZstdDecompress?: () => Duplex }\n).createZstdDecompress;\n\nfunction fetchStream(\n url: string,\n signal: AbortSignal,\n): Promise<IncomingMessage> {\n return new Promise((resolve, reject) => {\n const get = url.startsWith('https:') ? httpsGet : httpGet;\n const req = get(url, { signal }, resolve);\n req.on('error', reject);\n });\n}\n\n// .pipe doesn't forward errors, so a source failure would otherwise leave the\n// destination (and the zip writer reading from it) hanging forever.\nfunction pipe(src: Readable, dst: Duplex): Duplex {\n src.on('error', (e) => dst.destroy(e));\n return src.pipe(dst);\n}\n\n// Fetches a presigned URL with node:http(s), decompressing explicitly: the\n// entity shards are served with `Content-Encoding: zstd` and Node doesn't\n// auto-decompress that. downloadBackupToFile refuses to run without zstd\n// support, so the assertion below can't fire.\nasync function fetchBody(\n url: string,\n signal: AbortSignal,\n): Promise<ReadableStream<Uint8Array>> {\n const res = await fetchStream(url, signal);\n if (res.statusCode !== 200) {\n res.resume();\n throw new Error(`HTTP ${res.statusCode}`);\n }\n const encoding = res.headers['content-encoding'];\n let stream: Readable = res;\n if (encoding === 'zstd') {\n stream = pipe(stream, createZstdDecompress!());\n } else if (encoding === 'gzip') {\n stream = pipe(stream, zlib.createGunzip());\n } else if (encoding) {\n res.destroy();\n throw new Error(`Unsupported content encoding: ${encoding}`);\n }\n return Readable.toWeb(stream) as ReadableStream<Uint8Array>;\n}\n\nasync function createZipWriter(\n sink: WritableStream<Uint8Array>,\n signal: AbortSignal,\n): Promise<BackupArchiveWriter> {\n // Loaded on demand so every other CLI command skips parsing it.\n const { ZipWriter } = await import('@zip.js/zip.js');\n // zip64: without it any archive whose central-directory offset passes 4GB\n // writes a wrapped 32-bit offset and the zip is unreadable.\n return new ZipWriter(sink, { zip64: true, signal });\n}\n\n/**\n * Downloads a backup into a zip file at `outPath` via the shared\n * `BackupsManager.downloadArchive` pipeline, supplying the Node-specific\n * pieces:\n * presigned URLs are fetched with node:http(s) and decompressed explicitly,\n * and the archive streams to disk with backpressure so memory stays flat\n * regardless of backup size.\n *\n * Writes to `<outPath>.partial` and renames on success; a failed or aborted\n * download removes the partial file.\n */\nexport async function downloadBackupToFile(opts: {\n manager: BackupsManager;\n backup: AppBackup;\n outPath: string;\n signal: AbortSignal;\n onProgress: (progress: BackupDownloadProgress) => void;\n}): Promise<BackupDownloadResult> {\n // The entity shards are served with `Content-Encoding: zstd`; fail before\n // writing anything if this Node can't decompress them.\n if (!createZstdDecompress) {\n throw new Error(\n 'Downloading backups requires Node 22.15 or newer (for zstd support).',\n );\n }\n\n // Randomized so a stale partial or a concurrent download of the same\n // backup can't collide; 'wx' turns any remaining collision into an error\n // instead of silently truncating another run's file.\n const partialPath = `${opts.outPath}.partial-${randomBytes(4).toString('hex')}`;\n const fileStream = createWriteStream(partialPath, { flags: 'wx' });\n const awaitFileClosed = async () => {\n if (!fileStream.closed) await once(fileStream, 'close');\n };\n try {\n const result = await opts.manager.downloadArchive({\n backup: opts.backup,\n fetchBody,\n sink: Writable.toWeb(fileStream) as WritableStream<Uint8Array>,\n createWriter: createZipWriter,\n signal: opts.signal,\n onProgress: opts.onProgress,\n });\n await awaitFileClosed();\n // Flush to disk before the rename so a crash right after can't leave a\n // complete-looking zip with unwritten tails.\n const fh = await open(partialPath, 'r+');\n try {\n await fh.sync();\n } finally {\n await fh.close();\n }\n await rename(partialPath, opts.outPath);\n return result;\n } catch (e) {\n // The pipeline already aborted the sink; wait for the fd to close, then\n // discard the partial file on disk.\n await awaitFileClosed().catch(() => {});\n await unlink(partialPath).catch(() => {});\n throw e;\n }\n}\n"]}
1
+ {"version":3,"file":"backupDownload.js","sourceRoot":"","sources":["../../src/lib/backupDownload.ts"],"names":[],"mappings":"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","sourcesContent":["import { createWriteStream } from 'node:fs';\nimport { open, rename, unlink } from 'node:fs/promises';\nimport { randomBytes } from 'node:crypto';\nimport { once } from 'node:events';\nimport { get as httpGet, type IncomingMessage } from 'node:http';\nimport { get as httpsGet } from 'node:https';\nimport { Readable, Writable, type Duplex } from 'node:stream';\nimport zlib from 'node:zlib';\nimport type {\n AppBackup,\n BackupArchiveWriter,\n BackupDownloadProgress,\n BackupDownloadResult,\n BackupsManager,\n} from '@instantdb/platform';\nimport version from '../version.js';\n\n// Set a user-agent or else cloudfront might block the request\nconst userAgent = `instant-cli/${version}`;\n\nexport type {\n BackupDownloadProgress,\n BackupDownloadResult,\n} from '@instantdb/platform';\n\n// zstd landed in node:zlib in 22.15 / 23.8; on older Nodes the property is\n// absent, so feature-detect instead of assuming the type declarations match\n// the runtime.\nconst createZstdDecompress: (() => Duplex) | undefined = (\n zlib as { createZstdDecompress?: () => Duplex }\n).createZstdDecompress;\n\nfunction fetchStream(\n url: string,\n signal: AbortSignal,\n): Promise<IncomingMessage> {\n return new Promise((resolve, reject) => {\n const get = url.startsWith('https:') ? httpsGet : httpGet;\n const req = get(\n url,\n { signal, headers: { 'user-agent': userAgent } },\n resolve,\n );\n req.on('error', reject);\n });\n}\n\n// .pipe doesn't forward errors, so a source failure would otherwise leave the\n// destination (and the zip writer reading from it) hanging forever.\nfunction pipe(src: Readable, dst: Duplex): Duplex {\n src.on('error', (e) => dst.destroy(e));\n return src.pipe(dst);\n}\n\n// S3/CloudFront answer a rejected presigned URL with a short XML body naming\n// the actual cause (SignatureDoesNotMatch, AccessDenied, expired, …). Read a\n// bounded prefix so the error surfaces the reason instead of a bare status.\nasync function readErrorBody(\n res: IncomingMessage,\n signal: AbortSignal,\n): Promise<string> {\n // A stalled or dribbling error body must not hang the download; destroying\n // res ends the read below. The request's own signal already tears res down\n // on a caller abort, so this only guards the no-abort stall.\n const timer = setTimeout(() => res.destroy(), 5000);\n try {\n const chunks: Buffer[] = [];\n let total = 0;\n for await (const chunk of res) {\n chunks.push(chunk as Buffer);\n total += (chunk as Buffer).length;\n if (total >= 2048) break;\n }\n return Buffer.concat(chunks)\n .toString('utf8')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 500);\n } catch (e) {\n // A caller-initiated abort must propagate, not be masked as an empty body;\n // only other read failures fall back to no reason.\n if (signal.aborted) throw e;\n return '';\n } finally {\n clearTimeout(timer);\n res.destroy();\n }\n}\n\n// Fetches a presigned URL with node:http(s), decompressing explicitly: the\n// entity shards are served with `Content-Encoding: zstd` and Node doesn't\n// auto-decompress that. downloadBackupToFile refuses to run without zstd\n// support, so the assertion below can't fire.\nasync function fetchBody(\n url: string,\n signal: AbortSignal,\n): Promise<ReadableStream<Uint8Array>> {\n const res = await fetchStream(url, signal);\n if (res.statusCode !== 200) {\n const body = await readErrorBody(res, signal);\n throw new Error(`HTTP ${res.statusCode}${body ? ` — ${body}` : ''}`);\n }\n const encoding = res.headers['content-encoding'];\n let stream: Readable = res;\n if (encoding === 'zstd') {\n stream = pipe(stream, createZstdDecompress!());\n } else if (encoding === 'gzip') {\n stream = pipe(stream, zlib.createGunzip());\n } else if (encoding) {\n res.destroy();\n throw new Error(`Unsupported content encoding: ${encoding}`);\n }\n return Readable.toWeb(stream) as ReadableStream<Uint8Array>;\n}\n\nasync function createZipWriter(\n sink: WritableStream<Uint8Array>,\n signal: AbortSignal,\n): Promise<BackupArchiveWriter> {\n // Loaded on demand so every other CLI command skips parsing it.\n const { ZipWriter } = await import('@zip.js/zip.js');\n // zip64: without it any archive whose central-directory offset passes 4GB\n // writes a wrapped 32-bit offset and the zip is unreadable.\n return new ZipWriter(sink, { zip64: true, signal });\n}\n\n/**\n * Downloads a backup into a zip file at `outPath` via the shared\n * `BackupsManager.downloadArchive` pipeline, supplying the Node-specific\n * pieces:\n * presigned URLs are fetched with node:http(s) and decompressed explicitly,\n * and the archive streams to disk with backpressure so memory stays flat\n * regardless of backup size.\n *\n * Writes to `<outPath>.partial` and renames on success; a failed or aborted\n * download removes the partial file.\n */\nexport async function downloadBackupToFile(opts: {\n manager: BackupsManager;\n backup: AppBackup;\n outPath: string;\n signal: AbortSignal;\n onProgress: (progress: BackupDownloadProgress) => void;\n}): Promise<BackupDownloadResult> {\n // The entity shards are served with `Content-Encoding: zstd`; fail before\n // writing anything if this Node can't decompress them.\n if (!createZstdDecompress) {\n throw new Error(\n 'Downloading backups requires Node 22.15 or newer (for zstd support).',\n );\n }\n\n // Randomized so a stale partial or a concurrent download of the same\n // backup can't collide; 'wx' turns any remaining collision into an error\n // instead of silently truncating another run's file.\n const partialPath = `${opts.outPath}.partial-${randomBytes(4).toString('hex')}`;\n const fileStream = createWriteStream(partialPath, { flags: 'wx' });\n const awaitFileClosed = async () => {\n if (!fileStream.closed) await once(fileStream, 'close');\n };\n try {\n const result = await opts.manager.downloadArchive({\n backup: opts.backup,\n fetchBody,\n sink: Writable.toWeb(fileStream) as WritableStream<Uint8Array>,\n createWriter: createZipWriter,\n signal: opts.signal,\n onProgress: opts.onProgress,\n });\n await awaitFileClosed();\n // Flush to disk before the rename so a crash right after can't leave a\n // complete-looking zip with unwritten tails.\n const fh = await open(partialPath, 'r+');\n try {\n await fh.sync();\n } finally {\n await fh.close();\n }\n await rename(partialPath, opts.outPath);\n return result;\n } catch (e) {\n // The pipeline already aborted the sink; wait for the fd to close, then\n // discard the partial file on disk.\n await awaitFileClosed().catch(() => {});\n await unlink(partialPath).catch(() => {});\n throw e;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "instant-cli",
3
3
  "type": "module",
4
- "version": "1.0.64",
4
+ "version": "1.0.65",
5
5
  "description": "Instant's CLI",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://github.com/instantdb/instant/tree/main/client/packages/cli",
@@ -57,9 +57,9 @@
57
57
  "strip-ansi": "^7.1.2",
58
58
  "supports-hyperlinks": "^4.4.0",
59
59
  "unconfig": "^7.5.0",
60
- "@instantdb/core": "1.0.64",
61
- "@instantdb/platform": "1.0.64",
62
- "@instantdb/version": "1.0.64"
60
+ "@instantdb/core": "1.0.65",
61
+ "@instantdb/platform": "1.0.65",
62
+ "@instantdb/version": "1.0.65"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@babel/core": "^7.17.9",
@@ -13,6 +13,10 @@ import type {
13
13
  BackupDownloadResult,
14
14
  BackupsManager,
15
15
  } from '@instantdb/platform';
16
+ import version from '../version.js';
17
+
18
+ // Set a user-agent or else cloudfront might block the request
19
+ const userAgent = `instant-cli/${version}`;
16
20
 
17
21
  export type {
18
22
  BackupDownloadProgress,
@@ -32,7 +36,11 @@ function fetchStream(
32
36
  ): Promise<IncomingMessage> {
33
37
  return new Promise((resolve, reject) => {
34
38
  const get = url.startsWith('https:') ? httpsGet : httpGet;
35
- const req = get(url, { signal }, resolve);
39
+ const req = get(
40
+ url,
41
+ { signal, headers: { 'user-agent': userAgent } },
42
+ resolve,
43
+ );
36
44
  req.on('error', reject);
37
45
  });
38
46
  }
@@ -44,6 +52,41 @@ function pipe(src: Readable, dst: Duplex): Duplex {
44
52
  return src.pipe(dst);
45
53
  }
46
54
 
55
+ // S3/CloudFront answer a rejected presigned URL with a short XML body naming
56
+ // the actual cause (SignatureDoesNotMatch, AccessDenied, expired, …). Read a
57
+ // bounded prefix so the error surfaces the reason instead of a bare status.
58
+ async function readErrorBody(
59
+ res: IncomingMessage,
60
+ signal: AbortSignal,
61
+ ): Promise<string> {
62
+ // A stalled or dribbling error body must not hang the download; destroying
63
+ // res ends the read below. The request's own signal already tears res down
64
+ // on a caller abort, so this only guards the no-abort stall.
65
+ const timer = setTimeout(() => res.destroy(), 5000);
66
+ try {
67
+ const chunks: Buffer[] = [];
68
+ let total = 0;
69
+ for await (const chunk of res) {
70
+ chunks.push(chunk as Buffer);
71
+ total += (chunk as Buffer).length;
72
+ if (total >= 2048) break;
73
+ }
74
+ return Buffer.concat(chunks)
75
+ .toString('utf8')
76
+ .replace(/\s+/g, ' ')
77
+ .trim()
78
+ .slice(0, 500);
79
+ } catch (e) {
80
+ // A caller-initiated abort must propagate, not be masked as an empty body;
81
+ // only other read failures fall back to no reason.
82
+ if (signal.aborted) throw e;
83
+ return '';
84
+ } finally {
85
+ clearTimeout(timer);
86
+ res.destroy();
87
+ }
88
+ }
89
+
47
90
  // Fetches a presigned URL with node:http(s), decompressing explicitly: the
48
91
  // entity shards are served with `Content-Encoding: zstd` and Node doesn't
49
92
  // auto-decompress that. downloadBackupToFile refuses to run without zstd
@@ -54,8 +97,8 @@ async function fetchBody(
54
97
  ): Promise<ReadableStream<Uint8Array>> {
55
98
  const res = await fetchStream(url, signal);
56
99
  if (res.statusCode !== 200) {
57
- res.resume();
58
- throw new Error(`HTTP ${res.statusCode}`);
100
+ const body = await readErrorBody(res, signal);
101
+ throw new Error(`HTTP ${res.statusCode}${body ? ` — ${body}` : ''}`);
59
102
  }
60
103
  const encoding = res.headers['content-encoding'];
61
104
  let stream: Readable = res;