@supacloud/lite 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.4",
88
+ version: "0.9.0",
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",
@@ -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;
@@ -7838,8 +8284,7 @@ class ImageTransformCache {
7838
8284
  this.store(key, transformResult);
7839
8285
  return transformResult;
7840
8286
  } finally {
7841
- if (this.inFlight.get(key) === transform)
7842
- this.inFlight.delete(key);
8287
+ this.inFlight.delete(key);
7843
8288
  }
7844
8289
  }
7845
8290
  store(key, transform) {
@@ -10273,12 +10718,260 @@ async function serveBun(backend, opts = {}) {
10273
10718
  // src/runtime/node/native/engine.ts
10274
10719
  import { execFileSync, spawn, spawnSync } from "child_process";
10275
10720
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
10276
- 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";
10277
10722
  import { writeFile as writeFile2 } from "fs/promises";
10278
10723
  import { homedir, tmpdir } from "os";
10279
10724
  import { join as join2 } from "path";
10280
10725
  import { extract as extractTar } from "tar";
10281
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
+
10282
10975
  // src/runtime/node/native/wire.ts
10283
10976
  import { createConnection } from "net";
10284
10977
  import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
@@ -10310,7 +11003,7 @@ class PgWireClient {
10310
11003
  return client;
10311
11004
  }
10312
11005
  open(opts) {
10313
- return new Promise((resolve2, reject) => {
11006
+ return new Promise((resolve3, reject) => {
10314
11007
  this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
10315
11008
  this.socket.on("error", (e) => {
10316
11009
  if (this.pending)
@@ -10408,7 +11101,7 @@ class PgWireClient {
10408
11101
  this.buffer = Buffer.concat([this.buffer, c]);
10409
11102
  this.processMessages();
10410
11103
  });
10411
- resolve2();
11104
+ resolve3();
10412
11105
  return;
10413
11106
  }
10414
11107
  }
@@ -10417,10 +11110,10 @@ class PgWireClient {
10417
11110
  });
10418
11111
  }
10419
11112
  run(send) {
10420
- const op = this.queue.then(() => new Promise((resolve2, reject) => {
11113
+ const op = this.queue.then(() => new Promise((resolve3, reject) => {
10421
11114
  if (this.closed)
10422
11115
  return reject(new Error("connection closed"));
10423
- this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
11116
+ this.pending = { resolve: resolve3, reject, results: [], columns: [], error: null };
10424
11117
  send();
10425
11118
  }));
10426
11119
  this.queue = op.catch(() => {});
@@ -10451,11 +11144,11 @@ class PgWireClient {
10451
11144
  return results[0] ?? { rows: [] };
10452
11145
  }
10453
11146
  close() {
10454
- return new Promise((resolve2) => {
11147
+ return new Promise((resolve3) => {
10455
11148
  if (this.closed)
10456
- return resolve2();
11149
+ return resolve3();
10457
11150
  this.socket.write(message(88, Buffer.alloc(0)));
10458
- this.socket.end(() => resolve2());
11151
+ this.socket.end(() => resolve3());
10459
11152
  });
10460
11153
  }
10461
11154
  nextMessage() {
@@ -10844,7 +11537,7 @@ function reportedGlibcVersion() {
10844
11537
  }
10845
11538
  function glibcDynamicLoaderPresent() {
10846
11539
  const loaderPaths = process.arch === "x64" ? GLIBC_DYNAMIC_LOADERS.x64 : process.arch === "arm64" ? GLIBC_DYNAMIC_LOADERS.arm64 : [];
10847
- return loaderPaths.some((loaderPath) => existsSync(loaderPath));
11540
+ return loaderPaths.some((loaderPath) => existsSync2(loaderPath));
10848
11541
  }
10849
11542
  function lddVersion() {
10850
11543
  const command = spawnSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -10864,7 +11557,7 @@ function target() {
10864
11557
  throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
10865
11558
  }
10866
11559
  function isCompleteInstall(dir) {
10867
- 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"));
10868
11561
  }
10869
11562
  var PINNED_SHA256 = {
10870
11563
  "postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
@@ -10970,7 +11663,7 @@ async function fetchRelease(url) {
10970
11663
  lastError = error;
10971
11664
  }
10972
11665
  if (attempt < 3)
10973
- await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
11666
+ await new Promise((resolve3) => setTimeout(resolve3, attempt * 500));
10974
11667
  }
10975
11668
  throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
10976
11669
  }
@@ -10982,6 +11675,7 @@ dynamic_shared_memory_type = posix
10982
11675
  max_connections = 10
10983
11676
  wal_level = minimal
10984
11677
  max_wal_senders = 0
11678
+ max_replication_slots = 0
10985
11679
  logging_collector = off
10986
11680
  `;
10987
11681
  async function createNativeEngine(opts) {
@@ -10992,7 +11686,7 @@ async function createNativeEngine(opts) {
10992
11686
  try {
10993
11687
  const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
10994
11688
  const bin = (name) => join2(installDir, "bin", name);
10995
- if (!existsSync(join2(opts.dataDir, "PG_VERSION"))) {
11689
+ if (!existsSync2(join2(opts.dataDir, "PG_VERSION"))) {
10996
11690
  mkdirSync(opts.dataDir, { recursive: true });
10997
11691
  try {
10998
11692
  execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
@@ -11008,7 +11702,20 @@ ${stderr || error.message}`);
11008
11702
  removeStalePidFile(join2(opts.dataDir, "postmaster.pid"));
11009
11703
  socketDirectory = mkdtempSync(join2(tmpdir(), "scl-"));
11010
11704
  chmodSync(socketDirectory, 448);
11011
- 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], {
11012
11719
  stdio: ["ignore", "ignore", "pipe"],
11013
11720
  detached: false
11014
11721
  });
@@ -11024,7 +11731,8 @@ ${stderr || error.message}`);
11024
11731
  };
11025
11732
  process.once("exit", killPostgres);
11026
11733
  removeExitHandler = () => process.off("exit", killPostgres);
11027
- const socketPath = join2(socketDirectory, ".s.PGSQL.5432");
11734
+ const postgresPort = opts.replication?.port ?? 5432;
11735
+ const socketPath = join2(socketDirectory, `.s.PGSQL.${postgresPort}`);
11028
11736
  const connect = async () => {
11029
11737
  const deadline = Date.now() + 20000;
11030
11738
  while (Date.now() <= deadline) {
@@ -11039,7 +11747,7 @@ ${detail}` : " (no output)"}
11039
11747
  ` + `data dir: ${opts.dataDir}
11040
11748
  ` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
11041
11749
  }
11042
- await new Promise((resolve2) => setTimeout(resolve2, 150));
11750
+ await new Promise((resolve3) => setTimeout(resolve3, 150));
11043
11751
  }
11044
11752
  }
11045
11753
  throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
@@ -11067,19 +11775,19 @@ async function stopPostgres(postgres, hasExited) {
11067
11775
  if (hasExited())
11068
11776
  return;
11069
11777
  postgres.kill("SIGINT");
11070
- await new Promise((resolve2) => {
11778
+ await new Promise((resolve3) => {
11071
11779
  const killTimeout = setTimeout(() => {
11072
11780
  postgres.kill("SIGKILL");
11073
- resolve2();
11781
+ resolve3();
11074
11782
  }, 5000);
11075
11783
  postgres.once("exit", () => {
11076
11784
  clearTimeout(killTimeout);
11077
- resolve2();
11785
+ resolve3();
11078
11786
  });
11079
11787
  });
11080
11788
  }
11081
11789
  function removeStalePidFile(pidPath) {
11082
- if (!existsSync(pidPath))
11790
+ if (!existsSync2(pidPath))
11083
11791
  return;
11084
11792
  try {
11085
11793
  const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
@@ -11095,9 +11803,238 @@ function removeStalePidFile(pidPath) {
11095
11803
  }
11096
11804
  } catch {}
11097
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
+ }
11098
12035
  // src/project-runtime.ts
11099
12036
  import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
11100
- 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";
11101
12038
 
11102
12039
  // src/runtime/node/config-toml.ts
11103
12040
  import { readFileSync as readFileSync2 } from "fs";
@@ -11480,7 +12417,7 @@ import { pathToFileURL } from "url";
11480
12417
  // src/runtime/node/bundle-function.ts
11481
12418
  import { createHash as createHash3 } from "crypto";
11482
12419
  import { mkdir as mkdir3, readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
11483
- import { existsSync as existsSync2 } from "fs";
12420
+ import { existsSync as existsSync3 } from "fs";
11484
12421
  import { tmpdir as tmpdir2 } from "os";
11485
12422
  import { join as join4 } from "path";
11486
12423
  function rewriteRemoteSpecifier(spec) {
@@ -11494,7 +12431,7 @@ var HTTP_CACHE = join4(tmpdir2(), "supacloud-lite-fn-http");
11494
12431
  async function fetchModule(url) {
11495
12432
  const key = createHash3("sha256").update(url).digest("hex");
11496
12433
  const cached = join4(HTTP_CACHE, key);
11497
- if (existsSync2(cached))
12434
+ if (existsSync3(cached))
11498
12435
  return readFile3(cached, "utf8");
11499
12436
  const res = await fetch(url, { redirect: "follow" });
11500
12437
  if (!res.ok)
@@ -11701,7 +12638,7 @@ function isNotFound(error) {
11701
12638
 
11702
12639
  // src/project-runtime.ts
11703
12640
  function resolveProjectPaths(options = {}) {
11704
- const projectDir = resolve2(options.projectDir ?? process.cwd());
12641
+ const projectDir = resolve3(options.projectDir ?? process.cwd());
11705
12642
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
11706
12643
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
11707
12644
  const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join7(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -11777,6 +12714,7 @@ async function createProjectBackend(options = {}) {
11777
12714
  const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
11778
12715
  const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
11779
12716
  const databaseEngine = paths.databaseEngine;
12717
+ const replication = resolveNativeReplicationOptions(options, databaseEngine);
11780
12718
  if (paths.dataDir) {
11781
12719
  await mkdir4(paths.dataDir, { recursive: true, mode: 448 });
11782
12720
  await chmod(paths.dataDir, 448);
@@ -11784,7 +12722,7 @@ async function createProjectBackend(options = {}) {
11784
12722
  await mkdir4(paths.storageDir, { recursive: true, mode: 448 });
11785
12723
  await chmod(paths.storageDir, 448);
11786
12724
  const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
11787
- 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;
11788
12726
  let backend;
11789
12727
  try {
11790
12728
  backend = await createBackend({
@@ -11818,8 +12756,17 @@ async function createProjectBackend(options = {}) {
11818
12756
  storageDriver,
11819
12757
  log: options.log
11820
12758
  });
12759
+ if (replication && options.applyMigrations !== false) {
12760
+ await ensurePowerSyncReplicationCatalog(backend.db.engine, replication);
12761
+ }
11821
12762
  } catch (error) {
11822
- 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) {
11823
12770
  try {
11824
12771
  await engine.close();
11825
12772
  } catch (cleanupError) {
@@ -11839,9 +12786,33 @@ async function createProjectBackend(options = {}) {
11839
12786
  functionNames: [...functions.keys()],
11840
12787
  webhookCount: webhooks.length,
11841
12788
  storageBackend,
11842
- databaseEngine
12789
+ databaseEngine,
12790
+ replicationProfile: replication?.profile
11843
12791
  };
11844
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
+ }
11845
12816
  function resolveDatabaseEngine(value, memory = false) {
11846
12817
  const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
11847
12818
  if (configured !== "pglite" && configured !== "native") {
@@ -11917,7 +12888,7 @@ async function loadWebhooks(projectDir) {
11917
12888
  }
11918
12889
  }
11919
12890
  function resolvePath(projectDir, path) {
11920
- return isAbsolute(path) ? path : resolve2(projectDir, path);
12891
+ return isAbsolute(path) ? path : resolve3(projectDir, path);
11921
12892
  }
11922
12893
  function randomHex(bytes) {
11923
12894
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -11944,6 +12915,9 @@ function parsePort(value, fallback) {
11944
12915
  throw new Error(`invalid port: ${value}`);
11945
12916
  return port;
11946
12917
  }
12918
+ function commaSeparated(value, fallback = []) {
12919
+ return value === undefined ? fallback : value.split(",").map((entry) => entry.trim()).filter(Boolean);
12920
+ }
11947
12921
  function displayHost(host) {
11948
12922
  if (host === "0.0.0.0" || host === "::")
11949
12923
  return "127.0.0.1";
@@ -11959,7 +12933,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
11959
12933
  }
11960
12934
  // src/snapshot.ts
11961
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";
11962
- 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";
11963
12937
  import { create as createTar, extract as extractTar2 } from "tar";
11964
12938
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
11965
12939
  var SNAPSHOT_VERSION = 1;
@@ -11983,7 +12957,7 @@ async function createSnapshot(options) {
11983
12957
  postgresMajor: await readPostgresMajor(paths.dataDir)
11984
12958
  } : {}
11985
12959
  };
11986
- const output = resolve3(options.output);
12960
+ const output = resolve4(options.output);
11987
12961
  if (await existingInfo(output))
11988
12962
  throw new Error(`snapshot output already exists: ${output}`);
11989
12963
  await mkdir5(dirname5(output), { recursive: true });
@@ -12024,7 +12998,7 @@ async function restoreSnapshot(options) {
12024
12998
  await mkdir5(payloadRoot, { recursive: true });
12025
12999
  await extractTar2({
12026
13000
  cwd: payloadRoot,
12027
- file: resolve3(options.input),
13001
+ file: resolve4(options.input),
12028
13002
  preserveOwner: false,
12029
13003
  preservePaths: false,
12030
13004
  strict: true,
@@ -12101,11 +13075,11 @@ async function restoreSnapshot(options) {
12101
13075
  function normalizePaths(paths) {
12102
13076
  return {
12103
13077
  ...paths,
12104
- projectDir: resolve3(paths.projectDir),
12105
- stateDir: resolve3(paths.stateDir),
12106
- dataDir: paths.dataDir ? resolve3(paths.dataDir) : undefined,
12107
- storageDir: resolve3(paths.storageDir),
12108
- 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)
12109
13083
  };
12110
13084
  }
12111
13085
  async function assertSnapshotPaths(paths, options = {}) {
@@ -12135,7 +13109,7 @@ async function assertSnapshotPaths(paths, options = {}) {
12135
13109
  async function assertDirectoryOrMissing(path) {
12136
13110
  if (!path)
12137
13111
  return;
12138
- if (resolve3(path) === parse2(resolve3(path)).root)
13112
+ if (resolve4(path) === parse2(resolve4(path)).root)
12139
13113
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
12140
13114
  try {
12141
13115
  const info = await lstat2(path);
@@ -12357,29 +13331,35 @@ async function assertNoSymlinks(root) {
12357
13331
  }
12358
13332
  }
12359
13333
  function isWithin(parent, child) {
12360
- const normalizedParent = resolve3(parent);
12361
- const normalizedChild = resolve3(child);
13334
+ const normalizedParent = resolve4(parent);
13335
+ const normalizedChild = resolve4(child);
12362
13336
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
12363
13337
  }
12364
13338
  function pathsOverlap(left, right) {
12365
- const normalizedLeft = resolve3(left);
12366
- const normalizedRight = resolve3(right);
13339
+ const normalizedLeft = resolve4(left);
13340
+ const normalizedRight = resolve4(right);
12367
13341
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
12368
13342
  }
12369
13343
  export {
13344
+ writePowerSyncHba,
12370
13345
  verifyJwt,
13346
+ validatePowerSyncReplicationOptions,
12371
13347
  startProjectServer,
12372
13348
  signJwt,
12373
13349
  serveBun,
12374
13350
  restoreSnapshot,
12375
13351
  resolveStorageBackend,
12376
13352
  resolveProjectPaths,
13353
+ resolveNativeReplicationOptions,
12377
13354
  resolveDatabaseEngine,
12378
13355
  mintProjectKeys,
13356
+ liteCapabilities,
12379
13357
  isNativeEngineSupported,
13358
+ inspectPowerSyncReadiness,
12380
13359
  inspectDb,
12381
13360
  generateTypes,
12382
13361
  ensureProjectSecrets,
13362
+ ensurePowerSyncReplicationCatalog,
12383
13363
  ensurePostgres,
12384
13364
  decodeJwt,
12385
13365
  createSnapshot,
@@ -12387,8 +13367,11 @@ export {
12387
13367
  createPgliteEngine,
12388
13368
  createNativeEngine,
12389
13369
  createBackend as createLiteBackend,
13370
+ buildPowerSyncPostgresArgs,
12390
13371
  SUPACLOUD_LITE_VERSION,
12391
13372
  S3StorageDriver,
13373
+ POWERSYNC_REPLICATION_ROLE,
13374
+ POWERSYNC_PUBLICATION,
12392
13375
  MemoryStorageDriver,
12393
13376
  FsStorageDriver
12394
13377
  };