@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/index.js CHANGED
@@ -85,7 +85,7 @@ function randomToken(bytes = 32) {
85
85
  // package.json
86
86
  var package_default = {
87
87
  name: "@supacloud/lite",
88
- version: "0.8.5",
88
+ version: "0.9.1",
89
89
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
90
90
  type: "module",
91
91
  license: "Apache-2.0",
@@ -123,7 +123,7 @@ var package_default = {
123
123
  dev: "bun run src/cli.ts start",
124
124
  start: "bun run src/cli.ts start",
125
125
  test: "bun test --timeout 20000",
126
- "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts test/qauth-workflow-matrix.test.ts --timeout 180000",
126
+ "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",
127
127
  parity: "bun run parity/harness.ts",
128
128
  "parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
129
129
  "check:native": "bun run test:native && bun run parity:native",
@@ -139,11 +139,11 @@ var package_default = {
139
139
  },
140
140
  devDependencies: {
141
141
  "@supabase/supabase-js": "^2.112.2",
142
- "@types/bun": "^1.3.14",
142
+ "@types/bun": "^1.4.0",
143
143
  typescript: "^7.0.2"
144
144
  },
145
145
  engines: {
146
- bun: ">=1.3.14"
146
+ bun: ">=1.4.0"
147
147
  },
148
148
  publishConfig: {
149
149
  access: "public",
@@ -4772,6 +4772,452 @@ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_rol
4772
4772
  GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
4773
4773
  GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
4774
4774
  -- supacloud:sql-module:workflows-public:end
4775
+
4776
+ -- supacloud:sql-module:commands-public:start
4777
+ CREATE SCHEMA IF NOT EXISTS supacloud_commands;
4778
+ REVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC, anon, authenticated;
4779
+ GRANT USAGE ON SCHEMA supacloud_commands TO service_role;
4780
+
4781
+ CREATE TABLE IF NOT EXISTS supacloud_commands.receipts (
4782
+ id uuid PRIMARY KEY,
4783
+ command_type text NOT NULL
4784
+ CHECK (command_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4785
+ target_type text NOT NULL
4786
+ CHECK (target_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4787
+ target_id text NOT NULL CHECK (char_length(target_id) BETWEEN 1 AND 500),
4788
+ actor_id uuid,
4789
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb
4790
+ CHECK (jsonb_typeof(payload) = 'object'),
4791
+ payload_fingerprint text NOT NULL CHECK (char_length(payload_fingerprint) = 36),
4792
+ workflow_run_id uuid NOT NULL UNIQUE
4793
+ REFERENCES supacloud_workflows.runs(id) ON DELETE RESTRICT,
4794
+ created_at timestamptz NOT NULL DEFAULT now()
4795
+ );
4796
+
4797
+ CREATE INDEX IF NOT EXISTS supacloud_commands_target_idx
4798
+ ON supacloud_commands.receipts (target_type, target_id, created_at DESC, id);
4799
+
4800
+ CREATE OR REPLACE FUNCTION supacloud_commands.snapshot(
4801
+ p_command_id uuid,
4802
+ p_idempotent boolean DEFAULT false
4803
+ ) RETURNS jsonb
4804
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
4805
+ SELECT jsonb_build_object(
4806
+ 'commandId', receipt.id,
4807
+ 'commandType', receipt.command_type,
4808
+ 'targetType', receipt.target_type,
4809
+ 'targetId', receipt.target_id,
4810
+ 'actorId', receipt.actor_id,
4811
+ 'payloadFingerprint', receipt.payload_fingerprint,
4812
+ 'createdAt', receipt.created_at,
4813
+ 'idempotent', p_idempotent,
4814
+ 'workflow', supacloud_workflows.snapshot(receipt.workflow_run_id, false)
4815
+ )
4816
+ FROM supacloud_commands.receipts receipt
4817
+ WHERE receipt.id = p_command_id
4818
+ $$;
4819
+
4820
+ -- Application-owned SECURITY DEFINER RPCs may call this after their domain
4821
+ -- mutation. PostgreSQL commits the domain write, receipt, and workflow enqueue
4822
+ -- together, so a lost response can be replayed with the same command ID.
4823
+ DROP FUNCTION IF EXISTS supacloud_commands.submit(uuid, text, text, text, uuid, jsonb, integer);
4824
+ CREATE OR REPLACE FUNCTION supacloud_commands.submit(request jsonb)
4825
+ RETURNS jsonb
4826
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4827
+ DECLARE
4828
+ command_id_text text;
4829
+ command_id uuid;
4830
+ actor_id uuid;
4831
+ max_attempts integer;
4832
+ payload jsonb;
4833
+ normalized_command_type text;
4834
+ normalized_target_type text;
4835
+ normalized_target_id text;
4836
+ fingerprint text;
4837
+ existing supacloud_commands.receipts%ROWTYPE;
4838
+ BEGIN
4839
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4840
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4841
+ END IF;
4842
+ command_id_text := request ->> 'commandId';
4843
+ IF command_id_text IS NULL
4844
+ 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
4845
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4846
+ END IF;
4847
+ command_id := command_id_text::uuid;
4848
+ IF request ? 'actorId' AND request ->> 'actorId' IS NOT NULL THEN
4849
+ actor_id := (request ->> 'actorId')::uuid;
4850
+ END IF;
4851
+ max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
4852
+ payload := coalesce(request -> 'payload', '{}'::jsonb);
4853
+ normalized_command_type := nullif(btrim(request ->> 'commandType'), '');
4854
+ normalized_target_type := nullif(btrim(request ->> 'targetType'), '');
4855
+ normalized_target_id := nullif(btrim(request ->> 'targetId'), '');
4856
+ IF normalized_command_type IS NULL
4857
+ OR normalized_command_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4858
+ OR normalized_target_type IS NULL
4859
+ OR normalized_target_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4860
+ OR normalized_target_id IS NULL
4861
+ OR char_length(normalized_target_id) > 500
4862
+ OR jsonb_typeof(payload) IS DISTINCT FROM 'object'
4863
+ OR max_attempts NOT BETWEEN 1 AND 100 THEN
4864
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4865
+ END IF;
4866
+
4867
+ fingerprint := 'md5:' || md5(payload::text);
4868
+ PERFORM pg_advisory_xact_lock(hashtextextended(command_id::text, 0));
4869
+ SELECT * INTO existing FROM supacloud_commands.receipts WHERE id = command_id;
4870
+ IF FOUND THEN
4871
+ IF existing.command_type <> normalized_command_type
4872
+ OR existing.target_type <> normalized_target_type
4873
+ OR existing.target_id <> normalized_target_id
4874
+ OR existing.actor_id IS DISTINCT FROM actor_id
4875
+ OR existing.payload <> payload
4876
+ OR existing.payload_fingerprint <> fingerprint THEN
4877
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4878
+ END IF;
4879
+ RETURN supacloud_commands.snapshot(command_id, true);
4880
+ END IF;
4881
+
4882
+ PERFORM supacloud_workflows.start_run(
4883
+ command_id,
4884
+ 'command.' || normalized_command_type,
4885
+ '1',
4886
+ 'execute',
4887
+ jsonb_build_object(
4888
+ 'commandId', command_id,
4889
+ 'commandType', normalized_command_type,
4890
+ 'targetType', normalized_target_type,
4891
+ 'targetId', normalized_target_id,
4892
+ 'actorId', actor_id,
4893
+ 'payload', payload,
4894
+ 'payloadFingerprint', fingerprint
4895
+ ),
4896
+ max_attempts
4897
+ );
4898
+
4899
+ INSERT INTO supacloud_commands.receipts (
4900
+ id, command_type, target_type, target_id, actor_id, payload,
4901
+ payload_fingerprint, workflow_run_id
4902
+ ) VALUES (
4903
+ command_id, normalized_command_type, normalized_target_type,
4904
+ normalized_target_id, actor_id, payload, fingerprint, command_id
4905
+ );
4906
+ RETURN supacloud_commands.snapshot(command_id, false);
4907
+ EXCEPTION
4908
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4909
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_INVALID' USING ERRCODE = '22023';
4910
+ END;
4911
+ $$;
4912
+
4913
+ CREATE OR REPLACE FUNCTION public.supacloud_command_submit(request jsonb)
4914
+ RETURNS jsonb
4915
+ LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4916
+ SELECT supacloud_commands.submit(request)
4917
+ $$;
4918
+
4919
+ CREATE OR REPLACE FUNCTION public.supacloud_command_get(request jsonb)
4920
+ RETURNS jsonb
4921
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4922
+ DECLARE
4923
+ command_id_text text;
4924
+ BEGIN
4925
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4926
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';
4927
+ END IF;
4928
+ command_id_text := request ->> 'commandId';
4929
+ IF command_id_text IS NULL
4930
+ 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
4931
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';
4932
+ END IF;
4933
+ RETURN supacloud_commands.snapshot(command_id_text::uuid, false);
4934
+ EXCEPTION
4935
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4936
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_GET_INVALID' USING ERRCODE = '22023';
4937
+ END;
4938
+ $$;
4939
+
4940
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_commands
4941
+ FROM PUBLIC, anon, authenticated, service_role;
4942
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_commands
4943
+ FROM PUBLIC, anon, authenticated, service_role;
4944
+ REVOKE ALL ON FUNCTION public.supacloud_command_submit(jsonb) FROM PUBLIC, anon, authenticated;
4945
+ REVOKE ALL ON FUNCTION public.supacloud_command_get(jsonb) FROM PUBLIC, anon, authenticated;
4946
+ GRANT EXECUTE ON FUNCTION public.supacloud_command_submit(jsonb) TO service_role;
4947
+ GRANT EXECUTE ON FUNCTION public.supacloud_command_get(jsonb) TO service_role;
4948
+ -- supacloud:sql-module:commands-public:end
4949
+
4950
+ -- supacloud:sql-module:artifacts-public:start
4951
+ CREATE SCHEMA IF NOT EXISTS supacloud_artifacts;
4952
+ REVOKE ALL ON SCHEMA supacloud_artifacts FROM PUBLIC, anon, authenticated;
4953
+ GRANT USAGE ON SCHEMA supacloud_artifacts TO service_role;
4954
+
4955
+ CREATE TABLE IF NOT EXISTS supacloud_artifacts.artifacts (
4956
+ id uuid PRIMARY KEY,
4957
+ storage_object_id uuid NOT NULL UNIQUE REFERENCES storage.objects(id) ON DELETE RESTRICT,
4958
+ bucket_id text NOT NULL,
4959
+ object_path text NOT NULL,
4960
+ object_version text NOT NULL,
4961
+ artifact_type text NOT NULL
4962
+ CHECK (artifact_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4963
+ sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
4964
+ size_bytes bigint NOT NULL CHECK (size_bytes >= 0),
4965
+ mime_type text NOT NULL CHECK (char_length(mime_type) BETWEEN 1 AND 255),
4966
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
4967
+ retention_until timestamptz,
4968
+ created_by uuid,
4969
+ created_at timestamptz NOT NULL DEFAULT now(),
4970
+ CHECK (retention_until IS NULL OR retention_until >= created_at),
4971
+ UNIQUE (bucket_id, object_path, object_version)
4972
+ );
4973
+
4974
+ CREATE INDEX IF NOT EXISTS supacloud_artifacts_lookup_idx
4975
+ ON supacloud_artifacts.artifacts (artifact_type, created_at DESC, id);
4976
+
4977
+ CREATE TABLE IF NOT EXISTS supacloud_artifacts.lineage (
4978
+ parent_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,
4979
+ child_artifact_id uuid NOT NULL REFERENCES supacloud_artifacts.artifacts(id) ON DELETE RESTRICT,
4980
+ relation_type text NOT NULL
4981
+ CHECK (relation_type ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
4982
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
4983
+ created_at timestamptz NOT NULL DEFAULT now(),
4984
+ PRIMARY KEY (parent_artifact_id, child_artifact_id, relation_type),
4985
+ CHECK (parent_artifact_id <> child_artifact_id)
4986
+ );
4987
+
4988
+ CREATE INDEX IF NOT EXISTS supacloud_artifacts_lineage_child_idx
4989
+ ON supacloud_artifacts.lineage (child_artifact_id, created_at, parent_artifact_id);
4990
+
4991
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.guard_storage_object()
4992
+ RETURNS trigger
4993
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4994
+ BEGIN
4995
+ IF EXISTS (
4996
+ SELECT 1 FROM supacloud_artifacts.artifacts artifact
4997
+ WHERE artifact.storage_object_id = OLD.id
4998
+ ) THEN
4999
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IMMUTABLE' USING ERRCODE = '55000';
5000
+ END IF;
5001
+ RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
5002
+ END;
5003
+ $$;
5004
+
5005
+ DROP TRIGGER IF EXISTS supacloud_artifact_storage_fence ON storage.objects;
5006
+ CREATE TRIGGER supacloud_artifact_storage_fence
5007
+ BEFORE UPDATE OR DELETE ON storage.objects
5008
+ FOR EACH ROW EXECUTE FUNCTION supacloud_artifacts.guard_storage_object();
5009
+
5010
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.snapshot(
5011
+ p_artifact_id uuid,
5012
+ p_idempotent boolean DEFAULT false
5013
+ ) RETURNS jsonb
5014
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
5015
+ SELECT jsonb_build_object(
5016
+ 'artifactId', artifact.id,
5017
+ 'bucketId', artifact.bucket_id,
5018
+ 'objectPath', artifact.object_path,
5019
+ 'objectVersion', artifact.object_version,
5020
+ 'artifactType', artifact.artifact_type,
5021
+ 'sha256', artifact.sha256,
5022
+ 'sizeBytes', artifact.size_bytes::text,
5023
+ 'mimeType', artifact.mime_type,
5024
+ 'metadata', artifact.metadata,
5025
+ 'retentionUntil', artifact.retention_until,
5026
+ 'createdBy', artifact.created_by,
5027
+ 'createdAt', artifact.created_at,
5028
+ 'idempotent', p_idempotent,
5029
+ 'parents', coalesce((
5030
+ SELECT jsonb_agg(jsonb_build_object(
5031
+ 'artifactId', edge.parent_artifact_id,
5032
+ 'relationType', edge.relation_type,
5033
+ 'metadata', edge.metadata,
5034
+ 'createdAt', edge.created_at
5035
+ ) ORDER BY edge.created_at, edge.parent_artifact_id)
5036
+ FROM supacloud_artifacts.lineage edge
5037
+ WHERE edge.child_artifact_id = artifact.id
5038
+ ), '[]'::jsonb)
5039
+ )
5040
+ FROM supacloud_artifacts.artifacts artifact
5041
+ WHERE artifact.id = p_artifact_id
5042
+ $$;
5043
+
5044
+ DROP FUNCTION IF EXISTS supacloud_artifacts.register(uuid, text, text, text, text, bigint, text, jsonb, timestamptz, uuid);
5045
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.register(request jsonb)
5046
+ RETURNS jsonb
5047
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5048
+ DECLARE
5049
+ artifact_id uuid;
5050
+ size_bytes bigint;
5051
+ metadata jsonb;
5052
+ retention_until timestamptz;
5053
+ created_by uuid;
5054
+ normalized_bucket text;
5055
+ normalized_path text;
5056
+ normalized_type text;
5057
+ normalized_sha256 text;
5058
+ normalized_mime text;
5059
+ object_row storage.objects%ROWTYPE;
5060
+ existing supacloud_artifacts.artifacts%ROWTYPE;
5061
+ BEGIN
5062
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
5063
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';
5064
+ END IF;
5065
+ artifact_id := (request ->> 'artifactId')::uuid;
5066
+ size_bytes := (request ->> 'sizeBytes')::bigint;
5067
+ metadata := coalesce(request -> 'metadata', '{}'::jsonb);
5068
+ retention_until := (request ->> 'retentionUntil')::timestamptz;
5069
+ created_by := (request ->> 'createdBy')::uuid;
5070
+ normalized_bucket := nullif(btrim(request ->> 'bucketId'), '');
5071
+ normalized_path := nullif(btrim(request ->> 'objectPath'), '');
5072
+ normalized_type := nullif(btrim(request ->> 'artifactType'), '');
5073
+ normalized_sha256 := lower(nullif(btrim(request ->> 'sha256'), ''));
5074
+ normalized_mime := lower(nullif(btrim(request ->> 'mimeType'), ''));
5075
+ IF artifact_id IS NULL OR normalized_bucket IS NULL OR normalized_path IS NULL
5076
+ OR normalized_path LIKE '/%' OR normalized_path LIKE '%\\%'
5077
+ OR normalized_path ~ '(^|/)(.|..)(/|$)'
5078
+ OR normalized_type IS NULL
5079
+ OR normalized_type !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
5080
+ OR normalized_sha256 IS NULL OR normalized_sha256 !~ '^[0-9a-f]{64}$'
5081
+ OR size_bytes IS NULL OR size_bytes < 0
5082
+ OR normalized_mime IS NULL OR char_length(normalized_mime) > 255
5083
+ OR jsonb_typeof(metadata) IS DISTINCT FROM 'object'
5084
+ OR (retention_until IS NOT NULL AND retention_until < now()) THEN
5085
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';
5086
+ END IF;
5087
+
5088
+ SELECT * INTO object_row FROM storage.objects
5089
+ WHERE bucket_id = normalized_bucket AND name = normalized_path;
5090
+ IF NOT FOUND THEN
5091
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_OBJECT_NOT_FOUND' USING ERRCODE = 'P0002';
5092
+ END IF;
5093
+
5094
+ PERFORM pg_advisory_xact_lock(hashtextextended(artifact_id::text, 0));
5095
+ SELECT * INTO existing FROM supacloud_artifacts.artifacts WHERE id = artifact_id;
5096
+ IF FOUND THEN
5097
+ IF existing.storage_object_id <> object_row.id
5098
+ OR existing.artifact_type <> normalized_type
5099
+ OR existing.sha256 <> normalized_sha256
5100
+ OR existing.size_bytes <> size_bytes
5101
+ OR existing.mime_type <> normalized_mime
5102
+ OR existing.metadata <> metadata
5103
+ OR existing.retention_until IS DISTINCT FROM retention_until
5104
+ OR existing.created_by IS DISTINCT FROM created_by THEN
5105
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
5106
+ END IF;
5107
+ RETURN supacloud_artifacts.snapshot(artifact_id, true);
5108
+ END IF;
5109
+
5110
+ INSERT INTO supacloud_artifacts.artifacts (
5111
+ id, storage_object_id, bucket_id, object_path, object_version,
5112
+ artifact_type, sha256, size_bytes, mime_type, metadata,
5113
+ retention_until, created_by
5114
+ ) VALUES (
5115
+ artifact_id, object_row.id, normalized_bucket, normalized_path,
5116
+ object_row.version::text, normalized_type, normalized_sha256,
5117
+ size_bytes, normalized_mime, metadata, retention_until, created_by
5118
+ );
5119
+ RETURN supacloud_artifacts.snapshot(artifact_id, false);
5120
+ EXCEPTION
5121
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
5122
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_INVALID' USING ERRCODE = '22023';
5123
+ END;
5124
+ $$;
5125
+
5126
+ CREATE OR REPLACE FUNCTION supacloud_artifacts.link(
5127
+ p_parent_artifact_id uuid,
5128
+ p_child_artifact_id uuid,
5129
+ p_relation_type text,
5130
+ p_metadata jsonb
5131
+ ) RETURNS jsonb
5132
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5133
+ DECLARE
5134
+ normalized_relation text := nullif(btrim(p_relation_type), '');
5135
+ inserted_count integer;
5136
+ BEGIN
5137
+ IF p_parent_artifact_id IS NULL OR p_child_artifact_id IS NULL
5138
+ OR p_parent_artifact_id = p_child_artifact_id
5139
+ OR normalized_relation IS NULL
5140
+ OR normalized_relation !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
5141
+ OR jsonb_typeof(p_metadata) IS DISTINCT FROM 'object' THEN
5142
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';
5143
+ END IF;
5144
+ IF EXISTS (
5145
+ WITH RECURSIVE descendants(artifact_id) AS (
5146
+ SELECT edge.child_artifact_id
5147
+ FROM supacloud_artifacts.lineage edge
5148
+ WHERE edge.parent_artifact_id = p_child_artifact_id
5149
+ UNION
5150
+ SELECT edge.child_artifact_id
5151
+ FROM supacloud_artifacts.lineage edge
5152
+ JOIN descendants current ON edge.parent_artifact_id = current.artifact_id
5153
+ )
5154
+ SELECT 1 FROM descendants WHERE artifact_id = p_parent_artifact_id
5155
+ ) THEN
5156
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_CYCLE' USING ERRCODE = '23514';
5157
+ END IF;
5158
+ INSERT INTO supacloud_artifacts.lineage (
5159
+ parent_artifact_id, child_artifact_id, relation_type, metadata
5160
+ ) VALUES (
5161
+ p_parent_artifact_id, p_child_artifact_id, normalized_relation, p_metadata
5162
+ ) ON CONFLICT DO NOTHING;
5163
+ GET DIAGNOSTICS inserted_count = ROW_COUNT;
5164
+ IF inserted_count = 0 AND NOT EXISTS (
5165
+ SELECT 1 FROM supacloud_artifacts.lineage
5166
+ WHERE parent_artifact_id = p_parent_artifact_id
5167
+ AND child_artifact_id = p_child_artifact_id
5168
+ AND relation_type = normalized_relation
5169
+ AND metadata = p_metadata
5170
+ ) THEN
5171
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
5172
+ END IF;
5173
+ RETURN supacloud_artifacts.snapshot(p_child_artifact_id, inserted_count = 0);
5174
+ END;
5175
+ $$;
5176
+
5177
+ CREATE OR REPLACE FUNCTION public.supacloud_artifact_register(request jsonb)
5178
+ RETURNS jsonb
5179
+ LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5180
+ SELECT supacloud_artifacts.register(request)
5181
+ $$;
5182
+
5183
+ CREATE OR REPLACE FUNCTION public.supacloud_artifact_get(request jsonb)
5184
+ RETURNS jsonb
5185
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
5186
+ BEGIN
5187
+ RETURN supacloud_artifacts.snapshot((request ->> 'artifactId')::uuid, false);
5188
+ EXCEPTION
5189
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
5190
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_GET_INVALID' USING ERRCODE = '22023';
5191
+ END;
5192
+ $$;
5193
+
5194
+ CREATE OR REPLACE FUNCTION public.supacloud_artifact_link(request jsonb)
5195
+ RETURNS jsonb
5196
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
5197
+ BEGIN
5198
+ RETURN supacloud_artifacts.link(
5199
+ (request ->> 'parentArtifactId')::uuid,
5200
+ (request ->> 'childArtifactId')::uuid,
5201
+ request ->> 'relationType',
5202
+ coalesce(request -> 'metadata', '{}'::jsonb)
5203
+ );
5204
+ EXCEPTION
5205
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
5206
+ RAISE EXCEPTION 'SUPACLOUD_ARTIFACT_LINEAGE_INVALID' USING ERRCODE = '22023';
5207
+ END;
5208
+ $$;
5209
+
5210
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_artifacts
5211
+ FROM PUBLIC, anon, authenticated, service_role;
5212
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_artifacts
5213
+ FROM PUBLIC, anon, authenticated, service_role;
5214
+ REVOKE ALL ON FUNCTION public.supacloud_artifact_register(jsonb) FROM PUBLIC, anon, authenticated;
5215
+ REVOKE ALL ON FUNCTION public.supacloud_artifact_get(jsonb) FROM PUBLIC, anon, authenticated;
5216
+ REVOKE ALL ON FUNCTION public.supacloud_artifact_link(jsonb) FROM PUBLIC, anon, authenticated;
5217
+ GRANT EXECUTE ON FUNCTION public.supacloud_artifact_register(jsonb) TO service_role;
5218
+ GRANT EXECUTE ON FUNCTION public.supacloud_artifact_get(jsonb) TO service_role;
5219
+ GRANT EXECUTE ON FUNCTION public.supacloud_artifact_link(jsonb) TO service_role;
5220
+ -- supacloud:sql-module:artifacts-public:end
4775
5221
  `;
4776
5222
  var CRON_SQL = `
4777
5223
  create schema if not exists cron;
@@ -10272,12 +10718,260 @@ async function serveBun(backend, opts = {}) {
10272
10718
  // src/runtime/node/native/engine.ts
10273
10719
  import { execFileSync, spawn, spawnSync } from "child_process";
10274
10720
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
10275
- import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10721
+ import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10276
10722
  import { writeFile as writeFile2 } from "fs/promises";
10277
10723
  import { homedir, tmpdir } from "os";
10278
10724
  import { join as join2 } from "path";
10279
10725
  import { extract as extractTar } from "tar";
10280
10726
 
10727
+ // src/runtime/node/native/replication.ts
10728
+ import { existsSync, statSync, writeFileSync } from "fs";
10729
+ import { isIP } from "net";
10730
+ import { resolve as resolve2 } from "path";
10731
+ var POWERSYNC_REPLICATION_ROLE = "supacloud_powersync";
10732
+ var POWERSYNC_PUBLICATION = "powersync";
10733
+ function validatePowerSyncReplicationOptions(options) {
10734
+ const host = options.host.trim();
10735
+ validateConnectionInput(host, options.port, options.password);
10736
+ const publicationTables = uniqueSorted(options.publicationTables.map(normalizeQualifiedTable));
10737
+ const allowCidrs = uniqueSorted(options.allowCidrs.map(normalizeCidr));
10738
+ validateAllowlists(publicationTables, allowCidrs);
10739
+ validateNetworkBoundary(host, allowCidrs, Boolean(options.tls));
10740
+ const tls = options.tls ? validateTlsOptions(options.tls) : undefined;
10741
+ return { ...options, host, allowCidrs, publicationTables, tls };
10742
+ }
10743
+ function buildPowerSyncPostgresArgs(options, hbaFile) {
10744
+ const validated = validatePowerSyncReplicationOptions(options);
10745
+ const args = [
10746
+ "-c",
10747
+ `listen_addresses=${validated.host}`,
10748
+ "-c",
10749
+ `port=${validated.port}`,
10750
+ "-c",
10751
+ "wal_level=logical",
10752
+ "-c",
10753
+ "max_wal_senders=4",
10754
+ "-c",
10755
+ "max_replication_slots=4",
10756
+ "-c",
10757
+ "max_slot_wal_keep_size=1024MB",
10758
+ "-c",
10759
+ "wal_keep_size=64MB",
10760
+ "-c",
10761
+ `hba_file=${resolve2(hbaFile)}`
10762
+ ];
10763
+ if (!validated.tls)
10764
+ return [...args, "-c", "ssl=off"];
10765
+ args.push("-c", "ssl=on", "-c", `ssl_cert_file=${validated.tls.certFile}`, "-c", `ssl_key_file=${validated.tls.keyFile}`);
10766
+ return args;
10767
+ }
10768
+ function writePowerSyncHba(path, options) {
10769
+ const validated = validatePowerSyncReplicationOptions(options);
10770
+ const hostRecord = validated.tls ? "hostssl" : "host";
10771
+ const lines = [
10772
+ "# Managed by SupaCloud Lite. Changes are replaced when the PowerSync profile starts.",
10773
+ "local all postgres trust",
10774
+ ...validated.allowCidrs.flatMap((cidr) => [
10775
+ `${hostRecord} postgres ${POWERSYNC_REPLICATION_ROLE} ${cidr} scram-sha-256`,
10776
+ `${hostRecord} replication ${POWERSYNC_REPLICATION_ROLE} ${cidr} scram-sha-256`
10777
+ ]),
10778
+ "host all all 0.0.0.0/0 reject",
10779
+ "host all all ::/0 reject",
10780
+ ""
10781
+ ];
10782
+ writeFileSync(path, lines.join(`
10783
+ `), { mode: 384 });
10784
+ }
10785
+ async function ensurePowerSyncReplicationCatalog(engine, options) {
10786
+ const validated = validatePowerSyncReplicationOptions(options);
10787
+ await engine.transaction(async (tx) => {
10788
+ const tables = await inspectPublicationTables(tx, validated.publicationTables);
10789
+ validatePublicationTables(tables);
10790
+ await configureReplicationRole(tx, validated.password);
10791
+ await synchronizeReplicationGrants(tx, tables);
10792
+ await assertEffectiveSelectAllowlist(tx, tables);
10793
+ await synchronizePublication(tx, tables);
10794
+ });
10795
+ }
10796
+ function validatePublicationTables(tables) {
10797
+ const missing = tables.filter((table) => !table.present).map((table) => table.name);
10798
+ if (missing.length > 0) {
10799
+ throw new Error(`PowerSync publication tables do not exist: ${missing.join(", ")}`);
10800
+ }
10801
+ const unsupported2 = tables.filter((table) => table.present && !["r", "p"].includes(table.relkind ?? ""));
10802
+ if (unsupported2.length > 0) {
10803
+ throw new Error(`PowerSync publication only accepts ordinary or partitioned tables: ${unsupported2.map((table) => table.name).join(", ")}`);
10804
+ }
10805
+ }
10806
+ async function configureReplicationRole(tx, passwordValue) {
10807
+ await tx.exec(`
10808
+ DO $profile$
10809
+ BEGIN
10810
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${POWERSYNC_REPLICATION_ROLE}') THEN
10811
+ CREATE ROLE ${POWERSYNC_REPLICATION_ROLE};
10812
+ END IF;
10813
+ END
10814
+ $profile$;
10815
+ ALTER ROLE ${POWERSYNC_REPLICATION_ROLE}
10816
+ WITH LOGIN NOINHERIT REPLICATION BYPASSRLS CONNECTION LIMIT 4 PASSWORD ${quoteLiteral2(passwordValue)};
10817
+ ALTER ROLE ${POWERSYNC_REPLICATION_ROLE} SET search_path = pg_catalog;
10818
+ GRANT CONNECT ON DATABASE postgres TO ${POWERSYNC_REPLICATION_ROLE};
10819
+ `);
10820
+ }
10821
+ async function synchronizeReplicationGrants(tx, tables) {
10822
+ await revokeReplicationGrants(tx);
10823
+ await grantReplicationAllowlist(tx, tables);
10824
+ }
10825
+ async function revokeReplicationGrants(tx) {
10826
+ const staleGrants = await tx.query(`
10827
+ SELECT DISTINCT table_schema, table_name
10828
+ FROM information_schema.role_table_grants
10829
+ WHERE grantee = '${POWERSYNC_REPLICATION_ROLE}' AND privilege_type = 'SELECT'
10830
+ `);
10831
+ for (const grant of staleGrants.rows) {
10832
+ await tx.exec(`REVOKE SELECT ON TABLE ${quoteIdentifier(grant.table_schema)}.${quoteIdentifier(grant.table_name)} ` + `FROM ${POWERSYNC_REPLICATION_ROLE}`);
10833
+ }
10834
+ const applicationSchemas = await tx.query(`
10835
+ SELECT nspname AS schema_name
10836
+ FROM pg_namespace
10837
+ WHERE nspname NOT IN ('pg_catalog', 'information_schema')
10838
+ AND nspname !~ '^pg_toast'
10839
+ `);
10840
+ for (const schema of applicationSchemas.rows) {
10841
+ await tx.exec(`REVOKE USAGE ON SCHEMA ${quoteIdentifier(schema.schema_name)} FROM ${POWERSYNC_REPLICATION_ROLE}`);
10842
+ }
10843
+ }
10844
+ async function grantReplicationAllowlist(tx, tables) {
10845
+ const schemas = uniqueSorted(tables.map((table) => table.schema));
10846
+ for (const schema of schemas) {
10847
+ await tx.exec(`GRANT USAGE ON SCHEMA ${quoteIdentifier(schema)} TO ${POWERSYNC_REPLICATION_ROLE}`);
10848
+ }
10849
+ for (const table of tables) {
10850
+ await tx.exec(`GRANT SELECT ON TABLE ${quoteQualifiedTable(table.name)} TO ${POWERSYNC_REPLICATION_ROLE}`);
10851
+ }
10852
+ }
10853
+ async function assertEffectiveSelectAllowlist(tx, tables) {
10854
+ const allowed = new Set(tables.map((table) => table.name));
10855
+ const selectable = await tx.query(`
10856
+ SELECT namespace.nspname || '.' || relation.relname AS qualified_name
10857
+ FROM pg_class relation
10858
+ JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
10859
+ WHERE relation.relkind IN ('r', 'p')
10860
+ AND namespace.nspname NOT IN ('pg_catalog', 'information_schema')
10861
+ AND namespace.nspname !~ '^pg_toast'
10862
+ AND has_table_privilege('${POWERSYNC_REPLICATION_ROLE}', relation.oid, 'SELECT')
10863
+ ORDER BY namespace.nspname, relation.relname
10864
+ `);
10865
+ const outsideAllowlist = selectable.rows.map((row) => row.qualified_name).filter((table) => !allowed.has(table));
10866
+ if (outsideAllowlist.length > 0) {
10867
+ throw new Error(`PowerSync role has SELECT outside the publication allowlist: ${outsideAllowlist.join(", ")}`);
10868
+ }
10869
+ }
10870
+ async function synchronizePublication(tx, tables) {
10871
+ const publication = await tx.query(`SELECT puballtables FROM pg_publication WHERE pubname = '${POWERSYNC_PUBLICATION}'`);
10872
+ if (publication.rows[0]?.puballtables) {
10873
+ throw new Error("existing powersync publication uses FOR ALL TABLES; replace it with an explicit allowlist");
10874
+ }
10875
+ if (publication.rows.length === 0) {
10876
+ await tx.exec(`CREATE PUBLICATION ${POWERSYNC_PUBLICATION} WITH (publish = 'insert, update, delete')`);
10877
+ }
10878
+ 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')`);
10879
+ }
10880
+ async function inspectPublicationTables(tx, names) {
10881
+ const rows = [];
10882
+ for (const name of names) {
10883
+ const tableLookup = await tx.query(`
10884
+ SELECT namespace.nspname AS schema_name, relation.relkind
10885
+ FROM pg_class relation
10886
+ JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
10887
+ WHERE relation.oid = to_regclass($1)
10888
+ `, [name]);
10889
+ const row = tableLookup.rows[0];
10890
+ rows.push({ name, schema: row?.schema_name, relkind: row?.relkind, present: Boolean(row) });
10891
+ }
10892
+ return rows;
10893
+ }
10894
+ function validateConnectionInput(host, port, password) {
10895
+ if (!host || host !== "localhost" && isIP(host) === 0) {
10896
+ throw new Error("PowerSync replication host must be localhost or an explicit IP address");
10897
+ }
10898
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
10899
+ throw new Error("PowerSync replication port must be between 1 and 65535");
10900
+ }
10901
+ if (password.length < 32) {
10902
+ throw new Error("SUPACLOUD_LITE_POWERSYNC_PASSWORD must contain at least 32 characters");
10903
+ }
10904
+ if (password.includes("\x00"))
10905
+ throw new Error("SUPACLOUD_LITE_POWERSYNC_PASSWORD must not contain NUL bytes");
10906
+ }
10907
+ function validateAllowlists(publicationTables, allowCidrs) {
10908
+ if (publicationTables.length === 0) {
10909
+ throw new Error("PowerSync replication requires an explicit publication table allowlist");
10910
+ }
10911
+ if (allowCidrs.length === 0)
10912
+ throw new Error("PowerSync replication requires at least one client CIDR");
10913
+ }
10914
+ function validateNetworkBoundary(host, allowCidrs, tls) {
10915
+ if (!isLoopbackHost(host) && !tls) {
10916
+ throw new Error("non-loopback PowerSync replication requires TLS certificate and key files");
10917
+ }
10918
+ if (!tls && allowCidrs.some((cidr) => !isLoopbackCidr(cidr))) {
10919
+ throw new Error("non-loopback PowerSync client CIDRs require TLS");
10920
+ }
10921
+ }
10922
+ function validateTlsOptions(options) {
10923
+ const tls = {
10924
+ certFile: resolve2(options.certFile),
10925
+ keyFile: resolve2(options.keyFile)
10926
+ };
10927
+ for (const path of [tls.certFile, tls.keyFile]) {
10928
+ if (!existsSync(path) || !statSync(path).isFile())
10929
+ throw new Error(`PowerSync TLS file does not exist: ${path}`);
10930
+ }
10931
+ if (process.platform !== "win32" && (statSync(tls.keyFile).mode & 63) !== 0) {
10932
+ throw new Error("PowerSync TLS private key must not be readable by group or other users");
10933
+ }
10934
+ return tls;
10935
+ }
10936
+ function normalizeQualifiedTable(tableName) {
10937
+ const normalized = tableName.trim().toLowerCase();
10938
+ if (!/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/.test(normalized)) {
10939
+ throw new Error(`invalid PowerSync publication table: ${tableName}`);
10940
+ }
10941
+ return normalized;
10942
+ }
10943
+ function normalizeCidr(cidr) {
10944
+ const normalized = cidr.trim();
10945
+ const match = /^(.+)\/(\d{1,3})$/.exec(normalized);
10946
+ if (!match)
10947
+ throw new Error(`invalid PowerSync client CIDR: ${cidr}`);
10948
+ const address = match[1];
10949
+ const family = isIP(address);
10950
+ const prefix = Number(match[2]);
10951
+ if (family === 0 || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
10952
+ throw new Error(`invalid PowerSync client CIDR: ${cidr}`);
10953
+ }
10954
+ return `${address}/${prefix}`;
10955
+ }
10956
+ function isLoopbackHost(host) {
10957
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
10958
+ }
10959
+ function isLoopbackCidr(cidr) {
10960
+ return cidr === "127.0.0.1/32" || cidr === "::1/128";
10961
+ }
10962
+ function quoteIdentifier(identifier) {
10963
+ return `"${identifier.replaceAll('"', '""')}"`;
10964
+ }
10965
+ function quoteQualifiedTable(tableName) {
10966
+ return tableName.split(".").map(quoteIdentifier).join(".");
10967
+ }
10968
+ function quoteLiteral2(literal) {
10969
+ return `'${literal.replaceAll("'", "''")}'`;
10970
+ }
10971
+ function uniqueSorted(values) {
10972
+ return [...new Set(values)].sort();
10973
+ }
10974
+
10281
10975
  // src/runtime/node/native/wire.ts
10282
10976
  import { createConnection } from "net";
10283
10977
  import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
@@ -10309,7 +11003,7 @@ class PgWireClient {
10309
11003
  return client;
10310
11004
  }
10311
11005
  open(opts) {
10312
- return new Promise((resolve2, reject) => {
11006
+ return new Promise((resolve3, reject) => {
10313
11007
  this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
10314
11008
  this.socket.on("error", (e) => {
10315
11009
  if (this.pending)
@@ -10407,7 +11101,7 @@ class PgWireClient {
10407
11101
  this.buffer = Buffer.concat([this.buffer, c]);
10408
11102
  this.processMessages();
10409
11103
  });
10410
- resolve2();
11104
+ resolve3();
10411
11105
  return;
10412
11106
  }
10413
11107
  }
@@ -10416,10 +11110,10 @@ class PgWireClient {
10416
11110
  });
10417
11111
  }
10418
11112
  run(send) {
10419
- const op = this.queue.then(() => new Promise((resolve2, reject) => {
11113
+ const op = this.queue.then(() => new Promise((resolve3, reject) => {
10420
11114
  if (this.closed)
10421
11115
  return reject(new Error("connection closed"));
10422
- this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
11116
+ this.pending = { resolve: resolve3, reject, results: [], columns: [], error: null };
10423
11117
  send();
10424
11118
  }));
10425
11119
  this.queue = op.catch(() => {});
@@ -10450,11 +11144,11 @@ class PgWireClient {
10450
11144
  return results[0] ?? { rows: [] };
10451
11145
  }
10452
11146
  close() {
10453
- return new Promise((resolve2) => {
11147
+ return new Promise((resolve3) => {
10454
11148
  if (this.closed)
10455
- return resolve2();
11149
+ return resolve3();
10456
11150
  this.socket.write(message(88, Buffer.alloc(0)));
10457
- this.socket.end(() => resolve2());
11151
+ this.socket.end(() => resolve3());
10458
11152
  });
10459
11153
  }
10460
11154
  nextMessage() {
@@ -10843,7 +11537,7 @@ function reportedGlibcVersion() {
10843
11537
  }
10844
11538
  function glibcDynamicLoaderPresent() {
10845
11539
  const loaderPaths = process.arch === "x64" ? GLIBC_DYNAMIC_LOADERS.x64 : process.arch === "arm64" ? GLIBC_DYNAMIC_LOADERS.arm64 : [];
10846
- return loaderPaths.some((loaderPath) => existsSync(loaderPath));
11540
+ return loaderPaths.some((loaderPath) => existsSync2(loaderPath));
10847
11541
  }
10848
11542
  function lddVersion() {
10849
11543
  const command = spawnSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -10863,7 +11557,7 @@ function target() {
10863
11557
  throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
10864
11558
  }
10865
11559
  function isCompleteInstall(dir) {
10866
- return existsSync(join2(dir, "bin", "postgres")) && existsSync(join2(dir, "share", "postgres.bki"));
11560
+ return existsSync2(join2(dir, "bin", "postgres")) && existsSync2(join2(dir, "share", "postgres.bki"));
10867
11561
  }
10868
11562
  var PINNED_SHA256 = {
10869
11563
  "postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
@@ -10969,7 +11663,7 @@ async function fetchRelease(url) {
10969
11663
  lastError = error;
10970
11664
  }
10971
11665
  if (attempt < 3)
10972
- await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
11666
+ await new Promise((resolve3) => setTimeout(resolve3, attempt * 500));
10973
11667
  }
10974
11668
  throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
10975
11669
  }
@@ -10981,6 +11675,7 @@ dynamic_shared_memory_type = posix
10981
11675
  max_connections = 10
10982
11676
  wal_level = minimal
10983
11677
  max_wal_senders = 0
11678
+ max_replication_slots = 0
10984
11679
  logging_collector = off
10985
11680
  `;
10986
11681
  async function createNativeEngine(opts) {
@@ -10991,7 +11686,7 @@ async function createNativeEngine(opts) {
10991
11686
  try {
10992
11687
  const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
10993
11688
  const bin = (name) => join2(installDir, "bin", name);
10994
- if (!existsSync(join2(opts.dataDir, "PG_VERSION"))) {
11689
+ if (!existsSync2(join2(opts.dataDir, "PG_VERSION"))) {
10995
11690
  mkdirSync(opts.dataDir, { recursive: true });
10996
11691
  try {
10997
11692
  execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
@@ -11007,7 +11702,20 @@ ${stderr || error.message}`);
11007
11702
  removeStalePidFile(join2(opts.dataDir, "postmaster.pid"));
11008
11703
  socketDirectory = mkdtempSync(join2(tmpdir(), "scl-"));
11009
11704
  chmodSync(socketDirectory, 448);
11010
- postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
11705
+ const replicationHba = opts.replication ? join2(opts.dataDir, "supacloud-powersync-hba.conf") : undefined;
11706
+ if (opts.replication && replicationHba)
11707
+ writePowerSyncHba(replicationHba, opts.replication);
11708
+ const replicationArgs = opts.replication && replicationHba ? buildPowerSyncPostgresArgs(opts.replication, replicationHba) : [
11709
+ "-c",
11710
+ "listen_addresses=",
11711
+ "-c",
11712
+ "wal_level=minimal",
11713
+ "-c",
11714
+ "max_wal_senders=0",
11715
+ "-c",
11716
+ "max_replication_slots=0"
11717
+ ];
11718
+ postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC", ...replicationArgs], {
11011
11719
  stdio: ["ignore", "ignore", "pipe"],
11012
11720
  detached: false
11013
11721
  });
@@ -11023,7 +11731,8 @@ ${stderr || error.message}`);
11023
11731
  };
11024
11732
  process.once("exit", killPostgres);
11025
11733
  removeExitHandler = () => process.off("exit", killPostgres);
11026
- const socketPath = join2(socketDirectory, ".s.PGSQL.5432");
11734
+ const postgresPort = opts.replication?.port ?? 5432;
11735
+ const socketPath = join2(socketDirectory, `.s.PGSQL.${postgresPort}`);
11027
11736
  const connect = async () => {
11028
11737
  const deadline = Date.now() + 20000;
11029
11738
  while (Date.now() <= deadline) {
@@ -11038,7 +11747,7 @@ ${detail}` : " (no output)"}
11038
11747
  ` + `data dir: ${opts.dataDir}
11039
11748
  ` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
11040
11749
  }
11041
- await new Promise((resolve2) => setTimeout(resolve2, 150));
11750
+ await new Promise((resolve3) => setTimeout(resolve3, 150));
11042
11751
  }
11043
11752
  }
11044
11753
  throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
@@ -11066,19 +11775,19 @@ async function stopPostgres(postgres, hasExited) {
11066
11775
  if (hasExited())
11067
11776
  return;
11068
11777
  postgres.kill("SIGINT");
11069
- await new Promise((resolve2) => {
11778
+ await new Promise((resolve3) => {
11070
11779
  const killTimeout = setTimeout(() => {
11071
11780
  postgres.kill("SIGKILL");
11072
- resolve2();
11781
+ resolve3();
11073
11782
  }, 5000);
11074
11783
  postgres.once("exit", () => {
11075
11784
  clearTimeout(killTimeout);
11076
- resolve2();
11785
+ resolve3();
11077
11786
  });
11078
11787
  });
11079
11788
  }
11080
11789
  function removeStalePidFile(pidPath) {
11081
- if (!existsSync(pidPath))
11790
+ if (!existsSync2(pidPath))
11082
11791
  return;
11083
11792
  try {
11084
11793
  const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
@@ -11094,9 +11803,238 @@ function removeStalePidFile(pidPath) {
11094
11803
  }
11095
11804
  } catch {}
11096
11805
  }
11806
+ // src/runtime/node/native/readiness.ts
11807
+ function liteCapabilities(engine, replicationProfile) {
11808
+ if (engine === "pglite") {
11809
+ return {
11810
+ engine,
11811
+ state_machine_sql: "supported",
11812
+ durable_workflows: "supported",
11813
+ commands: "supported",
11814
+ artifacts: "supported",
11815
+ postgrest_schema_config: "static",
11816
+ logical_replication: "unsupported",
11817
+ powersync_source: "unsupported"
11818
+ };
11819
+ }
11820
+ return {
11821
+ engine,
11822
+ state_machine_sql: "supported",
11823
+ durable_workflows: "supported",
11824
+ commands: "supported",
11825
+ artifacts: "supported",
11826
+ postgrest_schema_config: "static",
11827
+ logical_replication: replicationProfile ? "supported" : "disabled",
11828
+ powersync_source: replicationProfile ? "supported" : "disabled",
11829
+ ...replicationProfile ? { replication_profile: replicationProfile } : {}
11830
+ };
11831
+ }
11832
+ async function inspectPowerSyncReadiness(engine, options) {
11833
+ return buildReadiness(await loadReplicationInventory(engine), options);
11834
+ }
11835
+ async function loadReplicationInventory(engine) {
11836
+ const [settings, senders, role, publication, publicationTables, selectableTables, slots] = await Promise.all([
11837
+ engine.query(`
11838
+ SELECT name, setting
11839
+ FROM pg_settings
11840
+ WHERE name IN (
11841
+ 'wal_level', 'max_wal_senders', 'max_replication_slots',
11842
+ 'max_slot_wal_keep_size', 'ssl', 'listen_addresses', 'port'
11843
+ )
11844
+ `),
11845
+ engine.query("SELECT count(*)::integer AS count FROM pg_stat_replication"),
11846
+ engine.query(`
11847
+ SELECT rolcanlogin, rolreplication, rolbypassrls
11848
+ FROM pg_roles WHERE rolname = '${POWERSYNC_REPLICATION_ROLE}'
11849
+ `),
11850
+ engine.query(`
11851
+ SELECT puballtables, pubinsert, pubupdate, pubdelete
11852
+ FROM pg_publication WHERE pubname = '${POWERSYNC_PUBLICATION}'
11853
+ `),
11854
+ engine.query(`
11855
+ SELECT
11856
+ quote_ident(namespace.nspname) || '.' || quote_ident(relation.relname) AS qualified_name,
11857
+ relation.relreplident = 'n' OR (
11858
+ relation.relreplident = 'd'
11859
+ AND NOT EXISTS (
11860
+ SELECT 1 FROM pg_index index_record
11861
+ WHERE index_record.indrelid = relation.oid AND index_record.indisprimary
11862
+ )
11863
+ ) AS replica_identity_missing
11864
+ FROM pg_publication_tables published
11865
+ JOIN pg_namespace namespace ON namespace.nspname = published.schemaname
11866
+ JOIN pg_class relation
11867
+ ON relation.relnamespace = namespace.oid AND relation.relname = published.tablename
11868
+ WHERE published.pubname = '${POWERSYNC_PUBLICATION}'
11869
+ ORDER BY namespace.nspname, relation.relname
11870
+ `),
11871
+ engine.query(`
11872
+ SELECT namespace.nspname || '.' || relation.relname AS qualified_name
11873
+ FROM pg_roles role_record
11874
+ JOIN pg_class relation ON true
11875
+ JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
11876
+ WHERE role_record.rolname = '${POWERSYNC_REPLICATION_ROLE}'
11877
+ AND relation.relkind IN ('r', 'p')
11878
+ AND namespace.nspname NOT IN ('pg_catalog', 'information_schema')
11879
+ AND namespace.nspname !~ '^pg_toast'
11880
+ AND has_table_privilege(role_record.oid, relation.oid, 'SELECT')
11881
+ ORDER BY namespace.nspname, relation.relname
11882
+ `),
11883
+ engine.query(`
11884
+ SELECT
11885
+ slot_name,
11886
+ active,
11887
+ wal_status,
11888
+ coalesce(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn), 0)::text AS retained_wal_bytes,
11889
+ coalesce(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn), 0)::text AS unconfirmed_wal_bytes,
11890
+ safe_wal_size::text,
11891
+ invalidation_reason
11892
+ FROM pg_replication_slots
11893
+ WHERE slot_type = 'logical'
11894
+ ORDER BY slot_name
11895
+ `)
11896
+ ]);
11897
+ return {
11898
+ settings: new Map(settings.rows.map((row) => [row.name, row.setting])),
11899
+ activeSenders: senders.rows[0]?.count ?? 0,
11900
+ role: role.rows[0],
11901
+ publication: publication.rows[0],
11902
+ publicationTables: publicationTables.rows,
11903
+ selectableTables: selectableTables.rows.map((row) => row.qualified_name),
11904
+ slots: slots.rows
11905
+ };
11906
+ }
11907
+ function buildReadiness(inventory, options) {
11908
+ const actualTables = catalogTableNames(inventory.publicationTables);
11909
+ const expectedTables = [...options.publicationTables].sort();
11910
+ const missingIdentity = missingReplicaIdentity(inventory.publicationTables);
11911
+ const blockers = readinessBlockers(inventory, actualTables, expectedTables, missingIdentity);
11912
+ return {
11913
+ ready: blockers.length === 0,
11914
+ blockers,
11915
+ warnings: readinessWarnings(inventory),
11916
+ connection: {
11917
+ host: options.host,
11918
+ port: options.port,
11919
+ tls: Boolean(options.tls),
11920
+ allowed_cidrs: options.allowCidrs
11921
+ },
11922
+ wal: {
11923
+ level: inventory.settings.get("wal_level") ?? "unknown",
11924
+ max_senders: integerSetting(inventory.settings.get("max_wal_senders")),
11925
+ active_senders: inventory.activeSenders,
11926
+ max_replication_slots: integerSetting(inventory.settings.get("max_replication_slots")),
11927
+ used_replication_slots: inventory.slots.length,
11928
+ max_slot_wal_keep_size: inventory.settings.get("max_slot_wal_keep_size") ?? "unknown"
11929
+ },
11930
+ role: roleReadiness(inventory.role, unexpectedSelectableTables(inventory, expectedTables)),
11931
+ publication: publicationReadiness(inventory.publication, actualTables, expectedTables, missingIdentity),
11932
+ slots: inventory.slots.map((slot) => ({
11933
+ name: slot.slot_name,
11934
+ active: slot.active,
11935
+ wal_status: slot.wal_status,
11936
+ retained_wal_bytes: slot.retained_wal_bytes,
11937
+ unconfirmed_wal_bytes: slot.unconfirmed_wal_bytes,
11938
+ safe_wal_size: slot.safe_wal_size,
11939
+ invalidation_reason: slot.invalidation_reason
11940
+ }))
11941
+ };
11942
+ }
11943
+ function readinessBlockers(inventory, actualTables, expectedTables, missingIdentity) {
11944
+ const blockers = capacityBlockers(inventory);
11945
+ if (!roleIsReady(inventory.role))
11946
+ blockers.push("POWERSYNC_ROLE_NOT_READY");
11947
+ if (unexpectedSelectableTables(inventory, expectedTables).length > 0) {
11948
+ blockers.push("POWERSYNC_ROLE_SELECT_OUTSIDE_ALLOWLIST");
11949
+ }
11950
+ blockers.push(...publicationBlockers(inventory.publication, actualTables, expectedTables, missingIdentity));
11951
+ return blockers;
11952
+ }
11953
+ function capacityBlockers(inventory) {
11954
+ const blockers = [];
11955
+ const maxSenders = integerSetting(inventory.settings.get("max_wal_senders"));
11956
+ const maxSlots = integerSetting(inventory.settings.get("max_replication_slots"));
11957
+ if (inventory.settings.get("wal_level") !== "logical")
11958
+ blockers.push("WAL_LEVEL_NOT_LOGICAL");
11959
+ if (maxSenders - inventory.activeSenders < 1)
11960
+ blockers.push("NO_FREE_WAL_SENDER");
11961
+ if (maxSlots - inventory.slots.length < 1)
11962
+ blockers.push("NO_FREE_REPLICATION_SLOT");
11963
+ return blockers;
11964
+ }
11965
+ function publicationBlockers(publication, actualTables, expectedTables, missingIdentity) {
11966
+ if (!publication)
11967
+ return ["POWERSYNC_PUBLICATION_MISSING"];
11968
+ const blockers = [];
11969
+ if (publication.puballtables)
11970
+ blockers.push("POWERSYNC_PUBLICATION_NOT_ALLOWLISTED");
11971
+ if (!publication.pubinsert || !publication.pubupdate || !publication.pubdelete) {
11972
+ blockers.push("POWERSYNC_PUBLICATION_DML_INCOMPLETE");
11973
+ }
11974
+ if (!sameStrings(actualTables, expectedTables))
11975
+ blockers.push("POWERSYNC_PUBLICATION_TABLE_MISMATCH");
11976
+ if (missingIdentity.length > 0)
11977
+ blockers.push("POWERSYNC_REPLICA_IDENTITY_INCOMPLETE");
11978
+ return blockers;
11979
+ }
11980
+ function readinessWarnings(inventory) {
11981
+ const warnings = [];
11982
+ if (inventory.settings.get("max_slot_wal_keep_size") === "-1")
11983
+ warnings.push("SLOT_WAL_KEEP_SIZE_UNBOUNDED");
11984
+ if (inventory.slots.some((slot) => slot.wal_status === "lost" || slot.invalidation_reason)) {
11985
+ warnings.push("INVALID_LOGICAL_SLOTS");
11986
+ }
11987
+ return warnings;
11988
+ }
11989
+ function roleIsReady(role) {
11990
+ return Boolean(role?.rolcanlogin && role.rolreplication && role.rolbypassrls);
11991
+ }
11992
+ function roleReadiness(role, unexpectedTables) {
11993
+ return {
11994
+ name: POWERSYNC_REPLICATION_ROLE,
11995
+ present: Boolean(role),
11996
+ login: role?.rolcanlogin ?? false,
11997
+ replication: role?.rolreplication ?? false,
11998
+ bypass_rls: role?.rolbypassrls ?? false,
11999
+ unexpected_selectable_tables: unexpectedTables
12000
+ };
12001
+ }
12002
+ function unexpectedSelectableTables(inventory, expectedTables) {
12003
+ const expected = new Set(expectedTables);
12004
+ return inventory.selectableTables.filter((table) => !expected.has(table));
12005
+ }
12006
+ function publicationReadiness(publication, tables, expectedTables, missingIdentity) {
12007
+ return {
12008
+ name: POWERSYNC_PUBLICATION,
12009
+ present: Boolean(publication),
12010
+ all_tables: publication?.puballtables ?? false,
12011
+ tables,
12012
+ expected_tables: expectedTables,
12013
+ publishes_insert: publication?.pubinsert ?? false,
12014
+ publishes_update: publication?.pubupdate ?? false,
12015
+ publishes_delete: publication?.pubdelete ?? false,
12016
+ replica_identity_missing_tables: missingIdentity
12017
+ };
12018
+ }
12019
+ function catalogTableNames(rows) {
12020
+ return rows.map((row) => normalizeCatalogTable(row.qualified_name));
12021
+ }
12022
+ function missingReplicaIdentity(rows) {
12023
+ return rows.filter((row) => row.replica_identity_missing).map((row) => normalizeCatalogTable(row.qualified_name));
12024
+ }
12025
+ function integerSetting(value) {
12026
+ const parsed = Number(value);
12027
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
12028
+ }
12029
+ function normalizeCatalogTable(value) {
12030
+ return value.replaceAll('"', "").toLowerCase();
12031
+ }
12032
+ function sameStrings(left, right) {
12033
+ return left.length === right.length && left.every((value, index) => value === right[index]);
12034
+ }
11097
12035
  // src/project-runtime.ts
11098
12036
  import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
11099
- import { dirname as dirname4, isAbsolute, join as join7, parse, relative, resolve as resolve2 } from "path";
12037
+ import { dirname as dirname4, isAbsolute, join as join7, parse, relative, resolve as resolve3 } from "path";
11100
12038
 
11101
12039
  // src/runtime/node/config-toml.ts
11102
12040
  import { readFileSync as readFileSync2 } from "fs";
@@ -11479,7 +12417,7 @@ import { pathToFileURL } from "url";
11479
12417
  // src/runtime/node/bundle-function.ts
11480
12418
  import { createHash as createHash3 } from "crypto";
11481
12419
  import { mkdir as mkdir3, readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
11482
- import { existsSync as existsSync2 } from "fs";
12420
+ import { existsSync as existsSync3 } from "fs";
11483
12421
  import { tmpdir as tmpdir2 } from "os";
11484
12422
  import { join as join4 } from "path";
11485
12423
  function rewriteRemoteSpecifier(spec) {
@@ -11493,7 +12431,7 @@ var HTTP_CACHE = join4(tmpdir2(), "supacloud-lite-fn-http");
11493
12431
  async function fetchModule(url) {
11494
12432
  const key = createHash3("sha256").update(url).digest("hex");
11495
12433
  const cached = join4(HTTP_CACHE, key);
11496
- if (existsSync2(cached))
12434
+ if (existsSync3(cached))
11497
12435
  return readFile3(cached, "utf8");
11498
12436
  const res = await fetch(url, { redirect: "follow" });
11499
12437
  if (!res.ok)
@@ -11700,7 +12638,7 @@ function isNotFound(error) {
11700
12638
 
11701
12639
  // src/project-runtime.ts
11702
12640
  function resolveProjectPaths(options = {}) {
11703
- const projectDir = resolve2(options.projectDir ?? process.cwd());
12641
+ const projectDir = resolve3(options.projectDir ?? process.cwd());
11704
12642
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
11705
12643
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
11706
12644
  const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join7(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -11776,6 +12714,7 @@ async function createProjectBackend(options = {}) {
11776
12714
  const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
11777
12715
  const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
11778
12716
  const databaseEngine = paths.databaseEngine;
12717
+ const replication = resolveNativeReplicationOptions(options, databaseEngine);
11779
12718
  if (paths.dataDir) {
11780
12719
  await mkdir4(paths.dataDir, { recursive: true, mode: 448 });
11781
12720
  await chmod(paths.dataDir, 448);
@@ -11783,7 +12722,7 @@ async function createProjectBackend(options = {}) {
11783
12722
  await mkdir4(paths.storageDir, { recursive: true, mode: 448 });
11784
12723
  await chmod(paths.storageDir, 448);
11785
12724
  const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
11786
- const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
12725
+ const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log, replication }) : undefined;
11787
12726
  let backend;
11788
12727
  try {
11789
12728
  backend = await createBackend({
@@ -11817,8 +12756,17 @@ async function createProjectBackend(options = {}) {
11817
12756
  storageDriver,
11818
12757
  log: options.log
11819
12758
  });
12759
+ if (replication && options.applyMigrations !== false) {
12760
+ await ensurePowerSyncReplicationCatalog(backend.db.engine, replication);
12761
+ }
11820
12762
  } catch (error) {
11821
- if (engine) {
12763
+ if (backend) {
12764
+ try {
12765
+ await backend.close();
12766
+ } catch (cleanupError) {
12767
+ throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
12768
+ }
12769
+ } else if (engine) {
11822
12770
  try {
11823
12771
  await engine.close();
11824
12772
  } catch (cleanupError) {
@@ -11838,9 +12786,33 @@ async function createProjectBackend(options = {}) {
11838
12786
  functionNames: [...functions.keys()],
11839
12787
  webhookCount: webhooks.length,
11840
12788
  storageBackend,
11841
- databaseEngine
12789
+ databaseEngine,
12790
+ replicationProfile: replication?.profile
11842
12791
  };
11843
12792
  }
12793
+ function resolveNativeReplicationOptions(options, databaseEngine = resolveDatabaseEngine(options.engine, options.memory)) {
12794
+ const profile = options.replicationProfile ?? process.env.SUPACLOUD_LITE_REPLICATION_PROFILE;
12795
+ if (!profile)
12796
+ return;
12797
+ if (profile !== "powersync")
12798
+ throw new Error(`unsupported SUPACLOUD_LITE_REPLICATION_PROFILE: ${profile}`);
12799
+ if (databaseEngine !== "native")
12800
+ throw new Error("PowerSync replication is only supported by --engine native");
12801
+ const tlsCertFile = options.replicationTlsCertFile ?? process.env.SUPACLOUD_LITE_REPLICATION_TLS_CERT_FILE;
12802
+ const tlsKeyFile = options.replicationTlsKeyFile ?? process.env.SUPACLOUD_LITE_REPLICATION_TLS_KEY_FILE;
12803
+ if (Boolean(tlsCertFile) !== Boolean(tlsKeyFile)) {
12804
+ throw new Error("PowerSync replication TLS requires both certificate and key files");
12805
+ }
12806
+ return validatePowerSyncReplicationOptions({
12807
+ profile,
12808
+ host: options.replicationHost ?? process.env.SUPACLOUD_LITE_REPLICATION_HOST ?? "127.0.0.1",
12809
+ port: options.replicationPort ?? parsePort(process.env.SUPACLOUD_LITE_REPLICATION_PORT, 54322),
12810
+ allowCidrs: options.replicationAllowCidrs ?? commaSeparated(process.env.SUPACLOUD_LITE_REPLICATION_ALLOW_CIDRS, ["127.0.0.1/32", "::1/128"]),
12811
+ publicationTables: options.powersyncPublicationTables ?? commaSeparated(process.env.SUPACLOUD_LITE_POWERSYNC_TABLES),
12812
+ password: options.powersyncPassword ?? process.env.SUPACLOUD_LITE_POWERSYNC_PASSWORD ?? "",
12813
+ tls: tlsCertFile && tlsKeyFile ? { certFile: tlsCertFile, keyFile: tlsKeyFile } : undefined
12814
+ });
12815
+ }
11844
12816
  function resolveDatabaseEngine(value, memory = false) {
11845
12817
  const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
11846
12818
  if (configured !== "pglite" && configured !== "native") {
@@ -11916,7 +12888,7 @@ async function loadWebhooks(projectDir) {
11916
12888
  }
11917
12889
  }
11918
12890
  function resolvePath(projectDir, path) {
11919
- return isAbsolute(path) ? path : resolve2(projectDir, path);
12891
+ return isAbsolute(path) ? path : resolve3(projectDir, path);
11920
12892
  }
11921
12893
  function randomHex(bytes) {
11922
12894
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -11943,6 +12915,9 @@ function parsePort(value, fallback) {
11943
12915
  throw new Error(`invalid port: ${value}`);
11944
12916
  return port;
11945
12917
  }
12918
+ function commaSeparated(value, fallback = []) {
12919
+ return value === undefined ? fallback : value.split(",").map((entry) => entry.trim()).filter(Boolean);
12920
+ }
11946
12921
  function displayHost(host) {
11947
12922
  if (host === "0.0.0.0" || host === "::")
11948
12923
  return "127.0.0.1";
@@ -11958,7 +12933,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
11958
12933
  }
11959
12934
  // src/snapshot.ts
11960
12935
  import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
11961
- import { dirname as dirname5, join as join8, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
12936
+ import { dirname as dirname5, join as join8, parse as parse2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
11962
12937
  import { create as createTar, extract as extractTar2 } from "tar";
11963
12938
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
11964
12939
  var SNAPSHOT_VERSION = 1;
@@ -11982,7 +12957,7 @@ async function createSnapshot(options) {
11982
12957
  postgresMajor: await readPostgresMajor(paths.dataDir)
11983
12958
  } : {}
11984
12959
  };
11985
- const output = resolve3(options.output);
12960
+ const output = resolve4(options.output);
11986
12961
  if (await existingInfo(output))
11987
12962
  throw new Error(`snapshot output already exists: ${output}`);
11988
12963
  await mkdir5(dirname5(output), { recursive: true });
@@ -12023,7 +12998,7 @@ async function restoreSnapshot(options) {
12023
12998
  await mkdir5(payloadRoot, { recursive: true });
12024
12999
  await extractTar2({
12025
13000
  cwd: payloadRoot,
12026
- file: resolve3(options.input),
13001
+ file: resolve4(options.input),
12027
13002
  preserveOwner: false,
12028
13003
  preservePaths: false,
12029
13004
  strict: true,
@@ -12100,11 +13075,11 @@ async function restoreSnapshot(options) {
12100
13075
  function normalizePaths(paths) {
12101
13076
  return {
12102
13077
  ...paths,
12103
- projectDir: resolve3(paths.projectDir),
12104
- stateDir: resolve3(paths.stateDir),
12105
- dataDir: paths.dataDir ? resolve3(paths.dataDir) : undefined,
12106
- storageDir: resolve3(paths.storageDir),
12107
- secretsFile: resolve3(paths.secretsFile)
13078
+ projectDir: resolve4(paths.projectDir),
13079
+ stateDir: resolve4(paths.stateDir),
13080
+ dataDir: paths.dataDir ? resolve4(paths.dataDir) : undefined,
13081
+ storageDir: resolve4(paths.storageDir),
13082
+ secretsFile: resolve4(paths.secretsFile)
12108
13083
  };
12109
13084
  }
12110
13085
  async function assertSnapshotPaths(paths, options = {}) {
@@ -12134,7 +13109,7 @@ async function assertSnapshotPaths(paths, options = {}) {
12134
13109
  async function assertDirectoryOrMissing(path) {
12135
13110
  if (!path)
12136
13111
  return;
12137
- if (resolve3(path) === parse2(resolve3(path)).root)
13112
+ if (resolve4(path) === parse2(resolve4(path)).root)
12138
13113
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
12139
13114
  try {
12140
13115
  const info = await lstat2(path);
@@ -12356,38 +13331,47 @@ async function assertNoSymlinks(root) {
12356
13331
  }
12357
13332
  }
12358
13333
  function isWithin(parent, child) {
12359
- const normalizedParent = resolve3(parent);
12360
- const normalizedChild = resolve3(child);
13334
+ const normalizedParent = resolve4(parent);
13335
+ const normalizedChild = resolve4(child);
12361
13336
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
12362
13337
  }
12363
13338
  function pathsOverlap(left, right) {
12364
- const normalizedLeft = resolve3(left);
12365
- const normalizedRight = resolve3(right);
13339
+ const normalizedLeft = resolve4(left);
13340
+ const normalizedRight = resolve4(right);
12366
13341
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
12367
13342
  }
12368
13343
  export {
12369
- verifyJwt,
12370
- startProjectServer,
12371
- signJwt,
12372
- serveBun,
12373
- restoreSnapshot,
12374
- resolveStorageBackend,
12375
- resolveProjectPaths,
12376
- resolveDatabaseEngine,
12377
- mintProjectKeys,
12378
- isNativeEngineSupported,
12379
- inspectDb,
12380
- generateTypes,
12381
- ensureProjectSecrets,
12382
- ensurePostgres,
12383
- decodeJwt,
12384
- createSnapshot,
12385
- createProjectBackend,
12386
- createPgliteEngine,
12387
- createNativeEngine,
12388
- createBackend as createLiteBackend,
12389
- SUPACLOUD_LITE_VERSION,
12390
- S3StorageDriver,
13344
+ FsStorageDriver,
12391
13345
  MemoryStorageDriver,
12392
- FsStorageDriver
13346
+ POWERSYNC_PUBLICATION,
13347
+ POWERSYNC_REPLICATION_ROLE,
13348
+ S3StorageDriver,
13349
+ SUPACLOUD_LITE_VERSION,
13350
+ buildPowerSyncPostgresArgs,
13351
+ createBackend as createLiteBackend,
13352
+ createNativeEngine,
13353
+ createPgliteEngine,
13354
+ createProjectBackend,
13355
+ createSnapshot,
13356
+ decodeJwt,
13357
+ ensurePostgres,
13358
+ ensurePowerSyncReplicationCatalog,
13359
+ ensureProjectSecrets,
13360
+ generateTypes,
13361
+ inspectDb,
13362
+ inspectPowerSyncReadiness,
13363
+ isNativeEngineSupported,
13364
+ liteCapabilities,
13365
+ mintProjectKeys,
13366
+ resolveDatabaseEngine,
13367
+ resolveNativeReplicationOptions,
13368
+ resolveProjectPaths,
13369
+ resolveStorageBackend,
13370
+ restoreSnapshot,
13371
+ serveBun,
13372
+ signJwt,
13373
+ startProjectServer,
13374
+ validatePowerSyncReplicationOptions,
13375
+ verifyJwt,
13376
+ writePowerSyncHba
12393
13377
  };