@supacloud/lite 0.28.1 → 0.28.3

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.3](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.2...supacloud-lite-v0.28.3) (2026-10-15)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * upgrade workspace dependencies ([#1940](https://github.com/vibeunion/supacloud/issues/1940)) ([77ad418](https://github.com/vibeunion/supacloud/commit/77ad418e0568ceeeea0b59a10a3bbc451a68a95f))
9
+
10
+ ## [0.28.2](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.1...supacloud-lite-v0.28.2) (2026-10-15)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **lite:** enforce RPC transaction access modes ([#1936](https://github.com/vibeunion/supacloud/issues/1936)) ([fb90dcc](https://github.com/vibeunion/supacloud/commit/fb90dcc43ed49a45656e1877493eaa641ef494da))
16
+
3
17
  ## [0.28.1](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.0...supacloud-lite-v0.28.1) (2026-10-15)
4
18
 
5
19
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ V1 的目标不是复刻完整 Supabase 平台控制面,而是让现有应用
13
13
  ### 状态
14
14
 
15
15
  - 运行时:npm 包需要 Bun 1.4.3+;单二进制发行版已内嵌 Bun 和 PGlite 资源
16
- - 数据库:PGlite 0.5.8,或 native PostgreSQL
16
+ - 数据库:PGlite 0.5.10,或 native PostgreSQL
17
17
  - 项目模型:单进程、单项目,内部 project ref 固定为 `local`
18
18
  - 客户端:直接使用官方 `@supabase/supabase-js`
19
19
  - 数据目录:`.supacloud-lite/db`
@@ -530,7 +530,7 @@ The goal of V1 is not to replicate the full Supabase platform control plane, but
530
530
  ### Status
531
531
 
532
532
  - Runtime: the npm package requires Bun 1.4.3+; the single-binary release embeds Bun and PGlite assets
533
- - Database: PGlite 0.5.8, or native PostgreSQL
533
+ - Database: PGlite 0.5.10, or native PostgreSQL
534
534
  - Project model: single process, single project, with an internal project ref fixed as `local`
535
535
  - Client: uses the official `@supabase/supabase-js` directly
536
536
  - Data directory: `.supacloud-lite/db`
@@ -20,10 +20,10 @@ SupaCloud Lite 使用 ElectricSQL PGlite 作为嵌入式 PostgreSQL 引擎。
20
20
 
21
21
  Lite also bundles the official PGlite extension packages:
22
22
 
23
- - `@electric-sql/pglite-pgvector` 0.0.9
24
- - `@electric-sql/pglite-postgis` 0.2.8
25
- - `@electric-sql/pglite-pg_textsearch` 0.0.10
26
- - `@electric-sql/pglite-pgmq` 0.0.6
23
+ - `@electric-sql/pglite-pgvector` 0.0.11
24
+ - `@electric-sql/pglite-postgis` 0.2.10
25
+ - `@electric-sql/pglite-pg_textsearch` 0.0.12
26
+ - `@electric-sql/pglite-pgmq` 0.0.8
27
27
 
28
28
  These packages are published by ElectricSQL under Apache-2.0 and their
29
29
  extension archives are embedded in the standalone binary.
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.1",
11
+ version: "0.28.3",
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",
@@ -62,36 +62,37 @@ var package_default = {
62
62
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
63
63
  },
64
64
  dependencies: {
65
- "@electric-sql/pglite": "0.5.8",
66
- "@electric-sql/pglite-pg_textsearch": "0.0.10",
67
- "@electric-sql/pglite-pgmq": "0.0.6",
68
- "@electric-sql/pglite-pgvector": "0.0.9",
69
- "@electric-sql/pglite-postgis": "0.2.8",
70
- "@supacloud/db": "^0.6.0",
65
+ "@electric-sql/pglite": "0.5.10",
66
+ "@electric-sql/pglite-pg_textsearch": "0.0.12",
67
+ "@electric-sql/pglite-pgmq": "0.0.8",
68
+ "@electric-sql/pglite-pgvector": "0.0.11",
69
+ "@electric-sql/pglite-postgis": "0.2.10",
70
+ "@supacloud/db": "^0.17.0",
71
71
  "exact-mirror": "1.2.6",
72
72
  tar: "^7.5.22",
73
- typebox: "1.3.36"
73
+ typebox: "1.3.37"
74
74
  },
75
75
  devDependencies: {
76
76
  "@supacloud/app": "file:../app",
77
+ "@supacloud/contracts": "file:../contracts",
77
78
  "@supacloud/compiler": "file:../compiler",
78
79
  "@supacloud/elysia": "file:../elysia",
79
80
  "@supabase/supabase-js": "^2.117.3",
80
81
  "@types/bun": "^1.4.3",
81
- effect: "4.0.2",
82
- elysia: "2.0.0-beta.21",
82
+ effect: "4.0.5",
83
+ elysia: "2.0.0-beta.32",
83
84
  jose: "^6.2.12",
84
85
  typescript: "^7.0.2"
85
86
  },
86
87
  overrides: {
87
- "@supacloud/db": "^0.6.0",
88
+ "@supacloud/db": "^0.17.0",
88
89
  "@supacloud/app": "file:../app",
89
90
  "@supacloud/compiler": "file:../compiler",
90
91
  "@supacloud/elysia": "file:../elysia",
91
92
  "@supacloud/contracts": "file:../contracts",
92
93
  "@supacloud/commands": "file:../commands",
93
94
  "@supacloud/delivery": "file:../delivery",
94
- "@graphql-tools/utils": "^12.0.3"
95
+ "@graphql-tools/utils": "^12.0.4"
95
96
  },
96
97
  engines: {
97
98
  bun: ">=1.4.3"
@@ -8368,6 +8369,8 @@ function statusForSqlState(code) {
8368
8369
  switch (code) {
8369
8370
  case "42501":
8370
8371
  return 403;
8372
+ case "25006":
8373
+ return 405;
8371
8374
  case "23503":
8372
8375
  case "23505":
8373
8376
  return 409;
@@ -8719,17 +8722,22 @@ class RestHandler {
8719
8722
  return await this.runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method);
8720
8723
  }
8721
8724
  async runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method) {
8725
+ const withRpcContext = (run) => this.db.withContext(ctx, async (query) => {
8726
+ if (method !== "POST" || fn.volatility !== "v")
8727
+ await query("set transaction read only");
8728
+ return run(query);
8729
+ });
8722
8730
  if (fn.returnType === "void" && !fn.returnsSet) {
8723
- await this.db.withContext(ctx, (query) => query(`select ${call}`, []));
8731
+ await withRpcContext((query) => query(`select ${call}`, []));
8724
8732
  return new Response(null, { status: 204 });
8725
8733
  }
8726
8734
  if (!fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8727
- const res = await this.db.withContext(ctx, (query) => query(`select to_json(${call}) as result`, []));
8735
+ const res = await withRpcContext((query) => query(`select to_json(${call}) as result`, []));
8728
8736
  const value = res.rows[0].result;
8729
8737
  return jsonResponse(200, value);
8730
8738
  }
8731
8739
  if (fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8732
- const res = await this.db.withContext(ctx, (query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8740
+ const res = await withRpcContext((query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8733
8741
  return jsonResponse(200, res.rows[0].body);
8734
8742
  }
8735
8743
  const alias = "_f";
@@ -8757,7 +8765,7 @@ class RestHandler {
8757
8765
  const core = `select ${exprs.join(", ")} from ${resultSource} as ${quoteIdent(alias)}${where}${order}${limitOffset}`;
8758
8766
  const bodySql = `select coalesce(json_agg(row_to_json(_r)), '[]'::json) as body from (${core}) _r`;
8759
8767
  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;
8760
- const { rows, count } = await this.db.withContext(ctx, async (query) => {
8768
+ const { rows, count } = await withRpcContext(async (query) => {
8761
8769
  const res = await query(sql, []);
8762
8770
  const rows = res.rows[0].body;
8763
8771
  if (wantsObject && !wantsCsv)
@@ -13208,6 +13216,7 @@ ORDER BY n.nspname, c.relname, p.polname
13208
13216
  var FUNCTIONS_SQL = `
13209
13217
  SELECT n.nspname AS schema,
13210
13218
  p.proname AS name,
13219
+ pg_catalog.oidvectortypes(p.proargtypes) AS identity_arguments,
13211
13220
  p.prosecdef AS security_definer,
13212
13221
  p.proconfig AS config,
13213
13222
  l.lanname AS language
@@ -13281,12 +13290,13 @@ async function readCatalog(executor, schemas = ["public"]) {
13281
13290
  name: row.name,
13282
13291
  command: POLCMD_MAP[row.command] ?? "all",
13283
13292
  roles: row.roles ?? [],
13284
- usingExpr: row.using_expr ?? undefined,
13285
- checkExpr: row.check_expr ?? undefined
13293
+ ...row.using_expr == null ? {} : { usingExpr: row.using_expr },
13294
+ ...row.check_expr == null ? {} : { checkExpr: row.check_expr }
13286
13295
  })),
13287
13296
  functions: functionRows.map((row) => ({
13288
13297
  schema: row.schema,
13289
13298
  name: row.name,
13299
+ identityArguments: row.identity_arguments,
13290
13300
  security: row.security_definer ? "definer" : "invoker",
13291
13301
  searchPath: extractSearchPath(row.config),
13292
13302
  language: row.language
@@ -13314,8 +13324,10 @@ function splitQualifiedName(name) {
13314
13324
  function isFixedSearchPath(searchPath) {
13315
13325
  if (searchPath === null)
13316
13326
  return false;
13327
+ if (searchPath.trim() === "" || searchPath.trim() === '""')
13328
+ return true;
13317
13329
  const parts = searchPath.split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
13318
- return parts.every((part) => part !== "" && part.toLowerCase() !== "pg_temp");
13330
+ return parts.every((part) => part !== "" && part.toLowerCase() !== "pg_temp" && part.toLowerCase() !== "$user");
13319
13331
  }
13320
13332
  function reconcileModule(module, catalog) {
13321
13333
  const issues = [];
@@ -13323,9 +13335,22 @@ function reconcileModule(module, catalog) {
13323
13335
  const push = (severity, code, object, message) => issues.push({ severity, code, object, message });
13324
13336
  for (const policy of module.policies) {
13325
13337
  const [schema, table] = splitQualifiedName(policy.table);
13326
- const found = catalog.policies.some((cp) => cp.schema === schema && cp.table === table && cp.name === policy.name);
13338
+ const found = catalog.policies.find((cp) => cp.schema === schema && cp.table === table && cp.name === policy.name);
13327
13339
  if (!found) {
13328
13340
  push("error", "missing-policy", `${policy.table}.${policy.name}`, `\u58F0\u660E\u7684\u7B56\u7565 ${policy.name} \u5728\u8868 ${policy.table} \u7684 catalog \u4E2D\u4E0D\u5B58\u5728`);
13341
+ } else {
13342
+ if (found.command !== policy.operation) {
13343
+ push("error", "policy-command-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u58F0\u660E\u4E3A ${policy.operation}\uFF0Ccatalog \u5B9E\u9645\u4E3A ${found.command}`);
13344
+ }
13345
+ if (!sameRoles(policy.roles, found.roles)) {
13346
+ push("error", "policy-roles-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u58F0\u660E\u89D2\u8272\u4E0E catalog \u4E0D\u4E00\u81F4`);
13347
+ }
13348
+ if (policy.usingExpr !== undefined && policy.usingExpr !== (found.usingExpr ?? null)) {
13349
+ push("error", "policy-using-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u7684 USING \u8868\u8FBE\u5F0F\u4E0E catalog \u4E0D\u4E00\u81F4`);
13350
+ }
13351
+ if (policy.checkExpr !== undefined && policy.checkExpr !== (found.checkExpr ?? null)) {
13352
+ push("error", "policy-check-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u7684 WITH CHECK \u8868\u8FBE\u5F0F\u4E0E catalog \u4E0D\u4E00\u81F4`);
13353
+ }
13329
13354
  }
13330
13355
  }
13331
13356
  const declaredPolicyKeys = new Set(module.policies.map((p) => `${p.table}::${p.name}`));
@@ -13337,11 +13362,16 @@ function reconcileModule(module, catalog) {
13337
13362
  }
13338
13363
  for (const fn of module.functions) {
13339
13364
  const [schema, name] = splitQualifiedName(fn.name);
13340
- const cf = catalog.functions.find((f) => f.schema === schema && f.name === name);
13341
- if (!cf) {
13365
+ const candidates = catalog.functions.filter((f) => f.schema === schema && f.name === name);
13366
+ if (candidates.length === 0) {
13342
13367
  push("error", "missing-function", fn.name, `\u58F0\u660E\u7684\u51FD\u6570 ${fn.name} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
13343
13368
  continue;
13344
13369
  }
13370
+ const cf = fn.signature !== undefined ? candidates.find((candidate) => functionSignature(candidate) === fn.signature) : candidates.length === 1 ? candidates[0] : undefined;
13371
+ if (!cf) {
13372
+ push("error", fn.signature !== undefined ? "missing-function" : "ambiguous-function-overload", fn.name, fn.signature !== undefined ? `\u58F0\u660E\u7684\u51FD\u6570\u7B7E\u540D ${fn.signature} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728\u6216\u7F3A\u5C11\u7B7E\u540D\u5143\u6570\u636E` : `\u51FD\u6570 ${fn.name} \u5B58\u5728\u591A\u4E2A\u91CD\u8F7D\uFF0C\u58F0\u660E\u5FC5\u987B\u5305\u542B\u5B8C\u6574 signature`);
13373
+ continue;
13374
+ }
13345
13375
  if (cf.security !== fn.security) {
13346
13376
  push("warn", "security-mismatch", fn.name, `\u51FD\u6570 ${fn.name} \u58F0\u660E\u4E3A security ${fn.security}\uFF0Ccatalog \u5B9E\u9645\u4E3A ${cf.security}`);
13347
13377
  }
@@ -13352,9 +13382,11 @@ function reconcileModule(module, catalog) {
13352
13382
  }
13353
13383
  for (const trigger of module.triggers) {
13354
13384
  const [schema, table] = splitQualifiedName(trigger.table);
13355
- const found = catalog.triggers.some((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
13385
+ const found = catalog.triggers.find((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
13356
13386
  if (!found) {
13357
13387
  push("error", "missing-trigger", `${trigger.table}.${trigger.name}`, `\u58F0\u660E\u7684\u89E6\u53D1\u5668 ${trigger.name} \u5728\u8868 ${trigger.table} \u7684 catalog \u4E2D\u4E0D\u5B58\u5728`);
13388
+ } else if (found.enabled !== (trigger.enabled ?? true)) {
13389
+ push("error", found.enabled ? "trigger-enabled-mismatch" : "disabled-trigger", `${trigger.table}.${trigger.name}`, `\u58F0\u660E\u7684\u89E6\u53D1\u5668 ${trigger.name} \u7684\u542F\u7528\u72B6\u6001\u4E0E catalog \u4E0D\u4E00\u81F4`);
13358
13390
  }
13359
13391
  }
13360
13392
  const declaredTriggerKeys = new Set(module.triggers.map((t) => `${t.table}::${t.name}`));
@@ -13367,7 +13399,9 @@ function reconcileModule(module, catalog) {
13367
13399
  for (const table of module.tables) {
13368
13400
  const [schema, name] = splitQualifiedName(table);
13369
13401
  const ct = catalog.tables.find((t) => t.schema === schema && t.name === name);
13370
- if (ct && !ct.rlsEnabled) {
13402
+ if (!ct) {
13403
+ push("error", "missing-table", table, `\u58F0\u660E\u7684\u8868 ${table} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
13404
+ } else if (!ct.rlsEnabled) {
13371
13405
  push("error", "rls-disabled", table, `\u5F52\u5C5E\u8868 ${table} \u672A\u5F00\u542F\u884C\u7EA7\u5B89\u5168\uFF08relrowsecurity = false\uFF09`);
13372
13406
  }
13373
13407
  }
@@ -13390,6 +13424,15 @@ function reconcileModule(module, catalog) {
13390
13424
  ok: !issues.some((issue) => issue.severity === "error")
13391
13425
  };
13392
13426
  }
13427
+ function sameRoles(expected, actual) {
13428
+ const normalize = (roles) => [...new Set(roles)].sort();
13429
+ return JSON.stringify(normalize(expected)) === JSON.stringify(normalize(actual));
13430
+ }
13431
+ function functionSignature(functionEntry) {
13432
+ if (functionEntry.identityArguments === undefined)
13433
+ return;
13434
+ return `${functionEntry.schema}.${functionEntry.name}(${functionEntry.identityArguments})`;
13435
+ }
13393
13436
  var SECURITY_DEFINER_RE = /\bsecurity\s+definer\b/i;
13394
13437
  var SET_SEARCH_PATH_RE = /\bset\s+search_path\b/i;
13395
13438
  var GRANT_TO_PUBLIC_RE = /\bgrant\b[^;]*\bto\s+public\b/i;
@@ -13470,14 +13513,15 @@ async function lintModule(module, readFile) {
13470
13513
  for (const [file, sql] of contents) {
13471
13514
  issues.push(...lintSql(sql, file));
13472
13515
  }
13473
- if (module.policies.length > 0) {
13516
+ const firstPolicy = module.policies[0];
13517
+ if (firstPolicy) {
13474
13518
  const anyEnable = module.policies.some((policy) => ENABLE_RLS_RE.test(contents.get(policy.source) ?? ""));
13475
13519
  if (!anyEnable) {
13476
13520
  issues.push({
13477
13521
  severity: "warn",
13478
13522
  code: "missing-rls-enable",
13479
13523
  message: `\u6A21\u5757 ${module.name} \u58F0\u660E\u4E86 ${module.policies.length} \u6761\u7B56\u7565\uFF0C\u4F46\u6240\u6709\u7B56\u7565\u6E90\u6587\u4EF6\u90FD\u6CA1\u6709 enable row level security`,
13480
- file: module.policies[0].source
13524
+ file: firstPolicy.source
13481
13525
  });
13482
13526
  }
13483
13527
  }
@@ -13523,10 +13567,10 @@ function keys(row, expected) {
13523
13567
  function validTarget(value) {
13524
13568
  const target = record(value);
13525
13569
  keys(target, ["environment", "projectRef"]);
13526
- if (typeof target.environment !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target.environment) || typeof target.projectRef !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target.projectRef)) {
13570
+ if (typeof target["environment"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target["environment"]) || typeof target["projectRef"] !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target["projectRef"])) {
13527
13571
  throw new Error("Invalid migration binding target");
13528
13572
  }
13529
- return target;
13573
+ return { environment: target["environment"], projectRef: target["projectRef"] };
13530
13574
  }
13531
13575
  function validFile(file) {
13532
13576
  return typeof file === "string" && /^[a-zA-Z0-9_./-]+\.sql$/.test(file) && !file.startsWith("/") && file.split("/").every((part) => part && part !== "." && part !== "..");
@@ -13534,32 +13578,37 @@ function validFile(file) {
13534
13578
  function parseMigrationBindingManifest(value) {
13535
13579
  const manifest = record(value);
13536
13580
  keys(manifest, ["schema", "targets", "templates"]);
13537
- if (manifest.schema !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest.targets) || !manifest.targets.length || !Array.isArray(manifest.templates)) {
13581
+ if (manifest["schema"] !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest["targets"]) || !manifest["targets"].length || !Array.isArray(manifest["templates"])) {
13538
13582
  throw new Error("Invalid migration binding manifest");
13539
13583
  }
13540
- const targets = manifest.targets.map(validTarget);
13584
+ const targets = manifest["targets"].map(validTarget);
13541
13585
  if (new Set(targets.map((target) => target.environment)).size !== targets.length) {
13542
13586
  throw new Error("Duplicate migration binding environment");
13543
13587
  }
13544
13588
  const files = new Set;
13545
- const templates = manifest.templates.map((value2) => {
13546
- const template = record(value2);
13589
+ const templates = manifest["templates"].map((value) => {
13590
+ const template = record(value);
13547
13591
  keys(template, ["file", "templateSha256", "parameters"]);
13548
- if (!validFile(template.file) || files.has(template.file) || typeof template.templateSha256 !== "string" || !HASH.test(template.templateSha256) || !Array.isArray(template.parameters) || !template.parameters.length) {
13592
+ if (!validFile(template["file"]) || files.has(template["file"]) || typeof template["templateSha256"] !== "string" || !HASH.test(template["templateSha256"]) || !Array.isArray(template["parameters"]) || !template["parameters"].length) {
13549
13593
  throw new Error("Invalid or duplicate migration binding template");
13550
13594
  }
13551
- files.add(template.file);
13595
+ files.add(template["file"]);
13552
13596
  const placeholders = new Set;
13553
- const parameters = template.parameters.map((value3) => {
13554
- const parameter = record(value3);
13597
+ const parameters = template["parameters"].map((value) => {
13598
+ const parameter = record(value);
13555
13599
  keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
13556
- if (typeof parameter.placeholder !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter.placeholder) || placeholders.has(parameter.placeholder) || typeof parameter.variable !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter.variable) || !["uuid", "https-url", "resource-name"].includes(String(parameter.type)) || !Number.isSafeInteger(parameter.occurrences) || Number(parameter.occurrences) < 1) {
13600
+ if (typeof parameter["placeholder"] !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter["placeholder"]) || placeholders.has(parameter["placeholder"]) || typeof parameter["variable"] !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter["variable"]) || parameter["type"] !== "uuid" && parameter["type"] !== "https-url" && parameter["type"] !== "resource-name" || typeof parameter["occurrences"] !== "number" || !Number.isSafeInteger(parameter["occurrences"]) || parameter["occurrences"] < 1) {
13557
13601
  throw new Error("Invalid or duplicate migration binding parameter");
13558
13602
  }
13559
- placeholders.add(parameter.placeholder);
13560
- return parameter;
13603
+ placeholders.add(parameter["placeholder"]);
13604
+ return {
13605
+ placeholder: parameter["placeholder"],
13606
+ variable: parameter["variable"],
13607
+ type: parameter["type"],
13608
+ occurrences: parameter["occurrences"]
13609
+ };
13561
13610
  });
13562
- return { file: template.file, templateSha256: template.templateSha256, parameters };
13611
+ return { file: template["file"], templateSha256: template["templateSha256"], parameters };
13563
13612
  });
13564
13613
  return { schema: "supacloud.migration-bindings.v1", targets, templates };
13565
13614
  }
@@ -13647,6 +13696,194 @@ function renderMigrationBindings(options) {
13647
13696
  }
13648
13697
  };
13649
13698
  }
13699
+ var COMMAND_PERSISTENCE_SQL = `
13700
+ DO $runtime$
13701
+ BEGIN
13702
+ IF to_regprocedure('supacloud_workflows.start_run(uuid,text,text,text,jsonb,integer)') IS NULL
13703
+ OR to_regprocedure('supacloud_commands.status(jsonb)') IS NULL THEN
13704
+ RAISE EXCEPTION 'Install the SupaCloud Commands/Workflow/PGMQ runtime before command persistence';
13705
+ END IF;
13706
+ END
13707
+ $runtime$;
13708
+ CREATE SCHEMA IF NOT EXISTS supacloud_commands;
13709
+ REVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC;
13710
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_receipts (
13711
+ tenant_id text NOT NULL,
13712
+ actor_id text NOT NULL,
13713
+ command text NOT NULL,
13714
+ operation_key text NOT NULL,
13715
+ kind text NOT NULL CHECK (kind IN ('transactional', 'external')),
13716
+ input_fingerprint text NOT NULL CHECK (input_fingerprint ~ '^[a-f0-9]{64}$'),
13717
+ input_payload text,
13718
+ dispatch_key uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
13719
+ status text NOT NULL CHECK (status IN ('pending', 'unknown', 'confirmed', 'rejected')),
13720
+ audit_state text NOT NULL CHECK (audit_state IN ('pending', 'complete')),
13721
+ result jsonb,
13722
+ created_at timestamptz NOT NULL DEFAULT now(),
13723
+ updated_at timestamptz NOT NULL DEFAULT now(),
13724
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
13725
+ CHECK ((status = 'confirmed') = (result IS NOT NULL)),
13726
+ CONSTRAINT execution_receipts_audit_terminal_check CHECK (
13727
+ (audit_state <> 'complete' OR status IN ('confirmed', 'rejected'))
13728
+ AND (status <> 'rejected' OR audit_state = 'complete')),
13729
+ CONSTRAINT execution_receipts_transactional_check CHECK (
13730
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete'))
13731
+ );
13732
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_audit (
13733
+ tenant_id text NOT NULL,
13734
+ actor_id text NOT NULL,
13735
+ command text NOT NULL,
13736
+ operation_key text NOT NULL,
13737
+ event text NOT NULL,
13738
+ details jsonb NOT NULL,
13739
+ created_at timestamptz NOT NULL DEFAULT now(),
13740
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
13741
+ FOREIGN KEY (tenant_id, actor_id, command, operation_key)
13742
+ REFERENCES supacloud_commands.execution_receipts (tenant_id, actor_id, command, operation_key)
13743
+ );
13744
+ REVOKE ALL ON supacloud_commands.execution_receipts, supacloud_commands.execution_audit FROM PUBLIC;
13745
+ CREATE OR REPLACE FUNCTION supacloud_commands.protect_terminal_execution()
13746
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $terminal$
13747
+ BEGIN
13748
+ IF OLD.status IN ('confirmed','rejected') AND (
13749
+ NEW.status IS DISTINCT FROM OLD.status OR NEW.result IS DISTINCT FROM OLD.result
13750
+ OR (OLD.audit_state = 'complete' AND NEW.audit_state IS DISTINCT FROM OLD.audit_state)) THEN
13751
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_TERMINAL_RECEIPT';
13752
+ END IF;
13753
+ RETURN NEW;
13754
+ END
13755
+ $terminal$;
13756
+ CREATE OR REPLACE TRIGGER execution_terminal_guard
13757
+ BEFORE UPDATE ON supacloud_commands.execution_receipts
13758
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.protect_terminal_execution();
13759
+
13760
+ CREATE OR REPLACE FUNCTION supacloud_commands.require_rejection_audit()
13761
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $rejection$
13762
+ BEGIN
13763
+ IF NEW.status = 'rejected' AND NOT EXISTS (
13764
+ SELECT FROM supacloud_commands.execution_audit a
13765
+ WHERE a.tenant_id=NEW.tenant_id AND a.actor_id=NEW.actor_id
13766
+ AND a.command=NEW.command AND a.operation_key=NEW.operation_key
13767
+ ) THEN
13768
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_REJECTION_AUDIT_REQUIRED';
13769
+ END IF;
13770
+ RETURN NEW;
13771
+ END
13772
+ $rejection$;
13773
+ DROP TRIGGER IF EXISTS execution_rejection_audit ON supacloud_commands.execution_receipts;
13774
+ CREATE CONSTRAINT TRIGGER execution_rejection_audit
13775
+ AFTER INSERT OR UPDATE ON supacloud_commands.execution_receipts
13776
+ DEFERRABLE INITIALLY DEFERRED
13777
+ FOR EACH ROW WHEN (NEW.status = 'rejected')
13778
+ EXECUTE FUNCTION supacloud_commands.require_rejection_audit();
13779
+ CREATE OR REPLACE FUNCTION supacloud_commands.enqueue_execution_recovery()
13780
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $recovery$
13781
+ DECLARE submission jsonb;
13782
+ BEGIN
13783
+ PERFORM pg_advisory_xact_lock(hashtextextended(NEW.dispatch_key::text,0));
13784
+ SELECT supacloud_commands.snapshot(NEW.dispatch_key,false) INTO submission;
13785
+ IF submission IS NOT NULL THEN
13786
+ IF submission->>'commandType' IS DISTINCT FROM NEW.command
13787
+ OR submission->>'actorId' IS DISTINCT FROM NEW.actor_id
13788
+ OR submission->>'tenantId' IS DISTINCT FROM NEW.tenant_id
13789
+ OR NEW.operation_key <> NEW.dispatch_key::text THEN
13790
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDENTITY_CONFLICT';
13791
+ END IF;
13792
+ RETURN NEW;
13793
+ END IF;
13794
+ IF NEW.kind = 'external' AND NEW.status <> 'rejected'
13795
+ AND (NEW.status <> 'confirmed' OR NEW.audit_state <> 'complete') THEN
13796
+ PERFORM supacloud_workflows.start_run(
13797
+ NEW.dispatch_key, 'supacloud.command.reconcile', '1', 'reconcile',
13798
+ jsonb_build_object('commandId', NEW.dispatch_key, 'tenantId', NEW.tenant_id,
13799
+ 'actorId', NEW.actor_id, 'command', NEW.command, 'operationId', NEW.operation_key),
13800
+ 20
13801
+ );
13802
+ END IF;
13803
+ RETURN NEW;
13804
+ END
13805
+ $recovery$;
13806
+ CREATE OR REPLACE TRIGGER execution_recovery_enqueue
13807
+ AFTER INSERT ON supacloud_commands.execution_receipts
13808
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.enqueue_execution_recovery();
13809
+
13810
+ CREATE OR REPLACE FUNCTION supacloud_commands.execution_status(request jsonb)
13811
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $status$
13812
+ SELECT jsonb_build_object('kind','execution','commandId',r.dispatch_key,
13813
+ 'execution',jsonb_build_object('tenantId',r.tenant_id,'actorId',r.actor_id,
13814
+ 'command',r.command,'operationId',r.operation_key,'dispatchKey',r.dispatch_key,
13815
+ 'status',r.status,'audit',r.audit_state)
13816
+ || CASE WHEN r.status='confirmed' THEN jsonb_build_object('result',r.result) ELSE '{}'::jsonb END,
13817
+ 'workflow',CASE WHEN w.id IS NULL THEN NULL ELSE jsonb_build_object('runId',w.id,'status',w.status) END)
13818
+ FROM supacloud_commands.execution_receipts r
13819
+ LEFT JOIN supacloud_workflows.runs w ON w.id=r.dispatch_key
13820
+ WHERE CASE WHEN request ? 'commandId' THEN r.dispatch_key::text=request->>'commandId'
13821
+ ELSE r.tenant_id=request->>'tenantId' AND r.actor_id=request->>'actorId'
13822
+ AND r.command=request->>'command' AND r.operation_key=request->>'operationId' END
13823
+ $status$;
13824
+ REVOKE ALL ON FUNCTION supacloud_commands.execution_status(jsonb) FROM PUBLIC;
13825
+ `;
13826
+ var COMMAND_PERSISTENCE_UPGRADE_SQL = `
13827
+ ALTER TABLE supacloud_commands.execution_receipts
13828
+ ADD COLUMN IF NOT EXISTS input_fingerprint text,
13829
+ ADD COLUMN IF NOT EXISTS input_payload text;
13830
+ DO $migration$
13831
+ BEGIN
13832
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='supacloud_commands'
13833
+ AND table_name='execution_receipts' AND column_name='input_key') THEN
13834
+ EXECUTE 'UPDATE supacloud_commands.execution_receipts
13835
+ SET input_fingerprint=encode(sha256(convert_to(input_key,''UTF8'')),''hex''),input_payload=input_key
13836
+ WHERE input_fingerprint IS NULL';
13837
+ ALTER TABLE supacloud_commands.execution_receipts DROP COLUMN input_key;
13838
+ END IF;
13839
+ END
13840
+ $migration$;
13841
+ ALTER TABLE supacloud_commands.execution_receipts ALTER COLUMN input_fingerprint SET NOT NULL;
13842
+ ALTER TABLE supacloud_commands.execution_receipts
13843
+ DROP CONSTRAINT IF EXISTS execution_receipts_status_check,
13844
+ DROP CONSTRAINT IF EXISTS execution_receipts_check1,
13845
+ DROP CONSTRAINT IF EXISTS execution_receipts_transactional_check,
13846
+ DROP CONSTRAINT IF EXISTS execution_receipts_audit_terminal_check;
13847
+ ALTER TABLE supacloud_commands.execution_receipts
13848
+ ADD CONSTRAINT execution_receipts_status_check CHECK (status IN ('pending','unknown','confirmed','rejected')),
13849
+ ADD CONSTRAINT execution_receipts_transactional_check CHECK (
13850
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete')),
13851
+ ADD CONSTRAINT execution_receipts_audit_terminal_check CHECK (
13852
+ (audit_state <> 'complete' OR status IN ('confirmed','rejected'))
13853
+ AND (status <> 'rejected' OR audit_state = 'complete'));
13854
+ DO $constraint$
13855
+ BEGIN
13856
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
13857
+ WHERE conrelid='supacloud_commands.execution_receipts'::regclass
13858
+ AND conname='execution_receipts_input_fingerprint_check') THEN
13859
+ ALTER TABLE supacloud_commands.execution_receipts
13860
+ ADD CONSTRAINT execution_receipts_input_fingerprint_check CHECK (input_fingerprint ~ '^[a-f0-9]{64}$');
13861
+ END IF;
13862
+ END
13863
+ $constraint$;
13864
+ ${COMMAND_PERSISTENCE_SQL}
13865
+ DO $backfill$
13866
+ DECLARE r record;
13867
+ BEGIN
13868
+ FOR r IN SELECT * FROM supacloud_commands.execution_receipts
13869
+ WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
13870
+ AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
13871
+ LOOP
13872
+ PERFORM supacloud_workflows.start_run(
13873
+ r.dispatch_key,'supacloud.command.reconcile','1','reconcile',
13874
+ jsonb_build_object('commandId',r.dispatch_key,'tenantId',r.tenant_id,
13875
+ 'actorId',r.actor_id,'command',r.command,'operationId',r.operation_key),20
13876
+ );
13877
+ END LOOP;
13878
+ END
13879
+ $backfill$;
13880
+ DROP INDEX IF EXISTS supacloud_commands.execution_pending_recovery;
13881
+ ALTER TABLE supacloud_commands.execution_receipts
13882
+ DROP COLUMN IF EXISTS lease_id,
13883
+ DROP COLUMN IF EXISTS lease_until,
13884
+ DROP COLUMN IF EXISTS next_attempt_at,
13885
+ DROP COLUMN IF EXISTS recovery_attempts;
13886
+ `;
13650
13887
 
13651
13888
  // src/runtime/node/db-check.ts
13652
13889
  async function loadDatabaseModules(moduleFile) {
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.1",
91
+ version: "0.28.3",
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",
@@ -142,36 +142,37 @@ var package_default = {
142
142
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
143
143
  },
144
144
  dependencies: {
145
- "@electric-sql/pglite": "0.5.8",
146
- "@electric-sql/pglite-pg_textsearch": "0.0.10",
147
- "@electric-sql/pglite-pgmq": "0.0.6",
148
- "@electric-sql/pglite-pgvector": "0.0.9",
149
- "@electric-sql/pglite-postgis": "0.2.8",
150
- "@supacloud/db": "^0.6.0",
145
+ "@electric-sql/pglite": "0.5.10",
146
+ "@electric-sql/pglite-pg_textsearch": "0.0.12",
147
+ "@electric-sql/pglite-pgmq": "0.0.8",
148
+ "@electric-sql/pglite-pgvector": "0.0.11",
149
+ "@electric-sql/pglite-postgis": "0.2.10",
150
+ "@supacloud/db": "^0.17.0",
151
151
  "exact-mirror": "1.2.6",
152
152
  tar: "^7.5.22",
153
- typebox: "1.3.36"
153
+ typebox: "1.3.37"
154
154
  },
155
155
  devDependencies: {
156
156
  "@supacloud/app": "file:../app",
157
+ "@supacloud/contracts": "file:../contracts",
157
158
  "@supacloud/compiler": "file:../compiler",
158
159
  "@supacloud/elysia": "file:../elysia",
159
160
  "@supabase/supabase-js": "^2.117.3",
160
161
  "@types/bun": "^1.4.3",
161
- effect: "4.0.2",
162
- elysia: "2.0.0-beta.21",
162
+ effect: "4.0.5",
163
+ elysia: "2.0.0-beta.32",
163
164
  jose: "^6.2.12",
164
165
  typescript: "^7.0.2"
165
166
  },
166
167
  overrides: {
167
- "@supacloud/db": "^0.6.0",
168
+ "@supacloud/db": "^0.17.0",
168
169
  "@supacloud/app": "file:../app",
169
170
  "@supacloud/compiler": "file:../compiler",
170
171
  "@supacloud/elysia": "file:../elysia",
171
172
  "@supacloud/contracts": "file:../contracts",
172
173
  "@supacloud/commands": "file:../commands",
173
174
  "@supacloud/delivery": "file:../delivery",
174
- "@graphql-tools/utils": "^12.0.3"
175
+ "@graphql-tools/utils": "^12.0.4"
175
176
  },
176
177
  engines: {
177
178
  bun: ">=1.4.3"
@@ -8362,6 +8363,8 @@ function statusForSqlState(code) {
8362
8363
  switch (code) {
8363
8364
  case "42501":
8364
8365
  return 403;
8366
+ case "25006":
8367
+ return 405;
8365
8368
  case "23503":
8366
8369
  case "23505":
8367
8370
  return 409;
@@ -8713,17 +8716,22 @@ class RestHandler {
8713
8716
  return await this.runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method);
8714
8717
  }
8715
8718
  async runRpc(ctx, fn, call, q, prefer, wantsObject, wantsCsv, schema, method) {
8719
+ const withRpcContext = (run) => this.db.withContext(ctx, async (query) => {
8720
+ if (method !== "POST" || fn.volatility !== "v")
8721
+ await query("set transaction read only");
8722
+ return run(query);
8723
+ });
8716
8724
  if (fn.returnType === "void" && !fn.returnsSet) {
8717
- await this.db.withContext(ctx, (query) => query(`select ${call}`, []));
8725
+ await withRpcContext((query) => query(`select ${call}`, []));
8718
8726
  return new Response(null, { status: 204 });
8719
8727
  }
8720
8728
  if (!fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8721
- const res = await this.db.withContext(ctx, (query) => query(`select to_json(${call}) as result`, []));
8729
+ const res = await withRpcContext((query) => query(`select to_json(${call}) as result`, []));
8722
8730
  const value = res.rows[0].result;
8723
8731
  return jsonResponse(200, value);
8724
8732
  }
8725
8733
  if (fn.returnsSet && ["b", "d", "e"].includes(fn.returnTypType)) {
8726
- const res = await this.db.withContext(ctx, (query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8734
+ const res = await withRpcContext((query) => query(`select coalesce(json_agg(_v), '[]'::json) as body from ${call} as _t(_v)`, []));
8727
8735
  return jsonResponse(200, res.rows[0].body);
8728
8736
  }
8729
8737
  const alias = "_f";
@@ -8751,7 +8759,7 @@ class RestHandler {
8751
8759
  const core = `select ${exprs.join(", ")} from ${resultSource} as ${quoteIdent(alias)}${where}${order}${limitOffset}`;
8752
8760
  const bodySql = `select coalesce(json_agg(row_to_json(_r)), '[]'::json) as body from (${core}) _r`;
8753
8761
  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;
8754
- const { rows, count } = await this.db.withContext(ctx, async (query) => {
8762
+ const { rows, count } = await withRpcContext(async (query) => {
8755
8763
  const res = await query(sql, []);
8756
8764
  const rows = res.rows[0].body;
8757
8765
  if (wantsObject && !wantsCsv)
@@ -13964,10 +13972,10 @@ function keys(row, expected) {
13964
13972
  function validTarget(value) {
13965
13973
  const target = record(value);
13966
13974
  keys(target, ["environment", "projectRef"]);
13967
- if (typeof target.environment !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target.environment) || typeof target.projectRef !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target.projectRef)) {
13975
+ if (typeof target["environment"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target["environment"]) || typeof target["projectRef"] !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target["projectRef"])) {
13968
13976
  throw new Error("Invalid migration binding target");
13969
13977
  }
13970
- return target;
13978
+ return { environment: target["environment"], projectRef: target["projectRef"] };
13971
13979
  }
13972
13980
  function validFile(file) {
13973
13981
  return typeof file === "string" && /^[a-zA-Z0-9_./-]+\.sql$/.test(file) && !file.startsWith("/") && file.split("/").every((part) => part && part !== "." && part !== "..");
@@ -13975,32 +13983,37 @@ function validFile(file) {
13975
13983
  function parseMigrationBindingManifest(value) {
13976
13984
  const manifest = record(value);
13977
13985
  keys(manifest, ["schema", "targets", "templates"]);
13978
- if (manifest.schema !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest.targets) || !manifest.targets.length || !Array.isArray(manifest.templates)) {
13986
+ if (manifest["schema"] !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest["targets"]) || !manifest["targets"].length || !Array.isArray(manifest["templates"])) {
13979
13987
  throw new Error("Invalid migration binding manifest");
13980
13988
  }
13981
- const targets = manifest.targets.map(validTarget);
13989
+ const targets = manifest["targets"].map(validTarget);
13982
13990
  if (new Set(targets.map((target) => target.environment)).size !== targets.length) {
13983
13991
  throw new Error("Duplicate migration binding environment");
13984
13992
  }
13985
13993
  const files = new Set;
13986
- const templates = manifest.templates.map((value2) => {
13987
- const template = record(value2);
13994
+ const templates = manifest["templates"].map((value) => {
13995
+ const template = record(value);
13988
13996
  keys(template, ["file", "templateSha256", "parameters"]);
13989
- if (!validFile(template.file) || files.has(template.file) || typeof template.templateSha256 !== "string" || !HASH.test(template.templateSha256) || !Array.isArray(template.parameters) || !template.parameters.length) {
13997
+ if (!validFile(template["file"]) || files.has(template["file"]) || typeof template["templateSha256"] !== "string" || !HASH.test(template["templateSha256"]) || !Array.isArray(template["parameters"]) || !template["parameters"].length) {
13990
13998
  throw new Error("Invalid or duplicate migration binding template");
13991
13999
  }
13992
- files.add(template.file);
14000
+ files.add(template["file"]);
13993
14001
  const placeholders = new Set;
13994
- const parameters = template.parameters.map((value3) => {
13995
- const parameter = record(value3);
14002
+ const parameters = template["parameters"].map((value) => {
14003
+ const parameter = record(value);
13996
14004
  keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
13997
- if (typeof parameter.placeholder !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter.placeholder) || placeholders.has(parameter.placeholder) || typeof parameter.variable !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter.variable) || !["uuid", "https-url", "resource-name"].includes(String(parameter.type)) || !Number.isSafeInteger(parameter.occurrences) || Number(parameter.occurrences) < 1) {
14005
+ if (typeof parameter["placeholder"] !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter["placeholder"]) || placeholders.has(parameter["placeholder"]) || typeof parameter["variable"] !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter["variable"]) || parameter["type"] !== "uuid" && parameter["type"] !== "https-url" && parameter["type"] !== "resource-name" || typeof parameter["occurrences"] !== "number" || !Number.isSafeInteger(parameter["occurrences"]) || parameter["occurrences"] < 1) {
13998
14006
  throw new Error("Invalid or duplicate migration binding parameter");
13999
14007
  }
14000
- placeholders.add(parameter.placeholder);
14001
- return parameter;
14008
+ placeholders.add(parameter["placeholder"]);
14009
+ return {
14010
+ placeholder: parameter["placeholder"],
14011
+ variable: parameter["variable"],
14012
+ type: parameter["type"],
14013
+ occurrences: parameter["occurrences"]
14014
+ };
14002
14015
  });
14003
- return { file: template.file, templateSha256: template.templateSha256, parameters };
14016
+ return { file: template["file"], templateSha256: template["templateSha256"], parameters };
14004
14017
  });
14005
14018
  return { schema: "supacloud.migration-bindings.v1", targets, templates };
14006
14019
  }
@@ -14088,6 +14101,194 @@ function renderMigrationBindings(options) {
14088
14101
  }
14089
14102
  };
14090
14103
  }
14104
+ var COMMAND_PERSISTENCE_SQL = `
14105
+ DO $runtime$
14106
+ BEGIN
14107
+ IF to_regprocedure('supacloud_workflows.start_run(uuid,text,text,text,jsonb,integer)') IS NULL
14108
+ OR to_regprocedure('supacloud_commands.status(jsonb)') IS NULL THEN
14109
+ RAISE EXCEPTION 'Install the SupaCloud Commands/Workflow/PGMQ runtime before command persistence';
14110
+ END IF;
14111
+ END
14112
+ $runtime$;
14113
+ CREATE SCHEMA IF NOT EXISTS supacloud_commands;
14114
+ REVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC;
14115
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_receipts (
14116
+ tenant_id text NOT NULL,
14117
+ actor_id text NOT NULL,
14118
+ command text NOT NULL,
14119
+ operation_key text NOT NULL,
14120
+ kind text NOT NULL CHECK (kind IN ('transactional', 'external')),
14121
+ input_fingerprint text NOT NULL CHECK (input_fingerprint ~ '^[a-f0-9]{64}$'),
14122
+ input_payload text,
14123
+ dispatch_key uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
14124
+ status text NOT NULL CHECK (status IN ('pending', 'unknown', 'confirmed', 'rejected')),
14125
+ audit_state text NOT NULL CHECK (audit_state IN ('pending', 'complete')),
14126
+ result jsonb,
14127
+ created_at timestamptz NOT NULL DEFAULT now(),
14128
+ updated_at timestamptz NOT NULL DEFAULT now(),
14129
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
14130
+ CHECK ((status = 'confirmed') = (result IS NOT NULL)),
14131
+ CONSTRAINT execution_receipts_audit_terminal_check CHECK (
14132
+ (audit_state <> 'complete' OR status IN ('confirmed', 'rejected'))
14133
+ AND (status <> 'rejected' OR audit_state = 'complete')),
14134
+ CONSTRAINT execution_receipts_transactional_check CHECK (
14135
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete'))
14136
+ );
14137
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_audit (
14138
+ tenant_id text NOT NULL,
14139
+ actor_id text NOT NULL,
14140
+ command text NOT NULL,
14141
+ operation_key text NOT NULL,
14142
+ event text NOT NULL,
14143
+ details jsonb NOT NULL,
14144
+ created_at timestamptz NOT NULL DEFAULT now(),
14145
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
14146
+ FOREIGN KEY (tenant_id, actor_id, command, operation_key)
14147
+ REFERENCES supacloud_commands.execution_receipts (tenant_id, actor_id, command, operation_key)
14148
+ );
14149
+ REVOKE ALL ON supacloud_commands.execution_receipts, supacloud_commands.execution_audit FROM PUBLIC;
14150
+ CREATE OR REPLACE FUNCTION supacloud_commands.protect_terminal_execution()
14151
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $terminal$
14152
+ BEGIN
14153
+ IF OLD.status IN ('confirmed','rejected') AND (
14154
+ NEW.status IS DISTINCT FROM OLD.status OR NEW.result IS DISTINCT FROM OLD.result
14155
+ OR (OLD.audit_state = 'complete' AND NEW.audit_state IS DISTINCT FROM OLD.audit_state)) THEN
14156
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_TERMINAL_RECEIPT';
14157
+ END IF;
14158
+ RETURN NEW;
14159
+ END
14160
+ $terminal$;
14161
+ CREATE OR REPLACE TRIGGER execution_terminal_guard
14162
+ BEFORE UPDATE ON supacloud_commands.execution_receipts
14163
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.protect_terminal_execution();
14164
+
14165
+ CREATE OR REPLACE FUNCTION supacloud_commands.require_rejection_audit()
14166
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $rejection$
14167
+ BEGIN
14168
+ IF NEW.status = 'rejected' AND NOT EXISTS (
14169
+ SELECT FROM supacloud_commands.execution_audit a
14170
+ WHERE a.tenant_id=NEW.tenant_id AND a.actor_id=NEW.actor_id
14171
+ AND a.command=NEW.command AND a.operation_key=NEW.operation_key
14172
+ ) THEN
14173
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_REJECTION_AUDIT_REQUIRED';
14174
+ END IF;
14175
+ RETURN NEW;
14176
+ END
14177
+ $rejection$;
14178
+ DROP TRIGGER IF EXISTS execution_rejection_audit ON supacloud_commands.execution_receipts;
14179
+ CREATE CONSTRAINT TRIGGER execution_rejection_audit
14180
+ AFTER INSERT OR UPDATE ON supacloud_commands.execution_receipts
14181
+ DEFERRABLE INITIALLY DEFERRED
14182
+ FOR EACH ROW WHEN (NEW.status = 'rejected')
14183
+ EXECUTE FUNCTION supacloud_commands.require_rejection_audit();
14184
+ CREATE OR REPLACE FUNCTION supacloud_commands.enqueue_execution_recovery()
14185
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $recovery$
14186
+ DECLARE submission jsonb;
14187
+ BEGIN
14188
+ PERFORM pg_advisory_xact_lock(hashtextextended(NEW.dispatch_key::text,0));
14189
+ SELECT supacloud_commands.snapshot(NEW.dispatch_key,false) INTO submission;
14190
+ IF submission IS NOT NULL THEN
14191
+ IF submission->>'commandType' IS DISTINCT FROM NEW.command
14192
+ OR submission->>'actorId' IS DISTINCT FROM NEW.actor_id
14193
+ OR submission->>'tenantId' IS DISTINCT FROM NEW.tenant_id
14194
+ OR NEW.operation_key <> NEW.dispatch_key::text THEN
14195
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDENTITY_CONFLICT';
14196
+ END IF;
14197
+ RETURN NEW;
14198
+ END IF;
14199
+ IF NEW.kind = 'external' AND NEW.status <> 'rejected'
14200
+ AND (NEW.status <> 'confirmed' OR NEW.audit_state <> 'complete') THEN
14201
+ PERFORM supacloud_workflows.start_run(
14202
+ NEW.dispatch_key, 'supacloud.command.reconcile', '1', 'reconcile',
14203
+ jsonb_build_object('commandId', NEW.dispatch_key, 'tenantId', NEW.tenant_id,
14204
+ 'actorId', NEW.actor_id, 'command', NEW.command, 'operationId', NEW.operation_key),
14205
+ 20
14206
+ );
14207
+ END IF;
14208
+ RETURN NEW;
14209
+ END
14210
+ $recovery$;
14211
+ CREATE OR REPLACE TRIGGER execution_recovery_enqueue
14212
+ AFTER INSERT ON supacloud_commands.execution_receipts
14213
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.enqueue_execution_recovery();
14214
+
14215
+ CREATE OR REPLACE FUNCTION supacloud_commands.execution_status(request jsonb)
14216
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $status$
14217
+ SELECT jsonb_build_object('kind','execution','commandId',r.dispatch_key,
14218
+ 'execution',jsonb_build_object('tenantId',r.tenant_id,'actorId',r.actor_id,
14219
+ 'command',r.command,'operationId',r.operation_key,'dispatchKey',r.dispatch_key,
14220
+ 'status',r.status,'audit',r.audit_state)
14221
+ || CASE WHEN r.status='confirmed' THEN jsonb_build_object('result',r.result) ELSE '{}'::jsonb END,
14222
+ 'workflow',CASE WHEN w.id IS NULL THEN NULL ELSE jsonb_build_object('runId',w.id,'status',w.status) END)
14223
+ FROM supacloud_commands.execution_receipts r
14224
+ LEFT JOIN supacloud_workflows.runs w ON w.id=r.dispatch_key
14225
+ WHERE CASE WHEN request ? 'commandId' THEN r.dispatch_key::text=request->>'commandId'
14226
+ ELSE r.tenant_id=request->>'tenantId' AND r.actor_id=request->>'actorId'
14227
+ AND r.command=request->>'command' AND r.operation_key=request->>'operationId' END
14228
+ $status$;
14229
+ REVOKE ALL ON FUNCTION supacloud_commands.execution_status(jsonb) FROM PUBLIC;
14230
+ `;
14231
+ var COMMAND_PERSISTENCE_UPGRADE_SQL = `
14232
+ ALTER TABLE supacloud_commands.execution_receipts
14233
+ ADD COLUMN IF NOT EXISTS input_fingerprint text,
14234
+ ADD COLUMN IF NOT EXISTS input_payload text;
14235
+ DO $migration$
14236
+ BEGIN
14237
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='supacloud_commands'
14238
+ AND table_name='execution_receipts' AND column_name='input_key') THEN
14239
+ EXECUTE 'UPDATE supacloud_commands.execution_receipts
14240
+ SET input_fingerprint=encode(sha256(convert_to(input_key,''UTF8'')),''hex''),input_payload=input_key
14241
+ WHERE input_fingerprint IS NULL';
14242
+ ALTER TABLE supacloud_commands.execution_receipts DROP COLUMN input_key;
14243
+ END IF;
14244
+ END
14245
+ $migration$;
14246
+ ALTER TABLE supacloud_commands.execution_receipts ALTER COLUMN input_fingerprint SET NOT NULL;
14247
+ ALTER TABLE supacloud_commands.execution_receipts
14248
+ DROP CONSTRAINT IF EXISTS execution_receipts_status_check,
14249
+ DROP CONSTRAINT IF EXISTS execution_receipts_check1,
14250
+ DROP CONSTRAINT IF EXISTS execution_receipts_transactional_check,
14251
+ DROP CONSTRAINT IF EXISTS execution_receipts_audit_terminal_check;
14252
+ ALTER TABLE supacloud_commands.execution_receipts
14253
+ ADD CONSTRAINT execution_receipts_status_check CHECK (status IN ('pending','unknown','confirmed','rejected')),
14254
+ ADD CONSTRAINT execution_receipts_transactional_check CHECK (
14255
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete')),
14256
+ ADD CONSTRAINT execution_receipts_audit_terminal_check CHECK (
14257
+ (audit_state <> 'complete' OR status IN ('confirmed','rejected'))
14258
+ AND (status <> 'rejected' OR audit_state = 'complete'));
14259
+ DO $constraint$
14260
+ BEGIN
14261
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
14262
+ WHERE conrelid='supacloud_commands.execution_receipts'::regclass
14263
+ AND conname='execution_receipts_input_fingerprint_check') THEN
14264
+ ALTER TABLE supacloud_commands.execution_receipts
14265
+ ADD CONSTRAINT execution_receipts_input_fingerprint_check CHECK (input_fingerprint ~ '^[a-f0-9]{64}$');
14266
+ END IF;
14267
+ END
14268
+ $constraint$;
14269
+ ${COMMAND_PERSISTENCE_SQL}
14270
+ DO $backfill$
14271
+ DECLARE r record;
14272
+ BEGIN
14273
+ FOR r IN SELECT * FROM supacloud_commands.execution_receipts
14274
+ WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
14275
+ AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
14276
+ LOOP
14277
+ PERFORM supacloud_workflows.start_run(
14278
+ r.dispatch_key,'supacloud.command.reconcile','1','reconcile',
14279
+ jsonb_build_object('commandId',r.dispatch_key,'tenantId',r.tenant_id,
14280
+ 'actorId',r.actor_id,'command',r.command,'operationId',r.operation_key),20
14281
+ );
14282
+ END LOOP;
14283
+ END
14284
+ $backfill$;
14285
+ DROP INDEX IF EXISTS supacloud_commands.execution_pending_recovery;
14286
+ ALTER TABLE supacloud_commands.execution_receipts
14287
+ DROP COLUMN IF EXISTS lease_id,
14288
+ DROP COLUMN IF EXISTS lease_until,
14289
+ DROP COLUMN IF EXISTS next_attempt_at,
14290
+ DROP COLUMN IF EXISTS recovery_attempts;
14291
+ `;
14091
14292
 
14092
14293
  // src/runtime/node/project.ts
14093
14294
  async function loadSupabaseProject(projectDir, seed = {}) {
@@ -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;IAgDzB,OAAO,CAAC,YAAY;YAqCN,SAAS;YAqET,MAAM;CAqFrB"}
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.1",
3
+ "version": "0.28.3",
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",
@@ -54,36 +54,37 @@
54
54
  "typecheck": "bun x tsc --noEmit -p tsconfig.json"
55
55
  },
56
56
  "dependencies": {
57
- "@electric-sql/pglite": "0.5.8",
58
- "@electric-sql/pglite-pg_textsearch": "0.0.10",
59
- "@electric-sql/pglite-pgmq": "0.0.6",
60
- "@electric-sql/pglite-pgvector": "0.0.9",
61
- "@electric-sql/pglite-postgis": "0.2.8",
62
- "@supacloud/db": "^0.6.0",
57
+ "@electric-sql/pglite": "0.5.10",
58
+ "@electric-sql/pglite-pg_textsearch": "0.0.12",
59
+ "@electric-sql/pglite-pgmq": "0.0.8",
60
+ "@electric-sql/pglite-pgvector": "0.0.11",
61
+ "@electric-sql/pglite-postgis": "0.2.10",
62
+ "@supacloud/db": "^0.17.0",
63
63
  "exact-mirror": "1.2.6",
64
64
  "tar": "^7.5.22",
65
- "typebox": "1.3.36"
65
+ "typebox": "1.3.37"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@supacloud/app": "file:../app",
69
+ "@supacloud/contracts": "file:../contracts",
69
70
  "@supacloud/compiler": "file:../compiler",
70
71
  "@supacloud/elysia": "file:../elysia",
71
72
  "@supabase/supabase-js": "^2.117.3",
72
73
  "@types/bun": "^1.4.3",
73
- "effect": "4.0.2",
74
- "elysia": "2.0.0-beta.21",
74
+ "effect": "4.0.5",
75
+ "elysia": "2.0.0-beta.32",
75
76
  "jose": "^6.2.12",
76
77
  "typescript": "^7.0.2"
77
78
  },
78
79
  "overrides": {
79
- "@supacloud/db": "^0.6.0",
80
+ "@supacloud/db": "^0.17.0",
80
81
  "@supacloud/app": "file:../app",
81
82
  "@supacloud/compiler": "file:../compiler",
82
83
  "@supacloud/elysia": "file:../elysia",
83
84
  "@supacloud/contracts": "file:../contracts",
84
85
  "@supacloud/commands": "file:../commands",
85
86
  "@supacloud/delivery": "file:../delivery",
86
- "@graphql-tools/utils": "^12.0.3"
87
+ "@graphql-tools/utils": "^12.0.4"
87
88
  },
88
89
  "engines": {
89
90
  "bun": ">=1.4.3"