@supacloud/lite 0.28.0 → 0.28.2

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.2](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.1...supacloud-lite-v0.28.2) (2026-10-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **lite:** enforce RPC transaction access modes ([#1936](https://github.com/vibeunion/supacloud/issues/1936)) ([fb90dcc](https://github.com/vibeunion/supacloud/commit/fb90dcc43ed49a45656e1877493eaa641ef494da))
9
+
10
+ ## [0.28.1](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.0...supacloud-lite-v0.28.1) (2026-10-15)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **lite:** roll back rejected singular REST writes ([#1925](https://github.com/vibeunion/supacloud/issues/1925)) ([0af235d](https://github.com/vibeunion/supacloud/commit/0af235d55a3619ffd150e4b16751a27a32d1f2e0))
16
+
3
17
  ## [0.28.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.27.1...supacloud-lite-v0.28.0) (2026-10-14)
4
18
 
5
19
 
package/RELEASING.md CHANGED
@@ -30,6 +30,15 @@ bun run build:standalone:host
30
30
  bun run test:standalone
31
31
  ```
32
32
 
33
+ 针对本地 Auth 的聚焦回归,只构建 host 产物并运行单个脚本的 Auth 入口:
34
+
35
+ ```bash
36
+ bun run build:standalone:host
37
+ bun run scripts/standalone-smoke.ts --auth-only
38
+ ```
39
+
40
+ 该入口在不含 Bun/Node/npm 的子进程 `PATH` 下启动二进制,通过真实 HTTP 和 `supabase-js` 验证注册、密码登录、刷新轮换、退出撤销,以及进程重启后的会话恢复、刷新与撤销保留。成功标记为 `standalone-auth-smoke-ok`。可用 `SUPACLOUD_LITE_STANDALONE_BINARY` 指定待验收产物。它不执行 Functions、扩展、升级、快照或恢复,不能替代完整发布验收;默认完整 smoke 也会在升级和快照恢复阶段验证同一 Auth 状态。
41
+
33
42
  Release workflow 为 Linux x64 baseline/arm64、macOS x64/arm64 和 Windows x64 baseline 构建独立产物。Linux x64 产物必须在发布 runner 上完成真实黑盒测试;其他交叉编译产物在具备对应原生 runner 前只视为已构建,不能替代平台验收。
34
43
 
35
44
  Lite release 会同时上传 `SHA256SUMS`、项目自身的 `SUPACLOUD-LITE-APACHE-2.0.txt`、`NOTICE`、`THIRD_PARTY_NOTICES.md` 和 `LICENSES/*.txt`,并为发布资产生成 provenance。正式对外分发 macOS 或 Windows 产物前,还应按项目发布策略完成代码签名;构建成功不代表已经签名或公证。
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { dirname as dirname8, join as join10, resolve as resolve6 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.28.0",
11
+ version: "0.28.2",
12
12
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
@@ -8368,6 +8368,8 @@ function statusForSqlState(code) {
8368
8368
  switch (code) {
8369
8369
  case "42501":
8370
8370
  return 403;
8371
+ case "25006":
8372
+ return 405;
8371
8373
  case "23503":
8372
8374
  case "23505":
8373
8375
  return 409;
@@ -8620,7 +8622,10 @@ class RestHandler {
8620
8622
  const { rows, affected } = await this.db.withContext(ctx, async (query) => {
8621
8623
  const res = await query(built.sql, built.params);
8622
8624
  if (opts.queryReturning) {
8623
- return { rows: res.rows[0].body, affected: null };
8625
+ const rows = res.rows[0].body;
8626
+ if (opts.wantsObject && !opts.wantsCsv)
8627
+ assertObjectCardinality(rows);
8628
+ return { rows, affected: null };
8624
8629
  }
8625
8630
  return { rows: null, affected: res.affectedRows ?? 0 };
8626
8631
  });
@@ -8652,14 +8657,7 @@ class RestHandler {
8652
8657
  return new Response(opts.head ? null : rowsToCsv(rows), { status: opts.status, headers });
8653
8658
  }
8654
8659
  if (opts.wantsObject) {
8655
- if (rows.length !== 1) {
8656
- return jsonResponse(406, {
8657
- code: "PGRST116",
8658
- message: "JSON object requested, multiple (or no) rows returned",
8659
- details: `The result contains ${rows.length} rows`,
8660
- hint: null
8661
- });
8662
- }
8660
+ assertObjectCardinality(rows);
8663
8661
  headers["content-type"] = `${OBJECT_MEDIA}; charset=utf-8`;
8664
8662
  return new Response(opts.head ? null : JSON.stringify(rows[0]), {
8665
8663
  status: opts.status,
@@ -8723,17 +8721,22 @@ class RestHandler {
8723
8721
  return await this.runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method);
8724
8722
  }
8725
8723
  async runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method) {
8724
+ const withRpcContext = (run) => this.db.withContext(ctx, async (query) => {
8725
+ if (method !== "POST" || fn.volatility !== "v")
8726
+ await query("set transaction read only");
8727
+ return run(query);
8728
+ });
8726
8729
  if (fn.returnType === "void" && !fn.returnsSet) {
8727
- await this.db.withContext(ctx, (query) => query(`select ${call}`, []));
8730
+ await withRpcContext((query) => query(`select ${call}`, []));
8728
8731
  return new Response(null, { status: 204 });
8729
8732
  }
8730
8733
  if (!fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8731
- const res = await this.db.withContext(ctx, (query) => query(`select to_json(${call}) as result`, []));
8734
+ const res = await withRpcContext((query) => query(`select to_json(${call}) as result`, []));
8732
8735
  const value = res.rows[0].result;
8733
8736
  return jsonResponse(200, value);
8734
8737
  }
8735
8738
  if (fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8736
- const res = await this.db.withContext(ctx, (query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8739
+ const res = await withRpcContext((query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8737
8740
  return jsonResponse(200, res.rows[0].body);
8738
8741
  }
8739
8742
  const alias = "_f";
@@ -8756,17 +8759,18 @@ class RestHandler {
8756
8759
  const where = conds.length > 0 ? ` where ${conds.join(" and ")}` : "";
8757
8760
  const order = builder.renderOrder([], alias);
8758
8761
  const limitOffset = builder.renderLimitOffset("");
8759
- const core = `select ${exprs.join(", ")} from ${call} as ${quoteIdent(alias)}${where}${order}${limitOffset}`;
8760
- const sql = `select coalesce(json_agg(row_to_json(_r)), '[]'::json) as body from (${core}) _r`;
8761
- const countSql = prefer.count !== undefined ? `select count(*)::int as count from ${call} as ${quoteIdent(alias)}${where}` : undefined;
8762
- const { rows, count } = await this.db.withContext(ctx, async (query) => {
8762
+ const wantsCount = prefer.count !== undefined;
8763
+ const resultSource = wantsCount ? quoteIdent("_rpc_result") : call;
8764
+ const core = `select ${exprs.join(", ")} from ${resultSource} as ${quoteIdent(alias)}${where}${order}${limitOffset}`;
8765
+ const bodySql = `select coalesce(json_agg(row_to_json(_r)), '[]'::json) as body from (${core}) _r`;
8766
+ const sql = wantsCount ? `with ${quoteIdent("_rpc_result")} as (select * from ${call}) select (${bodySql}) as body, (select count(*)::int from ${resultSource} as ${quoteIdent(alias)}${where}) as count` : bodySql;
8767
+ const { rows, count } = await withRpcContext(async (query) => {
8763
8768
  const res = await query(sql, []);
8764
- let count = null;
8765
- if (countSql) {
8766
- const c = await query(countSql, []);
8767
- count = c.rows[0].count;
8768
- }
8769
- return { rows: res.rows[0].body, count };
8769
+ const rows = res.rows[0].body;
8770
+ if (wantsObject && !wantsCsv)
8771
+ assertObjectCardinality(rows);
8772
+ const count = wantsCount ? res.rows[0].count : null;
8773
+ return { rows, count };
8770
8774
  });
8771
8775
  return this.dataResponse(rows, {
8772
8776
  status: 200,
@@ -8778,6 +8782,16 @@ class RestHandler {
8778
8782
  });
8779
8783
  }
8780
8784
  }
8785
+ function assertObjectCardinality(rows) {
8786
+ if (rows.length !== 1) {
8787
+ throw new ApiError(406, {
8788
+ code: "PGRST116",
8789
+ message: "JSON object requested, multiple (or no) rows returned",
8790
+ details: `The result contains ${rows.length} rows`,
8791
+ hint: null
8792
+ });
8793
+ }
8794
+ }
8781
8795
  function pickOverload(fns, providedNames) {
8782
8796
  const provided = new Set(providedNames);
8783
8797
  let best = null;
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ function randomToken(bytes = 32) {
88
88
  // package.json
89
89
  var package_default = {
90
90
  name: "@supacloud/lite",
91
- version: "0.28.0",
91
+ version: "0.28.2",
92
92
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
93
93
  type: "module",
94
94
  license: "Apache-2.0",
@@ -8362,6 +8362,8 @@ function statusForSqlState(code) {
8362
8362
  switch (code) {
8363
8363
  case "42501":
8364
8364
  return 403;
8365
+ case "25006":
8366
+ return 405;
8365
8367
  case "23503":
8366
8368
  case "23505":
8367
8369
  return 409;
@@ -8614,7 +8616,10 @@ class RestHandler {
8614
8616
  const { rows, affected } = await this.db.withContext(ctx, async (query) => {
8615
8617
  const res = await query(built.sql, built.params);
8616
8618
  if (opts.queryReturning) {
8617
- return { rows: res.rows[0].body, affected: null };
8619
+ const rows = res.rows[0].body;
8620
+ if (opts.wantsObject && !opts.wantsCsv)
8621
+ assertObjectCardinality(rows);
8622
+ return { rows, affected: null };
8618
8623
  }
8619
8624
  return { rows: null, affected: res.affectedRows ?? 0 };
8620
8625
  });
@@ -8646,14 +8651,7 @@ class RestHandler {
8646
8651
  return new Response(opts.head ? null : rowsToCsv(rows), { status: opts.status, headers });
8647
8652
  }
8648
8653
  if (opts.wantsObject) {
8649
- if (rows.length !== 1) {
8650
- return jsonResponse(406, {
8651
- code: "PGRST116",
8652
- message: "JSON object requested, multiple (or no) rows returned",
8653
- details: `The result contains ${rows.length} rows`,
8654
- hint: null
8655
- });
8656
- }
8654
+ assertObjectCardinality(rows);
8657
8655
  headers["content-type"] = `${OBJECT_MEDIA}; charset=utf-8`;
8658
8656
  return new Response(opts.head ? null : JSON.stringify(rows[0]), {
8659
8657
  status: opts.status,
@@ -8717,17 +8715,22 @@ class RestHandler {
8717
8715
  return await this.runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method);
8718
8716
  }
8719
8717
  async runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method) {
8718
+ const withRpcContext = (run) => this.db.withContext(ctx, async (query) => {
8719
+ if (method !== "POST" || fn.volatility !== "v")
8720
+ await query("set transaction read only");
8721
+ return run(query);
8722
+ });
8720
8723
  if (fn.returnType === "void" && !fn.returnsSet) {
8721
- await this.db.withContext(ctx, (query) => query(`select ${call}`, []));
8724
+ await withRpcContext((query) => query(`select ${call}`, []));
8722
8725
  return new Response(null, { status: 204 });
8723
8726
  }
8724
8727
  if (!fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8725
- const res = await this.db.withContext(ctx, (query) => query(`select to_json(${call}) as result`, []));
8728
+ const res = await withRpcContext((query) => query(`select to_json(${call}) as result`, []));
8726
8729
  const value = res.rows[0].result;
8727
8730
  return jsonResponse(200, value);
8728
8731
  }
8729
8732
  if (fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8730
- const res = await this.db.withContext(ctx, (query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8733
+ const res = await withRpcContext((query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8731
8734
  return jsonResponse(200, res.rows[0].body);
8732
8735
  }
8733
8736
  const alias = "_f";
@@ -8750,17 +8753,18 @@ class RestHandler {
8750
8753
  const where = conds.length > 0 ? ` where ${conds.join(" and ")}` : "";
8751
8754
  const order = builder.renderOrder([], alias);
8752
8755
  const limitOffset = builder.renderLimitOffset("");
8753
- const core = `select ${exprs.join(", ")} from ${call} as ${quoteIdent(alias)}${where}${order}${limitOffset}`;
8754
- const sql = `select coalesce(json_agg(row_to_json(_r)), '[]'::json) as body from (${core}) _r`;
8755
- const countSql = prefer.count !== undefined ? `select count(*)::int as count from ${call} as ${quoteIdent(alias)}${where}` : undefined;
8756
- const { rows, count } = await this.db.withContext(ctx, async (query) => {
8756
+ const wantsCount = prefer.count !== undefined;
8757
+ const resultSource = wantsCount ? quoteIdent("_rpc_result") : call;
8758
+ const core = `select ${exprs.join(", ")} from ${resultSource} as ${quoteIdent(alias)}${where}${order}${limitOffset}`;
8759
+ const bodySql = `select coalesce(json_agg(row_to_json(_r)), '[]'::json) as body from (${core}) _r`;
8760
+ const sql = wantsCount ? `with ${quoteIdent("_rpc_result")} as (select * from ${call}) select (${bodySql}) as body, (select count(*)::int from ${resultSource} as ${quoteIdent(alias)}${where}) as count` : bodySql;
8761
+ const { rows, count } = await withRpcContext(async (query) => {
8757
8762
  const res = await query(sql, []);
8758
- let count = null;
8759
- if (countSql) {
8760
- const c = await query(countSql, []);
8761
- count = c.rows[0].count;
8762
- }
8763
- return { rows: res.rows[0].body, count };
8763
+ const rows = res.rows[0].body;
8764
+ if (wantsObject && !wantsCsv)
8765
+ assertObjectCardinality(rows);
8766
+ const count = wantsCount ? res.rows[0].count : null;
8767
+ return { rows, count };
8764
8768
  });
8765
8769
  return this.dataResponse(rows, {
8766
8770
  status: 200,
@@ -8772,6 +8776,16 @@ class RestHandler {
8772
8776
  });
8773
8777
  }
8774
8778
  }
8779
+ function assertObjectCardinality(rows) {
8780
+ if (rows.length !== 1) {
8781
+ throw new ApiError(406, {
8782
+ code: "PGRST116",
8783
+ message: "JSON object requested, multiple (or no) rows returned",
8784
+ details: `The result contains ${rows.length} rows`,
8785
+ hint: null
8786
+ });
8787
+ }
8788
+ }
8775
8789
  function pickOverload(fns, providedNames) {
8776
8790
  const provided = new Set(providedNames);
8777
8791
  let best = null;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/runtime/rest/errors.ts"],"names":[],"mappings":"AAQA,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBtD;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,QAAQ,CAkBpD;AAED,sDAAsD;AACtD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,QAAQ,CAKV"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/runtime/rest/errors.ts"],"names":[],"mappings":"AAQA,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBtD;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,QAAQ,CAkBpD;AAED,sDAAsD;AACtD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,QAAQ,CAKV"}
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/rest/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAA4B,KAAK,QAAQ,EAAqB,MAAM,mBAAmB,CAAA;AAC9F,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAyF3D,oFAAoF;AACpF,qBAAa,WAAW;IAOpB,OAAO,CAAC,EAAE;IANZ,8FAA8F;IAC9F,OAAO,CAAC,cAAc,CAAU;IAChC,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAC,CAAQ;gBAGd,EAAE,EAAE,QAAQ,EACpB,IAAI,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAMxD,4FAA4F;IACtF,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;YAmC9D,WAAW;YAsHX,WAAW;IA6CzB,OAAO,CAAC,YAAY;YA4CN,SAAS;YAqET,MAAM;CAmFrB"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/rest/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAA4B,KAAK,QAAQ,EAAmC,MAAM,mBAAmB,CAAA;AAC5G,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAyF3D,oFAAoF;AACpF,qBAAa,WAAW;IAOpB,OAAO,CAAC,EAAE;IANZ,8FAA8F;IAC9F,OAAO,CAAC,cAAc,CAAU;IAChC,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAC,CAAQ;gBAGd,EAAE,EAAE,QAAQ,EACpB,IAAI,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAMxD,4FAA4F;IACtF,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;YAmC9D,WAAW;YAsHX,WAAW;IAgDzB,OAAO,CAAC,YAAY;YAqCN,SAAS;YAqET,MAAM;CA4FrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/lite",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",