@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/CHANGELOG.md +14 -0
- package/README.md +82 -0
- package/dist/cli.js +1107 -60
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1027 -44
- package/dist/project-runtime.d.ts +12 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/auth/password.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;
|
|
@@ -7838,8 +8284,7 @@ class ImageTransformCache {
|
|
|
7838
8284
|
this.store(key, transformResult);
|
|
7839
8285
|
return transformResult;
|
|
7840
8286
|
} finally {
|
|
7841
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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((
|
|
11147
|
+
return new Promise((resolve3) => {
|
|
10455
11148
|
if (this.closed)
|
|
10456
|
-
return
|
|
11149
|
+
return resolve3();
|
|
10457
11150
|
this.socket.write(message(88, Buffer.alloc(0)));
|
|
10458
|
-
this.socket.end(() =>
|
|
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) =>
|
|
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
|
|
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((
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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((
|
|
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((
|
|
11778
|
+
await new Promise((resolve3) => {
|
|
11071
11779
|
const killTimeout = setTimeout(() => {
|
|
11072
11780
|
postgres.kill("SIGKILL");
|
|
11073
|
-
|
|
11781
|
+
resolve3();
|
|
11074
11782
|
}, 5000);
|
|
11075
11783
|
postgres.once("exit", () => {
|
|
11076
11784
|
clearTimeout(killTimeout);
|
|
11077
|
-
|
|
11785
|
+
resolve3();
|
|
11078
11786
|
});
|
|
11079
11787
|
});
|
|
11080
11788
|
}
|
|
11081
11789
|
function removeStalePidFile(pidPath) {
|
|
11082
|
-
if (!
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 :
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
12105
|
-
stateDir:
|
|
12106
|
-
dataDir: paths.dataDir ?
|
|
12107
|
-
storageDir:
|
|
12108
|
-
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 (
|
|
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 =
|
|
12361
|
-
const normalizedChild =
|
|
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 =
|
|
12366
|
-
const normalizedRight =
|
|
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
|
};
|