@supacloud/lite 0.7.2 → 0.8.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSES/POSTGRESQL-17-COPYRIGHT.txt +23 -0
  3. package/LICENSES/THESEUS-POSTGRESQL-LICENSE.txt +7 -0
  4. package/THIRD_PARTY_NOTICES.md +11 -1
  5. package/dist/cli.js +2592 -347
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2463 -285
  9. package/dist/project-runtime.d.ts +5 -0
  10. package/dist/project-runtime.d.ts.map +1 -1
  11. package/dist/runtime/db/data-dir-lock.d.ts +2 -0
  12. package/dist/runtime/db/data-dir-lock.d.ts.map +1 -1
  13. package/dist/runtime/db/database.d.ts.map +1 -1
  14. package/dist/runtime/db/emulated.d.ts +2 -1
  15. package/dist/runtime/db/emulated.d.ts.map +1 -1
  16. package/dist/runtime/db/pglite-engine.d.ts.map +1 -1
  17. package/dist/runtime/node/bundle-function.d.ts.map +1 -1
  18. package/dist/runtime/node/fs-driver.d.ts +2 -0
  19. package/dist/runtime/node/fs-driver.d.ts.map +1 -1
  20. package/dist/runtime/node/load-functions.d.ts +3 -2
  21. package/dist/runtime/node/load-functions.d.ts.map +1 -1
  22. package/dist/runtime/node/native/engine.d.ts +23 -0
  23. package/dist/runtime/node/native/engine.d.ts.map +1 -0
  24. package/dist/runtime/node/native/wire-engine.d.ts +9 -0
  25. package/dist/runtime/node/native/wire-engine.d.ts.map +1 -0
  26. package/dist/runtime/node/native/wire.d.ts +56 -0
  27. package/dist/runtime/node/native/wire.d.ts.map +1 -0
  28. package/dist/runtime/rest/handler.d.ts.map +1 -1
  29. package/dist/runtime/storage/handler.d.ts +5 -0
  30. package/dist/runtime/storage/handler.d.ts.map +1 -1
  31. package/dist/runtime/storage/image-transform-cache.d.ts +13 -0
  32. package/dist/runtime/storage/image-transform-cache.d.ts.map +1 -0
  33. package/dist/runtime/storage/image-transform.d.ts +1 -1
  34. package/dist/runtime/storage/image-transform.d.ts.map +1 -1
  35. package/dist/runtime/storage/s3-driver.d.ts +2 -0
  36. package/dist/runtime/storage/s3-driver.d.ts.map +1 -1
  37. package/dist/runtime/types.d.ts +2 -0
  38. package/dist/runtime/types.d.ts.map +1 -1
  39. package/dist/snapshot.d.ts +5 -1
  40. package/dist/snapshot.d.ts.map +1 -1
  41. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -85,8 +85,8 @@ function randomToken(bytes = 32) {
85
85
  // package.json
86
86
  var package_default = {
87
87
  name: "@supacloud/lite",
88
- version: "0.7.2",
89
- description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
88
+ version: "0.8.0",
89
+ description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
90
90
  type: "module",
91
91
  license: "Apache-2.0",
92
92
  bin: {
@@ -122,6 +122,10 @@ var package_default = {
122
122
  dev: "bun run src/cli.ts start",
123
123
  start: "bun run src/cli.ts start",
124
124
  test: "bun test --timeout 20000",
125
+ "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts --timeout 180000",
126
+ parity: "bun run parity/harness.ts",
127
+ "parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
128
+ "check:native": "bun run test:native && bun run parity:native",
125
129
  "test:package": "bun run scripts/package-smoke.ts",
126
130
  "test:standalone": "bun run scripts/standalone-smoke.ts",
127
131
  prepack: "bun run build",
@@ -3550,41 +3554,1158 @@ $pgmq$;
3550
3554
  revoke all on all functions in schema pgmq from public, anon, authenticated;
3551
3555
  grant execute on all functions in schema pgmq to service_role;
3552
3556
 
3553
- create schema if not exists pgmq_public;
3554
- grant usage on schema pgmq_public to anon, authenticated, service_role;
3555
-
3556
- create or replace function pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer default 0)
3557
- returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3558
- select * from pgmq.send(queue_name, message, sleep_seconds);
3559
- $pgmq_public$;
3560
-
3561
- create or replace function pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer default 0)
3562
- returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3563
- select * from pgmq.send_batch(queue_name, messages, sleep_seconds);
3564
- $pgmq_public$;
3565
-
3566
- create or replace function pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
3567
- returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3568
- select * from pgmq.read(queue_name, sleep_seconds, n);
3569
- $pgmq_public$;
3570
-
3571
- create or replace function pgmq_public.pop(queue_name text)
3572
- returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3573
- select * from pgmq.pop(queue_name);
3574
- $pgmq_public$;
3575
-
3576
- create or replace function pgmq_public.archive(queue_name text, message_id bigint)
3577
- returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3578
- select pgmq.archive(queue_name, message_id);
3579
- $pgmq_public$;
3580
-
3581
- create or replace function pgmq_public."delete"(queue_name text, message_id bigint)
3582
- returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3583
- select pgmq.delete(queue_name, message_id);
3584
- $pgmq_public$;
3585
-
3586
- revoke all on all functions in schema pgmq_public from public;
3587
- grant execute on all functions in schema pgmq_public to anon, authenticated, service_role;
3557
+ -- supacloud:sql-module:pgmq-public:start
3558
+ DO $pgmq_extension$
3559
+ BEGIN
3560
+ IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
3561
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
3562
+ END IF;
3563
+ END
3564
+ $pgmq_extension$;
3565
+
3566
+ CREATE SCHEMA IF NOT EXISTS pgmq_public;
3567
+ GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
3568
+
3569
+ CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
3570
+ RETURNS text
3571
+ LANGUAGE plpgsql
3572
+ IMMUTABLE
3573
+ SET search_path = ''
3574
+ AS $$
3575
+ DECLARE
3576
+ normalized_queue_name text := lower(btrim(queue_name));
3577
+ BEGIN
3578
+ IF normalized_queue_name IS NULL
3579
+ OR left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN
3580
+ RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';
3581
+ END IF;
3582
+ RETURN normalized_queue_name;
3583
+ END;
3584
+ $$;
3585
+
3586
+ CREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)
3587
+ RETURNS SETOF bigint
3588
+ LANGUAGE sql
3589
+ VOLATILE
3590
+ SECURITY DEFINER
3591
+ SET search_path = ''
3592
+ AS $$ SELECT * FROM pgmq.send(pgmq_public.require_public_queue(queue_name), message, sleep_seconds); $$;
3593
+
3594
+ CREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)
3595
+ RETURNS SETOF bigint
3596
+ LANGUAGE sql
3597
+ VOLATILE
3598
+ SECURITY DEFINER
3599
+ SET search_path = ''
3600
+ AS $$ SELECT * FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), messages, sleep_seconds); $$;
3601
+
3602
+ CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
3603
+ RETURNS SETOF pgmq.message_record
3604
+ LANGUAGE sql
3605
+ VOLATILE
3606
+ SECURITY DEFINER
3607
+ SET search_path = ''
3608
+ AS $$ SELECT * FROM pgmq.read(pgmq_public.require_public_queue(queue_name), sleep_seconds, n); $$;
3609
+
3610
+ CREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)
3611
+ RETURNS SETOF pgmq.message_record
3612
+ LANGUAGE sql
3613
+ VOLATILE
3614
+ SECURITY DEFINER
3615
+ SET search_path = ''
3616
+ AS $$ SELECT * FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)); $$;
3617
+
3618
+ CREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)
3619
+ RETURNS boolean
3620
+ LANGUAGE sql
3621
+ VOLATILE
3622
+ SECURITY DEFINER
3623
+ SET search_path = ''
3624
+ AS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), message_id); $$;
3625
+
3626
+ CREATE OR REPLACE FUNCTION pgmq_public."delete"(queue_name text, message_id bigint)
3627
+ RETURNS boolean
3628
+ LANGUAGE sql
3629
+ VOLATILE
3630
+ SECURITY DEFINER
3631
+ SET search_path = ''
3632
+ AS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), message_id); $$;
3633
+
3634
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;
3635
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;
3636
+ -- supacloud:sql-module:pgmq-public:end
3637
+ `;
3638
+ var WORKFLOWS_SQL = `
3639
+ -- supacloud:sql-module:workflows-public:start
3640
+ DO $pgmq_extension$
3641
+ BEGIN
3642
+ IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
3643
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
3644
+ END IF;
3645
+ END
3646
+ $pgmq_extension$;
3647
+ CREATE SCHEMA IF NOT EXISTS supacloud_workflows;
3648
+ REVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;
3649
+ GRANT USAGE ON SCHEMA supacloud_workflows TO service_role;
3650
+
3651
+ SELECT pgmq.create('supacloud_internal_workflows');
3652
+
3653
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.runs (
3654
+ id uuid PRIMARY KEY,
3655
+ workflow_name text NOT NULL
3656
+ CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
3657
+ workflow_version text NOT NULL
3658
+ CHECK (char_length(workflow_version) BETWEEN 1 AND 80),
3659
+ status text NOT NULL DEFAULT 'queued'
3660
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
3661
+ input jsonb NOT NULL DEFAULT '{}'::jsonb
3662
+ CHECK (jsonb_typeof(input) = 'object'),
3663
+ output jsonb NOT NULL DEFAULT '{}'::jsonb
3664
+ CHECK (jsonb_typeof(output) = 'object'),
3665
+ error_message text NOT NULL DEFAULT ''
3666
+ CHECK (char_length(error_message) <= 4000),
3667
+ row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),
3668
+ created_at timestamptz NOT NULL DEFAULT now(),
3669
+ started_at timestamptz,
3670
+ completed_at timestamptz,
3671
+ updated_at timestamptz NOT NULL DEFAULT now()
3672
+ );
3673
+
3674
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.steps (
3675
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3676
+ run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
3677
+ step_key text NOT NULL
3678
+ CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
3679
+ status text NOT NULL DEFAULT 'queued'
3680
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),
3681
+ input jsonb NOT NULL DEFAULT '{}'::jsonb
3682
+ CHECK (jsonb_typeof(input) = 'object'),
3683
+ output jsonb NOT NULL DEFAULT '{}'::jsonb
3684
+ CHECK (jsonb_typeof(output) = 'object'),
3685
+ error_message text NOT NULL DEFAULT ''
3686
+ CHECK (char_length(error_message) <= 4000),
3687
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
3688
+ max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),
3689
+ retry_delay_seconds integer NOT NULL DEFAULT 0
3690
+ CHECK (retry_delay_seconds BETWEEN 0 AND 86400),
3691
+ queue_message_id bigint NOT NULL UNIQUE,
3692
+ claimed_by text,
3693
+ claimed_at timestamptz,
3694
+ completed_at timestamptz,
3695
+ next_step_key text,
3696
+ created_at timestamptz NOT NULL DEFAULT now(),
3697
+ updated_at timestamptz NOT NULL DEFAULT now(),
3698
+ UNIQUE (run_id, step_key)
3699
+ );
3700
+
3701
+ ALTER TABLE supacloud_workflows.steps
3702
+ ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0
3703
+ CHECK (retry_delay_seconds BETWEEN 0 AND 86400);
3704
+
3705
+ CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx
3706
+ ON supacloud_workflows.steps (run_id)
3707
+ WHERE status IN ('queued', 'running');
3708
+
3709
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx
3710
+ ON supacloud_workflows.runs (status, updated_at DESC, id);
3711
+
3712
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx
3713
+ ON supacloud_workflows.steps (run_id, created_at, id);
3714
+
3715
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.events (
3716
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
3717
+ run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
3718
+ step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,
3719
+ event_type text NOT NULL
3720
+ CHECK (event_type IN (
3721
+ 'run_started', 'step_claimed', 'step_retried', 'step_completed',
3722
+ 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'
3723
+ )),
3724
+ attempt integer CHECK (attempt IS NULL OR attempt > 0),
3725
+ details jsonb NOT NULL DEFAULT '{}'::jsonb
3726
+ CHECK (jsonb_typeof(details) = 'object'),
3727
+ created_at timestamptz NOT NULL DEFAULT now()
3728
+ );
3729
+
3730
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx
3731
+ ON supacloud_workflows.events (run_id, id);
3732
+
3733
+ CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx
3734
+ ON supacloud_workflows.events (step_id, attempt)
3735
+ WHERE event_type IN ('step_retried', 'step_dead_lettered')
3736
+ AND details ->> 'operation' = 'retry';
3737
+
3738
+ CREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(
3739
+ p_run_id uuid,
3740
+ p_idempotent boolean DEFAULT false
3741
+ ) RETURNS jsonb
3742
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
3743
+ SELECT jsonb_build_object(
3744
+ 'runId', run.id,
3745
+ 'workflowName', run.workflow_name,
3746
+ 'workflowVersion', run.workflow_version,
3747
+ 'status', run.status,
3748
+ 'input', run.input,
3749
+ 'output', run.output,
3750
+ 'errorMessage', run.error_message,
3751
+ 'rowVersion', run.row_version::text,
3752
+ 'createdAt', run.created_at,
3753
+ 'startedAt', run.started_at,
3754
+ 'completedAt', run.completed_at,
3755
+ 'updatedAt', run.updated_at,
3756
+ 'idempotent', p_idempotent,
3757
+ 'steps', coalesce((
3758
+ SELECT jsonb_agg(jsonb_build_object(
3759
+ 'stepId', step.id,
3760
+ 'stepKey', step.step_key,
3761
+ 'status', step.status,
3762
+ 'input', step.input,
3763
+ 'output', step.output,
3764
+ 'errorMessage', step.error_message,
3765
+ 'attempts', step.attempts,
3766
+ 'maxAttempts', step.max_attempts,
3767
+ 'retryDelaySeconds', step.retry_delay_seconds,
3768
+ 'queueMessageId', step.queue_message_id::text,
3769
+ 'claimedBy', step.claimed_by,
3770
+ 'claimedAt', step.claimed_at,
3771
+ 'completedAt', step.completed_at,
3772
+ 'nextStepKey', step.next_step_key,
3773
+ 'createdAt', step.created_at,
3774
+ 'updatedAt', step.updated_at
3775
+ ) ORDER BY step.created_at, step.id)
3776
+ FROM supacloud_workflows.steps step
3777
+ WHERE step.run_id = run.id
3778
+ ), '[]'::jsonb)
3779
+ )
3780
+ FROM supacloud_workflows.runs run
3781
+ WHERE run.id = p_run_id
3782
+ $$;
3783
+
3784
+ CREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(
3785
+ p_run_id uuid,
3786
+ p_step_key text,
3787
+ p_input jsonb,
3788
+ p_max_attempts integer
3789
+ ) RETURNS supacloud_workflows.steps
3790
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3791
+ DECLARE
3792
+ normalized_step_key text := nullif(btrim(p_step_key), '');
3793
+ step_id uuid := gen_random_uuid();
3794
+ message_id bigint;
3795
+ created_step supacloud_workflows.steps%ROWTYPE;
3796
+ BEGIN
3797
+ IF normalized_step_key IS NULL
3798
+ OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3799
+ OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
3800
+ OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
3801
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';
3802
+ END IF;
3803
+
3804
+ IF NOT EXISTS (
3805
+ SELECT 1 FROM supacloud_workflows.runs run
3806
+ WHERE run.id = p_run_id AND run.status IN ('queued', 'running')
3807
+ ) THEN
3808
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
3809
+ END IF;
3810
+
3811
+ SELECT queued_id INTO message_id
3812
+ FROM pgmq.send(
3813
+ 'supacloud_internal_workflows',
3814
+ jsonb_build_object('run_id', p_run_id, 'step_id', step_id),
3815
+ 0
3816
+ ) AS queued_id;
3817
+
3818
+ INSERT INTO supacloud_workflows.steps (
3819
+ id, run_id, step_key, input, max_attempts, queue_message_id
3820
+ ) VALUES (
3821
+ step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
3822
+ ) RETURNING * INTO created_step;
3823
+
3824
+ RETURN created_step;
3825
+ END;
3826
+ $$;
3827
+
3828
+ -- Clean-code exception: private transitions keep typed PostgreSQL arguments and
3829
+ -- the complete lock/queue/ledger/event mutation in one transaction. The public
3830
+ -- contract already uses one JSON request; revisit if a private routine gains a
3831
+ -- second caller or any transition can be decomposed without weakening atomicity.
3832
+ CREATE OR REPLACE FUNCTION supacloud_workflows.start_run(
3833
+ p_run_id uuid,
3834
+ p_workflow_name text,
3835
+ p_workflow_version text,
3836
+ p_first_step_key text,
3837
+ p_input jsonb,
3838
+ p_max_attempts integer
3839
+ ) RETURNS jsonb
3840
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3841
+ DECLARE
3842
+ normalized_name text := nullif(btrim(p_workflow_name), '');
3843
+ normalized_version text := nullif(btrim(p_workflow_version), '');
3844
+ normalized_first_step_key text := nullif(btrim(p_first_step_key), '');
3845
+ existing_run supacloud_workflows.runs%ROWTYPE;
3846
+ existing_step supacloud_workflows.steps%ROWTYPE;
3847
+ first_step supacloud_workflows.steps%ROWTYPE;
3848
+ BEGIN
3849
+ IF p_run_id IS NULL
3850
+ OR normalized_name IS NULL
3851
+ OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3852
+ OR normalized_version IS NULL
3853
+ OR char_length(normalized_version) > 80
3854
+ OR normalized_first_step_key IS NULL
3855
+ OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3856
+ OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
3857
+ OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
3858
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
3859
+ END IF;
3860
+
3861
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
3862
+ SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
3863
+ IF FOUND THEN
3864
+ SELECT * INTO existing_step
3865
+ FROM supacloud_workflows.steps
3866
+ WHERE run_id = p_run_id
3867
+ ORDER BY created_at, id
3868
+ LIMIT 1;
3869
+ IF NOT FOUND THEN
3870
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
3871
+ END IF;
3872
+ IF existing_run.workflow_name <> normalized_name
3873
+ OR existing_run.workflow_version <> normalized_version
3874
+ OR existing_run.input <> p_input
3875
+ OR existing_step.step_key <> normalized_first_step_key
3876
+ OR existing_step.input <> p_input
3877
+ OR existing_step.max_attempts <> p_max_attempts THEN
3878
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
3879
+ END IF;
3880
+ RETURN supacloud_workflows.snapshot(p_run_id, true);
3881
+ END IF;
3882
+
3883
+ INSERT INTO supacloud_workflows.runs (
3884
+ id, workflow_name, workflow_version, input
3885
+ ) VALUES (
3886
+ p_run_id, normalized_name, normalized_version, p_input
3887
+ );
3888
+ first_step := supacloud_workflows.enqueue_step(
3889
+ p_run_id, normalized_first_step_key, p_input, p_max_attempts
3890
+ );
3891
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)
3892
+ VALUES (p_run_id, first_step.id, 'run_started');
3893
+ RETURN supacloud_workflows.snapshot(p_run_id, false);
3894
+ END;
3895
+ $$;
3896
+
3897
+ CREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(
3898
+ p_worker_id text,
3899
+ p_visibility_timeout_seconds integer
3900
+ ) RETURNS jsonb
3901
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3902
+ DECLARE
3903
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
3904
+ queued_message pgmq.message_record;
3905
+ message_run_id text;
3906
+ message_step_id text;
3907
+ candidate_run_id uuid;
3908
+ claimed_step supacloud_workflows.steps%ROWTYPE;
3909
+ claimed_run supacloud_workflows.runs%ROWTYPE;
3910
+ BEGIN
3911
+ IF normalized_worker_id IS NULL
3912
+ OR char_length(normalized_worker_id) > 200
3913
+ OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN
3914
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
3915
+ END IF;
3916
+
3917
+ SELECT * INTO queued_message
3918
+ FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)
3919
+ LIMIT 1;
3920
+ IF NOT FOUND THEN RETURN NULL; END IF;
3921
+
3922
+ message_run_id := queued_message.message ->> 'run_id';
3923
+ message_step_id := queued_message.message ->> 'step_id';
3924
+ IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'
3925
+ OR message_run_id IS NULL
3926
+ OR message_step_id IS NULL
3927
+ OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
3928
+ OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
3929
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3930
+ RETURN jsonb_build_object(
3931
+ 'status', 'discarded',
3932
+ 'reason', 'invalid_message',
3933
+ 'messageId', queued_message.msg_id::text
3934
+ );
3935
+ END IF;
3936
+
3937
+ SELECT step.run_id INTO candidate_run_id
3938
+ FROM supacloud_workflows.steps step
3939
+ WHERE step.id::text = lower(message_step_id)
3940
+ AND step.run_id::text = lower(message_run_id)
3941
+ AND step.queue_message_id = queued_message.msg_id;
3942
+ IF NOT FOUND THEN
3943
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3944
+ RETURN jsonb_build_object(
3945
+ 'status', 'discarded',
3946
+ 'reason', 'orphaned_message',
3947
+ 'messageId', queued_message.msg_id::text
3948
+ );
3949
+ END IF;
3950
+
3951
+ IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
3952
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
3953
+ END IF;
3954
+ SELECT * INTO claimed_step
3955
+ FROM supacloud_workflows.steps
3956
+ WHERE id::text = lower(message_step_id)
3957
+ AND run_id = candidate_run_id
3958
+ AND queue_message_id = queued_message.msg_id
3959
+ FOR UPDATE;
3960
+ IF NOT FOUND THEN
3961
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3962
+ RETURN jsonb_build_object(
3963
+ 'status', 'discarded',
3964
+ 'reason', 'orphaned_message',
3965
+ 'messageId', queued_message.msg_id::text
3966
+ );
3967
+ END IF;
3968
+
3969
+ SELECT * INTO claimed_run
3970
+ FROM supacloud_workflows.runs
3971
+ WHERE id = claimed_step.run_id
3972
+ FOR UPDATE;
3973
+ IF claimed_run.status NOT IN ('queued', 'running')
3974
+ OR claimed_step.status NOT IN ('queued', 'running') THEN
3975
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3976
+ RETURN jsonb_build_object(
3977
+ 'status', 'discarded',
3978
+ 'reason', 'step_not_claimable',
3979
+ 'runId', claimed_step.run_id,
3980
+ 'stepId', claimed_step.id,
3981
+ 'messageId', queued_message.msg_id::text
3982
+ );
3983
+ END IF;
3984
+
3985
+ IF queued_message.read_ct > claimed_step.max_attempts THEN
3986
+ UPDATE supacloud_workflows.steps
3987
+ SET status = 'dead_lettered', attempts = queued_message.read_ct,
3988
+ error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()
3989
+ WHERE id = claimed_step.id;
3990
+ UPDATE supacloud_workflows.runs
3991
+ SET status = 'failed', error_message = 'maximum attempts exceeded',
3992
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
3993
+ WHERE id = claimed_step.run_id;
3994
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3995
+ INSERT INTO supacloud_workflows.events (
3996
+ run_id, step_id, event_type, attempt, details
3997
+ ) VALUES (
3998
+ claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,
3999
+ jsonb_build_object('errorMessage', 'maximum attempts exceeded')
4000
+ );
4001
+ RETURN jsonb_build_object(
4002
+ 'status', 'dead_lettered',
4003
+ 'runId', claimed_step.run_id,
4004
+ 'stepId', claimed_step.id,
4005
+ 'stepKey', claimed_step.step_key,
4006
+ 'messageId', queued_message.msg_id::text,
4007
+ 'attempt', queued_message.read_ct,
4008
+ 'maxAttempts', claimed_step.max_attempts
4009
+ );
4010
+ END IF;
4011
+
4012
+ UPDATE supacloud_workflows.steps
4013
+ SET status = 'running', attempts = queued_message.read_ct,
4014
+ retry_delay_seconds = 0, claimed_by = normalized_worker_id,
4015
+ claimed_at = now(), updated_at = now()
4016
+ WHERE id = claimed_step.id;
4017
+ UPDATE supacloud_workflows.runs
4018
+ SET status = 'running', started_at = coalesce(started_at, now()),
4019
+ updated_at = now(), row_version = row_version + 1
4020
+ WHERE id = claimed_step.run_id;
4021
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)
4022
+ VALUES (
4023
+ claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,
4024
+ jsonb_build_object('workerId', normalized_worker_id)
4025
+ );
4026
+
4027
+ RETURN jsonb_build_object(
4028
+ 'status', 'claimed',
4029
+ 'runId', claimed_step.run_id,
4030
+ 'workflowName', claimed_run.workflow_name,
4031
+ 'workflowVersion', claimed_run.workflow_version,
4032
+ 'stepId', claimed_step.id,
4033
+ 'stepKey', claimed_step.step_key,
4034
+ 'input', claimed_step.input,
4035
+ 'messageId', queued_message.msg_id::text,
4036
+ 'attempt', queued_message.read_ct,
4037
+ 'maxAttempts', claimed_step.max_attempts,
4038
+ 'workerId', normalized_worker_id
4039
+ );
4040
+ END;
4041
+ $$;
4042
+
4043
+ CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(
4044
+ p_step_id uuid
4045
+ ) RETURNS supacloud_workflows.steps
4046
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4047
+ DECLARE
4048
+ candidate_run_id uuid;
4049
+ active_step supacloud_workflows.steps%ROWTYPE;
4050
+ BEGIN
4051
+ SELECT step.run_id INTO candidate_run_id
4052
+ FROM supacloud_workflows.steps step
4053
+ WHERE step.id = p_step_id;
4054
+ IF NOT FOUND THEN
4055
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
4056
+ END IF;
4057
+ PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
4058
+ SELECT * INTO active_step
4059
+ FROM supacloud_workflows.steps
4060
+ WHERE id = p_step_id
4061
+ FOR UPDATE;
4062
+ IF NOT FOUND THEN
4063
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
4064
+ END IF;
4065
+ RETURN active_step;
4066
+ END;
4067
+ $$;
4068
+
4069
+ CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(
4070
+ p_step_id uuid,
4071
+ p_message_id bigint,
4072
+ p_attempt integer,
4073
+ p_worker_id text
4074
+ ) RETURNS supacloud_workflows.steps
4075
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4076
+ DECLARE
4077
+ active_step supacloud_workflows.steps%ROWTYPE;
4078
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
4079
+ BEGIN
4080
+ active_step := supacloud_workflows.lock_step(p_step_id);
4081
+ IF normalized_worker_id IS NULL
4082
+ OR p_message_id IS NULL
4083
+ OR p_attempt IS NULL
4084
+ OR active_step.queue_message_id IS DISTINCT FROM p_message_id
4085
+ OR active_step.attempts IS DISTINCT FROM p_attempt
4086
+ OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN
4087
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4088
+ END IF;
4089
+ RETURN active_step;
4090
+ END;
4091
+ $$;
4092
+
4093
+ CREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(
4094
+ p_step_id uuid,
4095
+ p_message_id bigint,
4096
+ p_attempt integer,
4097
+ p_worker_id text,
4098
+ p_output jsonb,
4099
+ p_next_step_key text,
4100
+ p_next_input jsonb,
4101
+ p_next_max_attempts integer
4102
+ ) RETURNS jsonb
4103
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4104
+ DECLARE
4105
+ current_step supacloud_workflows.steps%ROWTYPE;
4106
+ next_step supacloud_workflows.steps%ROWTYPE;
4107
+ normalized_next_step_key text := nullif(btrim(p_next_step_key), '');
4108
+ archived boolean;
4109
+ BEGIN
4110
+ IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'
4111
+ OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'
4112
+ OR normalized_next_step_key IS NULL
4113
+ OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4114
+ OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN
4115
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4116
+ END IF;
4117
+ current_step := supacloud_workflows.lock_step_attempt(
4118
+ p_step_id, p_message_id, p_attempt, p_worker_id
4119
+ );
4120
+
4121
+ IF current_step.status = 'completed' THEN
4122
+ SELECT * INTO next_step
4123
+ FROM supacloud_workflows.steps
4124
+ WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
4125
+ IF NOT FOUND
4126
+ OR current_step.output IS DISTINCT FROM p_output
4127
+ OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
4128
+ OR next_step.input IS DISTINCT FROM p_next_input
4129
+ OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN
4130
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4131
+ END IF;
4132
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4133
+ END IF;
4134
+ IF current_step.status <> 'running' THEN
4135
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4136
+ END IF;
4137
+
4138
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4139
+ IF archived IS DISTINCT FROM true THEN
4140
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4141
+ END IF;
4142
+ UPDATE supacloud_workflows.steps
4143
+ SET status = 'completed', output = p_output, error_message = '',
4144
+ completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()
4145
+ WHERE id = current_step.id;
4146
+ INSERT INTO supacloud_workflows.events (
4147
+ run_id, step_id, event_type, attempt, details
4148
+ ) VALUES (
4149
+ current_step.run_id, current_step.id, 'step_completed', p_attempt,
4150
+ jsonb_build_object('nextStepKey', normalized_next_step_key)
4151
+ );
4152
+ next_step := supacloud_workflows.enqueue_step(
4153
+ current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts
4154
+ );
4155
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4156
+ END;
4157
+ $$;
4158
+
4159
+ CREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(
4160
+ p_step_id uuid,
4161
+ p_message_id bigint,
4162
+ p_attempt integer,
4163
+ p_worker_id text,
4164
+ p_step_output jsonb,
4165
+ p_run_output jsonb
4166
+ ) RETURNS jsonb
4167
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4168
+ DECLARE
4169
+ current_step supacloud_workflows.steps%ROWTYPE;
4170
+ current_run supacloud_workflows.runs%ROWTYPE;
4171
+ archived boolean;
4172
+ BEGIN
4173
+ IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'
4174
+ OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN
4175
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4176
+ END IF;
4177
+ current_step := supacloud_workflows.lock_step_attempt(
4178
+ p_step_id, p_message_id, p_attempt, p_worker_id
4179
+ );
4180
+ SELECT * INTO current_run
4181
+ FROM supacloud_workflows.runs
4182
+ WHERE id = current_step.run_id
4183
+ FOR UPDATE;
4184
+
4185
+ IF current_step.status = 'completed' THEN
4186
+ IF current_step.next_step_key IS NOT NULL
4187
+ OR current_step.output IS DISTINCT FROM p_step_output
4188
+ OR current_run.status IS DISTINCT FROM 'completed'
4189
+ OR current_run.output IS DISTINCT FROM p_run_output THEN
4190
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4191
+ END IF;
4192
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4193
+ END IF;
4194
+ IF current_step.status <> 'running' THEN
4195
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4196
+ END IF;
4197
+ IF current_run.status <> 'running' THEN
4198
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4199
+ END IF;
4200
+
4201
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4202
+ IF archived IS DISTINCT FROM true THEN
4203
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4204
+ END IF;
4205
+ UPDATE supacloud_workflows.steps
4206
+ SET status = 'completed', output = p_step_output, error_message = '',
4207
+ completed_at = now(), updated_at = now()
4208
+ WHERE id = current_step.id;
4209
+ UPDATE supacloud_workflows.runs
4210
+ SET status = 'completed', output = p_run_output, error_message = '',
4211
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4212
+ WHERE id = current_step.run_id AND status = 'running';
4213
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
4214
+ VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);
4215
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
4216
+ VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);
4217
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4218
+ END;
4219
+ $$;
4220
+
4221
+ CREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(
4222
+ p_step_id uuid,
4223
+ p_message_id bigint,
4224
+ p_attempt integer,
4225
+ p_worker_id text,
4226
+ p_error_message text,
4227
+ p_delay_seconds integer
4228
+ ) RETURNS jsonb
4229
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4230
+ DECLARE
4231
+ current_step supacloud_workflows.steps%ROWTYPE;
4232
+ normalized_error text := nullif(btrim(p_error_message), '');
4233
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
4234
+ retry_receipt jsonb;
4235
+ queue_message_updated boolean;
4236
+ archived boolean;
4237
+ BEGIN
4238
+ IF normalized_error IS NULL OR char_length(normalized_error) > 4000
4239
+ OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN
4240
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4241
+ END IF;
4242
+ current_step := supacloud_workflows.lock_step(p_step_id);
4243
+ SELECT event.details INTO retry_receipt
4244
+ FROM supacloud_workflows.events event
4245
+ WHERE event.step_id = current_step.id
4246
+ AND event.attempt = p_attempt
4247
+ AND event.event_type IN ('step_retried', 'step_dead_lettered')
4248
+ AND event.details ->> 'operation' = 'retry'
4249
+ ORDER BY event.id DESC
4250
+ LIMIT 1;
4251
+ IF FOUND THEN
4252
+ IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text
4253
+ OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id
4254
+ OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error
4255
+ OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN
4256
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4257
+ END IF;
4258
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4259
+ END IF;
4260
+ IF normalized_worker_id IS NULL
4261
+ OR p_message_id IS NULL
4262
+ OR p_attempt IS NULL
4263
+ OR current_step.queue_message_id IS DISTINCT FROM p_message_id
4264
+ OR current_step.attempts IS DISTINCT FROM p_attempt
4265
+ OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id
4266
+ OR current_step.status <> 'running' THEN
4267
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4268
+ END IF;
4269
+
4270
+ IF current_step.attempts >= current_step.max_attempts THEN
4271
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4272
+ IF archived IS DISTINCT FROM true THEN
4273
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4274
+ END IF;
4275
+ UPDATE supacloud_workflows.steps
4276
+ SET status = 'dead_lettered', error_message = normalized_error,
4277
+ completed_at = now(), updated_at = now()
4278
+ WHERE id = current_step.id;
4279
+ UPDATE supacloud_workflows.runs
4280
+ SET status = 'failed', error_message = normalized_error,
4281
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4282
+ WHERE id = current_step.run_id AND status = 'running';
4283
+ IF NOT FOUND THEN
4284
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4285
+ END IF;
4286
+ INSERT INTO supacloud_workflows.events (
4287
+ run_id, step_id, event_type, attempt, details
4288
+ ) VALUES (
4289
+ current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt,
4290
+ jsonb_build_object(
4291
+ 'operation', 'retry',
4292
+ 'messageId', p_message_id::text,
4293
+ 'workerId', normalized_worker_id,
4294
+ 'errorMessage', normalized_error,
4295
+ 'delaySeconds', p_delay_seconds
4296
+ )
4297
+ );
4298
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4299
+ END IF;
4300
+
4301
+ SELECT EXISTS (
4302
+ SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)
4303
+ ) INTO queue_message_updated;
4304
+ IF queue_message_updated IS DISTINCT FROM true THEN
4305
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4306
+ END IF;
4307
+ UPDATE supacloud_workflows.steps
4308
+ SET status = 'queued', error_message = normalized_error,
4309
+ retry_delay_seconds = p_delay_seconds, updated_at = now()
4310
+ WHERE id = current_step.id;
4311
+ INSERT INTO supacloud_workflows.events (
4312
+ run_id, step_id, event_type, attempt, details
4313
+ ) VALUES (
4314
+ current_step.run_id, current_step.id, 'step_retried', p_attempt,
4315
+ jsonb_build_object(
4316
+ 'operation', 'retry',
4317
+ 'messageId', p_message_id::text,
4318
+ 'workerId', normalized_worker_id,
4319
+ 'errorMessage', normalized_error,
4320
+ 'delaySeconds', p_delay_seconds
4321
+ )
4322
+ );
4323
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4324
+ END;
4325
+ $$;
4326
+
4327
+ CREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(
4328
+ p_step_id uuid,
4329
+ p_message_id bigint,
4330
+ p_attempt integer,
4331
+ p_worker_id text,
4332
+ p_error_message text
4333
+ ) RETURNS jsonb
4334
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4335
+ DECLARE
4336
+ current_step supacloud_workflows.steps%ROWTYPE;
4337
+ current_run supacloud_workflows.runs%ROWTYPE;
4338
+ normalized_error text := nullif(btrim(p_error_message), '');
4339
+ archived boolean;
4340
+ BEGIN
4341
+ IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN
4342
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4343
+ END IF;
4344
+ current_step := supacloud_workflows.lock_step_attempt(
4345
+ p_step_id, p_message_id, p_attempt, p_worker_id
4346
+ );
4347
+ SELECT * INTO current_run
4348
+ FROM supacloud_workflows.runs
4349
+ WHERE id = current_step.run_id
4350
+ FOR UPDATE;
4351
+
4352
+ IF current_step.status = 'failed' THEN
4353
+ IF current_step.error_message IS DISTINCT FROM normalized_error
4354
+ OR current_run.status IS DISTINCT FROM 'failed'
4355
+ OR current_run.error_message IS DISTINCT FROM normalized_error THEN
4356
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4357
+ END IF;
4358
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4359
+ END IF;
4360
+ IF current_step.status <> 'running' THEN
4361
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4362
+ END IF;
4363
+ IF current_run.status <> 'running' THEN
4364
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4365
+ END IF;
4366
+
4367
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4368
+ IF archived IS DISTINCT FROM true THEN
4369
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4370
+ END IF;
4371
+ UPDATE supacloud_workflows.steps
4372
+ SET status = 'failed', error_message = normalized_error,
4373
+ completed_at = now(), updated_at = now()
4374
+ WHERE id = current_step.id;
4375
+ UPDATE supacloud_workflows.runs
4376
+ SET status = 'failed', error_message = normalized_error,
4377
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4378
+ WHERE id = current_step.run_id AND status = 'running';
4379
+ INSERT INTO supacloud_workflows.events (
4380
+ run_id, step_id, event_type, attempt, details
4381
+ ) VALUES (
4382
+ current_step.run_id, current_step.id, 'step_failed', p_attempt,
4383
+ jsonb_build_object('errorMessage', normalized_error)
4384
+ );
4385
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4386
+ END;
4387
+ $$;
4388
+
4389
+ CREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(
4390
+ p_run_id uuid,
4391
+ p_reason text
4392
+ ) RETURNS jsonb
4393
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4394
+ DECLARE
4395
+ normalized_reason text := nullif(btrim(p_reason), '');
4396
+ locked_run supacloud_workflows.runs%ROWTYPE;
4397
+ active_step supacloud_workflows.steps%ROWTYPE;
4398
+ BEGIN
4399
+ IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN
4400
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
4401
+ END IF;
4402
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
4403
+ SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
4404
+ IF NOT FOUND THEN
4405
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
4406
+ END IF;
4407
+ IF locked_run.status = 'cancelled' THEN
4408
+ IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN
4409
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4410
+ END IF;
4411
+ RETURN supacloud_workflows.snapshot(p_run_id, true);
4412
+ END IF;
4413
+ IF locked_run.status NOT IN ('queued', 'running') THEN
4414
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4415
+ END IF;
4416
+ SELECT * INTO active_step
4417
+ FROM supacloud_workflows.steps
4418
+ WHERE run_id = p_run_id AND status IN ('queued', 'running')
4419
+ FOR UPDATE;
4420
+ IF FOUND THEN
4421
+ PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
4422
+ UPDATE supacloud_workflows.steps
4423
+ SET status = 'cancelled', error_message = normalized_reason,
4424
+ completed_at = now(), updated_at = now()
4425
+ WHERE id = active_step.id;
4426
+ END IF;
4427
+ UPDATE supacloud_workflows.runs
4428
+ SET status = 'cancelled', error_message = normalized_reason,
4429
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4430
+ WHERE id = p_run_id;
4431
+ INSERT INTO supacloud_workflows.events (
4432
+ run_id, step_id, event_type, details
4433
+ ) VALUES (
4434
+ p_run_id, active_step.id, 'run_cancelled',
4435
+ jsonb_build_object('reason', normalized_reason)
4436
+ );
4437
+ RETURN supacloud_workflows.snapshot(p_run_id, false);
4438
+ END;
4439
+ $$;
4440
+
4441
+ CREATE OR REPLACE FUNCTION supacloud_workflows.run_events(
4442
+ p_run_id uuid,
4443
+ p_after_event_id bigint,
4444
+ p_limit integer
4445
+ ) RETURNS jsonb
4446
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
4447
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
4448
+ 'eventId', page.id::text,
4449
+ 'runId', page.run_id,
4450
+ 'stepId', page.step_id,
4451
+ 'eventType', page.event_type,
4452
+ 'attempt', page.attempt,
4453
+ 'details', page.details,
4454
+ 'createdAt', page.created_at
4455
+ ) ORDER BY page.id), '[]'::jsonb)
4456
+ FROM (
4457
+ SELECT event.*
4458
+ FROM supacloud_workflows.events event
4459
+ WHERE event.run_id = p_run_id AND event.id > p_after_event_id
4460
+ ORDER BY event.id
4461
+ LIMIT p_limit
4462
+ ) page
4463
+ $$;
4464
+
4465
+ CREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(
4466
+ request jsonb,
4467
+ key text
4468
+ ) RETURNS uuid
4469
+ LANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$
4470
+ DECLARE
4471
+ uuid_text text;
4472
+ BEGIN
4473
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4474
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
4475
+ END IF;
4476
+ uuid_text := request ->> key;
4477
+ IF uuid_text IS NULL
4478
+ OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
4479
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
4480
+ END IF;
4481
+ RETURN uuid_text::uuid;
4482
+ END;
4483
+ $$;
4484
+
4485
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)
4486
+ RETURNS jsonb
4487
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4488
+ DECLARE
4489
+ max_attempts integer;
4490
+ BEGIN
4491
+ max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
4492
+ RETURN supacloud_workflows.start_run(
4493
+ supacloud_workflows.request_uuid(request, 'runId'),
4494
+ request ->> 'workflowName',
4495
+ request ->> 'workflowVersion',
4496
+ request ->> 'firstStepKey',
4497
+ coalesce(request -> 'input', '{}'::jsonb),
4498
+ max_attempts
4499
+ );
4500
+ EXCEPTION
4501
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4502
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
4503
+ END;
4504
+ $$;
4505
+
4506
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)
4507
+ RETURNS jsonb
4508
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4509
+ DECLARE
4510
+ visibility_timeout_seconds integer;
4511
+ BEGIN
4512
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4513
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
4514
+ END IF;
4515
+ visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);
4516
+ RETURN supacloud_workflows.claim_step(
4517
+ request ->> 'workerId', visibility_timeout_seconds
4518
+ );
4519
+ EXCEPTION
4520
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4521
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
4522
+ END;
4523
+ $$;
4524
+
4525
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)
4526
+ RETURNS jsonb
4527
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4528
+ DECLARE
4529
+ message_id bigint;
4530
+ attempt integer;
4531
+ next_max_attempts integer;
4532
+ BEGIN
4533
+ message_id := (request ->> 'messageId')::bigint;
4534
+ attempt := (request ->> 'attempt')::integer;
4535
+ next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);
4536
+ IF message_id <= 0 OR attempt <= 0 THEN
4537
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4538
+ END IF;
4539
+ RETURN supacloud_workflows.advance_step(
4540
+ supacloud_workflows.request_uuid(request, 'stepId'),
4541
+ message_id,
4542
+ attempt,
4543
+ request ->> 'workerId',
4544
+ coalesce(request -> 'output', '{}'::jsonb),
4545
+ request ->> 'nextStepKey',
4546
+ coalesce(request -> 'nextInput', '{}'::jsonb),
4547
+ next_max_attempts
4548
+ );
4549
+ EXCEPTION
4550
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4551
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4552
+ END;
4553
+ $$;
4554
+
4555
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)
4556
+ RETURNS jsonb
4557
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4558
+ DECLARE
4559
+ message_id bigint;
4560
+ attempt integer;
4561
+ BEGIN
4562
+ message_id := (request ->> 'messageId')::bigint;
4563
+ attempt := (request ->> 'attempt')::integer;
4564
+ IF message_id <= 0 OR attempt <= 0 THEN
4565
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4566
+ END IF;
4567
+ RETURN supacloud_workflows.complete_run(
4568
+ supacloud_workflows.request_uuid(request, 'stepId'),
4569
+ message_id,
4570
+ attempt,
4571
+ request ->> 'workerId',
4572
+ coalesce(request -> 'stepOutput', '{}'::jsonb),
4573
+ coalesce(request -> 'runOutput', '{}'::jsonb)
4574
+ );
4575
+ EXCEPTION
4576
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4577
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4578
+ END;
4579
+ $$;
4580
+
4581
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)
4582
+ RETURNS jsonb
4583
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4584
+ DECLARE
4585
+ message_id bigint;
4586
+ attempt integer;
4587
+ delay_seconds integer;
4588
+ BEGIN
4589
+ message_id := (request ->> 'messageId')::bigint;
4590
+ attempt := (request ->> 'attempt')::integer;
4591
+ delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);
4592
+ IF message_id <= 0 OR attempt <= 0 THEN
4593
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4594
+ END IF;
4595
+ RETURN supacloud_workflows.retry_step(
4596
+ supacloud_workflows.request_uuid(request, 'stepId'),
4597
+ message_id,
4598
+ attempt,
4599
+ request ->> 'workerId',
4600
+ request ->> 'errorMessage',
4601
+ delay_seconds
4602
+ );
4603
+ EXCEPTION
4604
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4605
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4606
+ END;
4607
+ $$;
4608
+
4609
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)
4610
+ RETURNS jsonb
4611
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4612
+ DECLARE
4613
+ message_id bigint;
4614
+ attempt integer;
4615
+ BEGIN
4616
+ message_id := (request ->> 'messageId')::bigint;
4617
+ attempt := (request ->> 'attempt')::integer;
4618
+ IF message_id <= 0 OR attempt <= 0 THEN
4619
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4620
+ END IF;
4621
+ RETURN supacloud_workflows.fail_step(
4622
+ supacloud_workflows.request_uuid(request, 'stepId'),
4623
+ message_id,
4624
+ attempt,
4625
+ request ->> 'workerId',
4626
+ request ->> 'errorMessage'
4627
+ );
4628
+ EXCEPTION
4629
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4630
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4631
+ END;
4632
+ $$;
4633
+
4634
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)
4635
+ RETURNS jsonb
4636
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4637
+ BEGIN
4638
+ RETURN supacloud_workflows.cancel_run(
4639
+ supacloud_workflows.request_uuid(request, 'runId'),
4640
+ request ->> 'reason'
4641
+ );
4642
+ EXCEPTION
4643
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4644
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
4645
+ END;
4646
+ $$;
4647
+
4648
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)
4649
+ RETURNS jsonb
4650
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4651
+ BEGIN
4652
+ RETURN supacloud_workflows.snapshot(
4653
+ supacloud_workflows.request_uuid(request, 'runId'), false
4654
+ );
4655
+ EXCEPTION
4656
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4657
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';
4658
+ END;
4659
+ $$;
4660
+
4661
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)
4662
+ RETURNS jsonb
4663
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4664
+ DECLARE
4665
+ after_event_id bigint;
4666
+ event_limit integer;
4667
+ BEGIN
4668
+ after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);
4669
+ event_limit := coalesce((request ->> 'limit')::integer, 100);
4670
+ IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN
4671
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
4672
+ END IF;
4673
+ RETURN supacloud_workflows.run_events(
4674
+ supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit
4675
+ );
4676
+ EXCEPTION
4677
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4678
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
4679
+ END;
4680
+ $$;
4681
+
4682
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows
4683
+ FROM PUBLIC, anon, authenticated, service_role;
4684
+ REVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows
4685
+ FROM PUBLIC, anon, authenticated, service_role;
4686
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows
4687
+ FROM PUBLIC, anon, authenticated, service_role;
4688
+
4689
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;
4690
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;
4691
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;
4692
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;
4693
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;
4694
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;
4695
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;
4696
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;
4697
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;
4698
+
4699
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;
4700
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;
4701
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;
4702
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;
4703
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;
4704
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;
4705
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;
4706
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
4707
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
4708
+ -- supacloud:sql-module:workflows-public:end
3588
4709
  `;
3589
4710
  var CRON_SQL = `
