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