@supacloud/lite 0.8.5 → 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/CHANGELOG.md +7 -0
- package/README.md +82 -0
- package/dist/cli.js +1106 -58
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1026 -42
- package/dist/project-runtime.d.ts +12 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/db/emulated.d.ts +1 -1
- package/dist/runtime/db/emulated.d.ts.map +1 -1
- package/dist/runtime/node/native/engine.d.ts +3 -0
- package/dist/runtime/node/native/engine.d.ts.map +1 -1
- package/dist/runtime/node/native/readiness.d.ts +65 -0
- package/dist/runtime/node/native/readiness.d.ts.map +1 -0
- package/dist/runtime/node/native/replication.d.ts +22 -0
- package/dist/runtime/node/native/replication.d.ts.map +1 -0
- package/package.json +2 -2
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.
|
|
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;
|
|
@@ -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((
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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((
|
|
11147
|
+
return new Promise((resolve3) => {
|
|
10454
11148
|
if (this.closed)
|
|
10455
|
-
return
|
|
11149
|
+
return resolve3();
|
|
10456
11150
|
this.socket.write(message(88, Buffer.alloc(0)));
|
|
10457
|
-
this.socket.end(() =>
|
|
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) =>
|
|
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
|
|
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((
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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((
|
|
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((
|
|
11778
|
+
await new Promise((resolve3) => {
|
|
11070
11779
|
const killTimeout = setTimeout(() => {
|
|
11071
11780
|
postgres.kill("SIGKILL");
|
|
11072
|
-
|
|
11781
|
+
resolve3();
|
|
11073
11782
|
}, 5000);
|
|
11074
11783
|
postgres.once("exit", () => {
|
|
11075
11784
|
clearTimeout(killTimeout);
|
|
11076
|
-
|
|
11785
|
+
resolve3();
|
|
11077
11786
|
});
|
|
11078
11787
|
});
|
|
11079
11788
|
}
|
|
11080
11789
|
function removeStalePidFile(pidPath) {
|
|
11081
|
-
if (!
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 :
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
12104
|
-
stateDir:
|
|
12105
|
-
dataDir: paths.dataDir ?
|
|
12106
|
-
storageDir:
|
|
12107
|
-
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 (
|
|
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,29 +13331,35 @@ async function assertNoSymlinks(root) {
|
|
|
12356
13331
|
}
|
|
12357
13332
|
}
|
|
12358
13333
|
function isWithin(parent, child) {
|
|
12359
|
-
const normalizedParent =
|
|
12360
|
-
const normalizedChild =
|
|
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 =
|
|
12365
|
-
const normalizedRight =
|
|
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 {
|
|
13344
|
+
writePowerSyncHba,
|
|
12369
13345
|
verifyJwt,
|
|
13346
|
+
validatePowerSyncReplicationOptions,
|
|
12370
13347
|
startProjectServer,
|
|
12371
13348
|
signJwt,
|
|
12372
13349
|
serveBun,
|
|
12373
13350
|
restoreSnapshot,
|
|
12374
13351
|
resolveStorageBackend,
|
|
12375
13352
|
resolveProjectPaths,
|
|
13353
|
+
resolveNativeReplicationOptions,
|
|
12376
13354
|
resolveDatabaseEngine,
|
|
12377
13355
|
mintProjectKeys,
|
|
13356
|
+
liteCapabilities,
|
|
12378
13357
|
isNativeEngineSupported,
|
|
13358
|
+
inspectPowerSyncReadiness,
|
|
12379
13359
|
inspectDb,
|
|
12380
13360
|
generateTypes,
|
|
12381
13361
|
ensureProjectSecrets,
|
|
13362
|
+
ensurePowerSyncReplicationCatalog,
|
|
12382
13363
|
ensurePostgres,
|
|
12383
13364
|
decodeJwt,
|
|
12384
13365
|
createSnapshot,
|
|
@@ -12386,8 +13367,11 @@ export {
|
|
|
12386
13367
|
createPgliteEngine,
|
|
12387
13368
|
createNativeEngine,
|
|
12388
13369
|
createBackend as createLiteBackend,
|
|
13370
|
+
buildPowerSyncPostgresArgs,
|
|
12389
13371
|
SUPACLOUD_LITE_VERSION,
|
|
12390
13372
|
S3StorageDriver,
|
|
13373
|
+
POWERSYNC_REPLICATION_ROLE,
|
|
13374
|
+
POWERSYNC_PUBLICATION,
|
|
12391
13375
|
MemoryStorageDriver,
|
|
12392
13376
|
FsStorageDriver
|
|
12393
13377
|
};
|