@supacloud/lite 0.8.5 → 0.9.1

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/dist/cli.js CHANGED
@@ -3,12 +3,13 @@
3
3
  var __require = import.meta.require;
4
4
 
5
5
  // src/cli.ts
6
+ import { existsSync as existsSync4 } from "fs";
6
7
  import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
7
- import { dirname as dirname7, join as join10, resolve as resolve4 } from "path";
8
+ import { dirname as dirname7, join as join10, resolve as resolve5 } from "path";
8
9
  // package.json
9
10
  var package_default = {
10
11
  name: "@supacloud/lite",
11
- version: "0.8.5",
12
+ version: "0.9.1",
12
13
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
13
14
  type: "module",
14
15
  license: "Apache-2.0",
@@ -46,7 +47,7 @@ var package_default = {
46
47
  dev: "bun run src/cli.ts start",
47
48
  start: "bun run src/cli.ts start",
48
49
  test: "bun test --timeout 20000",
49
- "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts test/qauth-workflow-matrix.test.ts --timeout 180000",
50
+ "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts test/qauth-workflow-matrix.test.ts test/maker-checker-workflow.test.ts test/commands-artifacts.test.ts --timeout 180000",
50
51
  parity: "bun run parity/harness.ts",
51
52
  "parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
52
53
  "check:native": "bun run test:native && bun run parity:native",
@@ -62,11 +63,11 @@ var package_default = {
62
63
  },
63
64
  devDependencies: {
64
65
  "@supabase/supabase-js": "^2.112.2",
65
- "@types/bun": "^1.3.14",
66
+ "@types/bun": "^1.4.0",
66
67
  typescript: "^7.0.2"
67
68
  },
68
69
  engines: {
69
- bun: ">=1.3.14"
70
+ bun: ">=1.4.0"
70
71
  },
71
72
  publishConfig: {
72
73
  access: "public",
@@ -4767,6 +4768,452 @@ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_rol
4767
4768
  GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
4768
4769
  GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
4769
4770
  -- supacloud:sql-module:workflows-public:end
4771
+
4772
+ -- supacloud:sql-module:commands-public:start
4773
+ CREATE SCHEMA IF NOT EXISTS supacloud_commands;
4774
+ REVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC, anon, authenticated;
4775
+ GRANT USAGE ON SCHEMA supacloud_commands TO service_role;
4776
+
4777
+ CREATE TABLE IF NOT EXISTS supacloud_commands.receipts (
4778
+ id uuid PRIMARY KEY,
4779
+ command_type text NOT NULL
4780
+ CHECK (command_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4781
+ target_type text NOT NULL
4782
+ CHECK (target_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4783
+ target_id text NOT NULL CHECK (char_length(target_id) BETWEEN 1 AND 500),
4784
+ actor_id uuid,
4785
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb
4786
+ CHECK (jsonb_typeof(payload) = 'object'),
4787
+ payload_fingerprint text NOT NULL CHECK (char_length(payload_fingerprint) = 36),
4788
+ workflow_run_id uuid NOT NULL UNIQUE
4789
+ REFERENCES supacloud_workflows.runs(id) ON DELETE RESTRICT,
4790
+ created_at timestamptz NOT NULL DEFAULT now()
4791
+ );
4792
+
4793
+ CREATE INDEX IF NOT EXISTS supacloud_commands_target_idx
4794
+ ON supacloud_commands.receipts (target_type, target_id, created_at DESC, id);
4795
+
4796
+ CREATE OR REPLACE FUNCTION supacloud_commands.snapshot(
4797
+ p_command_id uuid,
4798
+ p_idempotent boolean DEFAULT false
4799
+ ) RETURNS jsonb
4800
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
4801
+ SELECT jsonb_build_object(
4802
+ 'commandId', receipt.id,
4803
+ 'commandType', receipt.command_type,
4804
+ 'targetType', receipt.target_type,
4805
+ 'targetId', receipt.target_id,
4806
+ 'actorId', receipt.actor_id,
4807
+ 'payloadFingerprint', receipt.payload_fingerprint,
4808
+ 'createdAt', receipt.created_at,
4809
+ 'idempotent', p_idempotent,
4810
+ 'workflow', supacloud_workflows.snapshot(receipt.workflow_run_id, false)
4811
+ )
4812
+ FROM supacloud_commands.receipts receipt
4813
+ WHERE receipt.id = p_command_id
4814
+ $$;
4815
+
4816
+ -- Application-owned SECURITY DEFINER RPCs may call this after their domain
4817
+ -- mutation. PostgreSQL commits the domain write, receipt, and workflow enqueue
4818
+ -- together, so a lost response can be replayed with the same command ID.
4819
+ DROP FUNCTION IF EXISTS supacloud_commands.submit(uuid, text, text, text, uuid, jsonb, integer);
4820
+ CREATE OR REPLACE FUNCTION supacloud_commands.submit(request jsonb)
4821
+ RETURNS jsonb
4822
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4823
+ DECLARE
4824
+ command_id_text text;
4825
+ command_id uuid;
4826
+ actor_id uuid;
4827
+ max_attempts integer;
4828
+ payload jsonb;
4829
+ normalized_command_type text;
4830
+ normalized_target_type text;
4831
+ normalized_target_id text;
4832
+ fingerprint text;
4833
+ existing supacloud_commands.receipts%ROWTYPE;
4834
+ BEGIN
4835
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4836
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4837
+ END IF;
4838
+ command_id_text := request ->> 'commandId';
4839
+ IF command_id_text IS NULL
4840
+ OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
4841
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4842
+ END IF;
4843
+ command_id := command_id_text::uuid;
4844
+ IF request ? 'actorId' AND request ->> 'actorId' IS NOT NULL THEN
4845
+ actor_id := (request ->> 'actorId')::uuid;
4846
+ END IF;
4847
+ max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
4848
+ payload := coalesce(request -> 'payload', '{}'::jsonb);
4849
+ normalized_command_type := nullif(btrim(request ->> 'commandType'), '');
4850
+ normalized_target_type := nullif(btrim(request ->> 'targetType'), '');
4851
+ normalized_target_id := nullif(btrim(request ->> 'targetId'), '');
4852
+ IF normalized_command_type IS NULL
4853
+ OR normalized_command_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4854
+ OR normalized_target_type IS NULL
4855
+ OR normalized_target_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4856
+ OR normalized_target_id IS NULL
4857
+ OR char_length(normalized_target_id) > 500
4858
+ OR jsonb_typeof(payload) IS DISTINCT FROM 'object'
4859
+ OR max_attempts NOT BETWEEN 1 AND 100 THEN
4860
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4861
+ END IF;
4862
+
4863
+ fingerprint := 'md5:' || md5(payload::text);
4864
+ PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));
4865
+ SELECT * INTO existing FROM supacloud_commands.receipts WHERE id = command_id;
4866
+ IF FOUND THEN
4867
+ IF existing.command_type <> normalized_command_type
4868
+ OR existing.target_type <> normalized_target_type
4869
+ OR existing.target_id <> normalized_target_id
4870
+ OR existing.actor_id IS DISTINCT FROM actor_id
4871
+ OR existing.payload <> payload
4872
+ OR existing.payload_fingerprint <> fingerprint THEN
4873
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4874
+ END IF;
4875
+ RETURN supacloud_commands.snapshot(command_id, true);
4876
+ END IF;
4877
+
4878
+ PERFORM supacloud_workflows.start_run(
4879
+ command_id,
4880
+ 'command.' || normalized_command_type,
4881
+ '1',
4882
+ 'execute',
4883
+ jsonb_build_object(
4884
+ 'commandId', command_id,
4885
+ 'commandType', normalized_command_type,
4886
+ 'targetType', normalized_target_type,
4887
+ 'targetId', normalized_target_id,
4888
+ 'actorId', actor_id,
4889
+ 'payload', payload,
4890
+ 'payloadFingerprint', fingerprint
4891
+ ),
4892
+ max_attempts
4893
+ );
4894
+
4895
+ INSERT INTO supacloud_commands.receipts (
4896
+ id, command_type, target_type, target_id, actor_id, payload,
4897
+ payload_fingerprint, workflow_run_id
4898
+ ) VALUES (
4899
+ command_id, normalized_command_type, normalized_target_type,
4900
+ normalized_target_id, actor_id, payload, fingerprint, command_id
4901
+ );
4902
+ RETURN supacloud_commands.snapshot(command_id, false);
4903
+ EXCEPTION
4904
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4905
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4906
+ END;
4907
+ $$;
4908
+
4909
+ CREATE OR REPLACE FUNCTION public.supacloud_command_submit(request jsonb)
4910
+ RETURNS jsonb
4911
+ LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4912
+ SELECT supacloud_commands.submit(request)
4913
+ $$;
4914
+
4915
+ CREATE OR REPLACE FUNCTION public.supacloud_command_get(request jsonb)
4916
+ RETURNS jsonb
4917
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4918
+ DECLARE
4919
+ command_id_text text;
4920
+ BEGIN
4921
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4922
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';
4923
+ END IF;
4924
+ command_id_text := request ->> 'commandId';
4925
+ IF command_id_text IS NULL
4926
+ OR command_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
4927
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';
4928
+ END IF;
4929
+ RETURN supacloud_commands.snapshot(command_id_text::uuid, false);
4930
+ EXCEPTION
4931
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4932
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';
4933
+ END;
4934
+ $$;
4935
+
4936
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_commands
4937
+ FROM PUBLIC, anon, authenticated, service_role;
4938
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_commands
4939
+ FROM PUBLIC, anon, authenticated, service_role;
4940
+ REVOKE ALL ON FUNCTION public.supacloud_command_submit(jsonb) FROM PUBLIC, anon, authenticated;
4941
+ REVOKE ALL ON FUNCTION public.supacloud_command_get(jsonb) FROM PUBLIC, anon, authenticated;
4942
+ GRANT EXECUTE ON FUNCTION public.supacloud_command_submit(jsonb) TO service_role;
4943
+ GRANT EXECUTE ON FUNCTION public.supacloud_command_get(jsonb) TO service_role;
4944
+ -- supacloud:sql-module:commands-public:end
4945
+
4946
+ -- supacloud:sql-module:artifacts-public:start
4947
+ CREATE SCHEMA IF NOT EXISTS supacloud_artifacts;
4948
+ REVOKE ALL ON SCHEMA supacloud_artifacts FROM PUBLIC, anon, authenticated;
4949
+ GRANT USAGE ON SCHEMA supacloud_artifacts TO service_role;
4950
+
4951
+ CREATE TABLE IF NOT EXISTS supacloud_artifacts.artifacts (
4952
+ id uuid PRIMARY KEY,
4953
+ storage_object_id uuid NOT NULL UNIQUE REFERENCES storage.objects(id) ON DELETE RESTRICT,
4954
+ bucket_id text NOT NULL,
4955
+ object_path text NOT NULL,
4956
+ object_version text NOT NULL,
4957
+ artifact_type text NOT NULL
4958
+ CHECK (artifact_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4959
+ sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
4960
+ size_bytes bigint NOT NULL CHECK (size_bytes >= 0),
4961
+ mime_type text NOT NULL CHECK (char_length(mime_type) BETWEEN 1 AND 255),
4962
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
4963
+ retention_until timestamptz,
4964
+ created_by uuid,
4965
+ created_at timestamptz NOT NULL DEFAULT now(),
4966
+ CHECK (retention_until IS NULL OR retention_until >= created_at),
4967
+ UNIQUE (bucket_id, object_path, object_version)
4968
+ );
4969
+
4970
+ CREATE INDEX IF NOT EXISTS supacloud_artifacts_lookup_idx
4971
+ ON supacloud_artifacts.artifacts (artifact_type, created_at DESC, id);
4972
+
4973
+ CREATE TABLE IF NOT EXISTS supacloud_artifacts.lineage (
4974
+ parent_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,
4975
+ child_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,
4976
+ relation_type text NOT NULL
4977
+ CHECK (relation_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4978
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
4979
+ created_at timestamptz NOT NULL DEFAULT now(),
4980
+ PRIMARY KEY (parent_artifact_id, child_artifact_id, relation_type),
4981
+ CHECK (parent_artifact_id <> child_artifact_id)
4982
+ );
4983
+
4984
+ CREATE INDEX IF NOT EXISTS supacloud_artifacts_lineage_child_idx
4985
+ ON supacloud_artifacts.lineage (child_artifact_id, created_at, parent_artifact_id);
4986
+
4987
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.guard_storage_object()
4988
+ RETURNS trigger
4989
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4990
+ BEGIN
4991
+ IF EXISTS (
4992
+ SELECT 1 FROM supacloud_artifacts.artifacts artifact
4993
+ WHERE artifact.storage_object_id = OLD.id
4994
+ ) THEN
4995
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IMMUTABLE' USING ERRCODE = '55000';
4996
+ END IF;
4997
+ RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
4998
+ END;
4999
+ $$;
5000
+
5001
+ DROP TRIGGER IF EXISTS supacloud_artifact_storage_fence ON storage.objects;
5002
+ CREATE TRIGGER supacloud_artifact_storage_fence
5003
+ BEFORE UPDATE OR DELETE ON storage.objects
5004
+ FOR EACH ROW EXECUTE FUNCTION supacloud_artifacts.guard_storage_object();
5005
+
5006
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.snapshot(
5007
+ p_artifact_id uuid,
5008
+ p_idempotent boolean DEFAULT false
5009
+ ) RETURNS jsonb
5010
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
5011
+ SELECT jsonb_build_object(
5012
+ 'artifactId', artifact.id,
5013
+ 'bucketId', artifact.bucket_id,
5014
+ 'objectPath', artifact.object_path,
5015
+ 'objectVersion', artifact.object_version,
5016
+ 'artifactType', artifact.artifact_type,
5017
+ 'sha256', artifact.sha256,
5018
+ 'sizeBytes', artifact.size_bytes::text,
5019
+ 'mimeType', artifact.mime_type,
5020
+ 'metadata', artifact.metadata,
5021
+ 'retentionUntil', artifact.retention_until,
5022
+ 'createdBy', artifact.created_by,
5023
+ 'createdAt', artifact.created_at,
5024
+ 'idempotent', p_idempotent,
5025
+ 'parents', coalesce((
5026
+ SELECT jsonb_agg(jsonb_build_object(
5027
+ 'artifactId', edge.parent_artifact_id,
5028
+ 'relationType', edge.relation_type,
5029
+ 'metadata', edge.metadata,
5030
+ 'createdAt', edge.created_at
5031
+ ) ORDER BY edge.created_at, edge.parent_artifact_id)
5032
+ FROM supacloud_artifacts.lineage edge
5033
+ WHERE edge.child_artifact_id = artifact.id
5034
+ ), '[]'::jsonb)
5035
+ )
5036
+ FROM supacloud_artifacts.artifacts artifact
5037
+ WHERE artifact.id = p_artifact_id
5038
+ $$;
5039
+
5040
+ DROP FUNCTION IF EXISTS supacloud_artifacts.register(uuid, text, text, text, text, bigint, text, jsonb, timestamptz, uuid);
5041
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.register(request jsonb)
5042
+ RETURNS jsonb
5043
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5044
+ DECLARE
5045
+ artifact_id uuid;
5046
+ size_bytes bigint;
5047
+ metadata jsonb;
5048
+ retention_until timestamptz;
5049
+ created_by uuid;
5050
+ normalized_bucket text;
5051
+ normalized_path text;
5052
+ normalized_type text;
5053
+ normalized_sha256 text;
5054
+ normalized_mime text;
5055
+ object_row storage.objects%ROWTYPE;
5056
+ existing supacloud_artifacts.artifacts%ROWTYPE;
5057
+ BEGIN
5058
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
5059
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';
5060
+ END IF;
5061
+ artifact_id := (request ->> 'artifactId')::uuid;
5062
+ size_bytes := (request ->> 'sizeBytes')::bigint;
5063
+ metadata := coalesce(request -> 'metadata', '{}'::jsonb);
5064
+ retention_until := (request ->> 'retentionUntil')::timestamptz;
5065
+ created_by := (request ->> 'createdBy')::uuid;
5066
+ normalized_bucket := nullif(btrim(request ->> 'bucketId'), '');
5067
+ normalized_path := nullif(btrim(request ->> 'objectPath'), '');
5068
+ normalized_type := nullif(btrim(request ->> 'artifactType'), '');
5069
+ normalized_sha256 := lower(nullif(btrim(request ->> 'sha256'), ''));
5070
+ normalized_mime := lower(nullif(btrim(request ->> 'mimeType'), ''));
5071
+ IF artifact_id IS NULL OR normalized_bucket IS NULL OR normalized_path IS NULL
5072
+ OR normalized_path LIKE '/%' OR normalized_path LIKE '%\\%'
5073
+ OR normalized_path ~ '(^|/)(.|..)(/|$)'
5074
+ OR normalized_type IS NULL
5075
+ OR normalized_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
5076
+ OR normalized_sha256 IS NULL OR normalized_sha256 !~ '^[0-9a-f]{64}$'
5077
+ OR size_bytes IS NULL OR size_bytes < 0
5078
+ OR normalized_mime IS NULL OR char_length(normalized_mime) > 255
5079
+ OR jsonb_typeof(metadata) IS DISTINCT FROM 'object'
5080
+ OR (retention_until IS NOT NULL AND retention_until < now()) THEN
5081
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';
5082
+ END IF;
5083
+
5084
+ SELECT * INTO object_row FROM storage.objects
5085
+ WHERE bucket_id = normalized_bucket AND name = normalized_path;
5086
+ IF NOT FOUND THEN
5087
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_OBJECT_NOT_FOUND' USING ERRCODE = 'P0002';
5088
+ END IF;
5089
+
5090
+ PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));
5091
+ SELECT * INTO existing FROM supacloud_artifacts.artifacts WHERE id = artifact_id;
5092
+ IF FOUND THEN
5093
+ IF existing.storage_object_id <> object_row.id
5094
+ OR existing.artifact_type <> normalized_type
5095
+ OR existing.sha256 <> normalized_sha256
5096
+ OR existing.size_bytes <> size_bytes
5097
+ OR existing.mime_type <> normalized_mime
5098
+ OR existing.metadata <> metadata
5099
+ OR existing.retention_until IS DISTINCT FROM retention_until
5100
+ OR existing.created_by IS DISTINCT FROM created_by THEN
5101
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
5102
+ END IF;
5103
+ RETURN supacloud_artifacts.snapshot(artifact_id, true);
5104
+ END IF;
5105
+
5106
+ INSERT INTO supacloud_artifacts.artifacts (
5107
+ id, storage_object_id, bucket_id, object_path, object_version,
5108
+ artifact_type, sha256, size_bytes, mime_type, metadata,
5109
+ retention_until, created_by
5110
+ ) VALUES (
5111
+ artifact_id, object_row.id, normalized_bucket, normalized_path,
5112
+ object_row.version::text, normalized_type, normalized_sha256,
5113
+ size_bytes, normalized_mime, metadata, retention_until, created_by
5114
+ );
5115
+ RETURN supacloud_artifacts.snapshot(artifact_id, false);
5116
+ EXCEPTION
5117
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
5118
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';
5119
+ END;
5120
+ $$;
5121
+
5122
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.link(
5123
+ p_parent_artifact_id uuid,
5124
+ p_child_artifact_id uuid,
5125
+ p_relation_type text,
5126
+ p_metadata jsonb
5127
+ ) RETURNS jsonb
5128
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5129
+ DECLARE
5130
+ normalized_relation text := nullif(btrim(p_relation_type), '');
5131
+ inserted_count integer;
5132
+ BEGIN
5133
+ IF p_parent_artifact_id IS NULL OR p_child_artifact_id IS NULL
5134
+ OR p_parent_artifact_id = p_child_artifact_id
5135
+ OR normalized_relation IS NULL
5136
+ OR normalized_relation !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
5137
+ OR jsonb_typeof(p_metadata) IS DISTINCT FROM 'object' THEN
5138
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';
5139
+ END IF;
5140
+ IF EXISTS (
5141
+ WITH RECURSIVE descendants(artifact_id) AS (
5142
+ SELECT edge.child_artifact_id
5143
+ FROM supacloud_artifacts.lineage edge
5144
+ WHERE edge.parent_artifact_id = p_child_artifact_id
5145
+ UNION
5146
+ SELECT edge.child_artifact_id
5147
+ FROM supacloud_artifacts.lineage edge
5148
+ JOIN descendants current ON edge.parent_artifact_id = current.artifact_id
5149
+ )
5150
+ SELECT 1 FROM descendants WHERE artifact_id = p_parent_artifact_id
5151
+ ) THEN
5152
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_CYCLE' USING ERRCODE = '23514';
5153
+ END IF;
5154
+ INSERT INTO supacloud_artifacts.lineage (
5155
+ parent_artifact_id, child_artifact_id, relation_type, metadata
5156
+ ) VALUES (
5157
+ p_parent_artifact_id, p_child_artifact_id, normalized_relation, p_metadata
5158
+ ) ON CONFLICT DO NOTHING;
5159
+ GET DIAGNOSTICS inserted_count = ROW_COUNT;
5160
+ IF inserted_count = 0 AND NOT EXISTS (
5161
+ SELECT 1 FROM supacloud_artifacts.lineage
5162
+ WHERE parent_artifact_id = p_parent_artifact_id
5163
+ AND child_artifact_id = p_child_artifact_id
5164
+ AND relation_type = normalized_relation
5165
+ AND metadata = p_metadata
5166
+ ) THEN
5167
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
5168
+ END IF;
5169
+ RETURN supacloud_artifacts.snapshot(p_child_artifact_id, inserted_count = 0);
5170
+ END;
5171
+ $$;
5172
+
5173
+ CREATE OR REPLACE FUNCTION public.supacloud_artifact_register(request jsonb)
5174
+ RETURNS jsonb
5175
+ LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5176
+ SELECT supacloud_artifacts.register(request)
5177
+ $$;
5178
+
5179
+ CREATE OR REPLACE FUNCTION public.supacloud_artifact_get(request jsonb)
5180
+ RETURNS jsonb
5181
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
5182
+ BEGIN
5183
+ RETURN supacloud_artifacts.snapshot((request ->> 'artifactId')::uuid, false);
5184
+ EXCEPTION
5185
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
5186
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_GET_INVALID' USING ERRCODE = '22023';
5187
+ END;
5188
+ $$;
5189
+
5190
+ CREATE OR REPLACE FUNCTION public.supacloud_artifact_link(request jsonb)
5191
+ RETURNS jsonb
5192
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5193
+ BEGIN
5194
+ RETURN supacloud_artifacts.link(
5195
+ (request ->> 'parentArtifactId')::uuid,
5196
+ (request ->> 'childArtifactId')::uuid,
5197
+ request ->> 'relationType',
5198
+ coalesce(request -> 'metadata', '{}'::jsonb)
5199
+ );
5200
+ EXCEPTION
5201
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
5202
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';
5203
+ END;
5204
+ $$;
5205
+
5206
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_artifacts
5207
+ FROM PUBLIC, anon, authenticated, service_role;
5208
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_artifacts
5209
+ FROM PUBLIC, anon, authenticated, service_role;
5210
+ REVOKE ALL ON FUNCTION public.supacloud_artifact_register(jsonb) FROM PUBLIC, anon, authenticated;
5211
+ REVOKE ALL ON FUNCTION public.supacloud_artifact_get(jsonb) FROM PUBLIC, anon, authenticated;
5212
+ REVOKE ALL ON FUNCTION public.supacloud_artifact_link(jsonb) FROM PUBLIC, anon, authenticated;
5213
+ GRANT EXECUTE ON FUNCTION public.supacloud_artifact_register(jsonb) TO service_role;
5214
+ GRANT EXECUTE ON FUNCTION public.supacloud_artifact_get(jsonb) TO service_role;
5215
+ GRANT EXECUTE ON FUNCTION public.supacloud_artifact_link(jsonb) TO service_role;
5216
+ -- supacloud:sql-module:artifacts-public:end
4770
5217
  `;
4771
5218
  var CRON_SQL = `
4772
5219
  create schema if not exists cron;
@@ -10259,12 +10706,260 @@ import { dirname as dirname2, join as join2 } from "path";
10259
10706
  // src/runtime/node/native/engine.ts
10260
10707
  import { execFileSync, spawn, spawnSync } from "child_process";
10261
10708
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
10262
- import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10709
+ import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10263
10710
  import { writeFile } from "fs/promises";
10264
10711
  import { homedir, tmpdir } from "os";
10265
10712
  import { join } from "path";
10266
10713
  import { extract as extractTar } from "tar";
10267
10714
 
10715
+ // src/runtime/node/native/replication.ts
10716
+ import { existsSync, statSync, writeFileSync } from "fs";
10717
+ import { isIP } from "net";
10718
+ import { resolve as resolve2 } from "path";
10719
+ var POWERSYNC_REPLICATION_ROLE = "supacloud_powersync";
10720
+ var POWERSYNC_PUBLICATION = "powersync";
10721
+ function validatePowerSyncReplicationOptions(options) {
10722
+ const host = options.host.trim();
10723
+ validateConnectionInput(host, options.port, options.password);
10724
+ const publicationTables = uniqueSorted(options.publicationTables.map(normalizeQualifiedTable));
10725
+ const allowCidrs = uniqueSorted(options.allowCidrs.map(normalizeCidr));
10726
+ validateAllowlists(publicationTables, allowCidrs);
10727
+ validateNetworkBoundary(host, allowCidrs, Boolean(options.tls));
10728
+ const tls = options.tls ? validateTlsOptions(options.tls) : undefined;
10729
+ return { ...options, host, allowCidrs, publicationTables, tls };
10730
+ }
10731
+ function buildPowerSyncPostgresArgs(options, hbaFile) {
10732
+ const validated = validatePowerSyncReplicationOptions(options);
10733
+ const args = [
10734
+ "-c",
10735
+ `listen_addresses=${validated.host}`,
10736
+ "-c",
10737
+ `port=${validated.port}`,
10738
+ "-c",
10739
+ "wal_level=logical",
10740
+ "-c",
10741
+ "max_wal_senders=4",
10742
+ "-c",
10743
+ "max_replication_slots=4",
10744
+ "-c",
10745
+ "max_slot_wal_keep_size=1024MB",
10746
+ "-c",
10747
+ "wal_keep_size=64MB",
10748
+ "-c",
10749
+ `hba_file=${resolve2(hbaFile)}`
10750
+ ];
10751
+ if (!validated.tls)
10752
+ return [...args, "-c", "ssl=off"];
10753
+ args.push("-c", "ssl=on", "-c", `ssl_cert_file=${validated.tls.certFile}`, "-c", `ssl_key_file=${validated.tls.keyFile}`);
10754
+ return args;
10755
+ }
10756
+ function writePowerSyncHba(path, options) {
10757
+ const validated = validatePowerSyncReplicationOptions(options);
10758
+ const hostRecord = validated.tls ? "hostssl" : "host";
10759
+ const lines = [
10760
+ "# Managed by SupaCloud Lite. Changes are replaced when the PowerSync profile starts.",
10761
+ "local all postgres trust",
10762
+ ...validated.allowCidrs.flatMap((cidr) => [
10763
+ `${hostRecord} postgres ${POWERSYNC_REPLICATION_ROLE} ${cidr} scram-sha-256`,
10764
+ `${hostRecord} replication ${POWERSYNC_REPLICATION_ROLE} ${cidr} scram-sha-256`
10765
+ ]),
10766
+ "host all all 0.0.0.0/0 reject",
10767
+ "host all all ::/0 reject",
10768
+ ""
10769
+ ];
10770
+ writeFileSync(path, lines.join(`
10771
+ `), { mode: 384 });
10772
+ }
10773
+ async function ensurePowerSyncReplicationCatalog(engine, options) {
10774
+ const validated = validatePowerSyncReplicationOptions(options);
10775
+ await engine.transaction(async (tx) => {
10776
+ const tables = await inspectPublicationTables(tx, validated.publicationTables);
10777
+ validatePublicationTables(tables);
10778
+ await configureReplicationRole(tx, validated.password);
10779
+ await synchronizeReplicationGrants(tx, tables);
10780
+ await assertEffectiveSelectAllowlist(tx, tables);
10781
+ await synchronizePublication(tx, tables);
10782
+ });
10783
+ }
10784
+ function validatePublicationTables(tables) {
10785
+ const missing = tables.filter((table) => !table.present).map((table) => table.name);
10786
+ if (missing.length > 0) {
10787
+ throw new Error(`PowerSync publication tables do not exist: ${missing.join(", ")}`);
10788
+ }
10789
+ const unsupported2 = tables.filter((table) => table.present && !["r", "p"].includes(table.relkind ?? ""));
10790
+ if (unsupported2.length > 0) {
10791
+ throw new Error(`PowerSync publication only accepts ordinary or partitioned tables: ${unsupported2.map((table) => table.name).join(", ")}`);
10792
+ }
10793
+ }
10794
+ async function configureReplicationRole(tx, passwordValue) {
10795
+ await tx.exec(`
10796
+ DO $profile$
10797
+ BEGIN
10798
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${POWERSYNC_REPLICATION_ROLE}') THEN
10799
+ CREATE ROLE ${POWERSYNC_REPLICATION_ROLE};
10800
+ END IF;
10801
+ END
10802
+ $profile$;
10803
+ ALTER ROLE ${POWERSYNC_REPLICATION_ROLE}
10804
+ WITH LOGIN NOINHERIT REPLICATION BYPASSRLS CONNECTION LIMIT 4 PASSWORD ${quoteLiteral2(passwordValue)};
10805
+ ALTER ROLE ${POWERSYNC_REPLICATION_ROLE} SET search_path = pg_catalog;
10806
+ GRANT CONNECT ON DATABASE postgres TO ${POWERSYNC_REPLICATION_ROLE};
10807
+ `);
10808
+ }
10809
+ async function synchronizeReplicationGrants(tx, tables) {
10810
+ await revokeReplicationGrants(tx);
10811
+ await grantReplicationAllowlist(tx, tables);
10812
+ }
10813
+ async function revokeReplicationGrants(tx) {
10814
+ const staleGrants = await tx.query(`
10815
+ SELECT DISTINCT table_schema, table_name
10816
+ FROM information_schema.role_table_grants
10817
+ WHERE grantee = '${POWERSYNC_REPLICATION_ROLE}' AND privilege_type = 'SELECT'
10818
+ `);
10819
+ for (const grant of staleGrants.rows) {
10820
+ await tx.exec(`REVOKE SELECT ON TABLE ${quoteIdentifier(grant.table_schema)}.${quoteIdentifier(grant.table_name)} ` + `FROM ${POWERSYNC_REPLICATION_ROLE}`);
10821
+ }
10822
+ const applicationSchemas = await tx.query(`
10823
+ SELECT nspname AS schema_name
10824
+ FROM pg_namespace
10825
+ WHERE nspname NOT IN ('pg_catalog', 'information_schema')
10826
+ AND nspname !~ '^pg_toast'
10827
+ `);
10828
+ for (const schema of applicationSchemas.rows) {
10829
+ await tx.exec(`REVOKE USAGE ON SCHEMA ${quoteIdentifier(schema.schema_name)} FROM ${POWERSYNC_REPLICATION_ROLE}`);
10830
+ }
10831
+ }
10832
+ async function grantReplicationAllowlist(tx, tables) {
10833
+ const schemas = uniqueSorted(tables.map((table) => table.schema));
10834
+ for (const schema of schemas) {
10835
+ await tx.exec(`GRANT USAGE ON SCHEMA ${quoteIdentifier(schema)} TO ${POWERSYNC_REPLICATION_ROLE}`);
10836
+ }
10837
+ for (const table of tables) {
10838
+ await tx.exec(`GRANT SELECT ON TABLE ${quoteQualifiedTable(table.name)} TO ${POWERSYNC_REPLICATION_ROLE}`);
10839
+ }
10840
+ }
10841
+ async function assertEffectiveSelectAllowlist(tx, tables) {
10842
+ const allowed = new Set(tables.map((table) => table.name));
10843
+ const selectable = await tx.query(`
10844
+ SELECT namespace.nspname || '.' || relation.relname AS qualified_name
10845
+ FROM pg_class relation
10846
+ JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
10847
+ WHERE relation.relkind IN ('r', 'p')
10848
+ AND namespace.nspname NOT IN ('pg_catalog', 'information_schema')
10849
+ AND namespace.nspname !~ '^pg_toast'
10850
+ AND has_table_privilege('${POWERSYNC_REPLICATION_ROLE}', relation.oid, 'SELECT')
10851
+ ORDER BY namespace.nspname, relation.relname
10852
+ `);
10853
+ const outsideAllowlist = selectable.rows.map((row) => row.qualified_name).filter((table) => !allowed.has(table));
10854
+ if (outsideAllowlist.length > 0) {
10855
+ throw new Error(`PowerSync role has SELECT outside the publication allowlist: ${outsideAllowlist.join(", ")}`);
10856
+ }
10857
+ }
10858
+ async function synchronizePublication(tx, tables) {
10859
+ const publication = await tx.query(`SELECT puballtables FROM pg_publication WHERE pubname = '${POWERSYNC_PUBLICATION}'`);
10860
+ if (publication.rows[0]?.puballtables) {
10861
+ throw new Error("existing powersync publication uses FOR ALL TABLES; replace it with an explicit allowlist");
10862
+ }
10863
+ if (publication.rows.length === 0) {
10864
+ await tx.exec(`CREATE PUBLICATION ${POWERSYNC_PUBLICATION} WITH (publish = 'insert, update, delete')`);
10865
+ }
10866
+ await tx.exec(`ALTER PUBLICATION ${POWERSYNC_PUBLICATION} SET TABLE ${tables.map((table) => quoteQualifiedTable(table.name)).join(", ")}; ` + `ALTER PUBLICATION ${POWERSYNC_PUBLICATION} SET (publish = 'insert, update, delete')`);
10867
+ }
10868
+ async function inspectPublicationTables(tx, names) {
10869
+ const rows = [];
10870
+ for (const name of names) {
10871
+ const tableLookup = await tx.query(`
10872
+ SELECT namespace.nspname AS schema_name, relation.relkind
10873
+ FROM pg_class relation
10874
+ JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
10875
+ WHERE relation.oid = to_regclass($1)
10876
+ `, [name]);
10877
+ const row = tableLookup.rows[0];
10878
+ rows.push({ name, schema: row?.schema_name, relkind: row?.relkind, present: Boolean(row) });
10879
+ }
10880
+ return rows;
10881
+ }
10882
+ function validateConnectionInput(host, port, password) {
10883
+ if (!host || host !== "localhost" && isIP(host) === 0) {
10884
+ throw new Error("PowerSync replication host must be localhost or an explicit IP address");
10885
+ }
10886
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
10887
+ throw new Error("PowerSync replication port must be between 1 and 65535");
10888
+ }
10889
+ if (password.length < 32) {
10890
+ throw new Error("SUPACLOUD_LITE_POWERSYNC_PASSWORD must contain at least 32 characters");
10891
+ }
10892
+ if (password.includes("\x00"))
10893
+ throw new Error("SUPACLOUD_LITE_POWERSYNC_PASSWORD must not contain NUL bytes");
10894
+ }
10895
+ function validateAllowlists(publicationTables, allowCidrs) {
10896
+ if (publicationTables.length === 0) {
10897
+ throw new Error("PowerSync replication requires an explicit publication table allowlist");
10898
+ }
10899
+ if (allowCidrs.length === 0)
10900
+ throw new Error("PowerSync replication requires at least one client CIDR");
10901
+ }
10902
+ function validateNetworkBoundary(host, allowCidrs, tls) {
10903
+ if (!isLoopbackHost(host) && !tls) {
10904
+ throw new Error("non-loopback PowerSync replication requires TLS certificate and key files");
10905
+ }
10906
+ if (!tls && allowCidrs.some((cidr) => !isLoopbackCidr(cidr))) {
10907
+ throw new Error("non-loopback PowerSync client CIDRs require TLS");
10908
+ }
10909
+ }
10910
+ function validateTlsOptions(options) {
10911
+ const tls = {
10912
+ certFile: resolve2(options.certFile),
10913
+ keyFile: resolve2(options.keyFile)
10914
+ };
10915
+ for (const path of [tls.certFile, tls.keyFile]) {
10916
+ if (!existsSync(path) || !statSync(path).isFile())
10917
+ throw new Error(`PowerSync TLS file does not exist: ${path}`);
10918
+ }
10919
+ if (process.platform !== "win32" && (statSync(tls.keyFile).mode & 63) !== 0) {
10920
+ throw new Error("PowerSync TLS private key must not be readable by group or other users");
10921
+ }
10922
+ return tls;
10923
+ }
10924
+ function normalizeQualifiedTable(tableName) {
10925
+ const normalized = tableName.trim().toLowerCase();
10926
+ if (!/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/.test(normalized)) {
10927
+ throw new Error(`invalid PowerSync publication table: ${tableName}`);
10928
+ }
10929
+ return normalized;
10930
+ }
10931
+ function normalizeCidr(cidr) {
10932
+ const normalized = cidr.trim();
10933
+ const match = /^(.+)\/(\d{1,3})$/.exec(normalized);
10934
+ if (!match)
10935
+ throw new Error(`invalid PowerSync client CIDR: ${cidr}`);
10936
+ const address = match[1];
10937
+ const family = isIP(address);
10938
+ const prefix = Number(match[2]);
10939
+ if (family === 0 || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
10940
+ throw new Error(`invalid PowerSync client CIDR: ${cidr}`);
10941
+ }
10942
+ return `${address}/${prefix}`;
10943
+ }
10944
+ function isLoopbackHost(host) {
10945
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
10946
+ }
10947
+ function isLoopbackCidr(cidr) {
10948
+ return cidr === "127.0.0.1/32" || cidr === "::1/128";
10949
+ }
10950
+ function quoteIdentifier(identifier) {
10951
+ return `"${identifier.replaceAll('"', '""')}"`;
10952
+ }
10953
+ function quoteQualifiedTable(tableName) {
10954
+ return tableName.split(".").map(quoteIdentifier).join(".");
10955
+ }
10956
+ function quoteLiteral2(literal) {
10957
+ return `'${literal.replaceAll("'", "''")}'`;
10958
+ }
10959
+ function uniqueSorted(values) {
10960
+ return [...new Set(values)].sort();
10961
+ }
10962
+
10268
10963
  // src/runtime/node/native/wire.ts
10269
10964
  import { createConnection } from "net";
10270
10965
  import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
@@ -10296,7 +10991,7 @@ class PgWireClient {
10296
10991
  return client;
10297
10992
  }
10298
10993
  open(opts) {
10299
- return new Promise((resolve2, reject) => {
10994
+ return new Promise((resolve3, reject) => {
10300
10995
  this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
10301
10996
  this.socket.on("error", (e) => {
10302
10997
  if (this.pending)
@@ -10394,7 +11089,7 @@ class PgWireClient {
10394
11089
  this.buffer = Buffer.concat([this.buffer, c]);
10395
11090
  this.processMessages();
10396
11091
  });
10397
- resolve2();
11092
+ resolve3();
10398
11093
  return;
10399
11094
  }
10400
11095
  }
@@ -10403,10 +11098,10 @@ class PgWireClient {
10403
11098
  });
10404
11099
  }
10405
11100
  run(send) {
10406
- const op = this.queue.then(() => new Promise((resolve2, reject) => {
11101
+ const op = this.queue.then(() => new Promise((resolve3, reject) => {
10407
11102
  if (this.closed)
10408
11103
  return reject(new Error("connection closed"));
10409
- this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
11104
+ this.pending = { resolve: resolve3, reject, results: [], columns: [], error: null };
10410
11105
  send();
10411
11106
  }));
10412
11107
  this.queue = op.catch(() => {});
@@ -10437,11 +11132,11 @@ class PgWireClient {
10437
11132
  return results[0] ?? { rows: [] };
10438
11133
  }
10439
11134
  close() {
10440
- return new Promise((resolve2) => {
11135
+ return new Promise((resolve3) => {
10441
11136
  if (this.closed)
10442
- return resolve2();
11137
+ return resolve3();
10443
11138
  this.socket.write(message(88, Buffer.alloc(0)));
10444
- this.socket.end(() => resolve2());
11139
+ this.socket.end(() => resolve3());
10445
11140
  });
10446
11141
  }
10447
11142
  nextMessage() {
@@ -10830,7 +11525,7 @@ function reportedGlibcVersion() {
10830
11525
  }
10831
11526
  function glibcDynamicLoaderPresent() {
10832
11527
  const loaderPaths = process.arch === "x64" ? GLIBC_DYNAMIC_LOADERS.x64 : process.arch === "arm64" ? GLIBC_DYNAMIC_LOADERS.arm64 : [];
10833
- return loaderPaths.some((loaderPath) => existsSync(loaderPath));
11528
+ return loaderPaths.some((loaderPath) => existsSync2(loaderPath));
10834
11529
  }
10835
11530
  function lddVersion() {
10836
11531
  const command = spawnSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -10850,7 +11545,7 @@ function target() {
10850
11545
  throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
10851
11546
  }
10852
11547
  function isCompleteInstall(dir) {
10853
- return existsSync(join(dir, "bin", "postgres")) && existsSync(join(dir, "share", "postgres.bki"));
11548
+ return existsSync2(join(dir, "bin", "postgres")) && existsSync2(join(dir, "share", "postgres.bki"));
10854
11549
  }
10855
11550
  var PINNED_SHA256 = {
10856
11551
  "postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
@@ -10956,7 +11651,7 @@ async function fetchRelease(url) {
10956
11651
  lastError = error;
10957
11652
  }
10958
11653
  if (attempt < 3)
10959
- await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
11654
+ await new Promise((resolve3) => setTimeout(resolve3, attempt * 500));
10960
11655
  }
10961
11656
  throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
10962
11657
  }
@@ -10968,6 +11663,7 @@ dynamic_shared_memory_type = posix
10968
11663
  max_connections = 10
10969
11664
  wal_level = minimal
10970
11665
  max_wal_senders = 0
11666
+ max_replication_slots = 0
10971
11667
  logging_collector = off
10972
11668
  `;
10973
11669
  async function createNativeEngine(opts) {
@@ -10978,7 +11674,7 @@ async function createNativeEngine(opts) {
10978
11674
  try {
10979
11675
  const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
10980
11676
  const bin = (name) => join(installDir, "bin", name);
10981
- if (!existsSync(join(opts.dataDir, "PG_VERSION"))) {
11677
+ if (!existsSync2(join(opts.dataDir, "PG_VERSION"))) {
10982
11678
  mkdirSync(opts.dataDir, { recursive: true });
10983
11679
  try {
10984
11680
  execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
@@ -10994,7 +11690,20 @@ ${stderr || error.message}`);
10994
11690
  removeStalePidFile(join(opts.dataDir, "postmaster.pid"));
10995
11691
  socketDirectory = mkdtempSync(join(tmpdir(), "scl-"));
10996
11692
  chmodSync(socketDirectory, 448);
10997
- postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
11693
+ const replicationHba = opts.replication ? join(opts.dataDir, "supacloud-powersync-hba.conf") : undefined;
11694
+ if (opts.replication && replicationHba)
11695
+ writePowerSyncHba(replicationHba, opts.replication);
11696
+ const replicationArgs = opts.replication && replicationHba ? buildPowerSyncPostgresArgs(opts.replication, replicationHba) : [
11697
+ "-c",
11698
+ "listen_addresses=",
11699
+ "-c",
11700
+ "wal_level=minimal",
11701
+ "-c",
11702
+ "max_wal_senders=0",
11703
+ "-c",
11704
+ "max_replication_slots=0"
11705
+ ];
11706
+ postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC", ...replicationArgs], {
10998
11707
  stdio: ["ignore", "ignore", "pipe"],
10999
11708
  detached: false
11000
11709
  });
@@ -11010,7 +11719,8 @@ ${stderr || error.message}`);
11010
11719
  };
11011
11720
  process.once("exit", killPostgres);
11012
11721
  removeExitHandler = () => process.off("exit", killPostgres);
11013
- const socketPath = join(socketDirectory, ".s.PGSQL.5432");
11722
+ const postgresPort = opts.replication?.port ?? 5432;
11723
+ const socketPath = join(socketDirectory, `.s.PGSQL.${postgresPort}`);
11014
11724
  const connect = async () => {
11015
11725
  const deadline = Date.now() + 20000;
11016
11726
  while (Date.now() <= deadline) {
@@ -11025,7 +11735,7 @@ ${detail}` : " (no output)"}
11025
11735
  ` + `data dir: ${opts.dataDir}
11026
11736
  ` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
11027
11737
  }
11028
- await new Promise((resolve2) => setTimeout(resolve2, 150));
11738
+ await new Promise((resolve3) => setTimeout(resolve3, 150));
11029
11739
  }
11030
11740
  }
11031
11741
  throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
@@ -11053,19 +11763,19 @@ async function stopPostgres(postgres, hasExited) {
11053
11763
  if (hasExited())
11054
11764
  return;
11055
11765
  postgres.kill("SIGINT");
11056
- await new Promise((resolve2) => {
11766
+ await new Promise((resolve3) => {
11057
11767
  const killTimeout = setTimeout(() => {
11058
11768
  postgres.kill("SIGKILL");
11059
- resolve2();
11769
+ resolve3();
11060
11770
  }, 5000);
11061
11771
  postgres.once("exit", () => {
11062
11772
  clearTimeout(killTimeout);
11063
- resolve2();
11773
+ resolve3();
11064
11774
  });
11065
11775
  });
11066
11776
  }
11067
11777
  function removeStalePidFile(pidPath) {
11068
- if (!existsSync(pidPath))
11778
+ if (!existsSync2(pidPath))
11069
11779
  return;
11070
11780
  try {
11071
11781
  const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
@@ -11202,6 +11912,236 @@ async function closeResources(...resources) {
11202
11912
  throw failed.reason;
11203
11913
  }
11204
11914
 
11915
+ // src/runtime/node/native/readiness.ts
11916
+ function liteCapabilities(engine, replicationProfile) {
11917
+ if (engine === "pglite") {
11918
+ return {
11919
+ engine,
11920
+ state_machine_sql: "supported",
11921
+ durable_workflows: "supported",
11922
+ commands: "supported",
11923
+ artifacts: "supported",
11924
+ postgrest_schema_config: "static",
11925
+ logical_replication: "unsupported",
11926
+ powersync_source: "unsupported"
11927
+ };
11928
+ }
11929
+ return {
11930
+ engine,
11931
+ state_machine_sql: "supported",
11932
+ durable_workflows: "supported",
11933
+ commands: "supported",
11934
+ artifacts: "supported",
11935
+ postgrest_schema_config: "static",
11936
+ logical_replication: replicationProfile ? "supported" : "disabled",
11937
+ powersync_source: replicationProfile ? "supported" : "disabled",
11938
+ ...replicationProfile ? { replication_profile: replicationProfile } : {}
11939
+ };
11940
+ }
11941
+ async function inspectPowerSyncReadiness(engine, options) {
11942
+ return buildReadiness(await loadReplicationInventory(engine), options);
11943
+ }
11944
+ async function loadReplicationInventory(engine) {
11945
+ const [settings, senders, role, publication, publicationTables, selectableTables, slots] = await Promise.all([
11946
+ engine.query(`
11947
+ SELECT name, setting
11948
+ FROM pg_settings
11949
+ WHERE name IN (
11950
+ 'wal_level', 'max_wal_senders', 'max_replication_slots',
11951
+ 'max_slot_wal_keep_size', 'ssl', 'listen_addresses', 'port'
11952
+ )
11953
+ `),
11954
+ engine.query("SELECT count(*)::integer AS count FROM pg_stat_replication"),
11955
+ engine.query(`
11956
+ SELECT rolcanlogin, rolreplication, rolbypassrls
11957
+ FROM pg_roles WHERE rolname = '${POWERSYNC_REPLICATION_ROLE}'
11958
+ `),
11959
+ engine.query(`
11960
+ SELECT puballtables, pubinsert, pubupdate, pubdelete
11961
+ FROM pg_publication WHERE pubname = '${POWERSYNC_PUBLICATION}'
11962
+ `),
11963
+ engine.query(`
11964
+ SELECT
11965
+ quote_ident(namespace.nspname) || '.' || quote_ident(relation.relname) AS qualified_name,
11966
+ relation.relreplident = 'n' OR (
11967
+ relation.relreplident = 'd'
11968
+ AND NOT EXISTS (
11969
+ SELECT 1 FROM pg_index index_record
11970
+ WHERE index_record.indrelid = relation.oid AND index_record.indisprimary
11971
+ )
11972
+ ) AS replica_identity_missing
11973
+ FROM pg_publication_tables published
11974
+ JOIN pg_namespace namespace ON namespace.nspname = published.schemaname
11975
+ JOIN pg_class relation
11976
+ ON relation.relnamespace = namespace.oid AND relation.relname = published.tablename
11977
+ WHERE published.pubname = '${POWERSYNC_PUBLICATION}'
11978
+ ORDER BY namespace.nspname, relation.relname
11979
+ `),
11980
+ engine.query(`
11981
+ SELECT namespace.nspname || '.' || relation.relname AS qualified_name
11982
+ FROM pg_roles role_record
11983
+ JOIN pg_class relation ON true
11984
+ JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
11985
+ WHERE role_record.rolname = '${POWERSYNC_REPLICATION_ROLE}'
11986
+ AND relation.relkind IN ('r', 'p')
11987
+ AND namespace.nspname NOT IN ('pg_catalog', 'information_schema')
11988
+ AND namespace.nspname !~ '^pg_toast'
11989
+ AND has_table_privilege(role_record.oid, relation.oid, 'SELECT')
11990
+ ORDER BY namespace.nspname, relation.relname
11991
+ `),
11992
+ engine.query(`
11993
+ SELECT
11994
+ slot_name,
11995
+ active,
11996
+ wal_status,
11997
+ coalesce(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn), 0)::text AS retained_wal_bytes,
11998
+ coalesce(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn), 0)::text AS unconfirmed_wal_bytes,
11999
+ safe_wal_size::text,
12000
+ invalidation_reason
12001
+ FROM pg_replication_slots
12002
+ WHERE slot_type = 'logical'
12003
+ ORDER BY slot_name
12004
+ `)
12005
+ ]);
12006
+ return {
12007
+ settings: new Map(settings.rows.map((row) => [row.name, row.setting])),
12008
+ activeSenders: senders.rows[0]?.count ?? 0,
12009
+ role: role.rows[0],
12010
+ publication: publication.rows[0],
12011
+ publicationTables: publicationTables.rows,
12012
+ selectableTables: selectableTables.rows.map((row) => row.qualified_name),
12013
+ slots: slots.rows
12014
+ };
12015
+ }
12016
+ function buildReadiness(inventory, options) {
12017
+ const actualTables = catalogTableNames(inventory.publicationTables);
12018
+ const expectedTables = [...options.publicationTables].sort();
12019
+ const missingIdentity = missingReplicaIdentity(inventory.publicationTables);
12020
+ const blockers = readinessBlockers(inventory, actualTables, expectedTables, missingIdentity);
12021
+ return {
12022
+ ready: blockers.length === 0,
12023
+ blockers,
12024
+ warnings: readinessWarnings(inventory),
12025
+ connection: {
12026
+ host: options.host,
12027
+ port: options.port,
12028
+ tls: Boolean(options.tls),
12029
+ allowed_cidrs: options.allowCidrs
12030
+ },
12031
+ wal: {
12032
+ level: inventory.settings.get("wal_level") ?? "unknown",
12033
+ max_senders: integerSetting(inventory.settings.get("max_wal_senders")),
12034
+ active_senders: inventory.activeSenders,
12035
+ max_replication_slots: integerSetting(inventory.settings.get("max_replication_slots")),
12036
+ used_replication_slots: inventory.slots.length,
12037
+ max_slot_wal_keep_size: inventory.settings.get("max_slot_wal_keep_size") ?? "unknown"
12038
+ },
12039
+ role: roleReadiness(inventory.role, unexpectedSelectableTables(inventory, expectedTables)),
12040
+ publication: publicationReadiness(inventory.publication, actualTables, expectedTables, missingIdentity),
12041
+ slots: inventory.slots.map((slot) => ({
12042
+ name: slot.slot_name,
12043
+ active: slot.active,
12044
+ wal_status: slot.wal_status,
12045
+ retained_wal_bytes: slot.retained_wal_bytes,
12046
+ unconfirmed_wal_bytes: slot.unconfirmed_wal_bytes,
12047
+ safe_wal_size: slot.safe_wal_size,
12048
+ invalidation_reason: slot.invalidation_reason
12049
+ }))
12050
+ };
12051
+ }
12052
+ function readinessBlockers(inventory, actualTables, expectedTables, missingIdentity) {
12053
+ const blockers = capacityBlockers(inventory);
12054
+ if (!roleIsReady(inventory.role))
12055
+ blockers.push("POWERSYNC_ROLE_NOT_READY");
12056
+ if (unexpectedSelectableTables(inventory, expectedTables).length > 0) {
12057
+ blockers.push("POWERSYNC_ROLE_SELECT_OUTSIDE_ALLOWLIST");
12058
+ }
12059
+ blockers.push(...publicationBlockers(inventory.publication, actualTables, expectedTables, missingIdentity));
12060
+ return blockers;
12061
+ }
12062
+ function capacityBlockers(inventory) {
12063
+ const blockers = [];
12064
+ const maxSenders = integerSetting(inventory.settings.get("max_wal_senders"));
12065
+ const maxSlots = integerSetting(inventory.settings.get("max_replication_slots"));
12066
+ if (inventory.settings.get("wal_level") !== "logical")
12067
+ blockers.push("WAL_LEVEL_NOT_LOGICAL");
12068
+ if (maxSenders - inventory.activeSenders < 1)
12069
+ blockers.push("NO_FREE_WAL_SENDER");
12070
+ if (maxSlots - inventory.slots.length < 1)
12071
+ blockers.push("NO_FREE_REPLICATION_SLOT");
12072
+ return blockers;
12073
+ }
12074
+ function publicationBlockers(publication, actualTables, expectedTables, missingIdentity) {
12075
+ if (!publication)
12076
+ return ["POWERSYNC_PUBLICATION_MISSING"];
12077
+ const blockers = [];
12078
+ if (publication.puballtables)
12079
+ blockers.push("POWERSYNC_PUBLICATION_NOT_ALLOWLISTED");
12080
+ if (!publication.pubinsert || !publication.pubupdate || !publication.pubdelete) {
12081
+ blockers.push("POWERSYNC_PUBLICATION_DML_INCOMPLETE");
12082
+ }
12083
+ if (!sameStrings(actualTables, expectedTables))
12084
+ blockers.push("POWERSYNC_PUBLICATION_TABLE_MISMATCH");
12085
+ if (missingIdentity.length > 0)
12086
+ blockers.push("POWERSYNC_REPLICA_IDENTITY_INCOMPLETE");
12087
+ return blockers;
12088
+ }
12089
+ function readinessWarnings(inventory) {
12090
+ const warnings = [];
12091
+ if (inventory.settings.get("max_slot_wal_keep_size") === "-1")
12092
+ warnings.push("SLOT_WAL_KEEP_SIZE_UNBOUNDED");
12093
+ if (inventory.slots.some((slot) => slot.wal_status === "lost" || slot.invalidation_reason)) {
12094
+ warnings.push("INVALID_LOGICAL_SLOTS");
12095
+ }
12096
+ return warnings;
12097
+ }
12098
+ function roleIsReady(role) {
12099
+ return Boolean(role?.rolcanlogin && role.rolreplication && role.rolbypassrls);
12100
+ }
12101
+ function roleReadiness(role, unexpectedTables) {
12102
+ return {
12103
+ name: POWERSYNC_REPLICATION_ROLE,
12104
+ present: Boolean(role),
12105
+ login: role?.rolcanlogin ?? false,
12106
+ replication: role?.rolreplication ?? false,
12107
+ bypass_rls: role?.rolbypassrls ?? false,
12108
+ unexpected_selectable_tables: unexpectedTables
12109
+ };
12110
+ }
12111
+ function unexpectedSelectableTables(inventory, expectedTables) {
12112
+ const expected = new Set(expectedTables);
12113
+ return inventory.selectableTables.filter((table) => !expected.has(table));
12114
+ }
12115
+ function publicationReadiness(publication, tables, expectedTables, missingIdentity) {
12116
+ return {
12117
+ name: POWERSYNC_PUBLICATION,
12118
+ present: Boolean(publication),
12119
+ all_tables: publication?.puballtables ?? false,
12120
+ tables,
12121
+ expected_tables: expectedTables,
12122
+ publishes_insert: publication?.pubinsert ?? false,
12123
+ publishes_update: publication?.pubupdate ?? false,
12124
+ publishes_delete: publication?.pubdelete ?? false,
12125
+ replica_identity_missing_tables: missingIdentity
12126
+ };
12127
+ }
12128
+ function catalogTableNames(rows) {
12129
+ return rows.map((row) => normalizeCatalogTable(row.qualified_name));
12130
+ }
12131
+ function missingReplicaIdentity(rows) {
12132
+ return rows.filter((row) => row.replica_identity_missing).map((row) => normalizeCatalogTable(row.qualified_name));
12133
+ }
12134
+ function integerSetting(value) {
12135
+ const parsed = Number(value);
12136
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
12137
+ }
12138
+ function normalizeCatalogTable(value) {
12139
+ return value.replaceAll('"', "").toLowerCase();
12140
+ }
12141
+ function sameStrings(left, right) {
12142
+ return left.length === right.length && left.every((value, index) => value === right[index]);
12143
+ }
12144
+
11205
12145
  // src/runtime/node/project.ts
11206
12146
  import { readdir, readFile as readFile2 } from "fs/promises";
11207
12147
  import { join as join3 } from "path";
@@ -11249,7 +12189,7 @@ function isNotFound(error) {
11249
12189
 
11250
12190
  // src/project-runtime.ts
11251
12191
  import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
11252
- import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve2 } from "path";
12192
+ import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve3 } from "path";
11253
12193
 
11254
12194
  // src/runtime/node/bun-server.ts
11255
12195
  async function serveBun(backend, opts = {}) {
@@ -11729,7 +12669,7 @@ import { pathToFileURL } from "url";
11729
12669
  // src/runtime/node/bundle-function.ts
11730
12670
  import { createHash as createHash3 } from "crypto";
11731
12671
  import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
11732
- import { existsSync as existsSync2 } from "fs";
12672
+ import { existsSync as existsSync3 } from "fs";
11733
12673
  import { tmpdir as tmpdir3 } from "os";
11734
12674
  import { join as join6 } from "path";
11735
12675
  function rewriteRemoteSpecifier(spec) {
@@ -11743,7 +12683,7 @@ var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
11743
12683
  async function fetchModule(url) {
11744
12684
  const key = createHash3("sha256").update(url).digest("hex");
11745
12685
  const cached = join6(HTTP_CACHE, key);
11746
- if (existsSync2(cached))
12686
+ if (existsSync3(cached))
11747
12687
  return readFile4(cached, "utf8");
11748
12688
  const res = await fetch(url, { redirect: "follow" });
11749
12689
  if (!res.ok)
@@ -12001,7 +12941,7 @@ class S3StorageDriver {
12001
12941
  var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
12002
12942
  var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
12003
12943
  function resolveProjectPaths(options = {}) {
12004
- const projectDir = resolve2(options.projectDir ?? process.cwd());
12944
+ const projectDir = resolve3(options.projectDir ?? process.cwd());
12005
12945
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
12006
12946
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
12007
12947
  const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -12016,7 +12956,7 @@ function resolveProjectPaths(options = {}) {
12016
12956
  };
12017
12957
  }
12018
12958
  async function assertResetPathsSafe(paths) {
12019
- const stateDir = resolve2(paths.stateDir);
12959
+ const stateDir = resolve3(paths.stateDir);
12020
12960
  if (stateDir === parse(stateDir).root)
12021
12961
  throw new Error("refusing to use the filesystem root as the state directory");
12022
12962
  const stateInfo = await requiredResetEntry(stateDir);
@@ -12024,7 +12964,7 @@ async function assertResetPathsSafe(paths) {
12024
12964
  throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
12025
12965
  }
12026
12966
  const canonicalStateDir = await realpath2(stateDir);
12027
- const secretsFile = resolve2(paths.secretsFile);
12967
+ const secretsFile = resolve3(paths.secretsFile);
12028
12968
  if (secretsFile !== join8(stateDir, "secrets.json")) {
12029
12969
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
12030
12970
  }
@@ -12038,7 +12978,7 @@ async function assertResetPathsSafe(paths) {
12038
12978
  ["storage", paths.storageDir]
12039
12979
  ];
12040
12980
  for (const [label, targetPath] of targets) {
12041
- const target2 = resolve2(targetPath);
12981
+ const target2 = resolve3(targetPath);
12042
12982
  const relativePath = relative(stateDir, target2);
12043
12983
  if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
12044
12984
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
@@ -12087,7 +13027,7 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2,
12087
13027
  current = parent;
12088
13028
  }
12089
13029
  const existingAncestor = await nearestExistingAncestor(target2);
12090
- const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target2));
13030
+ const canonicalTarget = resolve3(await realpath2(existingAncestor), relative(existingAncestor, target2));
12091
13031
  const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
12092
13032
  if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
12093
13033
  throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
@@ -12171,6 +13111,7 @@ async function createProjectBackend(options = {}) {
12171
13111
  const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
12172
13112
  const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
12173
13113
  const databaseEngine = paths.databaseEngine;
13114
+ const replication = resolveNativeReplicationOptions(options, databaseEngine);
12174
13115
  if (paths.dataDir) {
12175
13116
  await mkdir5(paths.dataDir, { recursive: true, mode: 448 });
12176
13117
  await chmod(paths.dataDir, 448);
@@ -12178,7 +13119,7 @@ async function createProjectBackend(options = {}) {
12178
13119
  await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
12179
13120
  await chmod(paths.storageDir, 448);
12180
13121
  const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
12181
- const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
13122
+ const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log, replication }) : undefined;
12182
13123
  let backend;
12183
13124
  try {
12184
13125
  backend = await createBackend({
@@ -12212,8 +13153,17 @@ async function createProjectBackend(options = {}) {
12212
13153
  storageDriver,
12213
13154
  log: options.log
12214
13155
  });
13156
+ if (replication && options.applyMigrations !== false) {
13157
+ await ensurePowerSyncReplicationCatalog(backend.db.engine, replication);
13158
+ }
12215
13159
  } catch (error) {
12216
- if (engine) {
13160
+ if (backend) {
13161
+ try {
13162
+ await backend.close();
13163
+ } catch (cleanupError) {
13164
+ throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
13165
+ }
13166
+ } else if (engine) {
12217
13167
  try {
12218
13168
  await engine.close();
12219
13169
  } catch (cleanupError) {
@@ -12233,9 +13183,33 @@ async function createProjectBackend(options = {}) {
12233
13183
  functionNames: [...functions.keys()],
12234
13184
  webhookCount: webhooks.length,
12235
13185
  storageBackend,
12236
- databaseEngine
13186
+ databaseEngine,
13187
+ replicationProfile: replication?.profile
12237
13188
  };
12238
13189
  }
13190
+ function resolveNativeReplicationOptions(options, databaseEngine = resolveDatabaseEngine(options.engine, options.memory)) {
13191
+ const profile = options.replicationProfile ?? process.env.SUPACLOUD_LITE_REPLICATION_PROFILE;
13192
+ if (!profile)
13193
+ return;
13194
+ if (profile !== "powersync")
13195
+ throw new Error(`unsupported SUPACLOUD_LITE_REPLICATION_PROFILE: ${profile}`);
13196
+ if (databaseEngine !== "native")
13197
+ throw new Error("PowerSync replication is only supported by --engine native");
13198
+ const tlsCertFile = options.replicationTlsCertFile ?? process.env.SUPACLOUD_LITE_REPLICATION_TLS_CERT_FILE;
13199
+ const tlsKeyFile = options.replicationTlsKeyFile ?? process.env.SUPACLOUD_LITE_REPLICATION_TLS_KEY_FILE;
13200
+ if (Boolean(tlsCertFile) !== Boolean(tlsKeyFile)) {
13201
+ throw new Error("PowerSync replication TLS requires both certificate and key files");
13202
+ }
13203
+ return validatePowerSyncReplicationOptions({
13204
+ profile,
13205
+ host: options.replicationHost ?? process.env.SUPACLOUD_LITE_REPLICATION_HOST ?? "127.0.0.1",
13206
+ port: options.replicationPort ?? parsePort(process.env.SUPACLOUD_LITE_REPLICATION_PORT, 54322),
13207
+ allowCidrs: options.replicationAllowCidrs ?? commaSeparated(process.env.SUPACLOUD_LITE_REPLICATION_ALLOW_CIDRS, ["127.0.0.1/32", "::1/128"]),
13208
+ publicationTables: options.powersyncPublicationTables ?? commaSeparated(process.env.SUPACLOUD_LITE_POWERSYNC_TABLES),
13209
+ password: options.powersyncPassword ?? process.env.SUPACLOUD_LITE_POWERSYNC_PASSWORD ?? "",
13210
+ tls: tlsCertFile && tlsKeyFile ? { certFile: tlsCertFile, keyFile: tlsKeyFile } : undefined
13211
+ });
13212
+ }
12239
13213
  function resolveDatabaseEngine(value, memory = false) {
12240
13214
  const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
12241
13215
  if (configured !== "pglite" && configured !== "native") {
@@ -12311,7 +13285,7 @@ async function loadWebhooks(projectDir) {
12311
13285
  }
12312
13286
  }
12313
13287
  function resolvePath(projectDir, path) {
12314
- return isAbsolute(path) ? path : resolve2(projectDir, path);
13288
+ return isAbsolute(path) ? path : resolve3(projectDir, path);
12315
13289
  }
12316
13290
  function randomHex(bytes) {
12317
13291
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -12338,6 +13312,9 @@ function parsePort(value, fallback) {
12338
13312
  throw new Error(`invalid port: ${value}`);
12339
13313
  return port;
12340
13314
  }
13315
+ function commaSeparated(value, fallback = []) {
13316
+ return value === undefined ? fallback : value.split(",").map((entry) => entry.trim()).filter(Boolean);
13317
+ }
12341
13318
  function displayHost(host) {
12342
13319
  if (host === "0.0.0.0" || host === "::")
12343
13320
  return "127.0.0.1";
@@ -12354,7 +13331,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
12354
13331
 
12355
13332
  // src/snapshot.ts
12356
13333
  import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
12357
- import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
13334
+ import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
12358
13335
  import { create as createTar, extract as extractTar2 } from "tar";
12359
13336
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
12360
13337
  var SNAPSHOT_VERSION = 1;
@@ -12378,7 +13355,7 @@ async function createSnapshot(options) {
12378
13355
  postgresMajor: await readPostgresMajor(paths.dataDir)
12379
13356
  } : {}
12380
13357
  };
12381
- const output = resolve3(options.output);
13358
+ const output = resolve4(options.output);
12382
13359
  if (await existingInfo(output))
12383
13360
  throw new Error(`snapshot output already exists: ${output}`);
12384
13361
  await mkdir6(dirname6(output), { recursive: true });
@@ -12419,7 +13396,7 @@ async function restoreSnapshot(options) {
12419
13396
  await mkdir6(payloadRoot, { recursive: true });
12420
13397
  await extractTar2({
12421
13398
  cwd: payloadRoot,
12422
- file: resolve3(options.input),
13399
+ file: resolve4(options.input),
12423
13400
  preserveOwner: false,
12424
13401
  preservePaths: false,
12425
13402
  strict: true,
@@ -12496,11 +13473,11 @@ async function restoreSnapshot(options) {
12496
13473
  function normalizePaths(paths) {
12497
13474
  return {
12498
13475
  ...paths,
12499
- projectDir: resolve3(paths.projectDir),
12500
- stateDir: resolve3(paths.stateDir),
12501
- dataDir: paths.dataDir ? resolve3(paths.dataDir) : undefined,
12502
- storageDir: resolve3(paths.storageDir),
12503
- secretsFile: resolve3(paths.secretsFile)
13476
+ projectDir: resolve4(paths.projectDir),
13477
+ stateDir: resolve4(paths.stateDir),
13478
+ dataDir: paths.dataDir ? resolve4(paths.dataDir) : undefined,
13479
+ storageDir: resolve4(paths.storageDir),
13480
+ secretsFile: resolve4(paths.secretsFile)
12504
13481
  };
12505
13482
  }
12506
13483
  async function assertSnapshotPaths(paths, options = {}) {
@@ -12530,7 +13507,7 @@ async function assertSnapshotPaths(paths, options = {}) {
12530
13507
  async function assertDirectoryOrMissing(path) {
12531
13508
  if (!path)
12532
13509
  return;
12533
- if (resolve3(path) === parse2(resolve3(path)).root)
13510
+ if (resolve4(path) === parse2(resolve4(path)).root)
12534
13511
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
12535
13512
  try {
12536
13513
  const info = await lstat2(path);
@@ -12752,13 +13729,13 @@ async function assertNoSymlinks(root) {
12752
13729
  }
12753
13730
  }
12754
13731
  function isWithin(parent, child) {
12755
- const normalizedParent = resolve3(parent);
12756
- const normalizedChild = resolve3(child);
13732
+ const normalizedParent = resolve4(parent);
13733
+ const normalizedChild = resolve4(child);
12757
13734
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
12758
13735
  }
12759
13736
  function pathsOverlap(left, right) {
12760
- const normalizedLeft = resolve3(left);
12761
- const normalizedRight = resolve3(right);
13737
+ const normalizedLeft = resolve4(left);
13738
+ const normalizedRight = resolve4(right);
12762
13739
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
12763
13740
  }
12764
13741
 
@@ -12787,7 +13764,8 @@ function parseArgs(argv) {
12787
13764
  host: process.env.SUPACLOUD_LITE_HOST ?? "127.0.0.1",
12788
13765
  port: process.env.SUPACLOUD_LITE_PORT || process.env.PORT ? Number.parseInt(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 10) : 54321,
12789
13766
  serviceRole: false,
12790
- force: false
13767
+ force: false,
13768
+ json: false
12791
13769
  };
12792
13770
  for (let index = 0;index < args.length; index++) {
12793
13771
  const argument = args[index];
@@ -12806,29 +13784,45 @@ function parseArgs(argv) {
12806
13784
  else if (argument === "--site-url")
12807
13785
  options.siteUrl = next();
12808
13786
  else if (argument === "--project-dir" || argument === "--dir")
12809
- options.projectDir = resolve4(next());
13787
+ options.projectDir = resolve5(next());
12810
13788
  else if (argument === "--state-dir")
12811
- options.stateDir = resolve4(next());
13789
+ options.stateDir = resolve5(next());
12812
13790
  else if (argument === "--data-dir")
12813
- options.dataDir = resolve4(next());
13791
+ options.dataDir = resolve5(next());
12814
13792
  else if (argument === "--storage-dir")
12815
- options.storageDir = resolve4(next());
13793
+ options.storageDir = resolve5(next());
12816
13794
  else if (argument === "--storage-backend")
12817
13795
  options.storageBackend = next();
12818
13796
  else if (argument === "--s3-prefix")
12819
13797
  options.s3 = { ...options.s3, prefix: next() };
12820
13798
  else if (argument === "--engine")
12821
13799
  options.engine = next();
13800
+ else if (argument === "--replication-profile") {
13801
+ options.replicationProfile = next();
13802
+ } else if (argument === "--replication-host")
13803
+ options.replicationHost = next();
13804
+ else if (argument === "--replication-port")
13805
+ options.replicationPort = Number.parseInt(next(), 10);
13806
+ else if (argument === "--replication-allow-cidrs")
13807
+ options.replicationAllowCidrs = commaSeparated2(next());
13808
+ else if (argument === "--powersync-tables")
13809
+ options.powersyncPublicationTables = commaSeparated2(next());
13810
+ else if (argument === "--replication-tls-cert")
13811
+ options.replicationTlsCertFile = resolve5(next());
13812
+ else if (argument === "--replication-tls-key")
13813
+ options.replicationTlsKeyFile = resolve5(next());
12822
13814
  else if (argument === "--memory")
12823
13815
  options.memory = true;
12824
13816
  else if (argument === "--output" || argument === "-o")
12825
- options.output = resolve4(next());
13817
+ options.output = resolve5(next());
12826
13818
  else if (argument === "--file" || argument === "-f")
12827
13819
  options.diffFile = next();
12828
13820
  else if (argument === "--service-role")
12829
13821
  options.serviceRole = true;
12830
13822
  else if (argument === "--force")
12831
13823
  options.force = true;
13824
+ else if (argument === "--json")
13825
+ options.json = true;
12832
13826
  else if (argument === "--version") {
12833
13827
  console.log(package_default.version);
12834
13828
  process.exit(0);
@@ -12917,6 +13911,27 @@ ${privilegedKey}
12917
13911
  }
12918
13912
  return;
12919
13913
  }
13914
+ if (options.command === "doctor") {
13915
+ const replication = resolveNativeReplicationOptions(options, paths.databaseEngine);
13916
+ const report = liteCapabilities(paths.databaseEngine, replication?.profile);
13917
+ if (paths.databaseEngine === "native" && replication) {
13918
+ if (!paths.dataDir || !existsSync4(join10(paths.dataDir, "PG_VERSION"))) {
13919
+ throw new Error("PowerSync readiness requires an initialized native database; run migrate first");
13920
+ }
13921
+ const engine = await createNativeEngine({ dataDir: paths.dataDir, log: quietLog, replication });
13922
+ try {
13923
+ report.powersync_readiness = await inspectPowerSyncReadiness(engine, replication);
13924
+ } finally {
13925
+ await engine.close();
13926
+ }
13927
+ }
13928
+ if (options.json)
13929
+ await writeStandardOutput(`${JSON.stringify(report, null, 2)}
13930
+ `);
13931
+ else
13932
+ await printDoctor(report);
13933
+ return;
13934
+ }
12920
13935
  if (options.command === "migrate" || options.command === "status") {
12921
13936
  const project2 = await createProjectBackend({
12922
13937
  ...options,
@@ -12988,9 +14003,13 @@ async function runDbCommand(options) {
12988
14003
  }
12989
14004
  return;
12990
14005
  }
12991
- const project = await loadSupabaseProject(resolve4(options.projectDir ?? process.cwd()));
14006
+ const project = await loadSupabaseProject(resolve5(options.projectDir ?? process.cwd()));
12992
14007
  if (subcommand === "diff") {
12993
- const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
14008
+ const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
14009
+ dataDir: paths.dataDir,
14010
+ log: quietLog,
14011
+ replication: resolveNativeReplicationOptions(options, paths.databaseEngine)
14012
+ }) : undefined;
12994
14013
  const ddl = await computeDbDiff({
12995
14014
  liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
12996
14015
  liveEngine,
@@ -13018,7 +14037,11 @@ async function runDbCommand(options) {
13018
14037
  return;
13019
14038
  }
13020
14039
  if (subcommand === "pull") {
13021
- const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
14040
+ const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
14041
+ dataDir: paths.dataDir,
14042
+ log: quietLog,
14043
+ replication: resolveNativeReplicationOptions(options, paths.databaseEngine)
14044
+ }) : undefined;
13022
14045
  const result = await pullSchema({
13023
14046
  liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
13024
14047
  liveEngine,
@@ -13063,7 +14086,7 @@ async function runSnapshotCommand(options) {
13063
14086
  const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
13064
14087
  const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
13065
14088
  await writeStandardOutput([
13066
- `Snapshot restored from ${resolve4(input)}`,
14089
+ `Snapshot restored from ${resolve5(input)}`,
13067
14090
  ...rollbackLines,
13068
14091
  ...reconnectLine
13069
14092
  ].join(`
@@ -13119,6 +14142,19 @@ async function printInspection(rows) {
13119
14142
  `)}
13120
14143
  `);
13121
14144
  }
14145
+ async function printDoctor(report) {
14146
+ const output = Object.entries(report).filter(([, value]) => typeof value !== "object").map(([name, value]) => `${name}: ${value}`);
14147
+ if (report.powersync_readiness) {
14148
+ output.push(`powersync_ready: ${report.powersync_readiness.ready}`);
14149
+ for (const blocker of report.powersync_readiness.blockers)
14150
+ output.push(`blocker: ${blocker}`);
14151
+ for (const warning of report.powersync_readiness.warnings)
14152
+ output.push(`warning: ${warning}`);
14153
+ }
14154
+ await writeStandardOutput(`${output.join(`
14155
+ `)}
14156
+ `);
14157
+ }
13122
14158
  async function writeStandardOutput(output) {
13123
14159
  await Bun.write(Bun.stdout, output);
13124
14160
  }
@@ -13129,8 +14165,11 @@ function timestamp() {
13129
14165
  return new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
13130
14166
  }
13131
14167
  function quietLog() {}
14168
+ function commaSeparated2(value) {
14169
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
14170
+ }
13132
14171
  function printHelp() {
13133
- console.log(`supacloud-lite - Bun-native Supabase-compatible backend on PGlite
14172
+ console.log(`supacloud-lite - Bun-native Supabase-compatible backend on PGlite or native PostgreSQL
13134
14173
 
13135
14174
  Usage: supacloud-lite [command] [options]
13136
14175
 
@@ -13148,6 +14187,7 @@ Commands:
13148
14187
  snapshot restore <f> restore a snapshot into an empty target
13149
14188
  upgrade snapshot first, then apply pending migrations
13150
14189
  inspect show table rows and sizes
14190
+ doctor report Lite capability and replication readiness
13151
14191
  version print the package version
13152
14192
 
13153
14193
  Fresh projects must run "supacloud-lite migrate" before "db reset".
@@ -13164,7 +14204,15 @@ Options:
13164
14204
  --storage-backend <b> fs, memory, or s3 (default fs)
13165
14205
  --s3-prefix <p> optional key prefix for the s3 backend
13166
14206
  --engine <e> pglite (default) or native (macOS/glibc Linux x64/arm64)
14207
+ --replication-profile <p> optional powersync profile (native only)
14208
+ --replication-host <ip> database listener (default 127.0.0.1)
14209
+ --replication-port <n> database listener port (default 54322)
14210
+ --replication-allow-cidrs <list> explicit client CIDRs
14211
+ --powersync-tables <list> schema-qualified publication table allowlist
14212
+ --replication-tls-cert <p> PostgreSQL TLS certificate
14213
+ --replication-tls-key <p> PostgreSQL TLS private key
13167
14214
  --memory use an in-memory PGlite database
14215
+ --json emit machine-readable doctor output
13168
14216
  -o, --output <p> output file for gen types
13169
14217
  -f, --file <name> migration suffix for db diff
13170
14218
  --force replace non-empty restore targets and retain rollback copies
@@ -13183,7 +14231,6 @@ function formatDatabaseEngine(engine, dataDir) {
13183
14231
  const label = engine === "native" ? "Native PostgreSQL" : "PGlite";
13184
14232
  return dataDir ? `${label} (${dataDir})` : `${label} (memory)`;
13185
14233
  }
13186
- var windowsLifecycleRef = process.platform === "win32" ? setInterval(() => {}, 1000) : null;
13187
14234
  var exitCode = 0;
13188
14235
  try {
13189
14236
  await main();
@@ -13191,8 +14238,5 @@ try {
13191
14238
  await writeStandardError(`${error instanceof Error ? error.message : String(error)}
13192
14239
  `);
13193
14240
  exitCode = 1;
13194
- } finally {
13195
- if (windowsLifecycleRef !== null)
13196
- clearInterval(windowsLifecycleRef);
13197
14241
  }
13198
14242
  process.exit(exitCode);