@supacloud/lite 0.8.5 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSES/{BUN-1.3.14-RUNTIME-NOTICES.txt → BUN-1.4.0-RUNTIME-NOTICES.txt} +22 -10
- package/README.md +82 -0
- package/THIRD_PARTY_NOTICES.md +6 -6
- package/dist/cli.js +1108 -64
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1051 -67
- package/dist/launcher.cjs +1 -1
- package/dist/project-runtime.d.ts +12 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/db/emulated.d.ts +1 -1
- package/dist/runtime/db/emulated.d.ts.map +1 -1
- package/dist/runtime/node/native/engine.d.ts +3 -0
- package/dist/runtime/node/native/engine.d.ts.map +1 -1
- package/dist/runtime/node/native/readiness.d.ts +65 -0
- package/dist/runtime/node/native/readiness.d.ts.map +1 -0
- package/dist/runtime/node/native/replication.d.ts +22 -0
- package/dist/runtime/node/native/replication.d.ts.map +1 -0
- package/package.json +4 -4
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.1",
|
|
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",
|
|
@@ -62,11 +63,11 @@ var package_default = {
|
|
|
62
63
|
},
|
|
63
64
|
devDependencies: {
|
|
64
65
|
"@supabase/supabase-js": "^2.112.2",
|
|
65
|
-
"@types/bun": "^1.
|
|
66
|
+
"@types/bun": "^1.4.0",
|
|
66
67
|
typescript: "^7.0.2"
|
|
67
68
|
},
|
|
68
69
|
engines: {
|
|
69
|
-
bun: ">=1.
|
|
70
|
+
bun: ">=1.4.0"
|
|
70
71
|
},
|
|
71
72
|
publishConfig: {
|
|
72
73
|
access: "public",
|
|
@@ -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;
|
|
@@ -10259,12 +10706,260 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
10259
10706
|
// src/runtime/node/native/engine.ts
|
|
10260
10707
|
import { execFileSync, spawn, spawnSync } from "child_process";
|
|
10261
10708
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
10262
|
-
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";
|
|
10263
10710
|
import { writeFile } from "fs/promises";
|
|
10264
10711
|
import { homedir, tmpdir } from "os";
|
|
10265
10712
|
import { join } from "path";
|
|
10266
10713
|
import { extract as extractTar } from "tar";
|
|
10267
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
|
+
|
|
10268
10963
|
// src/runtime/node/native/wire.ts
|
|
10269
10964
|
import { createConnection } from "net";
|
|
10270
10965
|
import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
|
|
@@ -10296,7 +10991,7 @@ class PgWireClient {
|
|
|
10296
10991
|
return client;
|
|
10297
10992
|
}
|
|
10298
10993
|
open(opts) {
|
|
10299
|
-
return new Promise((
|
|
10994
|
+
return new Promise((resolve3, reject) => {
|
|
10300
10995
|
this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
|
|
10301
10996
|
this.socket.on("error", (e) => {
|
|
10302
10997
|
if (this.pending)
|
|
@@ -10394,7 +11089,7 @@ class PgWireClient {
|
|
|
10394
11089
|
this.buffer = Buffer.concat([this.buffer, c]);
|
|
10395
11090
|
this.processMessages();
|
|
10396
11091
|
});
|
|
10397
|
-
|
|
11092
|
+
resolve3();
|
|
10398
11093
|
return;
|
|
10399
11094
|
}
|
|
10400
11095
|
}
|
|
@@ -10403,10 +11098,10 @@ class PgWireClient {
|
|
|
10403
11098
|
});
|
|
10404
11099
|
}
|
|
10405
11100
|
run(send) {
|
|
10406
|
-
const op = this.queue.then(() => new Promise((
|
|
11101
|
+
const op = this.queue.then(() => new Promise((resolve3, reject) => {
|
|
10407
11102
|
if (this.closed)
|
|
10408
11103
|
return reject(new Error("connection closed"));
|
|
10409
|
-
this.pending = { resolve:
|
|
11104
|
+
this.pending = { resolve: resolve3, reject, results: [], columns: [], error: null };
|
|
10410
11105
|
send();
|
|
10411
11106
|
}));
|
|
10412
11107
|
this.queue = op.catch(() => {});
|
|
@@ -10437,11 +11132,11 @@ class PgWireClient {
|
|
|
10437
11132
|
return results[0] ?? { rows: [] };
|
|
10438
11133
|
}
|
|
10439
11134
|
close() {
|
|
10440
|
-
return new Promise((
|
|
11135
|
+
return new Promise((resolve3) => {
|
|
10441
11136
|
if (this.closed)
|
|
10442
|
-
return
|
|
11137
|
+
return resolve3();
|
|
10443
11138
|
this.socket.write(message(88, Buffer.alloc(0)));
|
|
10444
|
-
this.socket.end(() =>
|
|
11139
|
+
this.socket.end(() => resolve3());
|
|
10445
11140
|
});
|
|
10446
11141
|
}
|
|
10447
11142
|
nextMessage() {
|
|
@@ -10830,7 +11525,7 @@ function reportedGlibcVersion() {
|
|
|
10830
11525
|
}
|
|
10831
11526
|
function glibcDynamicLoaderPresent() {
|
|
10832
11527
|
const loaderPaths = process.arch === "x64" ? GLIBC_DYNAMIC_LOADERS.x64 : process.arch === "arm64" ? GLIBC_DYNAMIC_LOADERS.arm64 : [];
|
|
10833
|
-
return loaderPaths.some((loaderPath) =>
|
|
11528
|
+
return loaderPaths.some((loaderPath) => existsSync2(loaderPath));
|
|
10834
11529
|
}
|
|
10835
11530
|
function lddVersion() {
|
|
10836
11531
|
const command = spawnSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -10850,7 +11545,7 @@ function target() {
|
|
|
10850
11545
|
throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
|
|
10851
11546
|
}
|
|
10852
11547
|
function isCompleteInstall(dir) {
|
|
10853
|
-
return
|
|
11548
|
+
return existsSync2(join(dir, "bin", "postgres")) && existsSync2(join(dir, "share", "postgres.bki"));
|
|
10854
11549
|
}
|
|
10855
11550
|
var PINNED_SHA256 = {
|
|
10856
11551
|
"postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
|
|
@@ -10956,7 +11651,7 @@ async function fetchRelease(url) {
|
|
|
10956
11651
|
lastError = error;
|
|
10957
11652
|
}
|
|
10958
11653
|
if (attempt < 3)
|
|
10959
|
-
await new Promise((
|
|
11654
|
+
await new Promise((resolve3) => setTimeout(resolve3, attempt * 500));
|
|
10960
11655
|
}
|
|
10961
11656
|
throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
|
|
10962
11657
|
}
|
|
@@ -10968,6 +11663,7 @@ dynamic_shared_memory_type = posix
|
|
|
10968
11663
|
max_connections = 10
|
|
10969
11664
|
wal_level = minimal
|
|
10970
11665
|
max_wal_senders = 0
|
|
11666
|
+
max_replication_slots = 0
|
|
10971
11667
|
logging_collector = off
|
|
10972
11668
|
`;
|
|
10973
11669
|
async function createNativeEngine(opts) {
|
|
@@ -10978,7 +11674,7 @@ async function createNativeEngine(opts) {
|
|
|
10978
11674
|
try {
|
|
10979
11675
|
const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
|
|
10980
11676
|
const bin = (name) => join(installDir, "bin", name);
|
|
10981
|
-
if (!
|
|
11677
|
+
if (!existsSync2(join(opts.dataDir, "PG_VERSION"))) {
|
|
10982
11678
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
10983
11679
|
try {
|
|
10984
11680
|
execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
|
|
@@ -10994,7 +11690,20 @@ ${stderr || error.message}`);
|
|
|
10994
11690
|
removeStalePidFile(join(opts.dataDir, "postmaster.pid"));
|
|
10995
11691
|
socketDirectory = mkdtempSync(join(tmpdir(), "scl-"));
|
|
10996
11692
|
chmodSync(socketDirectory, 448);
|
|
10997
|
-
|
|
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], {
|
|
10998
11707
|
stdio: ["ignore", "ignore", "pipe"],
|
|
10999
11708
|
detached: false
|
|
11000
11709
|
});
|
|
@@ -11010,7 +11719,8 @@ ${stderr || error.message}`);
|
|
|
11010
11719
|
};
|
|
11011
11720
|
process.once("exit", killPostgres);
|
|
11012
11721
|
removeExitHandler = () => process.off("exit", killPostgres);
|
|
11013
|
-
const
|
|
11722
|
+
const postgresPort = opts.replication?.port ?? 5432;
|
|
11723
|
+
const socketPath = join(socketDirectory, `.s.PGSQL.${postgresPort}`);
|
|
11014
11724
|
const connect = async () => {
|
|
11015
11725
|
const deadline = Date.now() + 20000;
|
|
11016
11726
|
while (Date.now() <= deadline) {
|
|
@@ -11025,7 +11735,7 @@ ${detail}` : " (no output)"}
|
|
|
11025
11735
|
` + `data dir: ${opts.dataDir}
|
|
11026
11736
|
` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
|
|
11027
11737
|
}
|
|
11028
|
-
await new Promise((
|
|
11738
|
+
await new Promise((resolve3) => setTimeout(resolve3, 150));
|
|
11029
11739
|
}
|
|
11030
11740
|
}
|
|
11031
11741
|
throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
|
|
@@ -11053,19 +11763,19 @@ async function stopPostgres(postgres, hasExited) {
|
|
|
11053
11763
|
if (hasExited())
|
|
11054
11764
|
return;
|
|
11055
11765
|
postgres.kill("SIGINT");
|
|
11056
|
-
await new Promise((
|
|
11766
|
+
await new Promise((resolve3) => {
|
|
11057
11767
|
const killTimeout = setTimeout(() => {
|
|
11058
11768
|
postgres.kill("SIGKILL");
|
|
11059
|
-
|
|
11769
|
+
resolve3();
|
|
11060
11770
|
}, 5000);
|
|
11061
11771
|
postgres.once("exit", () => {
|
|
11062
11772
|
clearTimeout(killTimeout);
|
|
11063
|
-
|
|
11773
|
+
resolve3();
|
|
11064
11774
|
});
|
|
11065
11775
|
});
|
|
11066
11776
|
}
|
|
11067
11777
|
function removeStalePidFile(pidPath) {
|
|
11068
|
-
if (!
|
|
11778
|
+
if (!existsSync2(pidPath))
|
|
11069
11779
|
return;
|
|
11070
11780
|
try {
|
|
11071
11781
|
const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
|
|
@@ -11202,6 +11912,236 @@ async function closeResources(...resources) {
|
|
|
11202
11912
|
throw failed.reason;
|
|
11203
11913
|
}
|
|
11204
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
|
+
|
|
11205
12145
|
// src/runtime/node/project.ts
|
|
11206
12146
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
11207
12147
|
import { join as join3 } from "path";
|
|
@@ -11249,7 +12189,7 @@ function isNotFound(error) {
|
|
|
11249
12189
|
|
|
11250
12190
|
// src/project-runtime.ts
|
|
11251
12191
|
import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
11252
|
-
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";
|
|
11253
12193
|
|
|
11254
12194
|
// src/runtime/node/bun-server.ts
|
|
11255
12195
|
async function serveBun(backend, opts = {}) {
|
|
@@ -11729,7 +12669,7 @@ import { pathToFileURL } from "url";
|
|
|
11729
12669
|
// src/runtime/node/bundle-function.ts
|
|
11730
12670
|
import { createHash as createHash3 } from "crypto";
|
|
11731
12671
|
import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
11732
|
-
import { existsSync as
|
|
12672
|
+
import { existsSync as existsSync3 } from "fs";
|
|
11733
12673
|
import { tmpdir as tmpdir3 } from "os";
|
|
11734
12674
|
import { join as join6 } from "path";
|
|
11735
12675
|
function rewriteRemoteSpecifier(spec) {
|
|
@@ -11743,7 +12683,7 @@ var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
|
|
|
11743
12683
|
async function fetchModule(url) {
|
|
11744
12684
|
const key = createHash3("sha256").update(url).digest("hex");
|
|
11745
12685
|
const cached = join6(HTTP_CACHE, key);
|
|
11746
|
-
if (
|
|
12686
|
+
if (existsSync3(cached))
|
|
11747
12687
|
return readFile4(cached, "utf8");
|
|
11748
12688
|
const res = await fetch(url, { redirect: "follow" });
|
|
11749
12689
|
if (!res.ok)
|
|
@@ -12001,7 +12941,7 @@ class S3StorageDriver {
|
|
|
12001
12941
|
var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
|
|
12002
12942
|
var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
|
|
12003
12943
|
function resolveProjectPaths(options = {}) {
|
|
12004
|
-
const projectDir =
|
|
12944
|
+
const projectDir = resolve3(options.projectDir ?? process.cwd());
|
|
12005
12945
|
const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
|
|
12006
12946
|
const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
|
|
12007
12947
|
const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
|
|
@@ -12016,7 +12956,7 @@ function resolveProjectPaths(options = {}) {
|
|
|
12016
12956
|
};
|
|
12017
12957
|
}
|
|
12018
12958
|
async function assertResetPathsSafe(paths) {
|
|
12019
|
-
const stateDir =
|
|
12959
|
+
const stateDir = resolve3(paths.stateDir);
|
|
12020
12960
|
if (stateDir === parse(stateDir).root)
|
|
12021
12961
|
throw new Error("refusing to use the filesystem root as the state directory");
|
|
12022
12962
|
const stateInfo = await requiredResetEntry(stateDir);
|
|
@@ -12024,7 +12964,7 @@ async function assertResetPathsSafe(paths) {
|
|
|
12024
12964
|
throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
|
|
12025
12965
|
}
|
|
12026
12966
|
const canonicalStateDir = await realpath2(stateDir);
|
|
12027
|
-
const secretsFile =
|
|
12967
|
+
const secretsFile = resolve3(paths.secretsFile);
|
|
12028
12968
|
if (secretsFile !== join8(stateDir, "secrets.json")) {
|
|
12029
12969
|
throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
|
|
12030
12970
|
}
|
|
@@ -12038,7 +12978,7 @@ async function assertResetPathsSafe(paths) {
|
|
|
12038
12978
|
["storage", paths.storageDir]
|
|
12039
12979
|
];
|
|
12040
12980
|
for (const [label, targetPath] of targets) {
|
|
12041
|
-
const target2 =
|
|
12981
|
+
const target2 = resolve3(targetPath);
|
|
12042
12982
|
const relativePath = relative(stateDir, target2);
|
|
12043
12983
|
if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
12044
12984
|
throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
|
|
@@ -12087,7 +13027,7 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2,
|
|
|
12087
13027
|
current = parent;
|
|
12088
13028
|
}
|
|
12089
13029
|
const existingAncestor = await nearestExistingAncestor(target2);
|
|
12090
|
-
const canonicalTarget =
|
|
13030
|
+
const canonicalTarget = resolve3(await realpath2(existingAncestor), relative(existingAncestor, target2));
|
|
12091
13031
|
const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
|
|
12092
13032
|
if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
|
|
12093
13033
|
throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
|
|
@@ -12171,6 +13111,7 @@ async function createProjectBackend(options = {}) {
|
|
|
12171
13111
|
const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
|
|
12172
13112
|
const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
|
|
12173
13113
|
const databaseEngine = paths.databaseEngine;
|
|
13114
|
+
const replication = resolveNativeReplicationOptions(options, databaseEngine);
|
|
12174
13115
|
if (paths.dataDir) {
|
|
12175
13116
|
await mkdir5(paths.dataDir, { recursive: true, mode: 448 });
|
|
12176
13117
|
await chmod(paths.dataDir, 448);
|
|
@@ -12178,7 +13119,7 @@ async function createProjectBackend(options = {}) {
|
|
|
12178
13119
|
await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
|
|
12179
13120
|
await chmod(paths.storageDir, 448);
|
|
12180
13121
|
const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
|
|
12181
|
-
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;
|
|
12182
13123
|
let backend;
|
|
12183
13124
|
try {
|
|
12184
13125
|
backend = await createBackend({
|
|
@@ -12212,8 +13153,17 @@ async function createProjectBackend(options = {}) {
|
|
|
12212
13153
|
storageDriver,
|
|
12213
13154
|
log: options.log
|
|
12214
13155
|
});
|
|
13156
|
+
if (replication && options.applyMigrations !== false) {
|
|
13157
|
+
await ensurePowerSyncReplicationCatalog(backend.db.engine, replication);
|
|
13158
|
+
}
|
|
12215
13159
|
} catch (error) {
|
|
12216
|
-
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) {
|
|
12217
13167
|
try {
|
|
12218
13168
|
await engine.close();
|
|
12219
13169
|
} catch (cleanupError) {
|
|
@@ -12233,9 +13183,33 @@ async function createProjectBackend(options = {}) {
|
|
|
12233
13183
|
functionNames: [...functions.keys()],
|
|
12234
13184
|
webhookCount: webhooks.length,
|
|
12235
13185
|
storageBackend,
|
|
12236
|
-
databaseEngine
|
|
13186
|
+
databaseEngine,
|
|
13187
|
+
replicationProfile: replication?.profile
|
|
12237
13188
|
};
|
|
12238
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
|
+
}
|
|
12239
13213
|
function resolveDatabaseEngine(value, memory = false) {
|
|
12240
13214
|
const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
|
|
12241
13215
|
if (configured !== "pglite" && configured !== "native") {
|
|
@@ -12311,7 +13285,7 @@ async function loadWebhooks(projectDir) {
|
|
|
12311
13285
|
}
|
|
12312
13286
|
}
|
|
12313
13287
|
function resolvePath(projectDir, path) {
|
|
12314
|
-
return isAbsolute(path) ? path :
|
|
13288
|
+
return isAbsolute(path) ? path : resolve3(projectDir, path);
|
|
12315
13289
|
}
|
|
12316
13290
|
function randomHex(bytes) {
|
|
12317
13291
|
const value = crypto.getRandomValues(new Uint8Array(bytes));
|
|
@@ -12338,6 +13312,9 @@ function parsePort(value, fallback) {
|
|
|
12338
13312
|
throw new Error(`invalid port: ${value}`);
|
|
12339
13313
|
return port;
|
|
12340
13314
|
}
|
|
13315
|
+
function commaSeparated(value, fallback = []) {
|
|
13316
|
+
return value === undefined ? fallback : value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
13317
|
+
}
|
|
12341
13318
|
function displayHost(host) {
|
|
12342
13319
|
if (host === "0.0.0.0" || host === "::")
|
|
12343
13320
|
return "127.0.0.1";
|
|
@@ -12354,7 +13331,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
|
|
|
12354
13331
|
|
|
12355
13332
|
// src/snapshot.ts
|
|
12356
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";
|
|
12357
|
-
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";
|
|
12358
13335
|
import { create as createTar, extract as extractTar2 } from "tar";
|
|
12359
13336
|
var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
|
|
12360
13337
|
var SNAPSHOT_VERSION = 1;
|
|
@@ -12378,7 +13355,7 @@ async function createSnapshot(options) {
|
|
|
12378
13355
|
postgresMajor: await readPostgresMajor(paths.dataDir)
|
|
12379
13356
|
} : {}
|
|
12380
13357
|
};
|
|
12381
|
-
const output =
|
|
13358
|
+
const output = resolve4(options.output);
|
|
12382
13359
|
if (await existingInfo(output))
|
|
12383
13360
|
throw new Error(`snapshot output already exists: ${output}`);
|
|
12384
13361
|
await mkdir6(dirname6(output), { recursive: true });
|
|
@@ -12419,7 +13396,7 @@ async function restoreSnapshot(options) {
|
|
|
12419
13396
|
await mkdir6(payloadRoot, { recursive: true });
|
|
12420
13397
|
await extractTar2({
|
|
12421
13398
|
cwd: payloadRoot,
|
|
12422
|
-
file:
|
|
13399
|
+
file: resolve4(options.input),
|
|
12423
13400
|
preserveOwner: false,
|
|
12424
13401
|
preservePaths: false,
|
|
12425
13402
|
strict: true,
|
|
@@ -12496,11 +13473,11 @@ async function restoreSnapshot(options) {
|
|
|
12496
13473
|
function normalizePaths(paths) {
|
|
12497
13474
|
return {
|
|
12498
13475
|
...paths,
|
|
12499
|
-
projectDir:
|
|
12500
|
-
stateDir:
|
|
12501
|
-
dataDir: paths.dataDir ?
|
|
12502
|
-
storageDir:
|
|
12503
|
-
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)
|
|
12504
13481
|
};
|
|
12505
13482
|
}
|
|
12506
13483
|
async function assertSnapshotPaths(paths, options = {}) {
|
|
@@ -12530,7 +13507,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
12530
13507
|
async function assertDirectoryOrMissing(path) {
|
|
12531
13508
|
if (!path)
|
|
12532
13509
|
return;
|
|
12533
|
-
if (
|
|
13510
|
+
if (resolve4(path) === parse2(resolve4(path)).root)
|
|
12534
13511
|
throw new Error(`snapshot path must not be the filesystem root: ${path}`);
|
|
12535
13512
|
try {
|
|
12536
13513
|
const info = await lstat2(path);
|
|
@@ -12752,13 +13729,13 @@ async function assertNoSymlinks(root) {
|
|
|
12752
13729
|
}
|
|
12753
13730
|
}
|
|
12754
13731
|
function isWithin(parent, child) {
|
|
12755
|
-
const normalizedParent =
|
|
12756
|
-
const normalizedChild =
|
|
13732
|
+
const normalizedParent = resolve4(parent);
|
|
13733
|
+
const normalizedChild = resolve4(child);
|
|
12757
13734
|
return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
|
|
12758
13735
|
}
|
|
12759
13736
|
function pathsOverlap(left, right) {
|
|
12760
|
-
const normalizedLeft =
|
|
12761
|
-
const normalizedRight =
|
|
13737
|
+
const normalizedLeft = resolve4(left);
|
|
13738
|
+
const normalizedRight = resolve4(right);
|
|
12762
13739
|
return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
|
|
12763
13740
|
}
|
|
12764
13741
|
|
|
@@ -12787,7 +13764,8 @@ function parseArgs(argv) {
|
|
|
12787
13764
|
host: process.env.SUPACLOUD_LITE_HOST ?? "127.0.0.1",
|
|
12788
13765
|
port: process.env.SUPACLOUD_LITE_PORT || process.env.PORT ? Number.parseInt(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 10) : 54321,
|
|
12789
13766
|
serviceRole: false,
|
|
12790
|
-
force: false
|
|
13767
|
+
force: false,
|
|
13768
|
+
json: false
|
|
12791
13769
|
};
|
|
12792
13770
|
for (let index = 0;index < args.length; index++) {
|
|
12793
13771
|
const argument = args[index];
|
|
@@ -12806,29 +13784,45 @@ function parseArgs(argv) {
|
|
|
12806
13784
|
else if (argument === "--site-url")
|
|
12807
13785
|
options.siteUrl = next();
|
|
12808
13786
|
else if (argument === "--project-dir" || argument === "--dir")
|
|
12809
|
-
options.projectDir =
|
|
13787
|
+
options.projectDir = resolve5(next());
|
|
12810
13788
|
else if (argument === "--state-dir")
|
|
12811
|
-
options.stateDir =
|
|
13789
|
+
options.stateDir = resolve5(next());
|
|
12812
13790
|
else if (argument === "--data-dir")
|
|
12813
|
-
options.dataDir =
|
|
13791
|
+
options.dataDir = resolve5(next());
|
|
12814
13792
|
else if (argument === "--storage-dir")
|
|
12815
|
-
options.storageDir =
|
|
13793
|
+
options.storageDir = resolve5(next());
|
|
12816
13794
|
else if (argument === "--storage-backend")
|
|
12817
13795
|
options.storageBackend = next();
|
|
12818
13796
|
else if (argument === "--s3-prefix")
|
|
12819
13797
|
options.s3 = { ...options.s3, prefix: next() };
|
|
12820
13798
|
else if (argument === "--engine")
|
|
12821
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());
|
|
12822
13814
|
else if (argument === "--memory")
|
|
12823
13815
|
options.memory = true;
|
|
12824
13816
|
else if (argument === "--output" || argument === "-o")
|
|
12825
|
-
options.output =
|
|
13817
|
+
options.output = resolve5(next());
|
|
12826
13818
|
else if (argument === "--file" || argument === "-f")
|
|
12827
13819
|
options.diffFile = next();
|
|
12828
13820
|
else if (argument === "--service-role")
|
|
12829
13821
|
options.serviceRole = true;
|
|
12830
13822
|
else if (argument === "--force")
|
|
12831
13823
|
options.force = true;
|
|
13824
|
+
else if (argument === "--json")
|
|
13825
|
+
options.json = true;
|
|
12832
13826
|
else if (argument === "--version") {
|
|
12833
13827
|
console.log(package_default.version);
|
|
12834
13828
|
process.exit(0);
|
|
@@ -12917,6 +13911,27 @@ ${privilegedKey}
|
|
|
12917
13911
|
}
|
|
12918
13912
|
return;
|
|
12919
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
|
+
}
|
|
12920
13935
|
if (options.command === "migrate" || options.command === "status") {
|
|
12921
13936
|
const project2 = await createProjectBackend({
|
|
12922
13937
|
...options,
|
|
@@ -12988,9 +14003,13 @@ async function runDbCommand(options) {
|
|
|
12988
14003
|
}
|
|
12989
14004
|
return;
|
|
12990
14005
|
}
|
|
12991
|
-
const project = await loadSupabaseProject(
|
|
14006
|
+
const project = await loadSupabaseProject(resolve5(options.projectDir ?? process.cwd()));
|
|
12992
14007
|
if (subcommand === "diff") {
|
|
12993
|
-
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;
|
|
12994
14013
|
const ddl = await computeDbDiff({
|
|
12995
14014
|
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12996
14015
|
liveEngine,
|
|
@@ -13018,7 +14037,11 @@ async function runDbCommand(options) {
|
|
|
13018
14037
|
return;
|
|
13019
14038
|
}
|
|
13020
14039
|
if (subcommand === "pull") {
|
|
13021
|
-
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;
|
|
13022
14045
|
const result = await pullSchema({
|
|
13023
14046
|
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
13024
14047
|
liveEngine,
|
|
@@ -13063,7 +14086,7 @@ async function runSnapshotCommand(options) {
|
|
|
13063
14086
|
const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
|
|
13064
14087
|
const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
|
|
13065
14088
|
await writeStandardOutput([
|
|
13066
|
-
`Snapshot restored from ${
|
|
14089
|
+
`Snapshot restored from ${resolve5(input)}`,
|
|
13067
14090
|
...rollbackLines,
|
|
13068
14091
|
...reconnectLine
|
|
13069
14092
|
].join(`
|
|
@@ -13119,6 +14142,19 @@ async function printInspection(rows) {
|
|
|
13119
14142
|
`)}
|
|
13120
14143
|
`);
|
|
13121
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
|
+
}
|
|
13122
14158
|
async function writeStandardOutput(output) {
|
|
13123
14159
|
await Bun.write(Bun.stdout, output);
|
|
13124
14160
|
}
|
|
@@ -13129,8 +14165,11 @@ function timestamp() {
|
|
|
13129
14165
|
return new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
13130
14166
|
}
|
|
13131
14167
|
function quietLog() {}
|
|
14168
|
+
function commaSeparated2(value) {
|
|
14169
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
14170
|
+
}
|
|
13132
14171
|
function printHelp() {
|
|
13133
|
-
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
|
|
13134
14173
|
|
|
13135
14174
|
Usage: supacloud-lite [command] [options]
|
|
13136
14175
|
|
|
@@ -13148,6 +14187,7 @@ Commands:
|
|
|
13148
14187
|
snapshot restore <f> restore a snapshot into an empty target
|
|
13149
14188
|
upgrade snapshot first, then apply pending migrations
|
|
13150
14189
|
inspect show table rows and sizes
|
|
14190
|
+
doctor report Lite capability and replication readiness
|
|
13151
14191
|
version print the package version
|
|
13152
14192
|
|
|
13153
14193
|
Fresh projects must run "supacloud-lite migrate" before "db reset".
|
|
@@ -13164,7 +14204,15 @@ Options:
|
|
|
13164
14204
|
--storage-backend <b> fs, memory, or s3 (default fs)
|
|
13165
14205
|
--s3-prefix <p> optional key prefix for the s3 backend
|
|
13166
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
|
|
13167
14214
|
--memory use an in-memory PGlite database
|
|
14215
|
+
--json emit machine-readable doctor output
|
|
13168
14216
|
-o, --output <p> output file for gen types
|
|
13169
14217
|
-f, --file <name> migration suffix for db diff
|
|
13170
14218
|
--force replace non-empty restore targets and retain rollback copies
|
|
@@ -13183,7 +14231,6 @@ function formatDatabaseEngine(engine, dataDir) {
|
|
|
13183
14231
|
const label = engine === "native" ? "Native PostgreSQL" : "PGlite";
|
|
13184
14232
|
return dataDir ? `${label} (${dataDir})` : `${label} (memory)`;
|
|
13185
14233
|
}
|
|
13186
|
-
var windowsLifecycleRef = process.platform === "win32" ? setInterval(() => {}, 1000) : null;
|
|
13187
14234
|
var exitCode = 0;
|
|
13188
14235
|
try {
|
|
13189
14236
|
await main();
|
|
@@ -13191,8 +14238,5 @@ try {
|
|
|
13191
14238
|
await writeStandardError(`${error instanceof Error ? error.message : String(error)}
|
|
13192
14239
|
`);
|
|
13193
14240
|
exitCode = 1;
|
|
13194
|
-
} finally {
|
|
13195
|
-
if (windowsLifecycleRef !== null)
|
|
13196
|
-
clearInterval(windowsLifecycleRef);
|
|
13197
14241
|
}
|
|
13198
14242
|
process.exit(exitCode);
|