@supacloud/lite 0.2.0 → 0.3.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.2.0...supacloud-lite-v0.3.0) (2026-07-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * extend Supabase-compatible platform capabilities ([adef019](https://github.com/zuohuadong/supacloud/commit/adef019261f82f123043ab4c7a047e6ad6956e56))
9
+ * harden Supabase Cloud compatibility ([ab64374](https://github.com/zuohuadong/supacloud/commit/ab643743b058ad08a0d32c124d26bed0863db397))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * use installed TypeScript in Lite smoke ([8a64050](https://github.com/zuohuadong/supacloud/commit/8a64050833c47778e931067f8f30e541141256bf))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * **lite:** document snapshot archive dependency ([1c0a8cc](https://github.com/zuohuadong/supacloud/commit/1c0a8cccc4e6cbb0804c7badf6573d6d986c2528))
20
+
3
21
  ## [0.2.0](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.1.0...supacloud-lite-v0.2.0) (2026-07-28)
4
22
 
5
23
 
package/README.md CHANGED
@@ -59,6 +59,19 @@ supabase/
59
59
 
60
60
  `config.toml` 当前支持 Auth、API schema/max rows、Storage bucket/size limit、seed 和 function entrypoint 等常用配置。
61
61
 
62
+ ### Auth 运行方式
63
+
64
+ Lite 不下载、安装或启动独立的 GoTrue 进程。`/auth/v1/*` 由同一个 Bun 进程中的内置 Auth 实现处理,并与该 Lite 项目的 PGlite `auth` schema 共享生命周期;这避免了 sidecar 的配置、端口和会话一致性负担。
65
+
66
+ Auth 默认启用。如项目不需要客户端登录接口,可在 `supabase/config.toml` 中关闭该路由:
67
+
68
+ ```toml
69
+ [auth]
70
+ enabled = false
71
+ ```
72
+
73
+ 关闭后 `/auth/v1/*` 返回 `404`,但不会把 Lite 变成完整 GoTrue 运行时,也不会自动移除已有的 `auth` schema 或 API key。需要完整 GoTrue 行为、多项目鉴权或独立鉴权进程时,应使用完整 SupaCloud 平台。
74
+
62
75
  ## CLI
63
76
 
64
77
  ```text
@@ -70,6 +83,9 @@ supacloud-lite gen types [-o database.types.ts]
70
83
  supacloud-lite db reset
71
84
  supacloud-lite db diff [-f migration_name]
72
85
  supacloud-lite db pull [migration_name]
86
+ supacloud-lite snapshot create [-o backup.tar.gz]
87
+ supacloud-lite snapshot restore <backup.tar.gz> [--force]
88
+ supacloud-lite upgrade [-o pre-upgrade.tar.gz]
73
89
  supacloud-lite inspect
74
90
  supacloud-lite version
75
91
  ```
@@ -107,12 +123,65 @@ S3 模式下 `db reset` 会被拒绝,因为 Lite 不能把数据库元数据
107
123
 
108
124
  网络暴露时必须提供足够强的 JWT secret 和独立 vault key。默认生成的密钥适合单机项目;不要把 `.supacloud-lite/secrets.json` 提交到版本库。
109
125
 
126
+ ## 升级、快照与恢复
127
+
128
+ 生产或持久化环境升级时,先更新项目锁定的 npm 依赖,再运行 Lite 的受控升级命令:
129
+
130
+ ```bash
131
+ # 明确指定目标版本;不要在生产启动命令中隐式使用 @latest
132
+ bun add @supacloud/lite@0.2.0
133
+
134
+ # 自动创建升级前快照,然后执行尚未应用的 supabase/migrations
135
+ bunx supacloud-lite upgrade
136
+ ```
137
+
138
+ `upgrade` 不会自行修改 `package.json` 或联网更新 npm 包。它使用当前项目已经安装并锁定的 Lite 版本,默认把升级前快照写入 `.supacloud-lite/backups/pre-upgrade-<timestamp>.tar.gz`,快照成功后才执行 migrations。升级失败时,快照会保留,并打印恢复命令。
139
+
140
+ 也可以单独创建可移植快照:
141
+
142
+ ```bash
143
+ # 使用默认文件名和目录
144
+ bunx supacloud-lite snapshot create
145
+
146
+ # 指定输出位置
147
+ bunx supacloud-lite snapshot create -o ./backups/project-a.tar.gz
148
+ ```
149
+
150
+ 快照是一个 gzip 压缩的 tar 文件,包含:
151
+
152
+ - PGlite 数据目录;
153
+ - `fs` 模式的对象文件;
154
+ - `secrets.json`,用于保持 JWT、会话和 Vault 解密兼容;
155
+ - 快照格式、Lite 版本和 Storage backend 清单。
156
+
157
+ 快照包含敏感密钥,输出文件在 Unix 系统上会设置为 `0600`;仍应按数据库备份级别加密、限制访问并设置保留周期。创建快照前必须停止 Lite。若检测到数据目录锁,命令会拒绝继续;只有确认进程已退出后才能人工删除陈旧锁。
158
+
159
+ 恢复到新项目或空状态目录:
160
+
161
+ ```bash
162
+ bunx supacloud-lite snapshot restore ./backups/project-a.tar.gz
163
+ ```
164
+
165
+ 目标状态目录非空时默认拒绝覆盖。确认要替换现有状态时显式使用:
166
+
167
+ ```bash
168
+ bunx supacloud-lite snapshot restore ./backups/project-a.tar.gz --force
169
+ ```
170
+
171
+ `--force` 不会直接删除旧数据,而是把旧状态目录重命名为 `.supacloud-lite.restore-<id>` 形式的回滚副本,并在输出中打印完整路径。验证新状态后再由运维人员清理该目录。
172
+
173
+ 使用自定义 `--state-dir`、`--data-dir` 或 `--storage-dir` 时,创建和恢复必须传入相同参数。数据库与 Storage 目录不得重叠,也不能指向文件系统根目录或符号链接。
174
+
175
+ S3 模式的快照只包含数据库中的 Storage 元数据和密钥,不复制远端对象,也不会读取或保存 S3 凭据。恢复时必须传入 `--storage-backend s3`,并重新提供原 bucket/prefix 的环境变量;跨 bucket 迁移仍需使用对象存储自身的复制工具。
176
+
177
+ 内存数据库没有可持久化的数据,因此 `snapshot` 和 `upgrade` 会拒绝 `--memory`。
178
+
110
179
  ## 兼容范围
111
180
 
112
181
  | 能力 | V1 状态 | 说明 |
113
182
  | --- | --- | --- |
114
183
  | `supabase.from()` | 已验证核心 | 自动测试覆盖 CRUD、过滤、RLS;嵌套关系、RPC 和高级 PostgREST 语法属于实验性兼容 |
115
- | `supabase.auth` | 已验证核心 | 自动测试覆盖邮箱密码、会话和 bcrypt;OTP/Magic Link、匿名用户、OAuth、MFA 属于实验性兼容 |
184
+ | `supabase.auth` | 已验证核心 | 由内置 Auth 实现而非独立 GoTrue 进程提供;自动测试覆盖邮箱密码、会话和 bcrypt;OTP/Magic Link、匿名用户、OAuth、MFA 属于实验性兼容 |
116
185
  | `supabase.storage` | 已验证核心 | 覆盖上传下载、列表、删除、TUS/RLS、远端 S3 驱动,以及 Bun.Image 的 `contain`/`fill`、格式和质量变换子集;`cover` 明确不支持 |
117
186
  | `supabase.channel()` | 已验证核心 | 自动测试覆盖 `postgres_changes`、DELETE RLS 隔离和事件快照校验;Broadcast、Presence 属于实验性兼容 |
118
187
  | `supabase.functions.invoke()` | 已验证核心 | 自动测试覆盖 Bun.build、`Deno.serve()`、公开函数和同进程重启 |
@@ -22,3 +22,11 @@ SupaCloud Lite 使用 ElectricSQL PGlite 作为嵌入式 PostgreSQL 引擎。
22
22
  ## Supabase JavaScript SDK
23
23
 
24
24
  `@supabase/supabase-js` 仅作为开发和兼容性测试依赖使用,没有 vendored 到发布产物中。
25
+
26
+ ## node-tar
27
+
28
+ SupaCloud Lite 使用 `tar` 作为跨平台流式快照归档依赖,避免把完整 PGlite 数据目录加载到内存。
29
+
30
+ - License: ISC
31
+ - Upstream: `https://github.com/isaacs/node-tar`
32
+ - Distribution: 作为独立 npm 依赖安装,许可证随依赖包分发
package/dist/cli.js CHANGED
@@ -3,12 +3,12 @@
3
3
  var __require = import.meta.require;
4
4
 
5
5
  // src/cli.ts
6
- import { mkdir as mkdir6, rm as rm3, writeFile as writeFile5 } from "fs/promises";
7
- import { dirname as dirname4, join as join8, resolve as resolve3 } from "path";
6
+ import { mkdir as mkdir7, rm as rm4, writeFile as writeFile6 } from "fs/promises";
7
+ import { dirname as dirname5, join as join9, resolve as resolve4 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.2.0",
11
+ version: "0.3.0",
12
12
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
@@ -32,7 +32,7 @@ var package_default = {
32
32
  ],
33
33
  scripts: {
34
34
  build: "bun run build:js && bun run build:types",
35
- "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite",
35
+ "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar",
36
36
  "build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
37
37
  check: "bun run typecheck && bun run test && bun run build && bun run test:package",
38
38
  dev: "bun run src/cli.ts start",
@@ -43,10 +43,11 @@ var package_default = {
43
43
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
44
44
  },
45
45
  dependencies: {
46
- "@electric-sql/pglite": "0.5.4"
46
+ "@electric-sql/pglite": "0.5.4",
47
+ tar: "^7.5.22"
47
48
  },
48
49
  devDependencies: {
49
- "@supabase/supabase-js": "^2.57.4",
50
+ "@supabase/supabase-js": "^2.110.9",
50
51
  "@types/bun": "^1.3.14",
51
52
  typescript: "^5.9.3"
52
53
  },
@@ -8736,6 +8737,392 @@ async function findEphemeralPort(host = "127.0.0.1") {
8736
8737
  return port;
8737
8738
  }
8738
8739
 
8740
+ // src/snapshot.ts
8741
+ import { chmod as chmod2, copyFile, link as link2, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
8742
+ import { dirname as dirname4, join as join8, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
8743
+ import { create as createTar, extract as extractTar } from "tar";
8744
+ var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
8745
+ var SNAPSHOT_VERSION = 1;
8746
+ async function createSnapshot(options) {
8747
+ const paths = normalizePaths(options.paths);
8748
+ await assertSnapshotPaths(paths);
8749
+ await assertNoDataDirectoryLock(paths);
8750
+ const manifest = {
8751
+ format: SNAPSHOT_FORMAT,
8752
+ version: SNAPSHOT_VERSION,
8753
+ createdAt: new Date().toISOString(),
8754
+ packageVersion: options.packageVersion,
8755
+ storageBackend: options.storageBackend,
8756
+ includesDatabase: Boolean(paths.dataDir),
8757
+ includesLocalStorage: options.storageBackend === "fs",
8758
+ includesSecrets: true
8759
+ };
8760
+ const output = resolve3(options.output);
8761
+ if (await existingInfo(output))
8762
+ throw new Error(`snapshot output already exists: ${output}`);
8763
+ await mkdir6(dirname4(output), { recursive: true });
8764
+ const stagingRoot = await mkdtemp(join8(dirname4(output), ".supacloud-lite-snapshot-"));
8765
+ try {
8766
+ await writeFile5(join8(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
8767
+ `);
8768
+ await stageFile(paths.secretsFile, join8(stagingRoot, "secrets.json"));
8769
+ if (paths.dataDir)
8770
+ await stageDirectory(paths.dataDir, join8(stagingRoot, "database"));
8771
+ if (options.storageBackend === "fs")
8772
+ await stageDirectory(paths.storageDir, join8(stagingRoot, "storage"));
8773
+ const entries = ["manifest.json", "secrets.json"];
8774
+ if (paths.dataDir)
8775
+ entries.push("database");
8776
+ if (options.storageBackend === "fs")
8777
+ entries.push("storage");
8778
+ await createTar({ cwd: stagingRoot, file: output, gzip: true, portable: true }, entries);
8779
+ if (process.platform !== "win32")
8780
+ await chmod2(output, 384);
8781
+ return manifest;
8782
+ } catch (error) {
8783
+ await rm3(output, { force: true });
8784
+ throw error;
8785
+ } finally {
8786
+ await rm3(stagingRoot, { recursive: true, force: true });
8787
+ }
8788
+ }
8789
+ async function restoreSnapshot(options) {
8790
+ const paths = normalizePaths(options.paths);
8791
+ await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
8792
+ await assertNoDataDirectoryLock(paths);
8793
+ const stagingRoot = await mkdtemp(join8(dirname4(paths.stateDir), ".supacloud-lite-restore-"));
8794
+ const payloadRoot = join8(stagingRoot, "payload");
8795
+ const rollbackId = crypto.randomUUID();
8796
+ const rollbackPaths = [];
8797
+ try {
8798
+ await mkdir6(payloadRoot, { recursive: true });
8799
+ await extractTar({
8800
+ cwd: payloadRoot,
8801
+ file: resolve3(options.input),
8802
+ preserveOwner: false,
8803
+ preservePaths: false,
8804
+ strict: true,
8805
+ unlink: true,
8806
+ filter: (path, entry) => {
8807
+ const normalized = path.replaceAll("\\", "/");
8808
+ const allowedPath = normalized === "manifest.json" || normalized === "secrets.json" || normalized === "database" || normalized.startsWith("database/") || normalized === "storage" || normalized.startsWith("storage/");
8809
+ if (!allowedPath || normalized.startsWith("/") || normalized.split("/").includes("..")) {
8810
+ throw new Error(`snapshot contains an unsafe path: ${path}`);
8811
+ }
8812
+ const entryType = "type" in entry ? entry.type : undefined;
8813
+ if (entryType !== "File" && entryType !== "Directory") {
8814
+ throw new Error(`snapshot contains an unsupported entry type: ${entryType ?? "unknown"}`);
8815
+ }
8816
+ return true;
8817
+ }
8818
+ });
8819
+ await assertNoSymlinks(payloadRoot);
8820
+ const manifest = await readManifest(payloadRoot);
8821
+ if (manifest.storageBackend !== options.storageBackend) {
8822
+ throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
8823
+ }
8824
+ if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
8825
+ throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
8826
+ }
8827
+ if (manifest.includesLocalStorage !== (options.storageBackend === "fs")) {
8828
+ throw new Error("snapshot storage payload does not match the target storage backend");
8829
+ }
8830
+ await assertSnapshotPayload(payloadRoot, manifest);
8831
+ if (manifest.includesDatabase)
8832
+ await mkdir6(join8(payloadRoot, "database"), { recursive: true });
8833
+ if (manifest.includesLocalStorage)
8834
+ await mkdir6(join8(payloadRoot, "storage"), { recursive: true });
8835
+ await assertRestoreTargets(paths, manifest, options.force === true);
8836
+ const stateStage = join8(stagingRoot, "state");
8837
+ await mkdir6(stateStage, { recursive: true });
8838
+ await copyEntry(join8(payloadRoot, "secrets.json"), join8(stateStage, "secrets.json"));
8839
+ if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
8840
+ await copyEntry(join8(payloadRoot, "database"), join8(stateStage, relative2(paths.stateDir, paths.dataDir)));
8841
+ }
8842
+ if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
8843
+ await copyEntry(join8(payloadRoot, "storage"), join8(stateStage, relative2(paths.stateDir, paths.storageDir)));
8844
+ }
8845
+ const swaps = [];
8846
+ try {
8847
+ await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
8848
+ if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
8849
+ await applyDirectorySwap(join8(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
8850
+ }
8851
+ if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
8852
+ await applyDirectorySwap(join8(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
8853
+ }
8854
+ } catch (error) {
8855
+ await rollbackDirectorySwaps(swaps);
8856
+ throw error;
8857
+ }
8858
+ rollbackPaths.push(...swaps.flatMap((swap) => swap.rollbackPath ? [swap.rollbackPath] : []));
8859
+ if (process.platform !== "win32") {
8860
+ await hardenRestoredTree(paths.stateDir);
8861
+ if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir))
8862
+ await hardenRestoredTree(paths.dataDir);
8863
+ if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
8864
+ await hardenRestoredTree(paths.storageDir);
8865
+ }
8866
+ }
8867
+ return { manifest, rollbackPaths };
8868
+ } catch (error) {
8869
+ throw error instanceof Error ? error : new Error(String(error));
8870
+ } finally {
8871
+ await rm3(stagingRoot, { recursive: true, force: true });
8872
+ }
8873
+ }
8874
+ function normalizePaths(paths) {
8875
+ return {
8876
+ ...paths,
8877
+ projectDir: resolve3(paths.projectDir),
8878
+ stateDir: resolve3(paths.stateDir),
8879
+ dataDir: paths.dataDir ? resolve3(paths.dataDir) : undefined,
8880
+ storageDir: resolve3(paths.storageDir),
8881
+ secretsFile: resolve3(paths.secretsFile)
8882
+ };
8883
+ }
8884
+ async function assertSnapshotPaths(paths, options = {}) {
8885
+ try {
8886
+ if (paths.stateDir === parse2(paths.stateDir).root)
8887
+ throw new Error("snapshot state directory must not be the filesystem root");
8888
+ if (paths.secretsFile !== join8(paths.stateDir, "secrets.json"))
8889
+ throw new Error("snapshot secrets path must be inside the state directory");
8890
+ const stateInfo = await lstat2(paths.stateDir);
8891
+ if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
8892
+ throw new Error(`state directory must be a real directory: ${paths.stateDir}`);
8893
+ } catch (error) {
8894
+ if (error.code !== "ENOENT" || options.allowMissingState !== true)
8895
+ throw error;
8896
+ }
8897
+ if (paths.dataDir && paths.storageDir && pathsOverlap(paths.dataDir, paths.storageDir)) {
8898
+ throw new Error("database and storage directories must not overlap");
8899
+ }
8900
+ await assertDirectoryOrMissing(paths.dataDir);
8901
+ await assertDirectoryOrMissing(paths.storageDir);
8902
+ if (options.requireSecrets !== false) {
8903
+ const secretInfo = await lstat2(paths.secretsFile);
8904
+ if (!secretInfo.isFile() || secretInfo.isSymbolicLink())
8905
+ throw new Error(`secrets file must be a regular file: ${paths.secretsFile}`);
8906
+ }
8907
+ }
8908
+ async function assertDirectoryOrMissing(path) {
8909
+ if (!path)
8910
+ return;
8911
+ if (resolve3(path) === parse2(resolve3(path)).root)
8912
+ throw new Error(`snapshot path must not be the filesystem root: ${path}`);
8913
+ try {
8914
+ const info = await lstat2(path);
8915
+ if (!info.isDirectory() || info.isSymbolicLink())
8916
+ throw new Error(`snapshot path must be a real directory: ${path}`);
8917
+ } catch (error) {
8918
+ if (error.code !== "ENOENT")
8919
+ throw error;
8920
+ }
8921
+ }
8922
+ async function assertNoDataDirectoryLock(paths) {
8923
+ if (!paths.dataDir)
8924
+ return;
8925
+ const lockPath = `${paths.dataDir}.supacloud-lite.lock`;
8926
+ try {
8927
+ await lstat2(lockPath);
8928
+ throw new Error(`data directory is in use or has a stale lock: ${lockPath}; stop Lite and remove the lock manually if it is stale`);
8929
+ } catch (error) {
8930
+ if (error.code !== "ENOENT")
8931
+ throw error;
8932
+ }
8933
+ }
8934
+ async function stageDirectory(root, destination) {
8935
+ try {
8936
+ const info = await lstat2(root);
8937
+ if (!info.isDirectory() || info.isSymbolicLink())
8938
+ throw new Error(`snapshot path must be a real directory: ${root}`);
8939
+ } catch (error) {
8940
+ if (error.code === "ENOENT") {
8941
+ await mkdir6(destination, { recursive: true });
8942
+ return;
8943
+ }
8944
+ throw error;
8945
+ }
8946
+ await mkdir6(destination, { recursive: true });
8947
+ const walk = async (current, target) => {
8948
+ for (const entry of await readdir3(current, { withFileTypes: true })) {
8949
+ const fullPath = join8(current, entry.name);
8950
+ const targetPath = join8(target, entry.name);
8951
+ if (entry.isSymbolicLink())
8952
+ throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
8953
+ if (entry.isDirectory()) {
8954
+ await mkdir6(targetPath, { recursive: true });
8955
+ await walk(fullPath, targetPath);
8956
+ } else if (entry.isFile()) {
8957
+ await stageFile(fullPath, targetPath);
8958
+ } else {
8959
+ throw new Error(`snapshot refuses unsupported filesystem entry: ${fullPath}`);
8960
+ }
8961
+ }
8962
+ };
8963
+ await walk(root, destination);
8964
+ }
8965
+ async function stageFile(source, target) {
8966
+ await mkdir6(dirname4(target), { recursive: true });
8967
+ try {
8968
+ await link2(source, target);
8969
+ } catch (error) {
8970
+ const code = error.code;
8971
+ if (code !== "EXDEV" && code !== "EPERM" && code !== "EACCES")
8972
+ throw error;
8973
+ await copyFile(source, target);
8974
+ }
8975
+ }
8976
+ async function readManifest(payloadRoot) {
8977
+ let parsed;
8978
+ try {
8979
+ parsed = JSON.parse(await readFile7(join8(payloadRoot, "manifest.json"), "utf8"));
8980
+ } catch (error) {
8981
+ throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
8982
+ }
8983
+ if (!isSnapshotManifest(parsed))
8984
+ throw new Error("unsupported or invalid SupaCloud Lite snapshot manifest");
8985
+ return parsed;
8986
+ }
8987
+ function isSnapshotManifest(value) {
8988
+ if (!value || typeof value !== "object")
8989
+ return false;
8990
+ const candidate = value;
8991
+ return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true;
8992
+ }
8993
+ async function assertSnapshotPayload(payloadRoot, manifest) {
8994
+ const required = ["manifest.json", "secrets.json"];
8995
+ for (const path of required) {
8996
+ try {
8997
+ await lstat2(join8(payloadRoot, path));
8998
+ } catch {
8999
+ throw new Error(`snapshot is missing required payload: ${path}`);
9000
+ }
9001
+ }
9002
+ const allowed = [...required];
9003
+ if (manifest.includesDatabase)
9004
+ allowed.push("database");
9005
+ if (manifest.includesLocalStorage)
9006
+ allowed.push("storage");
9007
+ for (const entry of await readdir3(payloadRoot)) {
9008
+ if (!allowed.includes(entry)) {
9009
+ throw new Error(`snapshot contains an unexpected payload entry: ${entry}`);
9010
+ }
9011
+ }
9012
+ }
9013
+ async function assertRestoreTargets(paths, manifest, force) {
9014
+ const targets = [paths.stateDir];
9015
+ if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir))
9016
+ targets.push(paths.dataDir);
9017
+ if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
9018
+ targets.push(paths.storageDir);
9019
+ if (!force) {
9020
+ for (const target of targets) {
9021
+ if (await directoryHasEntries(target))
9022
+ throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
9023
+ }
9024
+ }
9025
+ }
9026
+ async function directoryHasEntries(path) {
9027
+ try {
9028
+ return (await readdir3(path)).length > 0;
9029
+ } catch (error) {
9030
+ if (error.code === "ENOENT")
9031
+ return false;
9032
+ throw error;
9033
+ }
9034
+ }
9035
+ async function applyDirectorySwap(source, target, force, rollbackId, swaps) {
9036
+ const targetInfo = await existingInfo(target);
9037
+ if (targetInfo && !targetInfo.isDirectory())
9038
+ throw new Error(`restore target is not a directory: ${target}`);
9039
+ const swap = { target };
9040
+ if (targetInfo) {
9041
+ if (!force) {
9042
+ if (await directoryHasEntries(target))
9043
+ throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
9044
+ await rm3(target, { recursive: true, force: true });
9045
+ } else {
9046
+ swap.rollbackPath = join8(dirname4(target), `.${target.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
9047
+ await rename2(target, swap.rollbackPath);
9048
+ }
9049
+ }
9050
+ try {
9051
+ await mkdir6(dirname4(target), { recursive: true });
9052
+ await rename2(source, target);
9053
+ swaps.push(swap);
9054
+ } catch (error) {
9055
+ if (swap.rollbackPath)
9056
+ await rename2(swap.rollbackPath, target).catch(() => {});
9057
+ throw error;
9058
+ }
9059
+ }
9060
+ async function rollbackDirectorySwaps(swaps) {
9061
+ for (const swap of [...swaps].reverse()) {
9062
+ await rm3(swap.target, { recursive: true, force: true });
9063
+ if (swap.rollbackPath)
9064
+ await rename2(swap.rollbackPath, swap.target);
9065
+ }
9066
+ }
9067
+ async function existingInfo(path) {
9068
+ try {
9069
+ return await lstat2(path);
9070
+ } catch (error) {
9071
+ if (error.code === "ENOENT")
9072
+ return null;
9073
+ throw error;
9074
+ }
9075
+ }
9076
+ async function copyEntry(source, target) {
9077
+ const info = await lstat2(source);
9078
+ if (info.isSymbolicLink())
9079
+ throw new Error(`snapshot refuses symbolic link: ${source}`);
9080
+ if (info.isDirectory()) {
9081
+ await mkdir6(target, { recursive: true });
9082
+ for (const entry of await readdir3(source))
9083
+ await copyEntry(join8(source, entry), join8(target, entry));
9084
+ } else if (info.isFile()) {
9085
+ await mkdir6(dirname4(target), { recursive: true });
9086
+ await Bun.write(target, Bun.file(source));
9087
+ } else
9088
+ throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
9089
+ }
9090
+ async function hardenRestoredTree(root) {
9091
+ const info = await lstat2(root);
9092
+ if (info.isSymbolicLink())
9093
+ throw new Error(`snapshot refuses symbolic link: ${root}`);
9094
+ if (info.isDirectory()) {
9095
+ await chmod2(root, 448);
9096
+ for (const entry of await readdir3(root))
9097
+ await hardenRestoredTree(join8(root, entry));
9098
+ return;
9099
+ }
9100
+ if (info.isFile()) {
9101
+ await chmod2(root, 384);
9102
+ return;
9103
+ }
9104
+ throw new Error(`snapshot refuses unsupported filesystem entry: ${root}`);
9105
+ }
9106
+ async function assertNoSymlinks(root) {
9107
+ for (const entry of await readdir3(root, { withFileTypes: true })) {
9108
+ const fullPath = join8(root, entry.name);
9109
+ if (entry.isSymbolicLink())
9110
+ throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
9111
+ if (entry.isDirectory())
9112
+ await assertNoSymlinks(fullPath);
9113
+ }
9114
+ }
9115
+ function isWithin(parent, child) {
9116
+ const normalizedParent = resolve3(parent);
9117
+ const normalizedChild = resolve3(child);
9118
+ return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
9119
+ }
9120
+ function pathsOverlap(left, right) {
9121
+ const normalizedLeft = resolve3(left);
9122
+ const normalizedRight = resolve3(right);
9123
+ return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
9124
+ }
9125
+
8739
9126
  // src/cli.ts
8740
9127
  function parseArgs(argv) {
8741
9128
  const args = [...argv];
@@ -8746,7 +9133,8 @@ function parseArgs(argv) {
8746
9133
  projectDir: process.cwd(),
8747
9134
  host: process.env.SUPACLOUD_LITE_HOST ?? "127.0.0.1",
8748
9135
  port: process.env.SUPACLOUD_LITE_PORT || process.env.PORT ? Number.parseInt(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 10) : 54321,
8749
- serviceRole: false
9136
+ serviceRole: false,
9137
+ force: false
8750
9138
  };
8751
9139
  for (let index = 0;index < args.length; index++) {
8752
9140
  const argument = args[index];
@@ -8765,13 +9153,13 @@ function parseArgs(argv) {
8765
9153
  else if (argument === "--site-url")
8766
9154
  options.siteUrl = next();
8767
9155
  else if (argument === "--project-dir" || argument === "--dir")
8768
- options.projectDir = resolve3(next());
9156
+ options.projectDir = resolve4(next());
8769
9157
  else if (argument === "--state-dir")
8770
- options.stateDir = resolve3(next());
9158
+ options.stateDir = resolve4(next());
8771
9159
  else if (argument === "--data-dir")
8772
- options.dataDir = resolve3(next());
9160
+ options.dataDir = resolve4(next());
8773
9161
  else if (argument === "--storage-dir")
8774
- options.storageDir = resolve3(next());
9162
+ options.storageDir = resolve4(next());
8775
9163
  else if (argument === "--storage-backend")
8776
9164
  options.storageBackend = next();
8777
9165
  else if (argument === "--s3-prefix")
@@ -8779,11 +9167,13 @@ function parseArgs(argv) {
8779
9167
  else if (argument === "--memory")
8780
9168
  options.memory = true;
8781
9169
  else if (argument === "--output" || argument === "-o")
8782
- options.output = resolve3(next());
9170
+ options.output = resolve4(next());
8783
9171
  else if (argument === "--file" || argument === "-f")
8784
9172
  options.diffFile = next();
8785
9173
  else if (argument === "--service-role")
8786
9174
  options.serviceRole = true;
9175
+ else if (argument === "--force")
9176
+ options.force = true;
8787
9177
  else if (argument === "--version") {
8788
9178
  console.log(package_default.version);
8789
9179
  process.exit(0);
@@ -8822,6 +9212,14 @@ Use --service-role to print the privileged key.`);
8822
9212
  await runDbCommand(options);
8823
9213
  return;
8824
9214
  }
9215
+ if (options.command === "snapshot") {
9216
+ await runSnapshotCommand(options);
9217
+ return;
9218
+ }
9219
+ if (options.command === "upgrade") {
9220
+ await runUpgradeCommand(options);
9221
+ return;
9222
+ }
8825
9223
  if (options.command === "gen") {
8826
9224
  if (options.positionals[0] && options.positionals[0] !== "types" && options.positionals[0] !== "typescript") {
8827
9225
  throw new Error(`unknown gen subcommand: ${options.positionals[0]}`);
@@ -8830,8 +9228,8 @@ Use --service-role to print the privileged key.`);
8830
9228
  try {
8831
9229
  const source = await generateTypes(project2.backend.db, "public");
8832
9230
  if (options.output) {
8833
- await mkdir6(dirname4(options.output), { recursive: true });
8834
- await writeFile5(options.output, source);
9231
+ await mkdir7(dirname5(options.output), { recursive: true });
9232
+ await writeFile6(options.output, source);
8835
9233
  console.log(`Wrote ${options.output}`);
8836
9234
  } else
8837
9235
  process.stdout.write(source);
@@ -8901,8 +9299,8 @@ async function runDbCommand(options) {
8901
9299
  }
8902
9300
  await assertResetPathsSafe(paths);
8903
9301
  if (paths.dataDir)
8904
- await rm3(paths.dataDir, { recursive: true, force: true });
8905
- await rm3(paths.storageDir, { recursive: true, force: true });
9302
+ await rm4(paths.dataDir, { recursive: true, force: true });
9303
+ await rm4(paths.storageDir, { recursive: true, force: true });
8906
9304
  const project2 = await createProjectBackend({ ...options, includeFunctions: false, includeWebhooks: false, log: quietLog });
8907
9305
  try {
8908
9306
  const applied = await project2.backend.db.listAppliedMigrations();
@@ -8912,7 +9310,7 @@ async function runDbCommand(options) {
8912
9310
  }
8913
9311
  return;
8914
9312
  }
8915
- const project = await loadSupabaseProject(resolve3(options.projectDir ?? process.cwd()));
9313
+ const project = await loadSupabaseProject(resolve4(options.projectDir ?? process.cwd()));
8916
9314
  if (subcommand === "diff") {
8917
9315
  const ddl = await computeDbDiff({ liveDataDir: paths.dataDir, migrations: project.migrations });
8918
9316
  if (ddl.length === 0) {
@@ -8925,9 +9323,9 @@ async function runDbCommand(options) {
8925
9323
  `;
8926
9324
  if (options.diffFile) {
8927
9325
  const stamp = timestamp();
8928
- const output = join8(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
8929
- await mkdir6(join8(paths.projectDir, "supabase", "migrations"), { recursive: true });
8930
- await writeFile5(output, source);
9326
+ const output = join9(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
9327
+ await mkdir7(join9(paths.projectDir, "supabase", "migrations"), { recursive: true });
9328
+ await writeFile6(output, source);
8931
9329
  console.log(`Wrote ${output}`);
8932
9330
  } else
8933
9331
  process.stdout.write(source);
@@ -8937,7 +9335,7 @@ async function runDbCommand(options) {
8937
9335
  const result = await pullSchema({
8938
9336
  liveDataDir: paths.dataDir,
8939
9337
  migrations: project.migrations,
8940
- migrationsDir: join8(paths.projectDir, "supabase", "migrations"),
9338
+ migrationsDir: join9(paths.projectDir, "supabase", "migrations"),
8941
9339
  name: options.positionals[1] ?? "remote_schema"
8942
9340
  });
8943
9341
  if (!result.path)
@@ -8948,6 +9346,63 @@ async function runDbCommand(options) {
8948
9346
  }
8949
9347
  throw new Error(`unknown db subcommand: ${subcommand ?? "(none)"}`);
8950
9348
  }
9349
+ async function runSnapshotCommand(options) {
9350
+ const subcommand = options.positionals[0];
9351
+ const paths = resolveProjectPaths(options);
9352
+ const storageBackend = resolveStorageBackend(options.storageBackend);
9353
+ if (options.memory)
9354
+ throw new Error("snapshot does not support --memory because the database is not durable");
9355
+ if (subcommand === "create") {
9356
+ await ensureProjectSecrets(paths);
9357
+ const output = options.output ?? join9(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
9358
+ const manifest = await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
9359
+ console.log(`Snapshot created: ${output}`);
9360
+ if (manifest.storageBackend === "s3")
9361
+ console.log("S3 objects were not copied; the snapshot contains database metadata and secrets only.");
9362
+ return;
9363
+ }
9364
+ if (subcommand === "restore") {
9365
+ const input = options.positionals[1];
9366
+ if (!input)
9367
+ throw new Error("snapshot restore requires a snapshot file");
9368
+ const result = await restoreSnapshot({ paths, storageBackend, input, force: options.force });
9369
+ console.log(`Snapshot restored from ${resolve4(input)}`);
9370
+ for (const rollbackPath of result.rollbackPaths)
9371
+ console.log(`Previous state retained at ${rollbackPath}`);
9372
+ if (result.manifest.storageBackend === "s3")
9373
+ console.log("Reconnect the original S3 bucket/prefix before starting Lite.");
9374
+ return;
9375
+ }
9376
+ throw new Error(`unknown snapshot subcommand: ${subcommand ?? "(none)"}`);
9377
+ }
9378
+ async function runUpgradeCommand(options) {
9379
+ if (options.memory)
9380
+ throw new Error("upgrade does not support --memory because there is no durable database to back up");
9381
+ const paths = resolveProjectPaths(options);
9382
+ const storageBackend = resolveStorageBackend(options.storageBackend);
9383
+ await ensureProjectSecrets(paths);
9384
+ const output = options.output ?? join9(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
9385
+ await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
9386
+ console.log(`Pre-upgrade snapshot: ${output}`);
9387
+ try {
9388
+ const project = await createProjectBackend({
9389
+ ...options,
9390
+ applyMigrations: true,
9391
+ includeFunctions: false,
9392
+ includeWebhooks: false,
9393
+ includeSeed: true,
9394
+ log: quietLog
9395
+ });
9396
+ try {
9397
+ const applied = await project.backend.db.listAppliedMigrations();
9398
+ console.log(`Upgrade complete on @supacloud/lite ${package_default.version}: ${applied.length} migration(s) recorded.`);
9399
+ } finally {
9400
+ await project.backend.close();
9401
+ }
9402
+ } catch (error) {
9403
+ throw new Error(`upgrade failed; snapshot retained at ${output}. Restore it with ` + `"supacloud-lite snapshot restore ${output} --force". ${error instanceof Error ? error.message : String(error)}`);
9404
+ }
9405
+ }
8951
9406
  function printInspection(rows) {
8952
9407
  if (rows.length === 0) {
8953
9408
  console.log('No tables in schema "public".');
@@ -8984,6 +9439,9 @@ Commands:
8984
9439
  db reset wipe database and storage, then re-run migrations
8985
9440
  db diff print schema changes outside migrations
8986
9441
  db pull [name] write live schema changes as an applied migration
9442
+ snapshot create create a compressed database/storage/secrets snapshot
9443
+ snapshot restore <f> restore a snapshot into an empty target
9444
+ upgrade snapshot first, then apply pending migrations
8987
9445
  inspect show table rows and sizes
8988
9446
  version print the package version
8989
9447
 
@@ -9001,6 +9459,7 @@ Options:
9001
9459
  --memory use an in-memory PGlite database
9002
9460
  -o, --output <p> output file for gen types
9003
9461
  -f, --file <name> migration suffix for db diff
9462
+ --force replace non-empty restore targets and retain rollback copies
9004
9463
  `);
9005
9464
  }
9006
9465
  function formatStorage(backend, storageDir) {
package/dist/index.d.ts CHANGED
@@ -9,5 +9,6 @@ export { FsStorageDriver } from './vendor/tinbase/node/fs-driver.js';
9
9
  export { serveBun } from './vendor/tinbase/node/bun-server.js';
10
10
  export type { RunningServer, ServerHandle, ServeOptions } from './vendor/tinbase/node/bun-server.js';
11
11
  export { createProjectBackend, ensureProjectSecrets, mintProjectKeys, resolveStorageBackend, resolveProjectPaths, startProjectServer, } from './project-runtime.js';
12
+ export { createSnapshot, restoreSnapshot, type CreateSnapshotOptions, type RestoreSnapshotOptions, type RestoreSnapshotResult, type SnapshotManifest, } from './snapshot.js';
12
13
  export type { ConfiguredStorageBackend, ProjectBackend, ProjectPaths, ProjectRuntimeOptions, ProjectSecrets, RunningProjectServer, StorageBackend, } from './project-runtime.js';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,IAAI,iBAAiB,EAClC,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,SAAS,EACT,OAAO,EACP,SAAS,GACV,MAAM,2BAA2B,CAAA;AAClC,YAAY,EACV,aAAa,IAAI,mBAAmB,EACpC,cAAc,IAAI,oBAAoB,GACvC,MAAM,2BAA2B,CAAA;AAClC,YAAY,EACV,UAAU,EACV,MAAM,EACN,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EACL,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,uCAAuC,CAAA;AAC9C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAA;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAA;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAC9D,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AACpG,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,cAAc,GACf,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,IAAI,iBAAiB,EAClC,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,SAAS,EACT,OAAO,EACP,SAAS,GACV,MAAM,2BAA2B,CAAA;AAClC,YAAY,EACV,aAAa,IAAI,mBAAmB,EACpC,cAAc,IAAI,oBAAoB,GACvC,MAAM,2BAA2B,CAAA;AAClC,YAAY,EACV,UAAU,EACV,MAAM,EACN,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EACL,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,uCAAuC,CAAA;AAC9C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAA;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAA;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAC9D,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AACpG,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,cAAc,EACd,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,cAAc,GACf,MAAM,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -71,7 +71,7 @@ function randomToken(bytes = 32) {
71
71
  // package.json
72
72
  var package_default = {
73
73
  name: "@supacloud/lite",
74
- version: "0.2.0",
74
+ version: "0.3.0",
75
75
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
76
76
  type: "module",
77
77
  license: "Apache-2.0",
@@ -95,7 +95,7 @@ var package_default = {
95
95
  ],
96
96
  scripts: {
97
97
  build: "bun run build:js && bun run build:types",
98
- "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite",
98
+ "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar",
99
99
  "build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
100
100
  check: "bun run typecheck && bun run test && bun run build && bun run test:package",
101
101
  dev: "bun run src/cli.ts start",
@@ -106,10 +106,11 @@ var package_default = {
106
106
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
107
107
  },
108
108
  dependencies: {
109
- "@electric-sql/pglite": "0.5.4"
109
+ "@electric-sql/pglite": "0.5.4",
110
+ tar: "^7.5.22"
110
111
  },
111
112
  devDependencies: {
112
- "@supabase/supabase-js": "^2.57.4",
113
+ "@supabase/supabase-js": "^2.110.9",
113
114
  "@types/bun": "^1.3.14",
114
115
  typescript: "^5.9.3"
115
116
  },
@@ -8447,11 +8448,397 @@ async function findEphemeralPort(host = "127.0.0.1") {
8447
8448
  throw new Error("Bun did not allocate an ephemeral port");
8448
8449
  return port;
8449
8450
  }
8451
+ // src/snapshot.ts
8452
+ import { chmod as chmod2, copyFile, link as link2, lstat as lstat2, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
8453
+ import { dirname as dirname4, join as join7, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
8454
+ import { create as createTar, extract as extractTar } from "tar";
8455
+ var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
8456
+ var SNAPSHOT_VERSION = 1;
8457
+ async function createSnapshot(options) {
8458
+ const paths = normalizePaths(options.paths);
8459
+ await assertSnapshotPaths(paths);
8460
+ await assertNoDataDirectoryLock(paths);
8461
+ const manifest = {
8462
+ format: SNAPSHOT_FORMAT,
8463
+ version: SNAPSHOT_VERSION,
8464
+ createdAt: new Date().toISOString(),
8465
+ packageVersion: options.packageVersion,
8466
+ storageBackend: options.storageBackend,
8467
+ includesDatabase: Boolean(paths.dataDir),
8468
+ includesLocalStorage: options.storageBackend === "fs",
8469
+ includesSecrets: true
8470
+ };
8471
+ const output = resolve3(options.output);
8472
+ if (await existingInfo(output))
8473
+ throw new Error(`snapshot output already exists: ${output}`);
8474
+ await mkdir5(dirname4(output), { recursive: true });
8475
+ const stagingRoot = await mkdtemp(join7(dirname4(output), ".supacloud-lite-snapshot-"));
8476
+ try {
8477
+ await writeFile4(join7(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
8478
+ `);
8479
+ await stageFile(paths.secretsFile, join7(stagingRoot, "secrets.json"));
8480
+ if (paths.dataDir)
8481
+ await stageDirectory(paths.dataDir, join7(stagingRoot, "database"));
8482
+ if (options.storageBackend === "fs")
8483
+ await stageDirectory(paths.storageDir, join7(stagingRoot, "storage"));
8484
+ const entries = ["manifest.json", "secrets.json"];
8485
+ if (paths.dataDir)
8486
+ entries.push("database");
8487
+ if (options.storageBackend === "fs")
8488
+ entries.push("storage");
8489
+ await createTar({ cwd: stagingRoot, file: output, gzip: true, portable: true }, entries);
8490
+ if (process.platform !== "win32")
8491
+ await chmod2(output, 384);
8492
+ return manifest;
8493
+ } catch (error) {
8494
+ await rm3(output, { force: true });
8495
+ throw error;
8496
+ } finally {
8497
+ await rm3(stagingRoot, { recursive: true, force: true });
8498
+ }
8499
+ }
8500
+ async function restoreSnapshot(options) {
8501
+ const paths = normalizePaths(options.paths);
8502
+ await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
8503
+ await assertNoDataDirectoryLock(paths);
8504
+ const stagingRoot = await mkdtemp(join7(dirname4(paths.stateDir), ".supacloud-lite-restore-"));
8505
+ const payloadRoot = join7(stagingRoot, "payload");
8506
+ const rollbackId = crypto.randomUUID();
8507
+ const rollbackPaths = [];
8508
+ try {
8509
+ await mkdir5(payloadRoot, { recursive: true });
8510
+ await extractTar({
8511
+ cwd: payloadRoot,
8512
+ file: resolve3(options.input),
8513
+ preserveOwner: false,
8514
+ preservePaths: false,
8515
+ strict: true,
8516
+ unlink: true,
8517
+ filter: (path, entry) => {
8518
+ const normalized = path.replaceAll("\\", "/");
8519
+ const allowedPath = normalized === "manifest.json" || normalized === "secrets.json" || normalized === "database" || normalized.startsWith("database/") || normalized === "storage" || normalized.startsWith("storage/");
8520
+ if (!allowedPath || normalized.startsWith("/") || normalized.split("/").includes("..")) {
8521
+ throw new Error(`snapshot contains an unsafe path: ${path}`);
8522
+ }
8523
+ const entryType = "type" in entry ? entry.type : undefined;
8524
+ if (entryType !== "File" && entryType !== "Directory") {
8525
+ throw new Error(`snapshot contains an unsupported entry type: ${entryType ?? "unknown"}`);
8526
+ }
8527
+ return true;
8528
+ }
8529
+ });
8530
+ await assertNoSymlinks(payloadRoot);
8531
+ const manifest = await readManifest(payloadRoot);
8532
+ if (manifest.storageBackend !== options.storageBackend) {
8533
+ throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
8534
+ }
8535
+ if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
8536
+ throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
8537
+ }
8538
+ if (manifest.includesLocalStorage !== (options.storageBackend === "fs")) {
8539
+ throw new Error("snapshot storage payload does not match the target storage backend");
8540
+ }
8541
+ await assertSnapshotPayload(payloadRoot, manifest);
8542
+ if (manifest.includesDatabase)
8543
+ await mkdir5(join7(payloadRoot, "database"), { recursive: true });
8544
+ if (manifest.includesLocalStorage)
8545
+ await mkdir5(join7(payloadRoot, "storage"), { recursive: true });
8546
+ await assertRestoreTargets(paths, manifest, options.force === true);
8547
+ const stateStage = join7(stagingRoot, "state");
8548
+ await mkdir5(stateStage, { recursive: true });
8549
+ await copyEntry(join7(payloadRoot, "secrets.json"), join7(stateStage, "secrets.json"));
8550
+ if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
8551
+ await copyEntry(join7(payloadRoot, "database"), join7(stateStage, relative2(paths.stateDir, paths.dataDir)));
8552
+ }
8553
+ if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
8554
+ await copyEntry(join7(payloadRoot, "storage"), join7(stateStage, relative2(paths.stateDir, paths.storageDir)));
8555
+ }
8556
+ const swaps = [];
8557
+ try {
8558
+ await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
8559
+ if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
8560
+ await applyDirectorySwap(join7(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
8561
+ }
8562
+ if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
8563
+ await applyDirectorySwap(join7(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
8564
+ }
8565
+ } catch (error) {
8566
+ await rollbackDirectorySwaps(swaps);
8567
+ throw error;
8568
+ }
8569
+ rollbackPaths.push(...swaps.flatMap((swap) => swap.rollbackPath ? [swap.rollbackPath] : []));
8570
+ if (process.platform !== "win32") {
8571
+ await hardenRestoredTree(paths.stateDir);
8572
+ if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir))
8573
+ await hardenRestoredTree(paths.dataDir);
8574
+ if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
8575
+ await hardenRestoredTree(paths.storageDir);
8576
+ }
8577
+ }
8578
+ return { manifest, rollbackPaths };
8579
+ } catch (error) {
8580
+ throw error instanceof Error ? error : new Error(String(error));
8581
+ } finally {
8582
+ await rm3(stagingRoot, { recursive: true, force: true });
8583
+ }
8584
+ }
8585
+ function normalizePaths(paths) {
8586
+ return {
8587
+ ...paths,
8588
+ projectDir: resolve3(paths.projectDir),
8589
+ stateDir: resolve3(paths.stateDir),
8590
+ dataDir: paths.dataDir ? resolve3(paths.dataDir) : undefined,
8591
+ storageDir: resolve3(paths.storageDir),
8592
+ secretsFile: resolve3(paths.secretsFile)
8593
+ };
8594
+ }
8595
+ async function assertSnapshotPaths(paths, options = {}) {
8596
+ try {
8597
+ if (paths.stateDir === parse2(paths.stateDir).root)
8598
+ throw new Error("snapshot state directory must not be the filesystem root");
8599
+ if (paths.secretsFile !== join7(paths.stateDir, "secrets.json"))
8600
+ throw new Error("snapshot secrets path must be inside the state directory");
8601
+ const stateInfo = await lstat2(paths.stateDir);
8602
+ if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
8603
+ throw new Error(`state directory must be a real directory: ${paths.stateDir}`);
8604
+ } catch (error) {
8605
+ if (error.code !== "ENOENT" || options.allowMissingState !== true)
8606
+ throw error;
8607
+ }
8608
+ if (paths.dataDir && paths.storageDir && pathsOverlap(paths.dataDir, paths.storageDir)) {
8609
+ throw new Error("database and storage directories must not overlap");
8610
+ }
8611
+ await assertDirectoryOrMissing(paths.dataDir);
8612
+ await assertDirectoryOrMissing(paths.storageDir);
8613
+ if (options.requireSecrets !== false) {
8614
+ const secretInfo = await lstat2(paths.secretsFile);
8615
+ if (!secretInfo.isFile() || secretInfo.isSymbolicLink())
8616
+ throw new Error(`secrets file must be a regular file: ${paths.secretsFile}`);
8617
+ }
8618
+ }
8619
+ async function assertDirectoryOrMissing(path) {
8620
+ if (!path)
8621
+ return;
8622
+ if (resolve3(path) === parse2(resolve3(path)).root)
8623
+ throw new Error(`snapshot path must not be the filesystem root: ${path}`);
8624
+ try {
8625
+ const info = await lstat2(path);
8626
+ if (!info.isDirectory() || info.isSymbolicLink())
8627
+ throw new Error(`snapshot path must be a real directory: ${path}`);
8628
+ } catch (error) {
8629
+ if (error.code !== "ENOENT")
8630
+ throw error;
8631
+ }
8632
+ }
8633
+ async function assertNoDataDirectoryLock(paths) {
8634
+ if (!paths.dataDir)
8635
+ return;
8636
+ const lockPath = `${paths.dataDir}.supacloud-lite.lock`;
8637
+ try {
8638
+ await lstat2(lockPath);
8639
+ throw new Error(`data directory is in use or has a stale lock: ${lockPath}; stop Lite and remove the lock manually if it is stale`);
8640
+ } catch (error) {
8641
+ if (error.code !== "ENOENT")
8642
+ throw error;
8643
+ }
8644
+ }
8645
+ async function stageDirectory(root, destination) {
8646
+ try {
8647
+ const info = await lstat2(root);
8648
+ if (!info.isDirectory() || info.isSymbolicLink())
8649
+ throw new Error(`snapshot path must be a real directory: ${root}`);
8650
+ } catch (error) {
8651
+ if (error.code === "ENOENT") {
8652
+ await mkdir5(destination, { recursive: true });
8653
+ return;
8654
+ }
8655
+ throw error;
8656
+ }
8657
+ await mkdir5(destination, { recursive: true });
8658
+ const walk = async (current, target) => {
8659
+ for (const entry of await readdir3(current, { withFileTypes: true })) {
8660
+ const fullPath = join7(current, entry.name);
8661
+ const targetPath = join7(target, entry.name);
8662
+ if (entry.isSymbolicLink())
8663
+ throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
8664
+ if (entry.isDirectory()) {
8665
+ await mkdir5(targetPath, { recursive: true });
8666
+ await walk(fullPath, targetPath);
8667
+ } else if (entry.isFile()) {
8668
+ await stageFile(fullPath, targetPath);
8669
+ } else {
8670
+ throw new Error(`snapshot refuses unsupported filesystem entry: ${fullPath}`);
8671
+ }
8672
+ }
8673
+ };
8674
+ await walk(root, destination);
8675
+ }
8676
+ async function stageFile(source, target) {
8677
+ await mkdir5(dirname4(target), { recursive: true });
8678
+ try {
8679
+ await link2(source, target);
8680
+ } catch (error) {
8681
+ const code = error.code;
8682
+ if (code !== "EXDEV" && code !== "EPERM" && code !== "EACCES")
8683
+ throw error;
8684
+ await copyFile(source, target);
8685
+ }
8686
+ }
8687
+ async function readManifest(payloadRoot) {
8688
+ let parsed;
8689
+ try {
8690
+ parsed = JSON.parse(await readFile7(join7(payloadRoot, "manifest.json"), "utf8"));
8691
+ } catch (error) {
8692
+ throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
8693
+ }
8694
+ if (!isSnapshotManifest(parsed))
8695
+ throw new Error("unsupported or invalid SupaCloud Lite snapshot manifest");
8696
+ return parsed;
8697
+ }
8698
+ function isSnapshotManifest(value) {
8699
+ if (!value || typeof value !== "object")
8700
+ return false;
8701
+ const candidate = value;
8702
+ return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true;
8703
+ }
8704
+ async function assertSnapshotPayload(payloadRoot, manifest) {
8705
+ const required = ["manifest.json", "secrets.json"];
8706
+ for (const path of required) {
8707
+ try {
8708
+ await lstat2(join7(payloadRoot, path));
8709
+ } catch {
8710
+ throw new Error(`snapshot is missing required payload: ${path}`);
8711
+ }
8712
+ }
8713
+ const allowed = [...required];
8714
+ if (manifest.includesDatabase)
8715
+ allowed.push("database");
8716
+ if (manifest.includesLocalStorage)
8717
+ allowed.push("storage");
8718
+ for (const entry of await readdir3(payloadRoot)) {
8719
+ if (!allowed.includes(entry)) {
8720
+ throw new Error(`snapshot contains an unexpected payload entry: ${entry}`);
8721
+ }
8722
+ }
8723
+ }
8724
+ async function assertRestoreTargets(paths, manifest, force) {
8725
+ const targets = [paths.stateDir];
8726
+ if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir))
8727
+ targets.push(paths.dataDir);
8728
+ if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
8729
+ targets.push(paths.storageDir);
8730
+ if (!force) {
8731
+ for (const target of targets) {
8732
+ if (await directoryHasEntries(target))
8733
+ throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
8734
+ }
8735
+ }
8736
+ }
8737
+ async function directoryHasEntries(path) {
8738
+ try {
8739
+ return (await readdir3(path)).length > 0;
8740
+ } catch (error) {
8741
+ if (error.code === "ENOENT")
8742
+ return false;
8743
+ throw error;
8744
+ }
8745
+ }
8746
+ async function applyDirectorySwap(source, target, force, rollbackId, swaps) {
8747
+ const targetInfo = await existingInfo(target);
8748
+ if (targetInfo && !targetInfo.isDirectory())
8749
+ throw new Error(`restore target is not a directory: ${target}`);
8750
+ const swap = { target };
8751
+ if (targetInfo) {
8752
+ if (!force) {
8753
+ if (await directoryHasEntries(target))
8754
+ throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
8755
+ await rm3(target, { recursive: true, force: true });
8756
+ } else {
8757
+ swap.rollbackPath = join7(dirname4(target), `.${target.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
8758
+ await rename2(target, swap.rollbackPath);
8759
+ }
8760
+ }
8761
+ try {
8762
+ await mkdir5(dirname4(target), { recursive: true });
8763
+ await rename2(source, target);
8764
+ swaps.push(swap);
8765
+ } catch (error) {
8766
+ if (swap.rollbackPath)
8767
+ await rename2(swap.rollbackPath, target).catch(() => {});
8768
+ throw error;
8769
+ }
8770
+ }
8771
+ async function rollbackDirectorySwaps(swaps) {
8772
+ for (const swap of [...swaps].reverse()) {
8773
+ await rm3(swap.target, { recursive: true, force: true });
8774
+ if (swap.rollbackPath)
8775
+ await rename2(swap.rollbackPath, swap.target);
8776
+ }
8777
+ }
8778
+ async function existingInfo(path) {
8779
+ try {
8780
+ return await lstat2(path);
8781
+ } catch (error) {
8782
+ if (error.code === "ENOENT")
8783
+ return null;
8784
+ throw error;
8785
+ }
8786
+ }
8787
+ async function copyEntry(source, target) {
8788
+ const info = await lstat2(source);
8789
+ if (info.isSymbolicLink())
8790
+ throw new Error(`snapshot refuses symbolic link: ${source}`);
8791
+ if (info.isDirectory()) {
8792
+ await mkdir5(target, { recursive: true });
8793
+ for (const entry of await readdir3(source))
8794
+ await copyEntry(join7(source, entry), join7(target, entry));
8795
+ } else if (info.isFile()) {
8796
+ await mkdir5(dirname4(target), { recursive: true });
8797
+ await Bun.write(target, Bun.file(source));
8798
+ } else
8799
+ throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
8800
+ }
8801
+ async function hardenRestoredTree(root) {
8802
+ const info = await lstat2(root);
8803
+ if (info.isSymbolicLink())
8804
+ throw new Error(`snapshot refuses symbolic link: ${root}`);
8805
+ if (info.isDirectory()) {
8806
+ await chmod2(root, 448);
8807
+ for (const entry of await readdir3(root))
8808
+ await hardenRestoredTree(join7(root, entry));
8809
+ return;
8810
+ }
8811
+ if (info.isFile()) {
8812
+ await chmod2(root, 384);
8813
+ return;
8814
+ }
8815
+ throw new Error(`snapshot refuses unsupported filesystem entry: ${root}`);
8816
+ }
8817
+ async function assertNoSymlinks(root) {
8818
+ for (const entry of await readdir3(root, { withFileTypes: true })) {
8819
+ const fullPath = join7(root, entry.name);
8820
+ if (entry.isSymbolicLink())
8821
+ throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
8822
+ if (entry.isDirectory())
8823
+ await assertNoSymlinks(fullPath);
8824
+ }
8825
+ }
8826
+ function isWithin(parent, child) {
8827
+ const normalizedParent = resolve3(parent);
8828
+ const normalizedChild = resolve3(child);
8829
+ return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
8830
+ }
8831
+ function pathsOverlap(left, right) {
8832
+ const normalizedLeft = resolve3(left);
8833
+ const normalizedRight = resolve3(right);
8834
+ return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
8835
+ }
8450
8836
  export {
8451
8837
  verifyJwt,
8452
8838
  startProjectServer,
8453
8839
  signJwt,
8454
8840
  serveBun,
8841
+ restoreSnapshot,
8455
8842
  resolveStorageBackend,
8456
8843
  resolveProjectPaths,
8457
8844
  mintProjectKeys,
@@ -8459,6 +8846,7 @@ export {
8459
8846
  generateTypes,
8460
8847
  ensureProjectSecrets,
8461
8848
  decodeJwt,
8849
+ createSnapshot,
8462
8850
  createProjectBackend,
8463
8851
  createPgliteEngine,
8464
8852
  createBackend as createLiteBackend,
@@ -0,0 +1,41 @@
1
+ import type { ConfiguredStorageBackend, ProjectPaths } from './project-runtime.js';
2
+ declare const SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
3
+ declare const SNAPSHOT_VERSION = 1;
4
+ export interface SnapshotManifest {
5
+ format: typeof SNAPSHOT_FORMAT;
6
+ version: typeof SNAPSHOT_VERSION;
7
+ createdAt: string;
8
+ packageVersion: string;
9
+ storageBackend: ConfiguredStorageBackend;
10
+ includesDatabase: boolean;
11
+ includesLocalStorage: boolean;
12
+ includesSecrets: true;
13
+ }
14
+ export interface CreateSnapshotOptions {
15
+ paths: ProjectPaths;
16
+ packageVersion: string;
17
+ storageBackend: ConfiguredStorageBackend;
18
+ output: string;
19
+ }
20
+ export interface RestoreSnapshotOptions {
21
+ paths: ProjectPaths;
22
+ storageBackend: ConfiguredStorageBackend;
23
+ input: string;
24
+ force?: boolean;
25
+ }
26
+ export interface RestoreSnapshotResult {
27
+ manifest: SnapshotManifest;
28
+ rollbackPaths: string[];
29
+ }
30
+ /**
31
+ * Create a portable, compressed snapshot of the durable Lite state.
32
+ * The caller must stop Lite first; the data-directory lock is checked here.
33
+ */
34
+ export declare function createSnapshot(options: CreateSnapshotOptions): Promise<SnapshotManifest>;
35
+ /**
36
+ * Restore a snapshot without touching an existing non-empty target unless
37
+ * `force` is explicitly set. Existing targets are renamed aside for rollback.
38
+ */
39
+ export declare function restoreSnapshot(options: RestoreSnapshotOptions): Promise<RestoreSnapshotResult>;
40
+ export {};
41
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,wBAAwB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAElF,QAAA,MAAM,eAAe,4BAA4B,CAAA;AACjD,QAAA,MAAM,gBAAgB,IAAI,CAAA;AAE1B,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,eAAe,CAAA;IAC9B,OAAO,EAAE,OAAO,gBAAgB,CAAA;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,wBAAwB,CAAA;IACxC,gBAAgB,EAAE,OAAO,CAAA;IACzB,oBAAoB,EAAE,OAAO,CAAA;IAC7B,eAAe,EAAE,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,wBAAwB,CAAA;IACxC,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,CAAA;IACnB,cAAc,EAAE,wBAAwB,CAAA;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAA;CACxB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqC9F;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0FrG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/lite",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Bun-native, single-project Supabase-compatible backend powered by PGlite",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "build": "bun run build:js && bun run build:types",
27
- "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite",
27
+ "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar",
28
28
  "build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
29
29
  "check": "bun run typecheck && bun run test && bun run build && bun run test:package",
30
30
  "dev": "bun run src/cli.ts start",
@@ -35,10 +35,11 @@
35
35
  "typecheck": "bun x tsc --noEmit -p tsconfig.json"
36
36
  },
37
37
  "dependencies": {
38
- "@electric-sql/pglite": "0.5.4"
38
+ "@electric-sql/pglite": "0.5.4",
39
+ "tar": "^7.5.22"
39
40
  },
40
41
  "devDependencies": {
41
- "@supabase/supabase-js": "^2.57.4",
42
+ "@supabase/supabase-js": "^2.110.9",
42
43
  "@types/bun": "^1.3.14",
43
44
  "typescript": "^5.9.3"
44
45
  },