3590
4711
  create schema if not exists cron;
@@ -3911,12 +5032,32 @@ function pickTag(text, base) {
3911
5032
  return tag;
3912
5033
  }
3913
5034
 
3914
- // src/runtime/db/pglite-engine.ts
3915
- import { mkdir, open, unlink as unlink2 } from "fs/promises";
3916
- import { dirname, resolve } from "path";
3917
-
3918
5035
  // src/runtime/db/data-dir-lock.ts
3919
- import { readFile, unlink } from "fs/promises";
5036
+ import { mkdir, open, readFile, unlink } from "fs/promises";
5037
+ import { dirname, resolve } from "path";
5038
+ async function acquireDataDirLock(dataDir, engineName = "database") {
5039
+ if (!dataDir || dataDir.includes("://"))
5040
+ return async () => {};
5041
+ const absoluteDataDir = resolve(dataDir);
5042
+ const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
5043
+ await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
5044
+ const nonce = crypto.randomUUID();
5045
+ const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce, engineName);
5046
+ let released = false;
5047
+ return async () => {
5048
+ if (released)
5049
+ return;
5050
+ released = true;
5051
+ await handle.close();
5052
+ const owner = await readDataDirLockOwner(lockPath);
5053
+ if (owner?.nonce !== nonce)
5054
+ return;
5055
+ await unlink(lockPath).catch((error) => {
5056
+ if (error.code !== "ENOENT")
5057
+ throw error;
5058
+ });
5059
+ };
5060
+ }
3920
5061
  async function recoverStaleDataDirLock(lockPath) {
3921
5062
  const lockState = await inspectDataDirLock(lockPath);
3922
5063
  if (lockState.kind !== "stale")
@@ -3934,6 +5075,38 @@ async function readDataDirLockOwner(lockPath) {
3934
5075
  const lockState = await inspectDataDirLock(lockPath);
3935
5076
  return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
3936
5077
  }
5078
+ async function createDataDirLock(absoluteDataDir, lockPath, nonce, engineName) {
5079
+ for (let attempt = 0;attempt < 3; attempt++) {
5080
+ try {
5081
+ return await writeDataDirLock(lockPath, nonce);
5082
+ } catch (error) {
5083
+ if (error.code !== "EEXIST")
5084
+ throw error;
5085
+ const lockState = await recoverStaleDataDirLock(lockPath);
5086
+ if (lockState.kind === "active") {
5087
+ throw new Error(`${engineName} data directory is already in use: ${absoluteDataDir} (pid ${lockState.owner.pid})`);
5088
+ }
5089
+ if (lockState.kind === "unreadable")
5090
+ throw unreadableLockError(lockPath, engineName);
5091
+ }
5092
+ }
5093
+ throw unreadableLockError(lockPath, engineName);
5094
+ }
5095
+ async function writeDataDirLock(lockPath, nonce) {
5096
+ const handle = await open(lockPath, "wx", 384);
5097
+ try {
5098
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
5099
+ `);
5100
+ return handle;
5101
+ } catch (error) {
5102
+ await handle.close().catch(() => {});
5103
+ await unlink(lockPath).catch(() => {});
5104
+ throw error;
5105
+ }
5106
+ }
5107
+ function unreadableLockError(lockPath, engineName) {
5108
+ return new Error(`${engineName} data directory has an unreadable lock: ${lockPath}. ` + "Confirm no SupaCloud Lite process is using it, then remove the lock manually.");
5109
+ }
3937
5110
  async function inspectDataDirLock(lockPath) {
3938
5111
  let contents;
3939
5112
  try {
@@ -3987,7 +5160,7 @@ begin
3987
5160
  end $$;
3988
5161
  `;
3989
5162
  async function createPgliteEngine(dataDir) {
3990
- const releaseLock = await acquireDataDirLock(dataDir);
5163
+ const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
3991
5164
  let PGlite, extensions;
3992
5165
  const standaloneAssets = getStandaloneAssets();
3993
5166
  let cleanupStandaloneBundles = async () => {};
@@ -4111,63 +5284,6 @@ async function removePreparedBundles(cleanup) {
4111
5284
  console.error("Unable to remove temporary PGlite extension bundles:", error);
4112
5285
  }
4113
5286
  }
4114
- async function acquireDataDirLock(dataDir) {
4115
- if (!dataDir || dataDir.includes("://"))
4116
- return async () => {};
4117
- const absoluteDataDir = resolve(dataDir);
4118
- const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
4119
- await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
4120
- const nonce = crypto.randomUUID();
4121
- const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
4122
- let released = false;
4123
- return async () => {
4124
- if (released)
4125
- return;
4126
- released = true;
4127
- await handle?.close();
4128
- const owner = await readDataDirLockOwner(lockPath);
4129
- if (owner?.nonce !== nonce)
4130
- return;
4131
- await unlink2(lockPath).catch((error) => {
4132
- if (error.code !== "ENOENT")
4133
- throw error;
4134
- });
4135
- };
4136
- }
4137
- async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
4138
- for (let attempt = 0;attempt < 3; attempt++) {
4139
- try {
4140
- return await writeDataDirLock(lockPath, nonce);
4141
- } catch (error) {
4142
- if (error.code !== "EEXIST")
4143
- throw error;
4144
- const lockState = await recoverStaleDataDirLock(lockPath);
4145
- if (lockState.kind === "active")
4146
- throw lockInUseError(absoluteDataDir, lockState.owner.pid);
4147
- if (lockState.kind === "unreadable")
4148
- throw unreadableLockError(lockPath);
4149
- }
4150
- }
4151
- throw unreadableLockError(lockPath);
4152
- }
4153
- async function writeDataDirLock(lockPath, nonce) {
4154
- const handle = await open(lockPath, "wx", 384);
4155
- try {
4156
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
4157
- `);
4158
- return handle;
4159
- } catch (error) {
4160
- await handle.close().catch(() => {});
4161
- await unlink2(lockPath).catch(() => {});
4162
- throw error;
4163
- }
4164
- }
4165
- function lockInUseError(dataDir, pid) {
4166
- return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
4167
- }
4168
- function unreadableLockError(lockPath) {
4169
- return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
4170
- }
4171
5287
 
4172
5288
  // src/runtime/db/database.ts
4173
5289
  var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
@@ -4185,20 +5301,30 @@ class Database {
4185
5301
  }
4186
5302
  static async create(dataDirOrEngine, opts) {
4187
5303
  const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
4188
- if (engine.minimalBootstrap) {
4189
- await engine.exec(MINIMAL_BOOTSTRAP_SQL);
4190
- } else {
4191
- await engine.exec(BOOTSTRAP_SQL);
4192
- await engine.exec(PGMQ_SQL);
4193
- await engine.exec(CRON_SQL);
4194
- await engine.exec(NET_SQL);
4195
- await engine.exec(EXT_COMPAT_SQL);
4196
- await engine.exec(VAULT_SQL);
4197
- if (opts?.vaultKey) {
4198
- await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
5304
+ try {
5305
+ if (engine.minimalBootstrap) {
5306
+ await engine.exec(MINIMAL_BOOTSTRAP_SQL);
5307
+ } else {
5308
+ await engine.exec(BOOTSTRAP_SQL);
5309
+ await engine.exec(PGMQ_SQL);
5310
+ await engine.exec(WORKFLOWS_SQL);
5311
+ await engine.exec(CRON_SQL);
5312
+ await engine.exec(NET_SQL);
5313
+ await engine.exec(EXT_COMPAT_SQL);
5314
+ await engine.exec(VAULT_SQL);
5315
+ if (opts?.vaultKey) {
5316
+ await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
5317
+ }
4199
5318
  }
5319
+ return new Database(engine);
5320
+ } catch (error) {
5321
+ try {
5322
+ await engine.close();
5323
+ } catch (cleanupError) {
5324
+ throw new AggregateError([error, cleanupError], "database bootstrap and cleanup failed");
5325
+ }
5326
+ throw error;
4200
5327
  }
4201
- return new Database(engine);
4202
5328
  }
4203
5329
  query(sql, params) {
4204
5330
  return this.engine.query(sql, params);
@@ -5937,6 +7063,25 @@ function parsePrefer(header) {
5937
7063
  }
5938
7064
  return prefer;
5939
7065
  }
7066
+ function applyRequestRange(query, request) {
7067
+ const range = request.headers.get("range");
7068
+ if (!range || query.limits.has("") || query.offsets.has(""))
7069
+ return;
7070
+ const unit = request.headers.get("range-unit");
7071
+ if (unit !== null && unit.toLowerCase() !== "items")
7072
+ throw new ParseError(`unsupported range unit: ${unit}`);
7073
+ const match = range.match(/^(\d+)-(\d*)$/);
7074
+ if (!match)
7075
+ throw new ParseError(`invalid range: ${range}`);
7076
+ const start = Number(match[1]);
7077
+ const end = match[2] ? Number(match[2]) : undefined;
7078
+ if (!Number.isSafeInteger(start) || end !== undefined && (!Number.isSafeInteger(end) || end < start)) {
7079
+ throw new ParseError(`invalid range: ${range}`);
7080
+ }
7081
+ query.offsets.set("", start);
7082
+ if (end !== undefined)
7083
+ query.limits.set("", end - start + 1);
7084
+ }
5940
7085
  var OBJECT_MEDIA = "application/vnd.pgrst.object+json";
5941
7086
  var CSV_MEDIA = "text/csv";
5942
7087
  var PLAN_MEDIA = "application/vnd.pgrst.plan";
@@ -6013,6 +7158,8 @@ class RestHandler {
6013
7158
  const wantsObject = accept.includes(OBJECT_MEDIA);
6014
7159
  const wantsCsv = accept.includes(CSV_MEDIA);
6015
7160
  const q = parseQuery(url.searchParams);
7161
+ if (method === "GET" || method === "HEAD")
7162
+ applyRequestRange(q, req);
6016
7163
  if (this.maxRows !== undefined && (method === "GET" || method === "HEAD")) {
6017
7164
  const requested = q.limits.get("");
6018
7165
  q.limits.set("", requested === undefined ? this.maxRows : Math.min(requested, this.maxRows));
@@ -6055,10 +7202,11 @@ class RestHandler {
6055
7202
  }
6056
7203
  return { rows: res.rows[0].body, count: count2 };
6057
7204
  });
7205
+ const offset = q.offsets.get("") ?? 0;
6058
7206
  return this.dataResponse(rows, {
6059
- status: 200,
7207
+ status: count !== null && (offset > 0 || rows.length < count) ? 206 : 200,
6060
7208
  count,
6061
- offset: q.offsets.get("") ?? 0,
7209
+ offset,
6062
7210
  wantsObject,
6063
7211
  wantsCsv,
6064
7212
  head: method === "HEAD"
@@ -6489,8 +7637,10 @@ function applyResize(image, metadata, options) {
6489
7637
  const proportionalWidth = Math.max(1, Math.round(metadata.width * options.height / metadata.height));
6490
7638
  image.resize(proportionalWidth);
6491
7639
  }
6492
- async function transformImage(bytes, options) {
6493
- if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
7640
+ async function transformImage(source, options, knownSourceSize) {
7641
+ const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
7642
+ const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
7643
+ if (sourceSize > MAX_TRANSFORM_BYTES) {
6494
7644
  return {
6495
7645
  ok: false,
6496
7646
  status: 413,
@@ -6498,9 +7648,10 @@ async function transformImage(bytes, options) {
6498
7648
  message: "The source image exceeds the 25MB transformation limit"
6499
7649
  };
6500
7650
  }
7651
+ const image = new Bun.Image(source, { maxPixels: MAX_TRANSFORM_PIXELS });
6501
7652
  let metadata;
6502
7653
  try {
6503
- metadata = await new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS }).metadata();
7654
+ metadata = await image.metadata();
6504
7655
  } catch (error) {
6505
7656
  return mapImageError(error);
6506
7657
  }
@@ -6517,7 +7668,6 @@ async function transformImage(bytes, options) {
6517
7668
  message: `The source format ${outputFormat} is not supported by this runtime`
6518
7669
  };
6519
7670
  }
6520
- const image = new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS });
6521
7671
  applyResize(image, metadata, options);
6522
7672
  if (options.format === "jpeg" || options.format === "origin" && outputFormat === "jpeg" && options.quality !== undefined) {
6523
7673
  image.jpeg(options.quality === undefined ? undefined : { quality: options.quality });
@@ -6527,12 +7677,127 @@ async function transformImage(bytes, options) {
6527
7677
  image.webp(options.quality === undefined ? undefined : { quality: options.quality });
6528
7678
  }
6529
7679
  try {
6530
- return { ok: true, bytes: await image.bytes(), contentType };
7680
+ const bytes = await image.bytes();
7681
+ if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
7682
+ return {
7683
+ ok: false,
7684
+ status: 413,
7685
+ error: "ImageTooLarge",
7686
+ message: "The transformed image exceeds the 25MB transformation limit"
7687
+ };
7688
+ }
7689
+ return { ok: true, bytes, contentType };
6531
7690
  } catch (error) {
6532
7691
  return mapImageError(error);
6533
7692
  }
6534
7693
  }
6535
7694
 
7695
+ // src/runtime/storage/image-transform-cache.ts
7696
+ var DEFAULT_MAX_CACHE_BYTES = 64 * 1024 * 1024;
7697
+ var DEFAULT_MAX_CACHE_ENTRIES = 128;
7698
+ var OBJECT_VERSION_PREFIX = "v2-";
7699
+
7700
+ class Semaphore {
7701
+ limit;
7702
+ active = 0;
7703
+ waiters = [];
7704
+ constructor(limit) {
7705
+ this.limit = limit;
7706
+ }
7707
+ async run(operation) {
7708
+ await this.acquire();
7709
+ try {
7710
+ return await operation();
7711
+ } finally {
7712
+ this.release();
7713
+ }
7714
+ }
7715
+ async acquire() {
7716
+ if (this.active < this.limit) {
7717
+ this.active += 1;
7718
+ return;
7719
+ }
7720
+ await new Promise((resolve2) => this.waiters.push(resolve2));
7721
+ }
7722
+ release() {
7723
+ const next = this.waiters.shift();
7724
+ if (next) {
7725
+ next();
7726
+ return;
7727
+ }
7728
+ this.active -= 1;
7729
+ }
7730
+ }
7731
+ var globalImageTransformSemaphore = new Semaphore(1);
7732
+ function imageTransformCacheKey(version, options) {
7733
+ if (!version?.startsWith(OBJECT_VERSION_PREFIX))
7734
+ return null;
7735
+ return [
7736
+ version,
7737
+ options.width ?? "",
7738
+ options.height ?? "",
7739
+ options.resize,
7740
+ options.quality ?? "",
7741
+ options.format
7742
+ ].join("\x00");
7743
+ }
7744
+
7745
+ class ImageTransformCache {
7746
+ maxBytes;
7747
+ maxEntries;
7748
+ entries = new Map;
7749
+ inFlight = new Map;
7750
+ cachedBytes = 0;
7751
+ constructor(maxBytes = DEFAULT_MAX_CACHE_BYTES, maxEntries = DEFAULT_MAX_CACHE_ENTRIES) {
7752
+ this.maxBytes = maxBytes;
7753
+ this.maxEntries = maxEntries;
7754
+ }
7755
+ async getOrTransform(key, operation) {
7756
+ if (key === null)
7757
+ return globalImageTransformSemaphore.run(operation);
7758
+ const cached = this.entries.get(key);
7759
+ if (cached) {
7760
+ this.entries.delete(key);
7761
+ this.entries.set(key, cached);
7762
+ return cached.transform;
7763
+ }
7764
+ const pending = this.inFlight.get(key);
7765
+ if (pending)
7766
+ return pending;
7767
+ const transform = globalImageTransformSemaphore.run(operation);
7768
+ this.inFlight.set(key, transform);
7769
+ try {
7770
+ const transformResult = await transform;
7771
+ if (transformResult.ok)
7772
+ this.store(key, transformResult);
7773
+ return transformResult;
7774
+ } finally {
7775
+ if (this.inFlight.get(key) === transform)
7776
+ this.inFlight.delete(key);
7777
+ }
7778
+ }
7779
+ store(key, transform) {
7780
+ const size = transform.bytes.byteLength;
7781
+ if (size > this.maxBytes || this.maxEntries === 0)
7782
+ return;
7783
+ const previous = this.entries.get(key);
7784
+ if (previous) {
7785
+ this.cachedBytes -= previous.size;
7786
+ this.entries.delete(key);
7787
+ }
7788
+ this.entries.set(key, { transform, size });
7789
+ this.cachedBytes += size;
7790
+ while (this.entries.size > this.maxEntries || this.cachedBytes > this.maxBytes) {
7791
+ const oldestKey = this.entries.keys().next().value;
7792
+ if (oldestKey === undefined)
7793
+ break;
7794
+ const oldest = this.entries.get(oldestKey);
7795
+ this.entries.delete(oldestKey);
7796
+ this.cachedBytes -= oldest.size;
7797
+ }
7798
+ }
7799
+ }
7800
+
6536
7801
  // src/runtime/storage/handler.ts
6537
7802
  var MAX_SIGNED_URL_EXPIRY = 7 * 24 * 60 * 60;
6538
7803
  function clampExpiry(expiresIn) {
@@ -6611,7 +7876,7 @@ var MAX_COMPLETED_TUS_UPLOADS = 64;
6611
7876
  var COMPLETED_TUS_RETENTION_MS = 60 * 60 * 1000;
6612
7877
  var PREFLIGHT_ROLLBACK = Symbol("storage-preflight-rollback");
6613
7878
  var INTERNAL_STORAGE_BUCKET = ".supacloud-lite";
6614
- var OBJECT_VERSION_PREFIX = "v2-";
7879
+ var OBJECT_VERSION_PREFIX2 = "v2-";
6615
7880
 
6616
7881
  class StorageHandler {
6617
7882
  db;
@@ -6619,6 +7884,7 @@ class StorageHandler {
6619
7884
  config;
6620
7885
  tusUploads = new Map;
6621
7886
  mutationTail = Promise.resolve();
7887
+ imageTransforms = new ImageTransformCache;
6622
7888
  constructor(db, driver, config) {
6623
7889
  this.db = db;
6624
7890
  this.driver = driver;
@@ -6661,15 +7927,21 @@ class StorageHandler {
6661
7927
  const bucket2 = parts[3];
6662
7928
  const key2 = parts.slice(4).join("/");
6663
7929
  if (kind === "public" && (method === "GET" || method === "HEAD")) {
6664
- const source = await this.downloadPublic(bucket2, key2, false);
7930
+ const source = await this.loadPublicObject(bucket2, key2);
7931
+ if (source instanceof Response)
7932
+ return source;
6665
7933
  return await this.transformImageResponse(source, url, method === "HEAD");
6666
7934
  }
6667
7935
  if (kind === "authenticated" && (method === "GET" || method === "HEAD")) {
6668
- const source = await this.download(ctx, bucket2, key2, false);
7936
+ const source = await this.loadAuthenticatedObject(ctx, bucket2, key2);
7937
+ if (source instanceof Response)
7938
+ return source;
6669
7939
  return await this.transformImageResponse(source, url, method === "HEAD");
6670
7940
  }
6671
7941
  if (kind === "sign" && method === "GET") {
6672
- const source = await this.redeemSignedUrl(url, bucket2, key2);
7942
+ const source = await this.loadSignedObject(url, bucket2, key2);
7943
+ if (source instanceof Response)
7944
+ return source;
6673
7945
  return await this.transformImageResponse(source, url, false);
6674
7946
  }
6675
7947
  return storageError(404, "not_found", `unknown render endpoint: ${rest}`);
@@ -6882,21 +8154,31 @@ class StorageHandler {
6882
8154
  throw e;
6883
8155
  }
6884
8156
  }
6885
- async transformImageResponse(source, url, head) {
6886
- if (!source.ok)
6887
- return source;
8157
+ async transformImageResponse(row, url, head) {
6888
8158
  const parsed = parseImageTransform(url.searchParams);
6889
8159
  if (!parsed.ok)
6890
8160
  return storageError(parsed.status, parsed.error, parsed.message);
6891
- const result = await transformImage(new Uint8Array(await source.arrayBuffer()), parsed.value);
8161
+ let result;
8162
+ try {
8163
+ result = await this.imageTransforms.getOrTransform(imageTransformCacheKey(row.version, parsed.value), async () => {
8164
+ const source = await this.readObjectSource(row);
8165
+ if (source === null)
8166
+ throw new StorageObjectMissingError;
8167
+ return transformImage(source, parsed.value, objectSize(row));
8168
+ });
8169
+ } catch (error) {
8170
+ if (error instanceof StorageObjectMissingError) {
8171
+ return storageError(404, "not_found", "Object not found");
8172
+ }
8173
+ throw error;
8174
+ }
6892
8175
  if (!result.ok)
6893
8176
  return storageError(result.status, result.error, result.message);
6894
- const headers = new Headers(source.headers);
8177
+ const headers = objectHeaders(row, result.bytes.length);
6895
8178
  headers.delete("content-disposition");
6896
8179
  headers.delete("etag");
6897
8180
  headers.set("content-type", result.contentType);
6898
- headers.set("content-length", String(result.bytes.length));
6899
- return new Response(head ? null : result.bytes, { status: source.status, headers });
8181
+ return new Response(head ? null : result.bytes, { status: 200, headers });
6900
8182
  }
6901
8183
  async persistObject(ctx, bucketId, key, bytes, contentType, cacheControl, upsert) {
6902
8184
  const metadata = objectMetadata(bytes.length, contentType, cacheControl);
@@ -7166,13 +8448,25 @@ class StorageHandler {
7166
8448
  return null;
7167
8449
  }
7168
8450
  async download(ctx, bucketId, key, head) {
8451
+ const row = await this.loadAuthenticatedObject(ctx, bucketId, key);
8452
+ if (row instanceof Response)
8453
+ return row;
8454
+ return this.serveObject(row, head);
8455
+ }
8456
+ async loadAuthenticatedObject(ctx, bucketId, key) {
7169
8457
  const res = await this.db.withContext(ctx, (q) => q(`select * from storage.objects where bucket_id = $1 and name = $2`, [bucketId, key]));
7170
8458
  const row = res.rows[0];
7171
8459
  if (!row)
7172
8460
  return storageError(404, "not_found", "Object not found");
7173
- return this.serveObject(row, head);
8461
+ return row;
7174
8462
  }
7175
8463
  async downloadPublic(bucketId, key, head) {
8464
+ const row = await this.loadPublicObject(bucketId, key);
8465
+ if (row instanceof Response)
8466
+ return row;
8467
+ return this.serveObject(row, head);
8468
+ }
8469
+ async loadPublicObject(bucketId, key) {
7176
8470
  const bucket = await this.loadBucket(bucketId);
7177
8471
  if (!bucket?.public)
7178
8472
  return storageError(400, "not_found", "Bucket is not public");
@@ -7183,24 +8477,16 @@ class StorageHandler {
7183
8477
  const row = res.rows[0];
7184
8478
  if (!row)
7185
8479
  return storageError(404, "not_found", "Object not found");
7186
- return this.serveObject(row, head);
8480
+ return row;
7187
8481
  }
7188
8482
  async serveObject(row, head) {
7189
8483
  const bytes = await this.readObjectBytes(row);
7190
8484
  if (bytes === null)
7191
8485
  return storageError(404, "not_found", "Object not found");
7192
- const meta = row.metadata ?? {};
7193
- const contentType = String(meta.mimetype ?? "application/octet-stream");
7194
- const headers = {
7195
- "content-type": contentType,
7196
- "content-length": String(bytes.length),
7197
- "cache-control": String(meta.cacheControl ?? "no-cache"),
7198
- etag: String(meta.eTag ?? '""'),
7199
- "last-modified": new Date(String(meta.lastModified ?? Date.now())).toUTCString(),
7200
- "x-content-type-options": "nosniff"
7201
- };
8486
+ const contentType = String(row.metadata?.mimetype ?? "application/octet-stream");
8487
+ const headers = objectHeaders(row, bytes.length);
7202
8488
  if (isRenderableActiveType(contentType))
7203
- headers["content-disposition"] = "attachment";
8489
+ headers.set("content-disposition", "attachment");
7204
8490
  return new Response(head ? null : bytes, { status: 200, headers });
7205
8491
  }
7206
8492
  async removeObjects(req, ctx, bucketId) {
@@ -7311,9 +8597,11 @@ class StorageHandler {
7311
8597
  throw error;
7312
8598
  }
7313
8599
  async readObjectBytes(row) {
7314
- if (isVersionedObjectVersion(row.version))
7315
- return this.driver.get(objectVersionKey(row.version));
7316
- return this.driver.get(legacyObjectKey(row));
8600
+ return this.driver.get(storageKey(row));
8601
+ }
8602
+ async readObjectSource(row) {
8603
+ const key = storageKey(row);
8604
+ return this.driver.getBlob ? this.driver.getBlob(key) : this.driver.get(key);
7317
8605
  }
7318
8606
  async cleanupObjectRows(rows) {
7319
8607
  const keys = rows.flatMap((row) => [
@@ -7416,6 +8704,12 @@ class StorageHandler {
7416
8704
  return json3(200, out);
7417
8705
  }
7418
8706
  async redeemSignedUrl(url, bucketId, key) {
8707
+ const row = await this.loadSignedObject(url, bucketId, key);
8708
+ if (row instanceof Response)
8709
+ return row;
8710
+ return this.serveObject(row, false);
8711
+ }
8712
+ async loadSignedObject(url, bucketId, key) {
7419
8713
  const token = url.searchParams.get("token") ?? "";
7420
8714
  const claims = await verifyJwt(token, this.config.jwtSecret);
7421
8715
  if (!claims || claims.url !== `${bucketId}/${key}` || claims.type !== "download") {
@@ -7428,7 +8722,7 @@ class StorageHandler {
7428
8722
  const row = res.rows[0];
7429
8723
  if (!row)
7430
8724
  return storageError(404, "not_found", "Object not found");
7431
- return this.serveObject(row, false);
8725
+ return row;
7432
8726
  }
7433
8727
  async signUploadUrl(ctx, bucketId, key) {
7434
8728
  const keyErr = invalidObjectKey(key);
@@ -7486,6 +8780,9 @@ function objectJson(r) {
7486
8780
 
7487
8781
  class StorageValidationError extends Error {
7488
8782
  }
8783
+
8784
+ class StorageObjectMissingError extends Error {
8785
+ }
7489
8786
  function parseSizeLimit(v) {
7490
8787
  if (v === null || v === undefined || v === "")
7491
8788
  return null;
@@ -7508,14 +8805,32 @@ function objectMetadata(size, contentType, cacheControl) {
7508
8805
  httpStatusCode: 200
7509
8806
  };
7510
8807
  }
8808
+ function objectHeaders(row, contentLength) {
8809
+ const metadata = row.metadata ?? {};
8810
+ return new Headers({
8811
+ "content-type": String(metadata.mimetype ?? "application/octet-stream"),
8812
+ "content-length": String(contentLength),
8813
+ "cache-control": String(metadata.cacheControl ?? "no-cache"),
8814
+ etag: String(metadata.eTag ?? '""'),
8815
+ "last-modified": new Date(String(metadata.lastModified ?? Date.now())).toUTCString(),
8816
+ "x-content-type-options": "nosniff"
8817
+ });
8818
+ }
8819
+ function objectSize(row) {
8820
+ const size = Number(row.metadata?.size);
8821
+ return Number.isFinite(size) && size >= 0 ? size : undefined;
8822
+ }
7511
8823
  function objectVersionKey(version) {
7512
8824
  return `.supacloud-lite/objects/${version}`;
7513
8825
  }
8826
+ function storageKey(row) {
8827
+ return isVersionedObjectVersion(row.version) ? objectVersionKey(row.version) : legacyObjectKey(row);
8828
+ }
7514
8829
  function createObjectVersion() {
7515
- return `${OBJECT_VERSION_PREFIX}${crypto.randomUUID()}`;
8830
+ return `${OBJECT_VERSION_PREFIX2}${crypto.randomUUID()}`;
7516
8831
  }
7517
8832
  function isVersionedObjectVersion(version) {
7518
- return version?.startsWith(OBJECT_VERSION_PREFIX) ?? false;
8833
+ return version?.startsWith(OBJECT_VERSION_PREFIX2) ?? false;
7519
8834
  }
7520
8835
  function isInternalStorageBucket(bucketId) {
7521
8836
  return bucketId === INTERNAL_STORAGE_BUCKET || bucketId.startsWith(`${INTERNAL_STORAGE_BUCKET}/`);
@@ -8638,7 +9953,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
8638
9953
  const res = await safeHandle(req);
8639
9954
  try {
8640
9955
  const p = new URL(req.url).pathname;
8641
- if (p !== "/health" && p !== "/") {
9956
+ if (p !== "/health" && p !== "/" && p !== "/auth/v1/health") {
8642
9957
  const level = res.status >= 500 ? "error" : res.status >= 400 ? "warn" : "info";
8643
9958
  logs.push(`${req.method} ${p} \u2192 ${res.status}`, level);
8644
9959
  }
@@ -8767,6 +10082,21 @@ class S3StorageDriver {
8767
10082
  throw error;
8768
10083
  }
8769
10084
  }
10085
+ async getBlob(key) {
10086
+ const file = this.client.file(this.objectKey(key));
10087
+ if (!await file.exists())
10088
+ return null;
10089
+ if (typeof file.arrayBuffer === "function")
10090
+ return file;
10091
+ try {
10092
+ const bytes = Uint8Array.from(await file.bytes());
10093
+ return new Blob([bytes.buffer]);
10094
+ } catch (error) {
10095
+ if (isNotFoundError(error))
10096
+ return null;
10097
+ throw error;
10098
+ }
10099
+ }
8770
10100
  async delete(key) {
8771
10101
  await this.client.file(this.objectKey(key)).delete();
8772
10102
  }
@@ -8816,6 +10146,10 @@ class FsStorageDriver {
8816
10146
  throw e;
8817
10147
  }
8818
10148
  }
10149
+ async getBlob(key) {
10150
+ const file = Bun.file(this.resolve(key));
10151
+ return await file.exists() ? file : null;
10152
+ }
8819
10153
  async delete(key) {
8820
10154
  await rm(this.resolve(key), { force: true });
8821
10155
  }
@@ -8870,20 +10204,787 @@ async function serveBun(backend, opts = {}) {
8870
10204
  }
8871
10205
  };
8872
10206
  }
10207
+ // src/runtime/node/native/engine.ts
10208
+ import { execFileSync, spawn } from "child_process";
10209
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
10210
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10211
+ import { writeFile as writeFile2 } from "fs/promises";
10212
+ import { homedir, tmpdir } from "os";
10213
+ import { join as join2 } from "path";
10214
+ import { extract as extractTar } from "tar";
10215
+
10216
+ // src/runtime/node/native/wire.ts
10217
+ import { createConnection } from "net";
10218
+ import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
10219
+
10220
+ class PgWireError extends Error {
10221
+ code;
10222
+ detail;
10223
+ hint;
10224
+ severity;
10225
+ constructor(fields) {
10226
+ super(fields.get("M") ?? "postgres error");
10227
+ this.code = fields.get("C");
10228
+ this.detail = fields.get("D");
10229
+ this.hint = fields.get("H");
10230
+ this.severity = fields.get("S");
10231
+ }
10232
+ }
10233
+
10234
+ class PgWireClient {
10235
+ socket;
10236
+ buffer = Buffer.alloc(0);
10237
+ pending = null;
10238
+ queue = Promise.resolve();
10239
+ closed = false;
10240
+ onNotification = null;
10241
+ static async connect(opts) {
10242
+ const client = new PgWireClient;
10243
+ await client.open(opts);
10244
+ return client;
10245
+ }
10246
+ open(opts) {
10247
+ return new Promise((resolve2, reject) => {
10248
+ this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
10249
+ this.socket.on("error", (e) => {
10250
+ if (this.pending)
10251
+ this.pending.reject(e);
10252
+ reject(e);
10253
+ });
10254
+ this.socket.on("close", () => {
10255
+ this.closed = true;
10256
+ this.pending?.reject(new Error("connection closed"));
10257
+ });
10258
+ this.socket.on("connect", () => {
10259
+ const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
10260
+ const body = Buffer.from(params, "utf8");
10261
+ const msg = Buffer.alloc(8 + body.length);
10262
+ msg.writeInt32BE(8 + body.length, 0);
10263
+ msg.writeInt32BE(196608, 4);
10264
+ body.copy(msg, 8);
10265
+ this.socket.write(msg);
10266
+ });
10267
+ let clientNonce = "";
10268
+ let clientFirstBare = "";
10269
+ let serverSignature = "";
10270
+ const needPassword = () => {
10271
+ if (opts.password == null) {
10272
+ reject(new Error("the server requested a password but none was provided"));
10273
+ return false;
10274
+ }
10275
+ return true;
10276
+ };
10277
+ const startupHandler = (chunk) => {
10278
+ this.buffer = Buffer.concat([this.buffer, chunk]);
10279
+ let msg;
10280
+ while ((msg = this.nextMessage()) !== null) {
10281
+ const [type, payload] = msg;
10282
+ if (type === 82) {
10283
+ const code = payload.readInt32BE(0);
10284
+ if (code === 0) {} else if (code === 3) {
10285
+ if (!needPassword())
10286
+ return;
10287
+ this.socket.write(message(112, cstring(opts.password)));
10288
+ } else if (code === 5) {
10289
+ if (!needPassword())
10290
+ return;
10291
+ const salt = payload.subarray(4, 8);
10292
+ const inner = md5Hex(Buffer.from(opts.password + opts.user, "utf8"));
10293
+ const token = "md5" + md5Hex(Buffer.concat([Buffer.from(inner, "utf8"), salt]));
10294
+ this.socket.write(message(112, cstring(token)));
10295
+ } else if (code === 10) {
10296
+ if (!needPassword())
10297
+ return;
10298
+ const mechs = payload.subarray(4).toString("utf8").split("\x00").filter(Boolean);
10299
+ if (!mechs.includes("SCRAM-SHA-256")) {
10300
+ reject(new Error(`no supported SASL mechanism (server offered: ${mechs.join(", ")})`));
10301
+ return;
10302
+ }
10303
+ clientNonce = randomBytes(18).toString("base64");
10304
+ clientFirstBare = `n=,r=${clientNonce}`;
10305
+ const initial = Buffer.from(`n,,${clientFirstBare}`, "utf8");
10306
+ this.socket.write(message(112, Buffer.concat([cstring("SCRAM-SHA-256"), int32(initial.length), initial])));
10307
+ } else if (code === 11) {
10308
+ const serverFirst = payload.subarray(4).toString("utf8");
10309
+ const attrs = scramAttrs(serverFirst);
10310
+ if (!attrs.r?.startsWith(clientNonce)) {
10311
+ reject(new Error("SCRAM: server nonce does not extend client nonce"));
10312
+ return;
10313
+ }
10314
+ const salt = Buffer.from(attrs.s, "base64");
10315
+ const iterations = parseInt(attrs.i, 10);
10316
+ const saltedPassword = pbkdf2Sync(opts.password, salt, iterations, 32, "sha256");
10317
+ const clientKey = hmac(saltedPassword, "Client Key");
10318
+ const storedKey = sha256(clientKey);
10319
+ const finalNoProof = `c=biws,r=${attrs.r}`;
10320
+ const authMessage = `${clientFirstBare},${serverFirst},${finalNoProof}`;
10321
+ const clientSignature = hmac(storedKey, authMessage);
10322
+ const proof = xorBuffers(clientKey, clientSignature);
10323
+ serverSignature = hmac(hmac(saltedPassword, "Server Key"), authMessage).toString("base64");
10324
+ const clientFinal = `${finalNoProof},p=${proof.toString("base64")}`;
10325
+ this.socket.write(message(112, Buffer.from(clientFinal, "utf8")));
10326
+ } else if (code === 12) {
10327
+ const v = scramAttrs(payload.subarray(4).toString("utf8")).v;
10328
+ if (v && serverSignature && v !== serverSignature) {
10329
+ reject(new Error("SCRAM: server signature verification failed"));
10330
+ return;
10331
+ }
10332
+ } else {
10333
+ reject(new Error(`unsupported auth method ${code}`));
10334
+ return;
10335
+ }
10336
+ } else if (type === 69) {
10337
+ reject(new PgWireError(parseErrorFields(payload)));
10338
+ return;
10339
+ } else if (type === 90) {
10340
+ this.socket.off("data", startupHandler);
10341
+ this.socket.on("data", (c) => {
10342
+ this.buffer = Buffer.concat([this.buffer, c]);
10343
+ this.processMessages();
10344
+ });
10345
+ resolve2();
10346
+ return;
10347
+ }
10348
+ }
10349
+ };
10350
+ this.socket.on("data", startupHandler);
10351
+ });
10352
+ }
10353
+ run(send) {
10354
+ const op = this.queue.then(() => new Promise((resolve2, reject) => {
10355
+ if (this.closed)
10356
+ return reject(new Error("connection closed"));
10357
+ this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
10358
+ send();
10359
+ }));
10360
+ this.queue = op.catch(() => {});
10361
+ return op;
10362
+ }
10363
+ async exec(sql) {
10364
+ return this.run(() => this.socket.write(message(81, cstring(sql))));
10365
+ }
10366
+ async query(sql, params = []) {
10367
+ const results = await this.run(() => {
10368
+ const parse = message(80, Buffer.concat([cstring(""), cstring(sql), int16(0)]));
10369
+ const paramBufs = [int16(0), int16(params.length)];
10370
+ for (const p of params) {
10371
+ if (p === null || p === undefined) {
10372
+ paramBufs.push(int32(-1));
10373
+ } else {
10374
+ const b = Buffer.from(String(p), "utf8");
10375
+ paramBufs.push(int32(b.length), b);
10376
+ }
10377
+ }
10378
+ paramBufs.push(int16(0));
10379
+ const bind = message(66, Buffer.concat([cstring(""), cstring(""), ...paramBufs]));
10380
+ const describe = message(68, Buffer.concat([Buffer.from("P"), cstring("")]));
10381
+ const execute = message(69, Buffer.concat([cstring(""), int32(0)]));
10382
+ const sync = message(83, Buffer.alloc(0));
10383
+ this.socket.write(Buffer.concat([parse, bind, describe, execute, sync]));
10384
+ });
10385
+ return results[0] ?? { rows: [] };
10386
+ }
10387
+ close() {
10388
+ return new Promise((resolve2) => {
10389
+ if (this.closed)
10390
+ return resolve2();
10391
+ this.socket.write(message(88, Buffer.alloc(0)));
10392
+ this.socket.end(() => resolve2());
10393
+ });
10394
+ }
10395
+ nextMessage() {
10396
+ if (this.buffer.length < 5)
10397
+ return null;
10398
+ const type = this.buffer[0];
10399
+ const length = this.buffer.readInt32BE(1);
10400
+ if (this.buffer.length < 1 + length)
10401
+ return null;
10402
+ const payload = this.buffer.subarray(5, 1 + length);
10403
+ this.buffer = this.buffer.subarray(1 + length);
10404
+ return [type, Buffer.from(payload)];
10405
+ }
10406
+ processMessages() {
10407
+ let msg;
10408
+ while ((msg = this.nextMessage()) !== null) {
10409
+ const [type, payload] = msg;
10410
+ const p = this.pending;
10411
+ switch (type) {
10412
+ case 84: {
10413
+ if (!p)
10414
+ break;
10415
+ const count = payload.readInt16BE(0);
10416
+ let off = 2;
10417
+ const columns = [];
10418
+ for (let i = 0;i < count; i++) {
10419
+ const end = payload.indexOf(0, off);
10420
+ const name = payload.toString("utf8", off, end);
10421
+ off = end + 1;
10422
+ const typeOid = payload.readInt32BE(off + 6);
10423
+ off += 18;
10424
+ columns.push({ name, typeOid });
10425
+ }
10426
+ p.columns = columns;
10427
+ break;
10428
+ }
10429
+ case 68: {
10430
+ if (!p)
10431
+ break;
10432
+ const count = payload.readInt16BE(0);
10433
+ let off = 2;
10434
+ const row = {};
10435
+ for (let i = 0;i < count; i++) {
10436
+ const len = payload.readInt32BE(off);
10437
+ off += 4;
10438
+ let value = null;
10439
+ if (len >= 0) {
10440
+ value = decodeValue(payload.toString("utf8", off, off + len), p.columns[i]?.typeOid ?? 25);
10441
+ off += len;
10442
+ }
10443
+ row[p.columns[i]?.name ?? `col${i}`] = value;
10444
+ }
10445
+ if (p.results.length === 0)
10446
+ p.results.push({ rows: [] });
10447
+ p.results[p.results.length - 1].rows.push(row);
10448
+ break;
10449
+ }
10450
+ case 67: {
10451
+ if (!p)
10452
+ break;
10453
+ const tag = payload.toString("utf8", 0, payload.length - 1);
10454
+ const parts = tag.split(" ");
10455
+ const affected = parseInt(parts[parts.length - 1], 10);
10456
+ if (p.results.length === 0)
10457
+ p.results.push({ rows: [] });
10458
+ const current = p.results[p.results.length - 1];
10459
+ if (!Number.isNaN(affected))
10460
+ current.affectedRows = affected;
10461
+ p.results.push({ rows: [] });
10462
+ p.columns = [];
10463
+ break;
10464
+ }
10465
+ case 69: {
10466
+ if (p)
10467
+ p.error = new PgWireError(parseErrorFields(payload));
10468
+ break;
10469
+ }
10470
+ case 65: {
10471
+ payload.readInt32BE(0);
10472
+ const channelEnd = payload.indexOf(0, 4);
10473
+ const channel = payload.toString("utf8", 4, channelEnd);
10474
+ const payloadEnd = payload.indexOf(0, channelEnd + 1);
10475
+ const body = payload.toString("utf8", channelEnd + 1, payloadEnd);
10476
+ this.onNotification?.(channel, body);
10477
+ break;
10478
+ }
10479
+ case 90: {
10480
+ if (!p)
10481
+ break;
10482
+ this.pending = null;
10483
+ if (p.error)
10484
+ p.reject(p.error);
10485
+ else {
10486
+ const results = p.results.filter((r, i) => i < p.results.length - 1 || r.rows.length > 0 || r.affectedRows !== undefined);
10487
+ p.resolve(results.length > 0 ? results : [{ rows: [] }]);
10488
+ }
10489
+ break;
10490
+ }
10491
+ }
10492
+ }
10493
+ }
10494
+ }
10495
+ var hmac = (key, data) => createHmac("sha256", key).update(data, "utf8").digest();
10496
+ var sha256 = (b) => createHash("sha256").update(b).digest();
10497
+ var md5Hex = (b) => createHash("md5").update(b).digest("hex");
10498
+ function xorBuffers(a, b) {
10499
+ const out = Buffer.alloc(a.length);
10500
+ for (let i = 0;i < a.length; i++)
10501
+ out[i] = a[i] ^ b[i];
10502
+ return out;
10503
+ }
10504
+ function scramAttrs(s) {
10505
+ const out = {};
10506
+ for (const part of s.split(",")) {
10507
+ const eq = part.indexOf("=");
10508
+ if (eq > 0)
10509
+ out[part.slice(0, eq)] = part.slice(eq + 1);
10510
+ }
10511
+ return out;
10512
+ }
10513
+ function message(type, body) {
10514
+ const out = Buffer.alloc(5 + body.length);
10515
+ out[0] = type;
10516
+ out.writeInt32BE(4 + body.length, 1);
10517
+ body.copy(out, 5);
10518
+ return out;
10519
+ }
10520
+ var cstring = (s) => Buffer.from(s + "\x00", "utf8");
10521
+ var int16 = (n) => {
10522
+ const b = Buffer.alloc(2);
10523
+ b.writeInt16BE(n);
10524
+ return b;
10525
+ };
10526
+ var int32 = (n) => {
10527
+ const b = Buffer.alloc(4);
10528
+ b.writeInt32BE(n);
10529
+ return b;
10530
+ };
10531
+ function parseErrorFields(payload) {
10532
+ const fields = new Map;
10533
+ let off = 0;
10534
+ while (off < payload.length && payload[off] !== 0) {
10535
+ const key = String.fromCharCode(payload[off]);
10536
+ const end = payload.indexOf(0, off + 1);
10537
+ fields.set(key, payload.toString("utf8", off + 1, end));
10538
+ off = end + 1;
10539
+ }
10540
+ return fields;
10541
+ }
10542
+ function decodeValue(text, oid) {
10543
+ switch (oid) {
10544
+ case 16:
10545
+ return text === "t";
10546
+ case 20: {
10547
+ const n = Number(text);
10548
+ return Number.isSafeInteger(n) ? n : text;
10549
+ }
10550
+ case 21:
10551
+ case 23:
10552
+ case 26:
10553
+ return Number(text);
10554
+ case 700:
10555
+ case 701:
10556
+ return Number(text);
10557
+ case 114:
10558
+ case 3802:
10559
+ return JSON.parse(text);
10560
+ case 1114:
10561
+ return new Date(text.replace(" ", "T") + "Z");
10562
+ case 1184: {
10563
+ let iso3 = text.replace(" ", "T");
10564
+ if (/[+-]\d\d$/.test(iso3))
10565
+ iso3 += ":00";
10566
+ return new Date(iso3);
10567
+ }
10568
+ case 1000:
10569
+ return parsePgArray(text).map((v) => v === "t");
10570
+ case 1007:
10571
+ return parsePgArray(text).map((v) => v === null ? null : Number(v));
10572
+ case 1016:
10573
+ return parsePgArray(text).map((v) => {
10574
+ if (v === null)
10575
+ return null;
10576
+ const n = Number(v);
10577
+ return Number.isSafeInteger(n) ? n : v;
10578
+ });
10579
+ case 1003:
10580
+ case 1009:
10581
+ case 1015:
10582
+ return parsePgArray(text);
10583
+ default:
10584
+ return text;
10585
+ }
10586
+ }
10587
+ function parsePgArray(text) {
10588
+ const out = [];
10589
+ if (text.length < 2)
10590
+ return out;
10591
+ let i = 1;
10592
+ while (i < text.length - 1) {
10593
+ if (text[i] === ",") {
10594
+ i++;
10595
+ continue;
10596
+ }
10597
+ if (text[i] === '"') {
10598
+ let value = "";
10599
+ i++;
10600
+ while (text[i] !== '"') {
10601
+ if (text[i] === "\\")
10602
+ i++;
10603
+ value += text[i++];
10604
+ }
10605
+ i++;
10606
+ out.push(value);
10607
+ } else {
10608
+ let value = "";
10609
+ while (i < text.length - 1 && text[i] !== ",")
10610
+ value += text[i++];
10611
+ out.push(value === "NULL" ? null : value);
10612
+ }
10613
+ }
10614
+ return out;
10615
+ }
10616
+
10617
+ // src/runtime/db/engine.ts
10618
+ class Mutex {
10619
+ tail = Promise.resolve();
10620
+ async lock() {
10621
+ let release;
10622
+ const next = new Promise((r) => release = r);
10623
+ const prev = this.tail;
10624
+ this.tail = this.tail.then(() => next);
10625
+ await prev;
10626
+ return release;
10627
+ }
10628
+ async run(fn) {
10629
+ const release = await this.lock();
10630
+ try {
10631
+ return await fn();
10632
+ } finally {
10633
+ release();
10634
+ }
10635
+ }
10636
+ }
10637
+
10638
+ // src/runtime/node/native/wire-engine.ts
10639
+ async function buildWireEngine(options) {
10640
+ const queryClient = await options.connect();
10641
+ let listenerClient;
10642
+ try {
10643
+ listenerClient = await options.connect();
10644
+ } catch (error) {
10645
+ await queryClient.close().catch(() => {});
10646
+ throw error;
10647
+ }
10648
+ const queryMutex = new Mutex;
10649
+ const listenerMutex = new Mutex;
10650
+ const listeners = new Map;
10651
+ listenerClient.onNotification = (channel, payload) => {
10652
+ for (const listener of listeners.get(channel) ?? [])
10653
+ listener(payload);
10654
+ };
10655
+ const transactionClient = {
10656
+ async query(sql, params) {
10657
+ const queryResult = await queryClient.query(sql, normalizeParams(params));
10658
+ return { rows: queryResult.rows, affectedRows: queryResult.affectedRows };
10659
+ },
10660
+ async exec(sql) {
10661
+ await queryClient.exec(sql);
10662
+ }
10663
+ };
10664
+ let closePromise = null;
10665
+ return {
10666
+ query(sql, params) {
10667
+ return queryMutex.run(() => transactionClient.query(sql, params));
10668
+ },
10669
+ exec(sql) {
10670
+ return queryMutex.run(() => transactionClient.exec(sql));
10671
+ },
10672
+ transaction(callback) {
10673
+ return queryMutex.run(async () => {
10674
+ await queryClient.exec("begin");
10675
+ try {
10676
+ const response = await callback(transactionClient);
10677
+ await queryClient.exec("commit");
10678
+ return response;
10679
+ } catch (error) {
10680
+ await queryClient.exec("rollback").catch(() => {});
10681
+ throw error;
10682
+ }
10683
+ });
10684
+ },
10685
+ async listen(channel, listener) {
10686
+ return listenerMutex.run(async () => {
10687
+ let channelListeners = listeners.get(channel);
10688
+ if (!channelListeners) {
10689
+ channelListeners = new Set;
10690
+ await listenerClient.exec(`listen "${channel.replaceAll('"', '""')}"`);
10691
+ listeners.set(channel, channelListeners);
10692
+ }
10693
+ channelListeners.add(listener);
10694
+ return () => {
10695
+ channelListeners.delete(listener);
10696
+ };
10697
+ });
10698
+ },
10699
+ close() {
10700
+ closePromise ??= closeWireEngine(queryClient, listenerClient, options.onClose);
10701
+ return closePromise;
10702
+ }
10703
+ };
10704
+ }
10705
+ async function closeWireEngine(queryClient, listenerClient, onClose) {
10706
+ const closeResults = await Promise.allSettled([queryClient.close(), listenerClient.close()]);
10707
+ let engineCleanupError;
10708
+ try {
10709
+ await onClose?.();
10710
+ } catch (error) {
10711
+ engineCleanupError = error;
10712
+ }
10713
+ const connectionErrors = closeResults.flatMap((closeResult) => closeResult.status === "rejected" ? [closeResult.reason] : []);
10714
+ if (engineCleanupError !== undefined)
10715
+ connectionErrors.push(engineCleanupError);
10716
+ if (connectionErrors.length > 0)
10717
+ throw new AggregateError(connectionErrors, "native database cleanup failed");
10718
+ }
10719
+ function normalizeParams(params) {
10720
+ return params?.map((parameter) => {
10721
+ if (parameter === null || parameter === undefined)
10722
+ return null;
10723
+ if (Array.isArray(parameter))
10724
+ return toPgArrayLiteral(parameter);
10725
+ if (parameter instanceof Date)
10726
+ return parameter.toISOString();
10727
+ if (typeof parameter === "object")
10728
+ return JSON.stringify(parameter);
10729
+ return parameter;
10730
+ });
10731
+ }
10732
+ function toPgArrayLiteral(array) {
10733
+ const encoded = array.map((element) => {
10734
+ if (element === null || element === undefined)
10735
+ return "NULL";
10736
+ if (Array.isArray(element))
10737
+ return toPgArrayLiteral(element);
10738
+ if (typeof element === "number" || typeof element === "boolean")
10739
+ return String(element);
10740
+ const text = typeof element === "object" ? JSON.stringify(element) : String(element);
10741
+ return `"${text.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
10742
+ });
10743
+ return `{${encoded.join(",")}}`;
10744
+ }
10745
+
10746
+ // src/runtime/node/native/engine.ts
10747
+ var DEFAULT_PG_VERSION = "17.7.0";
10748
+ var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
10749
+ function isNativeEngineSupported() {
10750
+ return (process.platform === "darwin" || process.platform === "linux") && (process.arch === "arm64" || process.arch === "x64") && (process.platform !== "linux" || isGlibcLinux());
10751
+ }
10752
+ function isGlibcLinux() {
10753
+ try {
10754
+ const version = execFileSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
10755
+ return /glibc|gnu libc/i.test(version);
10756
+ } catch {
10757
+ return false;
10758
+ }
10759
+ }
10760
+ function target() {
10761
+ const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
10762
+ if (!arch)
10763
+ throw new Error(`unsupported architecture for native engine: ${process.arch}`);
10764
+ if (process.platform === "darwin")
10765
+ return `${arch}-apple-darwin`;
10766
+ if (process.platform === "linux")
10767
+ return `${arch}-unknown-linux-gnu`;
10768
+ throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
10769
+ }
10770
+ function isCompleteInstall(dir) {
10771
+ return existsSync(join2(dir, "bin", "postgres")) && existsSync(join2(dir, "share", "postgres.bki"));
10772
+ }
10773
+ var PINNED_SHA256 = {
10774
+ "postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
10775
+ "postgresql-17.7.0-aarch64-unknown-linux-gnu": "89cc2f089880cc8e5e6b7a29387829ec4e4779427855bc0b9fa187c8fce33c8b",
10776
+ "postgresql-17.7.0-x86_64-apple-darwin": "0dd8c25173524bad4ae8ef6b970da1ac40f4c1f231150c416ccb8cd06feff8f2",
10777
+ "postgresql-17.7.0-aarch64-apple-darwin": "727ac08d20a704014a0d51eb3300aa0c8e292c1cf0a1c99d4f4b1002e1420220"
10778
+ };
10779
+ async function verifyTarball(tarball, key, url) {
10780
+ const actual = createHash2("sha256").update(readFileSync(tarball)).digest("hex");
10781
+ const pinned = PINNED_SHA256[key];
10782
+ if (pinned) {
10783
+ if (actual !== pinned) {
10784
+ throw new Error(`postgres binary checksum mismatch for ${key}: expected ${pinned}, got ${actual}`);
10785
+ }
10786
+ return;
10787
+ }
10788
+ const res = await fetchRelease(`${url}.sha256`);
10789
+ if (!res.ok)
10790
+ throw new Error(`could not fetch checksum for ${key}: HTTP ${res.status}`);
10791
+ const expected = (await res.text()).trim().split(/\s+/)[0].toLowerCase();
10792
+ if (!/^[0-9a-f]{64}$/.test(expected))
10793
+ throw new Error(`malformed published checksum for ${key}`);
10794
+ if (actual !== expected) {
10795
+ throw new Error(`postgres binary checksum mismatch for ${key}: expected ${expected}, got ${actual}`);
10796
+ }
10797
+ }
10798
+ async function ensurePostgres(version = DEFAULT_PG_VERSION, cacheDir, log) {
10799
+ const t = target();
10800
+ const root = cacheDir ?? join2(homedir(), ".cache", "supacloud-lite");
10801
+ const dir = join2(root, `postgresql-${version}-${t}`);
10802
+ if (isCompleteInstall(dir))
10803
+ return dir;
10804
+ const url = `https://github.com/theseus-rs/postgresql-binaries/releases/download/${version}/postgresql-${version}-${t}.tar.gz`;
10805
+ mkdirSync(root, { recursive: true });
10806
+ const uniq = `${process.pid}-${randomBytes2(6).toString("hex")}`;
10807
+ const tarball = join2(root, `pg-${version}-${uniq}.tar.gz`);
10808
+ const tmpDir = join2(root, `.tmp-${version}-${t}-${uniq}`);
10809
+ try {
10810
+ if (isCompleteInstall(dir))
10811
+ return dir;
10812
+ log?.(`downloading postgres ${version} (${t})\u2026`);
10813
+ const res = await fetchRelease(url);
10814
+ if (!res.ok)
10815
+ throw new Error(`failed to download ${url}: HTTP ${res.status}`);
10816
+ await writeFile2(tarball, Buffer.from(await res.arrayBuffer()));
10817
+ await verifyTarball(tarball, `postgresql-${version}-${t}`, url);
10818
+ mkdirSync(tmpDir, { recursive: true });
10819
+ await extractTar({ cwd: tmpDir, file: tarball, gzip: true, preserveOwner: false, strict: true, strip: 1 });
10820
+ if (!isCompleteInstall(tmpDir))
10821
+ throw new Error("postgres archive extracted incompletely");
10822
+ try {
10823
+ renameSync(tmpDir, dir);
10824
+ } catch {
10825
+ if (!isCompleteInstall(dir)) {
10826
+ rmSync(dir, { recursive: true, force: true });
10827
+ renameSync(tmpDir, dir);
10828
+ }
10829
+ }
10830
+ log?.(`postgres installed to ${dir}`);
10831
+ return dir;
10832
+ } finally {
10833
+ rmSync(tarball, { force: true });
10834
+ rmSync(tmpDir, { recursive: true, force: true });
10835
+ }
10836
+ }
10837
+ async function fetchRelease(url) {
10838
+ let lastError;
10839
+ for (let attempt = 1;attempt <= 3; attempt++) {
10840
+ try {
10841
+ const response = await fetch(url);
10842
+ if (response.ok || response.status < 500)
10843
+ return response;
10844
+ lastError = new Error(`failed to download ${url}: HTTP ${response.status}`);
10845
+ } catch (error) {
10846
+ lastError = error;
10847
+ }
10848
+ if (attempt < 3)
10849
+ await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
10850
+ }
10851
+ throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
10852
+ }
10853
+ var TUNED_CONF = `
10854
+ # supacloud-lite: memory-lean settings for an embedded, single-app Postgres
10855
+ listen_addresses = ''
10856
+ shared_buffers = 16MB
10857
+ dynamic_shared_memory_type = posix
10858
+ max_connections = 10
10859
+ wal_level = minimal
10860
+ max_wal_senders = 0
10861
+ logging_collector = off
10862
+ `;
10863
+ async function createNativeEngine(opts) {
10864
+ const releaseLock = await acquireDataDirLock(opts.dataDir, "native PostgreSQL");
10865
+ let socketDirectory;
10866
+ let postgres;
10867
+ let removeExitHandler;
10868
+ try {
10869
+ const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log);
10870
+ const bin = (name) => join2(installDir, "bin", name);
10871
+ if (!existsSync(join2(opts.dataDir, "PG_VERSION"))) {
10872
+ mkdirSync(opts.dataDir, { recursive: true });
10873
+ try {
10874
+ execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
10875
+ stdio: "pipe"
10876
+ });
10877
+ } catch (error) {
10878
+ const stderr = error.stderr?.toString() ?? "";
10879
+ throw new Error(`initdb failed:
10880
+ ${stderr || error.message}`);
10881
+ }
10882
+ appendFileSync(join2(opts.dataDir, "postgresql.conf"), TUNED_CONF);
10883
+ }
10884
+ removeStalePidFile(join2(opts.dataDir, "postmaster.pid"));
10885
+ socketDirectory = mkdtempSync(join2(tmpdir(), "scl-"));
10886
+ chmodSync(socketDirectory, 448);
10887
+ postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
10888
+ stdio: ["ignore", "ignore", "pipe"],
10889
+ detached: false
10890
+ });
10891
+ let postgresExited = false;
10892
+ let postgresStderr = "";
10893
+ postgres.stderr?.on("data", (chunk) => {
10894
+ postgresStderr = (postgresStderr + chunk.toString()).slice(-4000);
10895
+ });
10896
+ postgres.on("exit", () => postgresExited = true);
10897
+ const killPostgres = () => {
10898
+ if (!postgresExited)
10899
+ postgres?.kill("SIGTERM");
10900
+ };
10901
+ process.once("exit", killPostgres);
10902
+ removeExitHandler = () => process.off("exit", killPostgres);
10903
+ const socketPath = join2(socketDirectory, ".s.PGSQL.5432");
10904
+ const connect = async () => {
10905
+ const deadline = Date.now() + 20000;
10906
+ while (Date.now() <= deadline) {
10907
+ try {
10908
+ return await PgWireClient.connect({ socketPath, user: "postgres", database: "postgres" });
10909
+ } catch (error) {
10910
+ if (postgresExited) {
10911
+ const detail = postgresStderr.trim();
10912
+ throw new Error(`embedded postgres failed to start${detail ? `:
10913
+ ${detail}` : " (no output)"}
10914
+
10915
+ ` + `data dir: ${opts.dataDir}
10916
+ ` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
10917
+ }
10918
+ await new Promise((resolve2) => setTimeout(resolve2, 150));
10919
+ }
10920
+ }
10921
+ throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
10922
+ };
10923
+ return await buildWireEngine({
10924
+ connect,
10925
+ onClose: async () => {
10926
+ removeExitHandler?.();
10927
+ await stopPostgres(postgres, () => postgresExited);
10928
+ rmSync(socketDirectory, { recursive: true, force: true });
10929
+ await releaseLock();
10930
+ }
10931
+ });
10932
+ } catch (error) {
10933
+ removeExitHandler?.();
10934
+ if (postgres)
10935
+ await stopPostgres(postgres, () => postgres.exitCode !== null);
10936
+ if (socketDirectory)
10937
+ rmSync(socketDirectory, { recursive: true, force: true });
10938
+ await releaseLock();
10939
+ throw error;
10940
+ }
10941
+ }
10942
+ async function stopPostgres(postgres, hasExited) {
10943
+ if (hasExited())
10944
+ return;
10945
+ postgres.kill("SIGINT");
10946
+ await new Promise((resolve2) => {
10947
+ const killTimeout = setTimeout(() => {
10948
+ postgres.kill("SIGKILL");
10949
+ resolve2();
10950
+ }, 5000);
10951
+ postgres.once("exit", () => {
10952
+ clearTimeout(killTimeout);
10953
+ resolve2();
10954
+ });
10955
+ });
10956
+ }
10957
+ function removeStalePidFile(pidPath) {
10958
+ if (!existsSync(pidPath))
10959
+ return;
10960
+ try {
10961
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
10962
+ `)[0]?.trim() ?? "", 10);
10963
+ if (!pid) {
10964
+ rmSync(pidPath, { force: true });
10965
+ return;
10966
+ }
10967
+ try {
10968
+ process.kill(pid, 0);
10969
+ } catch {
10970
+ rmSync(pidPath, { force: true });
10971
+ }
10972
+ } catch {}
10973
+ }
8873
10974
  // src/project-runtime.ts
8874
- import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
8875
- import { dirname as dirname3, isAbsolute, join as join6, parse, relative, resolve as resolve2 } from "path";
10975
+ import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
10976
+ import { dirname as dirname4, isAbsolute, join as join7, parse, relative, resolve as resolve2 } from "path";
8876
10977
 
8877
10978
  // src/runtime/node/config-toml.ts
8878
- import { readFileSync } from "fs";
8879
- import { join as join2 } from "path";
10979
+ import { readFileSync as readFileSync2 } from "fs";
10980
+ import { join as join3 } from "path";
8880
10981
  function emptyTable() {
8881
10982
  return { values: new Map, children: new Map };
8882
10983
  }
8883
10984
  function loadConfigToml(projectDir, env = process.env) {
8884
10985
  let text;
8885
10986
  try {
8886
- text = readFileSync(join2(projectDir, "supabase", "config.toml"), "utf8");
10987
+ text = readFileSync2(join3(projectDir, "supabase", "config.toml"), "utf8");
8887
10988
  } catch {
8888
10989
  return emptyTable();
8889
10990
  }
@@ -9248,16 +11349,16 @@ function readFunctions(root) {
9248
11349
  }
9249
11350
 
9250
11351
  // src/runtime/node/load-functions.ts
9251
- import { readdir, readFile as readFile4, realpath, rm as rm2, stat } from "fs/promises";
9252
- import { join as join4 } from "path";
11352
+ import { readdir, readFile as readFile4, realpath, rm as rm3, stat } from "fs/promises";
11353
+ import { dirname as dirname3, join as join5 } from "path";
9253
11354
  import { pathToFileURL } from "url";
9254
11355
 
9255
11356
  // src/runtime/node/bundle-function.ts
9256
- import { createHash } from "crypto";
9257
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
9258
- import { existsSync } from "fs";
9259
- import { tmpdir } from "os";
9260
- import { join as join3 } from "path";
11357
+ import { createHash as createHash3 } from "crypto";
11358
+ import { mkdir as mkdir3, readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
11359
+ import { existsSync as existsSync2 } from "fs";
11360
+ import { tmpdir as tmpdir2 } from "os";
11361
+ import { join as join4 } from "path";
9261
11362
  function rewriteRemoteSpecifier(spec) {
9262
11363
  if (spec.startsWith("npm:"))
9263
11364
  return `https://esm.sh/${spec.slice(4)}`;
@@ -9265,18 +11366,18 @@ function rewriteRemoteSpecifier(spec) {
9265
11366
  return `https://esm.sh/jsr/${spec.slice(4)}`;
9266
11367
  return spec;
9267
11368
  }
9268
- var HTTP_CACHE = join3(tmpdir(), "supacloud-lite-fn-http");
11369
+ var HTTP_CACHE = join4(tmpdir2(), "supacloud-lite-fn-http");
9269
11370
  async function fetchModule(url) {
9270
- const key = createHash("sha256").update(url).digest("hex");
9271
- const cached = join3(HTTP_CACHE, key);
9272
- if (existsSync(cached))
11371
+ const key = createHash3("sha256").update(url).digest("hex");
11372
+ const cached = join4(HTTP_CACHE, key);
11373
+ if (existsSync2(cached))
9273
11374
  return readFile3(cached, "utf8");
9274
11375
  const res = await fetch(url, { redirect: "follow" });
9275
11376
  if (!res.ok)
9276
11377
  throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
9277
11378
  const body = await res.text();
9278
11379
  await mkdir3(HTTP_CACHE, { recursive: true });
9279
- await writeFile2(cached, body);
11380
+ await writeFile3(cached, body);
9280
11381
  return body;
9281
11382
  }
9282
11383
  function remotePlugin() {
@@ -9299,28 +11400,37 @@ function remotePlugin() {
9299
11400
  };
9300
11401
  }
9301
11402
  async function bundleFunction(entryPath, name) {
9302
- const outDir = join3(tmpdir(), "supacloud-lite-fn-bundle");
11403
+ const outDir = join4(tmpdir2(), "supacloud-lite-fn-bundle", name);
9303
11404
  await mkdir3(outDir, { recursive: true });
9304
- const result = await Bun.build({
9305
- entrypoints: [entryPath],
9306
- format: "esm",
9307
- target: "bun",
9308
- outdir: outDir,
9309
- naming: `${name}-[hash].[ext]`,
9310
- plugins: [remotePlugin()]
9311
- });
9312
- if (!result.success || result.outputs.length === 0) {
9313
- throw new Error(result.logs.map((item) => item.message).join(`
11405
+ try {
11406
+ const buildOutput = await Bun.build({
11407
+ entrypoints: [entryPath],
11408
+ format: "esm",
11409
+ target: "bun",
11410
+ outdir: outDir,
11411
+ naming: `${name}-[hash].[ext]`,
11412
+ plugins: [remotePlugin()]
11413
+ });
11414
+ if (!buildOutput.success || buildOutput.outputs.length === 0) {
11415
+ throw new Error(buildOutput.logs.map((item) => item.message).join(`
9314
11416
  `) || `failed to bundle ${entryPath}`);
11417
+ }
11418
+ return buildOutput.outputs[0].path;
11419
+ } catch (buildError) {
11420
+ try {
11421
+ await rm2(outDir, { recursive: true, force: true });
11422
+ } catch (cleanupError) {
11423
+ throw new AggregateError([buildError, cleanupError], `failed to clean function bundle directory ${outDir}`);
11424
+ }
11425
+ throw buildError;
9315
11426
  }
9316
- return result.outputs[0].path;
9317
11427
  }
9318
11428
 
9319
11429
  // src/runtime/node/load-functions.ts
9320
11430
  async function loadFunctionEnv(projectDir) {
9321
11431
  let text;
9322
11432
  try {
9323
- text = await readFile4(join4(projectDir, "supabase", "functions", ".env"), "utf8");
11433
+ text = await readFile4(join5(projectDir, "supabase", "functions", ".env"), "utf8");
9324
11434
  } catch {
9325
11435
  return {};
9326
11436
  }
@@ -9359,7 +11469,7 @@ async function loadFunctions2(projectDir, options = {}) {
9359
11469
  }
9360
11470
  async function loadFunctionsUnlocked(projectDir, options) {
9361
11471
  const functions = new Map;
9362
- const root = join4(projectDir, "supabase", "functions");
11472
+ const root = join5(projectDir, "supabase", "functions");
9363
11473
  let entries = [];
9364
11474
  try {
9365
11475
  entries = await readdir(root);
@@ -9372,10 +11482,10 @@ async function loadFunctionsUnlocked(projectDir, options) {
9372
11482
  continue;
9373
11483
  if (options[name]?.enabled === false)
9374
11484
  continue;
9375
- const dir = join4(root, name);
11485
+ const dir = join5(root, name);
9376
11486
  if (!(await stat(dir)).isDirectory())
9377
11487
  continue;
9378
- const candidates = options[name]?.entrypoint ? [join4(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join4(dir, f));
11488
+ const candidates = options[name]?.entrypoint ? [join5(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join5(dir, f));
9379
11489
  for (const path of candidates) {
9380
11490
  try {
9381
11491
  await stat(path);
@@ -9396,11 +11506,12 @@ async function loadFunctionsUnlocked(projectDir, options) {
9396
11506
  resetCapturedHandler();
9397
11507
  const mod = await import(importUrl);
9398
11508
  const denoHandler = takeCapturedHandler();
9399
- const handler = typeof mod.default === "function" ? mod.default : denoHandler ? (req) => denoHandler(req) : undefined;
11509
+ const defaultExport = mod.default;
11510
+ const handler = typeof defaultExport === "function" ? defaultExport : defaultExport && typeof defaultExport.fetch === "function" ? defaultExport.fetch.bind(defaultExport) : denoHandler ? (req) => denoHandler(req) : undefined;
9400
11511
  if (handler) {
9401
11512
  functions.set(name, handler);
9402
11513
  } else {
9403
- console.warn(` warning: function "${name}" has no default export or Deno.serve() handler, skipped`);
11514
+ console.warn(` warning: function "${name}" has no default function, fetch object, or Deno.serve() handler, skipped`);
9404
11515
  }
9405
11516
  } catch (e) {
9406
11517
  const msg = e instanceof Error ? e.message : String(e);
@@ -9411,7 +11522,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
9411
11522
  }
9412
11523
  } finally {
9413
11524
  if (bundledPath)
9414
- await rm2(bundledPath, { force: true }).catch(() => {});
11525
+ await rm3(dirname3(bundledPath), { recursive: true, force: true }).catch(() => {});
9415
11526
  }
9416
11527
  break;
9417
11528
  }
@@ -9421,9 +11532,9 @@ async function loadFunctionsUnlocked(projectDir, options) {
9421
11532
 
9422
11533
  // src/runtime/node/project.ts
9423
11534
  import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
9424
- import { join as join5 } from "path";
11535
+ import { join as join6 } from "path";
9425
11536
  async function loadSupabaseProject(projectDir, seed = {}) {
9426
- const migrationsDir = join5(projectDir, "supabase", "migrations");
11537
+ const migrationsDir = join6(projectDir, "supabase", "migrations");
9427
11538
  const migrations = [];
9428
11539
  let entries = [];
9429
11540
  try {
@@ -9435,19 +11546,19 @@ async function loadSupabaseProject(projectDir, seed = {}) {
9435
11546
  for (const entry of entries.sort()) {
9436
11547
  if (!entry.endsWith(".sql"))
9437
11548
  continue;
9438
- const sql = await readFile5(join5(migrationsDir, entry), "utf8");
11549
+ const sql = await readFile5(join6(migrationsDir, entry), "utf8");
9439
11550
  migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
9440
11551
  }
9441
11552
  let seedSql;
9442
11553
  if (seed.enabled !== false) {
9443
11554
  const parts = [];
9444
- const supabaseDir = join5(projectDir, "supabase");
11555
+ const supabaseDir = join6(projectDir, "supabase");
9445
11556
  for (const configuredPath of seed.paths ?? ["seed.sql"]) {
9446
11557
  const pattern = configuredPath.replace(/^\.\//, "");
9447
11558
  const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
9448
11559
  for (const relativePath of matches) {
9449
11560
  try {
9450
- parts.push(await readFile5(join5(supabaseDir, relativePath), "utf8"));
11561
+ parts.push(await readFile5(join6(supabaseDir, relativePath), "utf8"));
9451
11562
  } catch (error) {
9452
11563
  if (!isNotFound(error))
9453
11564
  throw error;
@@ -9468,14 +11579,16 @@ function isNotFound(error) {
9468
11579
  function resolveProjectPaths(options = {}) {
9469
11580
  const projectDir = resolve2(options.projectDir ?? process.cwd());
9470
11581
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
9471
- const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join6(stateDir, "db"));
9472
- const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join6(stateDir, "storage"));
11582
+ const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
11583
+ const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join7(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
11584
+ const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join7(stateDir, "storage"));
9473
11585
  return {
9474
11586
  projectDir,
9475
11587
  stateDir,
9476
11588
  dataDir,
9477
11589
  storageDir,
9478
- secretsFile: join6(stateDir, "secrets.json")
11590
+ secretsFile: join7(stateDir, "secrets.json"),
11591
+ databaseEngine
9479
11592
  };
9480
11593
  }
9481
11594
  async function ensureProjectSecrets(paths) {
@@ -9498,7 +11611,7 @@ async function ensureProjectSecrets(paths) {
9498
11611
  };
9499
11612
  const temporaryFile = `${paths.secretsFile}.${crypto.randomUUID()}.tmp`;
9500
11613
  try {
9501
- await writeFile3(temporaryFile, `${JSON.stringify(candidate, null, 2)}
11614
+ await writeFile4(temporaryFile, `${JSON.stringify(candidate, null, 2)}
9502
11615
  `, { mode: 384, flag: "wx" });
9503
11616
  await link(temporaryFile, paths.secretsFile);
9504
11617
  stored = candidate;
@@ -9507,7 +11620,7 @@ async function ensureProjectSecrets(paths) {
9507
11620
  throw error2;
9508
11621
  stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
9509
11622
  } finally {
9510
- await unlink3(temporaryFile).catch((error2) => {
11623
+ await unlink2(temporaryFile).catch((error2) => {
9511
11624
  if (error2.code !== "ENOENT")
9512
11625
  throw error2;
9513
11626
  });
@@ -9539,42 +11652,58 @@ async function createProjectBackend(options = {}) {
9539
11652
  const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
9540
11653
  const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
9541
11654
  const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
11655
+ const databaseEngine = paths.databaseEngine;
9542
11656
  if (paths.dataDir) {
9543
11657
  await mkdir4(paths.dataDir, { recursive: true, mode: 448 });
9544
11658
  await chmod(paths.dataDir, 448);
9545
11659
  }
9546
11660
  await mkdir4(paths.storageDir, { recursive: true, mode: 448 });
9547
11661
  await chmod(paths.storageDir, 448);
9548
- const backend = await createBackend({
9549
- dataDir: paths.dataDir,
9550
- jwtSecret: secrets.jwtSecret,
9551
- vaultKey: secrets.vaultKey,
9552
- apiUrl: url,
9553
- siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
9554
- host,
9555
- jwtExpiry: config.auth.jwtExpiry,
9556
- uriAllowList: config.auth.uriAllowList,
9557
- authEnabled: config.auth.enabled,
9558
- authSettings: config.auth.settings,
9559
- authRateLimits: config.auth.rateLimits,
9560
- sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
9561
- sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
9562
- oauthProviders: config.auth.oauthProviders,
9563
- smsSender: options.smsSender,
9564
- dbSchemas: config.api.schemas,
9565
- maxRows: config.api.maxRows,
9566
- storageFileSizeLimit: config.storage.fileSizeLimit,
9567
- buckets: config.storage.buckets,
9568
- migrations: options.applyMigrations === false ? [] : project.migrations,
9569
- seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
9570
- functions,
9571
- functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
9572
- functionEnv,
9573
- webhooks,
9574
- startRuntimeServices: options.startRuntimeServices,
9575
- storageDriver: options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3),
9576
- log: options.log
9577
- });
11662
+ const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
11663
+ const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
11664
+ let backend;
11665
+ try {
11666
+ backend = await createBackend({
11667
+ engine,
11668
+ dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
11669
+ jwtSecret: secrets.jwtSecret,
11670
+ vaultKey: secrets.vaultKey,
11671
+ apiUrl: url,
11672
+ siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
11673
+ host,
11674
+ jwtExpiry: config.auth.jwtExpiry,
11675
+ uriAllowList: config.auth.uriAllowList,
11676
+ authEnabled: config.auth.enabled,
11677
+ authSettings: config.auth.settings,
11678
+ authRateLimits: config.auth.rateLimits,
11679
+ sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
11680
+ sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
11681
+ oauthProviders: config.auth.oauthProviders,
11682
+ smsSender: options.smsSender,
11683
+ dbSchemas: config.api.schemas,
11684
+ maxRows: config.api.maxRows,
11685
+ storageFileSizeLimit: config.storage.fileSizeLimit,
11686
+ buckets: config.storage.buckets,
11687
+ migrations: options.applyMigrations === false ? [] : project.migrations,
11688
+ seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
11689
+ functions,
11690
+ functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
11691
+ functionEnv,
11692
+ webhooks,
11693
+ startRuntimeServices: options.startRuntimeServices,
11694
+ storageDriver,
11695
+ log: options.log
11696
+ });
11697
+ } catch (error) {
11698
+ if (engine) {
11699
+ try {
11700
+ await engine.close();
11701
+ } catch (cleanupError) {
11702
+ throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
11703
+ }
11704
+ }
11705
+ throw error;
11706
+ }
9578
11707
  return {
9579
11708
  backend,
9580
11709
  config,
@@ -9585,9 +11714,22 @@ async function createProjectBackend(options = {}) {
9585
11714
  migrationCount: project.migrations.length,
9586
11715
  functionNames: [...functions.keys()],
9587
11716
  webhookCount: webhooks.length,
9588
- storageBackend
11717
+ storageBackend,
11718
+ databaseEngine
9589
11719
  };
9590
11720
  }
11721
+ function resolveDatabaseEngine(value, memory = false) {
11722
+ const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
11723
+ if (configured !== "pglite" && configured !== "native") {
11724
+ throw new Error(`unsupported SUPACLOUD_LITE_ENGINE: ${configured}`);
11725
+ }
11726
+ if (configured === "native" && memory)
11727
+ throw new Error("--memory is only supported by the pglite engine");
11728
+ if (configured === "native" && !isNativeEngineSupported()) {
11729
+ throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ` + `${process.platform}/${process.arch} must use --engine pglite`);
11730
+ }
11731
+ return configured;
11732
+ }
9591
11733
  function resolveStorageBackend(value) {
9592
11734
  const configured = value ?? process.env.SUPACLOUD_LITE_STORAGE_BACKEND ?? "fs";
9593
11735
  if (configured === "fs" || configured === "memory" || configured === "s3")
@@ -9642,7 +11784,7 @@ async function startProjectServer(options = {}) {
9642
11784
  }
9643
11785
  async function loadWebhooks(projectDir) {
9644
11786
  try {
9645
- const parsed = JSON.parse(await readFile6(join6(projectDir, "supabase", "webhooks.json"), "utf8"));
11787
+ const parsed = JSON.parse(await readFile6(join7(projectDir, "supabase", "webhooks.json"), "utf8"));
9646
11788
  return Array.isArray(parsed) ? parsed : [];
9647
11789
  } catch (error) {
9648
11790
  if (error.code === "ENOENT")
@@ -9692,9 +11834,9 @@ async function findEphemeralPort(host = "127.0.0.1") {
9692
11834
  return port;
9693
11835
  }
9694
11836
  // src/snapshot.ts
9695
- import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
9696
- import { dirname as dirname4, join as join7, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
9697
- import { create as createTar, extract as extractTar } from "tar";
11837
+ import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
11838
+ import { dirname as dirname5, join as join8, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
11839
+ import { create as createTar, extract as extractTar2 } from "tar";
9698
11840
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
9699
11841
  var SNAPSHOT_VERSION = 1;
9700
11842
  async function createSnapshot(options) {
@@ -9709,21 +11851,27 @@ async function createSnapshot(options) {
9709
11851
  storageBackend: options.storageBackend,
9710
11852
  includesDatabase: Boolean(paths.dataDir),
9711
11853
  includesLocalStorage: options.storageBackend === "fs",
9712
- includesSecrets: true
11854
+ includesSecrets: true,
11855
+ databaseEngine: paths.databaseEngine,
11856
+ ...paths.databaseEngine === "native" ? {
11857
+ platform: process.platform,
11858
+ architecture: process.arch,
11859
+ postgresMajor: await readPostgresMajor(paths.dataDir)
11860
+ } : {}
9713
11861
  };
9714
11862
  const output = resolve3(options.output);
9715
11863
  if (await existingInfo(output))
9716
11864
  throw new Error(`snapshot output already exists: ${output}`);
9717
- await mkdir5(dirname4(output), { recursive: true });
9718
- const stagingRoot = await mkdtemp(join7(dirname4(output), ".supacloud-lite-snapshot-"));
11865
+ await mkdir5(dirname5(output), { recursive: true });
11866
+ const stagingRoot = await mkdtemp(join8(dirname5(output), ".supacloud-lite-snapshot-"));
9719
11867
  try {
9720
- await writeFile4(join7(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
11868
+ await writeFile5(join8(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
9721
11869
  `);
9722
- await stageFile(paths.secretsFile, join7(stagingRoot, "secrets.json"));
11870
+ await stageFile(paths.secretsFile, join8(stagingRoot, "secrets.json"));
9723
11871
  if (paths.dataDir)
9724
- await stageDirectory(paths.dataDir, join7(stagingRoot, "database"));
11872
+ await stageDirectory(paths.dataDir, join8(stagingRoot, "database"));
9725
11873
  if (options.storageBackend === "fs")
9726
- await stageDirectory(paths.storageDir, join7(stagingRoot, "storage"));
11874
+ await stageDirectory(paths.storageDir, join8(stagingRoot, "storage"));
9727
11875
  const entries = ["manifest.json", "secrets.json"];
9728
11876
  if (paths.dataDir)
9729
11877
  entries.push("database");
@@ -9734,23 +11882,23 @@ async function createSnapshot(options) {
9734
11882
  await chmod2(output, 384);
9735
11883
  return manifest;
9736
11884
  } catch (error) {
9737
- await rm3(output, { force: true });
11885
+ await rm4(output, { force: true });
9738
11886
  throw error;
9739
11887
  } finally {
9740
- await rm3(stagingRoot, { recursive: true, force: true });
11888
+ await rm4(stagingRoot, { recursive: true, force: true });
9741
11889
  }
9742
11890
  }
9743
11891
  async function restoreSnapshot(options) {
9744
11892
  const paths = normalizePaths(options.paths);
9745
11893
  await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
9746
11894
  await assertNoDataDirectoryLock(paths);
9747
- const stagingRoot = await mkdtemp(join7(dirname4(paths.stateDir), ".supacloud-lite-restore-"));
9748
- const payloadRoot = join7(stagingRoot, "payload");
11895
+ const stagingRoot = await mkdtemp(join8(dirname5(paths.stateDir), ".supacloud-lite-restore-"));
11896
+ const payloadRoot = join8(stagingRoot, "payload");
9749
11897
  const rollbackId = crypto.randomUUID();
9750
11898
  const rollbackPaths = [];
9751
11899
  try {
9752
11900
  await mkdir5(payloadRoot, { recursive: true });
9753
- await extractTar({
11901
+ await extractTar2({
9754
11902
  cwd: payloadRoot,
9755
11903
  file: resolve3(options.input),
9756
11904
  preserveOwner: false,
@@ -9775,6 +11923,7 @@ async function restoreSnapshot(options) {
9775
11923
  if (manifest.storageBackend !== options.storageBackend) {
9776
11924
  throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
9777
11925
  }
11926
+ assertDatabaseSnapshotCompatible(manifest, paths);
9778
11927
  if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
9779
11928
  throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
9780
11929
  }
@@ -9783,27 +11932,27 @@ async function restoreSnapshot(options) {
9783
11932
  }
9784
11933
  await assertSnapshotPayload(payloadRoot, manifest);
9785
11934
  if (manifest.includesDatabase)
9786
- await mkdir5(join7(payloadRoot, "database"), { recursive: true });
11935
+ await mkdir5(join8(payloadRoot, "database"), { recursive: true });
9787
11936
  if (manifest.includesLocalStorage)
9788
- await mkdir5(join7(payloadRoot, "storage"), { recursive: true });
11937
+ await mkdir5(join8(payloadRoot, "storage"), { recursive: true });
9789
11938
  await assertRestoreTargets(paths, manifest, options.force === true);
9790
- const stateStage = join7(stagingRoot, "state");
11939
+ const stateStage = join8(stagingRoot, "state");
9791
11940
  await mkdir5(stateStage, { recursive: true });
9792
- await copyEntry(join7(payloadRoot, "secrets.json"), join7(stateStage, "secrets.json"));
11941
+ await copyEntry(join8(payloadRoot, "secrets.json"), join8(stateStage, "secrets.json"));
9793
11942
  if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
9794
- await copyEntry(join7(payloadRoot, "database"), join7(stateStage, relative2(paths.stateDir, paths.dataDir)));
11943
+ await copyEntry(join8(payloadRoot, "database"), join8(stateStage, relative2(paths.stateDir, paths.dataDir)));
9795
11944
  }
9796
11945
  if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
9797
- await copyEntry(join7(payloadRoot, "storage"), join7(stateStage, relative2(paths.stateDir, paths.storageDir)));
11946
+ await copyEntry(join8(payloadRoot, "storage"), join8(stateStage, relative2(paths.stateDir, paths.storageDir)));
9798
11947
  }
9799
11948
  const swaps = [];
9800
11949
  try {
9801
11950
  await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
9802
11951
  if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
9803
- await applyDirectorySwap(join7(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
11952
+ await applyDirectorySwap(join8(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
9804
11953
  }
9805
11954
  if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
9806
- await applyDirectorySwap(join7(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
11955
+ await applyDirectorySwap(join8(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
9807
11956
  }
9808
11957
  } catch (error) {
9809
11958
  await rollbackDirectorySwaps(swaps);
@@ -9822,7 +11971,7 @@ async function restoreSnapshot(options) {
9822
11971
  } catch (error) {
9823
11972
  throw error instanceof Error ? error : new Error(String(error));
9824
11973
  } finally {
9825
- await rm3(stagingRoot, { recursive: true, force: true });
11974
+ await rm4(stagingRoot, { recursive: true, force: true });
9826
11975
  }
9827
11976
  }
9828
11977
  function normalizePaths(paths) {
@@ -9839,7 +11988,7 @@ async function assertSnapshotPaths(paths, options = {}) {
9839
11988
  try {
9840
11989
  if (paths.stateDir === parse2(paths.stateDir).root)
9841
11990
  throw new Error("snapshot state directory must not be the filesystem root");
9842
- if (paths.secretsFile !== join7(paths.stateDir, "secrets.json"))
11991
+ if (paths.secretsFile !== join8(paths.stateDir, "secrets.json"))
9843
11992
  throw new Error("snapshot secrets path must be inside the state directory");
9844
11993
  const stateInfo = await lstat2(paths.stateDir);
9845
11994
  if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
@@ -9897,10 +12046,10 @@ async function stageDirectory(root, destination) {
9897
12046
  throw error;
9898
12047
  }
9899
12048
  await mkdir5(destination, { recursive: true });
9900
- const walk = async (current, target) => {
12049
+ const walk = async (current, target2) => {
9901
12050
  for (const entry of await readdir3(current, { withFileTypes: true })) {
9902
- const fullPath = join7(current, entry.name);
9903
- const targetPath = join7(target, entry.name);
12051
+ const fullPath = join8(current, entry.name);
12052
+ const targetPath = join8(target2, entry.name);
9904
12053
  if (entry.isSymbolicLink())
9905
12054
  throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
9906
12055
  if (entry.isDirectory()) {
@@ -9915,14 +12064,14 @@ async function stageDirectory(root, destination) {
9915
12064
  };
9916
12065
  await walk(root, destination);
9917
12066
  }
9918
- async function stageFile(source, target) {
9919
- await mkdir5(dirname4(target), { recursive: true });
9920
- await copyFile(source, target);
12067
+ async function stageFile(source, target2) {
12068
+ await mkdir5(dirname5(target2), { recursive: true });
12069
+ await copyFile(source, target2);
9921
12070
  }
9922
12071
  async function readManifest(payloadRoot) {
9923
12072
  let parsed;
9924
12073
  try {
9925
- parsed = JSON.parse(await readFile7(join7(payloadRoot, "manifest.json"), "utf8"));
12074
+ parsed = JSON.parse(await readFile7(join8(payloadRoot, "manifest.json"), "utf8"));
9926
12075
  } catch (error) {
9927
12076
  throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
9928
12077
  }
@@ -9934,13 +12083,38 @@ function isSnapshotManifest(value) {
9934
12083
  if (!value || typeof value !== "object")
9935
12084
  return false;
9936
12085
  const candidate = value;
9937
- return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true;
12086
+ return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true && (candidate.databaseEngine === undefined || candidate.databaseEngine === "pglite" || candidate.databaseEngine === "native") && (candidate.platform === undefined || typeof candidate.platform === "string") && (candidate.architecture === undefined || typeof candidate.architecture === "string") && (candidate.postgresMajor === undefined || typeof candidate.postgresMajor === "string");
12087
+ }
12088
+ function assertDatabaseSnapshotCompatible(manifest, paths) {
12089
+ const sourceEngine = manifest.databaseEngine ?? "pglite";
12090
+ if (sourceEngine !== paths.databaseEngine) {
12091
+ throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
12092
+ }
12093
+ if (sourceEngine !== "native")
12094
+ return;
12095
+ if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
12096
+ throw new Error(`native PostgreSQL snapshots require the same platform and architecture; ` + `source is ${manifest.platform ?? "unknown"}/${manifest.architecture ?? "unknown"}, ` + `target is ${process.platform}/${process.arch}`);
12097
+ }
12098
+ if (manifest.postgresMajor !== NATIVE_POSTGRES_MAJOR) {
12099
+ throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but this Lite build uses ${NATIVE_POSTGRES_MAJOR}`);
12100
+ }
12101
+ }
12102
+ async function readPostgresMajor(dataDir) {
12103
+ if (!dataDir)
12104
+ return;
12105
+ try {
12106
+ return (await readFile7(join8(dataDir, "PG_VERSION"), "utf8")).trim();
12107
+ } catch (error) {
12108
+ if (error.code === "ENOENT")
12109
+ return;
12110
+ throw error;
12111
+ }
9938
12112
  }
9939
12113
  async function assertSnapshotPayload(payloadRoot, manifest) {
9940
12114
  const required = ["manifest.json", "secrets.json"];
9941
12115
  for (const path of required) {
9942
12116
  try {
9943
- await lstat2(join7(payloadRoot, path));
12117
+ await lstat2(join8(payloadRoot, path));
9944
12118
  } catch {
9945
12119
  throw new Error(`snapshot is missing required payload: ${path}`);
9946
12120
  }
@@ -9963,9 +12137,9 @@ async function assertRestoreTargets(paths, manifest, force) {
9963
12137
  if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
9964
12138
  targets.push(paths.storageDir);
9965
12139
  if (!force) {
9966
- for (const target of targets) {
9967
- if (await directoryHasEntries(target))
9968
- throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
12140
+ for (const target2 of targets) {
12141
+ if (await directoryHasEntries(target2))
12142
+ throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
9969
12143
  }
9970
12144
  }
9971
12145
  }
@@ -9978,34 +12152,34 @@ async function directoryHasEntries(path) {
9978
12152
  throw error;
9979
12153
  }
9980
12154
  }
9981
- async function applyDirectorySwap(source, target, force, rollbackId, swaps) {
9982
- const targetInfo = await existingInfo(target);
12155
+ async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
12156
+ const targetInfo = await existingInfo(target2);
9983
12157
  if (targetInfo && !targetInfo.isDirectory())
9984
- throw new Error(`restore target is not a directory: ${target}`);
9985
- const swap = { target };
12158
+ throw new Error(`restore target is not a directory: ${target2}`);
12159
+ const swap = { target: target2 };
9986
12160
  if (targetInfo) {
9987
12161
  if (!force) {
9988
- if (await directoryHasEntries(target))
9989
- throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
9990
- await rm3(target, { recursive: true, force: true });
12162
+ if (await directoryHasEntries(target2))
12163
+ throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
12164
+ await rm4(target2, { recursive: true, force: true });
9991
12165
  } else {
9992
- swap.rollbackPath = join7(dirname4(target), `.${target.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
9993
- await rename2(target, swap.rollbackPath);
12166
+ swap.rollbackPath = join8(dirname5(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
12167
+ await rename2(target2, swap.rollbackPath);
9994
12168
  }
9995
12169
  }
9996
12170
  try {
9997
- await mkdir5(dirname4(target), { recursive: true });
9998
- await rename2(source, target);
12171
+ await mkdir5(dirname5(target2), { recursive: true });
12172
+ await rename2(source, target2);
9999
12173
  swaps.push(swap);
10000
12174
  } catch (error) {
10001
12175
  if (swap.rollbackPath)
10002
- await rename2(swap.rollbackPath, target).catch(() => {});
12176
+ await rename2(swap.rollbackPath, target2).catch(() => {});
10003
12177
  throw error;
10004
12178
  }
10005
12179
  }
10006
12180
  async function rollbackDirectorySwaps(swaps) {
10007
12181
  for (const swap of [...swaps].reverse()) {
10008
- await rm3(swap.target, { recursive: true, force: true });
12182
+ await rm4(swap.target, { recursive: true, force: true });
10009
12183
  if (swap.rollbackPath)
10010
12184
  await rename2(swap.rollbackPath, swap.target);
10011
12185
  }
@@ -10019,17 +12193,17 @@ async function existingInfo(path) {
10019
12193
  throw error;
10020
12194
  }
10021
12195
  }
10022
- async function copyEntry(source, target) {
12196
+ async function copyEntry(source, target2) {
10023
12197
  const info = await lstat2(source);
10024
12198
  if (info.isSymbolicLink())
10025
12199
  throw new Error(`snapshot refuses symbolic link: ${source}`);
10026
12200
  if (info.isDirectory()) {
10027
- await mkdir5(target, { recursive: true });
12201
+ await mkdir5(target2, { recursive: true });
10028
12202
  for (const entry of await readdir3(source))
10029
- await copyEntry(join7(source, entry), join7(target, entry));
12203
+ await copyEntry(join8(source, entry), join8(target2, entry));
10030
12204
  } else if (info.isFile()) {
10031
- await mkdir5(dirname4(target), { recursive: true });
10032
- await Bun.write(target, Bun.file(source));
12205
+ await mkdir5(dirname5(target2), { recursive: true });
12206
+ await Bun.write(target2, Bun.file(source));
10033
12207
  } else
10034
12208
  throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
10035
12209
  }
@@ -10040,7 +12214,7 @@ async function hardenRestoredTree(root) {
10040
12214
  if (info.isDirectory()) {
10041
12215
  await chmod2(root, 448);
10042
12216
  for (const entry of await readdir3(root))
10043
- await hardenRestoredTree(join7(root, entry));
12217
+ await hardenRestoredTree(join8(root, entry));
10044
12218
  return;
10045
12219
  }
10046
12220
  if (info.isFile()) {
@@ -10051,7 +12225,7 @@ async function hardenRestoredTree(root) {
10051
12225
  }
10052
12226
  async function assertNoSymlinks(root) {
10053
12227
  for (const entry of await readdir3(root, { withFileTypes: true })) {
10054
- const fullPath = join7(root, entry.name);
12228
+ const fullPath = join8(root, entry.name);
10055
12229
  if (entry.isSymbolicLink())
10056
12230
  throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
10057
12231
  if (entry.isDirectory())
@@ -10076,14 +12250,18 @@ export {
10076
12250
  restoreSnapshot,
10077
12251
  resolveStorageBackend,
10078
12252
  resolveProjectPaths,
12253
+ resolveDatabaseEngine,
10079
12254
  mintProjectKeys,
12255
+ isNativeEngineSupported,
10080
12256
  inspectDb,
10081
12257
  generateTypes,
10082
12258
  ensureProjectSecrets,
12259
+ ensurePostgres,
10083
12260
  decodeJwt,
10084
12261
  createSnapshot,
10085
12262
  createProjectBackend,
10086
12263
  createPgliteEngine,
12264
+ createNativeEngine,
10087
12265
  createBackend as createLiteBackend,
10088
12266
  SUPACLOUD_LITE_VERSION,
10089
12267
  S3StorageDriver,