ardent-cli 0.0.35 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1049 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -740,11 +740,9 @@ function diffAction() {
|
|
|
740
740
|
console.error(
|
|
741
741
|
`${red}error:${reset2} \`ardent branch diff\` has been removed.
|
|
742
742
|
|
|
743
|
-
|
|
744
|
-
deprecated in favour of pgstream. A pgstream-backed diff view is on the
|
|
745
|
-
roadmap but not yet shipped.
|
|
743
|
+
A replacement is on the roadmap.
|
|
746
744
|
|
|
747
|
-
${yellow}
|
|
745
|
+
${yellow}To view the current state of a branch:${reset2} \`ardent branch info <name>\`
|
|
748
746
|
`
|
|
749
747
|
);
|
|
750
748
|
process.exit(2);
|
|
@@ -798,6 +796,31 @@ function printDegradedWarnings(warnings) {
|
|
|
798
796
|
console.log(" Review this connector with: ardent connector list");
|
|
799
797
|
}
|
|
800
798
|
|
|
799
|
+
// src/lib/connector_status_display.ts
|
|
800
|
+
var ENGINE_STATUS_DISPLAY = {
|
|
801
|
+
healthy: "ready",
|
|
802
|
+
degraded: "ready (with warnings)",
|
|
803
|
+
configuration_verified: "setup pending",
|
|
804
|
+
configuration_failed: "configuration check failed",
|
|
805
|
+
validating: "validation in progress",
|
|
806
|
+
failed_validation: "validation failed",
|
|
807
|
+
unhealthy: "unhealthy",
|
|
808
|
+
unchecked: "not yet configured"
|
|
809
|
+
};
|
|
810
|
+
var CONNECTION_STATUS_DISPLAY = {
|
|
811
|
+
connected: "connected",
|
|
812
|
+
failed: "connection failed",
|
|
813
|
+
unknown: "unknown"
|
|
814
|
+
};
|
|
815
|
+
function engineStatusDisplay(status) {
|
|
816
|
+
if (!status) return "not ready";
|
|
817
|
+
return ENGINE_STATUS_DISPLAY[status] ?? "not ready";
|
|
818
|
+
}
|
|
819
|
+
function connectorStatusDisplay(status) {
|
|
820
|
+
if (!status) return "unknown";
|
|
821
|
+
return CONNECTION_STATUS_DISPLAY[status] ?? ENGINE_STATUS_DISPLAY[status] ?? "not ready";
|
|
822
|
+
}
|
|
823
|
+
|
|
801
824
|
// src/lib/engine_setup_result.ts
|
|
802
825
|
var SUCCESS_ENGINE_STATUSES = /* @__PURE__ */ new Set(["healthy", "degraded"]);
|
|
803
826
|
var RETRYABLE_ENGINE_STATUSES = /* @__PURE__ */ new Set(["configuration_verified"]);
|
|
@@ -836,23 +859,63 @@ function assertEngineSetupCompleted(operation, connectorName) {
|
|
|
836
859
|
throw new EngineSetupTerminalStatusError(
|
|
837
860
|
"retryable_engine_status",
|
|
838
861
|
engineStatus,
|
|
839
|
-
`Engine setup needs retry (
|
|
862
|
+
`Engine setup needs retry (${engineStatusDisplay(engineStatus)}). Run \`ardent connector retry-setup ${connectorName}\` to retry, or \`ardent connector list\` to inspect connector state first.`
|
|
840
863
|
);
|
|
841
864
|
}
|
|
842
865
|
if (FAILED_ENGINE_STATUSES.has(engineStatus)) {
|
|
843
866
|
throw new EngineSetupTerminalStatusError(
|
|
844
867
|
"failed_engine_status",
|
|
845
868
|
engineStatus,
|
|
846
|
-
`Engine setup failed (
|
|
869
|
+
`Engine setup failed (${engineStatusDisplay(engineStatus)}). Run \`ardent connector list\` to inspect connector state.`
|
|
847
870
|
);
|
|
848
871
|
}
|
|
849
872
|
throw new EngineSetupTerminalStatusError(
|
|
850
873
|
"unexpected_engine_status",
|
|
851
874
|
engineStatus,
|
|
852
|
-
|
|
875
|
+
"Engine setup completed in an unexpected state. Contact Ardent support."
|
|
853
876
|
);
|
|
854
877
|
}
|
|
855
878
|
|
|
879
|
+
// src/lib/operation_stage_display.ts
|
|
880
|
+
var STAGE_DISPLAY = {
|
|
881
|
+
// engine_setup_worker / postgres_engine_setup
|
|
882
|
+
"dispatched": "Starting",
|
|
883
|
+
"preparing": "Preparing",
|
|
884
|
+
"creating-neon-project": "Provisioning the branch target",
|
|
885
|
+
"preparing-target-databases": "Preparing target databases",
|
|
886
|
+
"deploying-pgstream": "Starting replication",
|
|
887
|
+
"applying-rls": "Applying RLS policies",
|
|
888
|
+
"storing-credentials": "Storing connection credentials",
|
|
889
|
+
"validating": "Validating the branch target",
|
|
890
|
+
// reset_worker / reset_connector
|
|
891
|
+
"resetting": "Resetting",
|
|
892
|
+
"deleting-pgstream": "Stopping replication",
|
|
893
|
+
"rediscovering-source": "Re-checking the source database",
|
|
894
|
+
"resetting-neon-main": "Resetting the branch target",
|
|
895
|
+
"creating-target-schemas": "Recreating target schemas",
|
|
896
|
+
"redeploying-pgstream": "Restarting replication",
|
|
897
|
+
// environment deploy_worker
|
|
898
|
+
"loading_config": "Loading environment configuration",
|
|
899
|
+
"deploying_infrastructure": "Provisioning environment infrastructure",
|
|
900
|
+
"recording_success": "Finalizing environment",
|
|
901
|
+
"cleaning_failed_deploy": "Cleaning up failed environment provisioning",
|
|
902
|
+
// environment destroy_worker
|
|
903
|
+
"deleting_private_links": "Removing private network links",
|
|
904
|
+
"destroying_infrastructure": "Tearing down environment infrastructure",
|
|
905
|
+
"recording_destroy_success": "Finalizing environment teardown"
|
|
906
|
+
};
|
|
907
|
+
function humanizeRawStage(_raw) {
|
|
908
|
+
return "Working";
|
|
909
|
+
}
|
|
910
|
+
function operationStageDisplay(stage) {
|
|
911
|
+
if (!stage) return "Working";
|
|
912
|
+
const colonIndex = stage.indexOf(":");
|
|
913
|
+
const base = colonIndex === -1 ? stage : stage.slice(0, colonIndex);
|
|
914
|
+
const suffix = colonIndex === -1 ? "" : stage.slice(colonIndex + 1);
|
|
915
|
+
const label = STAGE_DISPLAY[base] ?? humanizeRawStage(base);
|
|
916
|
+
return suffix ? `${label} (for ${suffix})` : label;
|
|
917
|
+
}
|
|
918
|
+
|
|
856
919
|
// src/lib/engine_setup.ts
|
|
857
920
|
var EngineSetupTimeoutError = class extends Error {
|
|
858
921
|
constructor(message) {
|
|
@@ -904,7 +967,7 @@ async function runEngineSetupWithPolling(connectorId, connectorName) {
|
|
|
904
967
|
}
|
|
905
968
|
if (op.stage && op.stage !== lastStage) {
|
|
906
969
|
const progressLabel = op.progress != null ? ` (${op.progress}%)` : "";
|
|
907
|
-
console.log(` ${op.stage}${progressLabel}`);
|
|
970
|
+
console.log(` ${operationStageDisplay(op.stage)}${progressLabel}`);
|
|
908
971
|
lastStage = op.stage;
|
|
909
972
|
}
|
|
910
973
|
if (op.status === "completed") {
|
|
@@ -919,7 +982,7 @@ async function runEngineSetupWithPolling(connectorId, connectorName) {
|
|
|
919
982
|
}
|
|
920
983
|
}
|
|
921
984
|
throw new EngineSetupTimeoutError(
|
|
922
|
-
`Engine setup did not complete within ${maxWaitMs / 6e4} minutes. Setup may still be running server-side \u2014 do NOT delete the connector. Check status with: ardent connector list
|
|
985
|
+
`Engine setup did not complete within ${maxWaitMs / 6e4} minutes. Setup may still be running server-side \u2014 do NOT delete the connector. Check status with: ardent connector list. If you contact Ardent support, reference operation id ${operationId}.`
|
|
923
986
|
);
|
|
924
987
|
}
|
|
925
988
|
|
|
@@ -1258,7 +1321,9 @@ function logSubmissionDisallowed() {
|
|
|
1258
1321
|
console.error(
|
|
1259
1322
|
" Wait for the current setup attempt to finish, then retry setup if the connector still shows setup pending."
|
|
1260
1323
|
);
|
|
1261
|
-
console.error(
|
|
1324
|
+
console.error(
|
|
1325
|
+
" Contact Ardent support if the connector's setup does not progress within a few minutes."
|
|
1326
|
+
);
|
|
1262
1327
|
}
|
|
1263
1328
|
function logNonInteractiveRequired(decisionsNeeded) {
|
|
1264
1329
|
const noun = decisionsNeeded === 1 ? "table" : "tables";
|
|
@@ -1462,7 +1527,7 @@ async function promptForUnsupportedExtensions(connectorId, unsupported, alreadyP
|
|
|
1462
1527
|
await api.put(`/v1/connectors/${connectorId}`, {
|
|
1463
1528
|
drop_extensions: merged2
|
|
1464
1529
|
});
|
|
1465
|
-
console.log(`\u2713 Saved
|
|
1530
|
+
console.log(`\u2713 Saved extension exclusions: ${merged2.join(", ")}`);
|
|
1466
1531
|
return "applied";
|
|
1467
1532
|
}
|
|
1468
1533
|
console.log(
|
|
@@ -1489,7 +1554,7 @@ async function promptForUnsupportedExtensions(connectorId, unsupported, alreadyP
|
|
|
1489
1554
|
await api.put(`/v1/connectors/${connectorId}`, {
|
|
1490
1555
|
drop_extensions: merged
|
|
1491
1556
|
});
|
|
1492
|
-
console.log(`\u2713 Saved
|
|
1557
|
+
console.log(`\u2713 Saved extension exclusions: ${merged.join(", ")}`);
|
|
1493
1558
|
return "applied";
|
|
1494
1559
|
}
|
|
1495
1560
|
function printUnloggedTablesPreflight(preflight) {
|
|
@@ -1845,7 +1910,7 @@ async function listAction2() {
|
|
|
1845
1910
|
if (render.kind === "engine_pending") enginePendingCount += 1;
|
|
1846
1911
|
const warnings = connector.warnings ?? [];
|
|
1847
1912
|
const warningSuffix = warnings.length > 0 ? ` ${yellow}\u26A0 ${warnings.length}${reset2}` : "";
|
|
1848
|
-
const statusSuffix = render.kind === "ready" ? "" : ` ${render.color}[${connector.status}]${reset2}`;
|
|
1913
|
+
const statusSuffix = render.kind === "ready" ? "" : ` ${render.color}[${connectorStatusDisplay(connector.status)}]${reset2}`;
|
|
1849
1914
|
const nameLine = isCurrent ? `${green2}* ${render.color}${render.icon}${green2} ${connector.name}${reset2}${warningSuffix}${statusSuffix}` : ` ${render.color}${render.icon}${reset2} ${connector.name}${warningSuffix}${statusSuffix}`;
|
|
1850
1915
|
console.log(nameLine);
|
|
1851
1916
|
if (isCurrent) {
|
|
@@ -2862,6 +2927,964 @@ async function removeAction2(key) {
|
|
|
2862
2927
|
}
|
|
2863
2928
|
}
|
|
2864
2929
|
|
|
2930
|
+
// src/lib/supabase_link.ts
|
|
2931
|
+
import { execFileSync } from "child_process";
|
|
2932
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync } from "fs";
|
|
2933
|
+
import { homedir as homedir2 } from "os";
|
|
2934
|
+
import { dirname, join as join2 } from "path";
|
|
2935
|
+
var CONFIG_DIR2 = join2(homedir2(), ".ardent");
|
|
2936
|
+
var STATE_DIR = join2(CONFIG_DIR2, "supabase-links");
|
|
2937
|
+
var ORIGINAL_SUFFIX = "-ardent-original";
|
|
2938
|
+
var SERVICE_SUFFIXES = ["rest", "auth", "storage", "realtime", "pg_meta"];
|
|
2939
|
+
var DOCKER_HOST_GATEWAY = "host.docker.internal";
|
|
2940
|
+
var REALTIME_LIST_CHANGES_MIGRATION_VERSION = "20230328144023";
|
|
2941
|
+
var SENSITIVE_DOCKER_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
2942
|
+
"DATABASE_URL",
|
|
2943
|
+
"DB_PASSWORD",
|
|
2944
|
+
"GOTRUE_DB_DATABASE_URL",
|
|
2945
|
+
"PG_META_DB_PASSWORD",
|
|
2946
|
+
"PGRST_DB_URI",
|
|
2947
|
+
"PGPASSWORD"
|
|
2948
|
+
]);
|
|
2949
|
+
function redactDockerArgs(args2) {
|
|
2950
|
+
return args2.map((arg) => {
|
|
2951
|
+
const separatorIndex = arg.indexOf("=");
|
|
2952
|
+
if (separatorIndex === -1) {
|
|
2953
|
+
return arg;
|
|
2954
|
+
}
|
|
2955
|
+
const key = arg.slice(0, separatorIndex);
|
|
2956
|
+
if (SENSITIVE_DOCKER_ENV_KEYS.has(key)) {
|
|
2957
|
+
return `${key}=***`;
|
|
2958
|
+
}
|
|
2959
|
+
return arg;
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
function redactDockerErrorMessage(message) {
|
|
2963
|
+
let redactedMessage = message.replace(
|
|
2964
|
+
/(postgres(?:ql)?:\/\/[^:\s/]+:)[^@\s]+@/g,
|
|
2965
|
+
"$1***@"
|
|
2966
|
+
);
|
|
2967
|
+
for (const key of SENSITIVE_DOCKER_ENV_KEYS) {
|
|
2968
|
+
redactedMessage = redactedMessage.replace(
|
|
2969
|
+
new RegExp(`${key}=[^\\s]+`, "g"),
|
|
2970
|
+
`${key}=***`
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
return redactedMessage;
|
|
2974
|
+
}
|
|
2975
|
+
function runDocker(args2, options = {}) {
|
|
2976
|
+
try {
|
|
2977
|
+
return execFileSync("docker", args2, {
|
|
2978
|
+
encoding: "utf-8",
|
|
2979
|
+
stdio: options.stdio ?? "pipe"
|
|
2980
|
+
}).trim();
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
if (error instanceof Error) {
|
|
2983
|
+
throw new Error(
|
|
2984
|
+
`Docker command failed: docker ${redactDockerArgs(args2).join(" ")}
|
|
2985
|
+
${redactDockerErrorMessage(error.message)}`
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2988
|
+
throw error;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function runDockerWithInput(args2, input, options = {}) {
|
|
2992
|
+
try {
|
|
2993
|
+
return execFileSync("docker", args2, {
|
|
2994
|
+
encoding: "utf-8",
|
|
2995
|
+
input,
|
|
2996
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2997
|
+
timeout: options.timeoutMs
|
|
2998
|
+
}).trim();
|
|
2999
|
+
} catch (error) {
|
|
3000
|
+
if (error instanceof Error) {
|
|
3001
|
+
throw new Error(
|
|
3002
|
+
`Docker command failed: docker ${redactDockerArgs(args2).join(" ")}
|
|
3003
|
+
${redactDockerErrorMessage(error.message)}`
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
throw error;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
function dockerExists(name) {
|
|
3010
|
+
const output = runDocker(["ps", "-a", "--format", "{{.Names}}"]);
|
|
3011
|
+
return output.split("\n").includes(name);
|
|
3012
|
+
}
|
|
3013
|
+
function inspectContainer(name) {
|
|
3014
|
+
const output = runDocker(["inspect", name]);
|
|
3015
|
+
const parsed = JSON.parse(output);
|
|
3016
|
+
if (parsed.length !== 1) {
|
|
3017
|
+
throw new Error(`Expected one Docker container named ${name}`);
|
|
3018
|
+
}
|
|
3019
|
+
return parsed[0];
|
|
3020
|
+
}
|
|
3021
|
+
function containerIpAddress(name, network) {
|
|
3022
|
+
const container = inspectContainer(name);
|
|
3023
|
+
const networkState = container.NetworkSettings.Networks[network];
|
|
3024
|
+
const ipAddress = networkState?.IPAddress;
|
|
3025
|
+
if (!ipAddress) {
|
|
3026
|
+
throw new Error(`Container ${name} is not attached to network ${network}`);
|
|
3027
|
+
}
|
|
3028
|
+
return ipAddress;
|
|
3029
|
+
}
|
|
3030
|
+
function statePath(projectId) {
|
|
3031
|
+
return join2(STATE_DIR, `${projectId.replace(/[^A-Za-z0-9_.-]/g, "_")}.json`);
|
|
3032
|
+
}
|
|
3033
|
+
function readState(projectId) {
|
|
3034
|
+
const path = statePath(projectId);
|
|
3035
|
+
if (!existsSync2(path)) {
|
|
3036
|
+
return void 0;
|
|
3037
|
+
}
|
|
3038
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
3039
|
+
}
|
|
3040
|
+
function writeState(state) {
|
|
3041
|
+
mkdirSync2(dirname(statePath(state.projectId)), { recursive: true });
|
|
3042
|
+
writeFileSync2(statePath(state.projectId), JSON.stringify(state, null, 2));
|
|
3043
|
+
}
|
|
3044
|
+
function deleteState(projectId) {
|
|
3045
|
+
const path = statePath(projectId);
|
|
3046
|
+
if (existsSync2(path)) {
|
|
3047
|
+
rmSync(path);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
function detectSupabaseProject(explicitProject) {
|
|
3051
|
+
if (explicitProject) {
|
|
3052
|
+
return explicitProject;
|
|
3053
|
+
}
|
|
3054
|
+
const output = runDocker([
|
|
3055
|
+
"ps",
|
|
3056
|
+
"-a",
|
|
3057
|
+
"--format",
|
|
3058
|
+
'{{.Names}} {{.Label "com.supabase.cli.project"}}'
|
|
3059
|
+
]);
|
|
3060
|
+
const projects = /* @__PURE__ */ new Set();
|
|
3061
|
+
for (const line of output.split("\n")) {
|
|
3062
|
+
const parts = line.split(" ");
|
|
3063
|
+
if (parts.length !== 2) {
|
|
3064
|
+
continue;
|
|
3065
|
+
}
|
|
3066
|
+
const projectId2 = parts[1]?.trim();
|
|
3067
|
+
if (projectId2) {
|
|
3068
|
+
projects.add(projectId2);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
if (projects.size === 0) {
|
|
3072
|
+
throw new Error("No local Supabase CLI containers found. Run `supabase start` first.");
|
|
3073
|
+
}
|
|
3074
|
+
if (projects.size > 1) {
|
|
3075
|
+
throw new Error(
|
|
3076
|
+
`Multiple Supabase projects found: ${Array.from(projects).join(", ")}. Pass --project <id>.`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
const projectId = Array.from(projects)[0];
|
|
3080
|
+
if (!projectId) {
|
|
3081
|
+
throw new Error("Could not detect local Supabase project id.");
|
|
3082
|
+
}
|
|
3083
|
+
return projectId;
|
|
3084
|
+
}
|
|
3085
|
+
function containerName(prefix, projectId) {
|
|
3086
|
+
return `supabase_${prefix}_${projectId}`;
|
|
3087
|
+
}
|
|
3088
|
+
function networkName(projectId) {
|
|
3089
|
+
return `supabase_network_${projectId}`;
|
|
3090
|
+
}
|
|
3091
|
+
async function resolveBranch(branchName) {
|
|
3092
|
+
const requestedBranchName = branchName || getCurrentBranch();
|
|
3093
|
+
if (!requestedBranchName) {
|
|
3094
|
+
throw new Error("No branch specified and no current branch is selected.");
|
|
3095
|
+
}
|
|
3096
|
+
const currentConnectorId = getConfig("currentConnectorId");
|
|
3097
|
+
if (!currentConnectorId) {
|
|
3098
|
+
throw new Error("No connector selected. Run `ardent connector switch` first.");
|
|
3099
|
+
}
|
|
3100
|
+
const result = await api.get(
|
|
3101
|
+
`/v1/cli/branches?connector_id=${encodeURIComponent(currentConnectorId)}`
|
|
3102
|
+
);
|
|
3103
|
+
if (!Array.isArray(result.branches)) {
|
|
3104
|
+
throw new Error("API returned invalid response: missing branches array");
|
|
3105
|
+
}
|
|
3106
|
+
setCacheEntry("branches", result.branches);
|
|
3107
|
+
const branch = result.branches.find((candidate) => candidate.name === requestedBranchName);
|
|
3108
|
+
if (!branch) {
|
|
3109
|
+
const cached = getCacheEntry("branches");
|
|
3110
|
+
const cachedBranch = cached?.data.find((candidate) => candidate.name === requestedBranchName);
|
|
3111
|
+
if (cachedBranch) {
|
|
3112
|
+
return cachedBranch;
|
|
3113
|
+
}
|
|
3114
|
+
throw new Error(`Branch "${requestedBranchName}" not found.`);
|
|
3115
|
+
}
|
|
3116
|
+
if (!branch.branch_url) {
|
|
3117
|
+
throw new Error(`Branch "${branch.name}" does not have a connection URL.`);
|
|
3118
|
+
}
|
|
3119
|
+
return branch;
|
|
3120
|
+
}
|
|
3121
|
+
function parseBranchUrl(branchUrl) {
|
|
3122
|
+
const parsed = new URL(branchUrl);
|
|
3123
|
+
if (!parsed.hostname || !parsed.username || !parsed.password) {
|
|
3124
|
+
throw new Error("Branch URL is missing host, username, or password.");
|
|
3125
|
+
}
|
|
3126
|
+
return parsed;
|
|
3127
|
+
}
|
|
3128
|
+
function branchTargetConnectHost(hostname) {
|
|
3129
|
+
if (hostname === "routing.localhost" || hostname.endsWith(".routing.localhost")) {
|
|
3130
|
+
return DOCKER_HOST_GATEWAY;
|
|
3131
|
+
}
|
|
3132
|
+
return hostname;
|
|
3133
|
+
}
|
|
3134
|
+
function shouldVerifyBranchTargetCertificate(hostname) {
|
|
3135
|
+
return !(hostname === "routing.localhost" || hostname.endsWith(".routing.localhost"));
|
|
3136
|
+
}
|
|
3137
|
+
function proxyScript() {
|
|
3138
|
+
return String.raw`
|
|
3139
|
+
const net = require("net");
|
|
3140
|
+
const tls = require("tls");
|
|
3141
|
+
|
|
3142
|
+
const listenPort = Number(process.env.LISTEN_PORT || "5432");
|
|
3143
|
+
const targetHost = process.env.TARGET_HOST;
|
|
3144
|
+
const targetConnectHost = process.env.TARGET_CONNECT_HOST || targetHost;
|
|
3145
|
+
const targetPort = Number(process.env.TARGET_PORT || "5432");
|
|
3146
|
+
const targetDatabase = process.env.TARGET_DATABASE;
|
|
3147
|
+
const rejectUnauthorized = process.env.TARGET_REJECT_UNAUTHORIZED !== "false";
|
|
3148
|
+
|
|
3149
|
+
if (!targetHost || !targetDatabase) {
|
|
3150
|
+
throw new Error("TARGET_HOST and TARGET_DATABASE are required");
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
const SSL_REQUEST = 80877103;
|
|
3154
|
+
const AUTH_SASL = 10;
|
|
3155
|
+
|
|
3156
|
+
function readCString(buffer, offset) {
|
|
3157
|
+
const end = buffer.indexOf(0, offset);
|
|
3158
|
+
if (end === -1) {
|
|
3159
|
+
throw new Error("Invalid startup packet: unterminated string");
|
|
3160
|
+
}
|
|
3161
|
+
return [buffer.toString("utf8", offset, end), end + 1];
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
function rewriteStartupPacket(packet) {
|
|
3165
|
+
const length = packet.readInt32BE(0);
|
|
3166
|
+
const protocol = packet.readInt32BE(4);
|
|
3167
|
+
const params = [];
|
|
3168
|
+
let offset = 8;
|
|
3169
|
+
|
|
3170
|
+
while (offset < length - 1) {
|
|
3171
|
+
let key;
|
|
3172
|
+
[key, offset] = readCString(packet, offset);
|
|
3173
|
+
if (!key) {
|
|
3174
|
+
break;
|
|
3175
|
+
}
|
|
3176
|
+
let value;
|
|
3177
|
+
[value, offset] = readCString(packet, offset);
|
|
3178
|
+
if (key === "database" && (value === "postgres" || value === "_supabase")) {
|
|
3179
|
+
value = targetDatabase;
|
|
3180
|
+
}
|
|
3181
|
+
params.push([key, value]);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
const chunks = [];
|
|
3185
|
+
const header = Buffer.alloc(4);
|
|
3186
|
+
header.writeInt32BE(protocol, 0);
|
|
3187
|
+
chunks.push(header);
|
|
3188
|
+
for (const [key, value] of params) {
|
|
3189
|
+
chunks.push(Buffer.from(key + "\0" + value + "\0", "utf8"));
|
|
3190
|
+
}
|
|
3191
|
+
chunks.push(Buffer.from([0]));
|
|
3192
|
+
const body = Buffer.concat(chunks);
|
|
3193
|
+
const output = Buffer.alloc(4 + body.length);
|
|
3194
|
+
output.writeInt32BE(output.length, 0);
|
|
3195
|
+
body.copy(output, 4);
|
|
3196
|
+
return output;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
function filterServerPacket(packet) {
|
|
3200
|
+
if (packet.length < 9 || packet[0] !== 82) {
|
|
3201
|
+
return packet;
|
|
3202
|
+
}
|
|
3203
|
+
const authCode = packet.readInt32BE(5);
|
|
3204
|
+
if (authCode !== AUTH_SASL) {
|
|
3205
|
+
return packet;
|
|
3206
|
+
}
|
|
3207
|
+
const length = packet.readInt32BE(1);
|
|
3208
|
+
const body = packet.subarray(9, 1 + length);
|
|
3209
|
+
const trailing = packet.subarray(1 + length);
|
|
3210
|
+
const mechanisms = body.toString("utf8").split("\0").filter(Boolean);
|
|
3211
|
+
const filtered = mechanisms.filter((mechanism) => mechanism !== "SCRAM-SHA-256-PLUS");
|
|
3212
|
+
if (filtered.length === mechanisms.length) {
|
|
3213
|
+
return packet;
|
|
3214
|
+
}
|
|
3215
|
+
filtered.push("");
|
|
3216
|
+
const newBody = Buffer.from(filtered.join("\0") + "\0", "utf8");
|
|
3217
|
+
const output = Buffer.alloc(1 + 4 + 4 + newBody.length);
|
|
3218
|
+
output[0] = 82;
|
|
3219
|
+
output.writeInt32BE(4 + 4 + newBody.length, 1);
|
|
3220
|
+
output.writeInt32BE(AUTH_SASL, 5);
|
|
3221
|
+
newBody.copy(output, 9);
|
|
3222
|
+
return Buffer.concat([output, trailing]);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
function connectUpstream(startupPacket, client) {
|
|
3226
|
+
const socket = net.connect(targetPort, targetConnectHost, () => {
|
|
3227
|
+
const sslRequest = Buffer.alloc(8);
|
|
3228
|
+
sslRequest.writeInt32BE(8, 0);
|
|
3229
|
+
sslRequest.writeInt32BE(SSL_REQUEST, 4);
|
|
3230
|
+
socket.write(sslRequest);
|
|
3231
|
+
});
|
|
3232
|
+
|
|
3233
|
+
socket.once("data", (response) => {
|
|
3234
|
+
if (response[0] !== 83) {
|
|
3235
|
+
client.destroy(new Error("Upstream PostgreSQL server did not accept SSL"));
|
|
3236
|
+
socket.destroy();
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
const secureSocket = tls.connect({
|
|
3241
|
+
socket,
|
|
3242
|
+
servername: targetHost,
|
|
3243
|
+
rejectUnauthorized,
|
|
3244
|
+
}, () => {
|
|
3245
|
+
let rewrittenStartupPacket;
|
|
3246
|
+
try {
|
|
3247
|
+
rewrittenStartupPacket = rewriteStartupPacket(startupPacket);
|
|
3248
|
+
} catch {
|
|
3249
|
+
client.destroy();
|
|
3250
|
+
secureSocket.destroy();
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
secureSocket.write(rewrittenStartupPacket);
|
|
3254
|
+
});
|
|
3255
|
+
|
|
3256
|
+
secureSocket.on("data", (chunk) => {
|
|
3257
|
+
client.write(filterServerPacket(chunk));
|
|
3258
|
+
});
|
|
3259
|
+
client.on("data", (chunk) => secureSocket.write(chunk));
|
|
3260
|
+
secureSocket.on("error", () => client.destroy());
|
|
3261
|
+
client.on("error", () => secureSocket.destroy());
|
|
3262
|
+
secureSocket.on("close", () => client.destroy());
|
|
3263
|
+
client.on("close", () => secureSocket.destroy());
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
socket.on("error", () => client.destroy());
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
const server = net.createServer((client) => {
|
|
3270
|
+
let buffered = Buffer.alloc(0);
|
|
3271
|
+
let sawSslRequest = false;
|
|
3272
|
+
|
|
3273
|
+
client.on("data", function onFirstData(chunk) {
|
|
3274
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
3275
|
+
if (buffered.length < 8) {
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
const length = buffered.readInt32BE(0);
|
|
3280
|
+
const code = buffered.readInt32BE(4);
|
|
3281
|
+
if (!sawSslRequest && length === 8 && code === SSL_REQUEST) {
|
|
3282
|
+
client.write("N");
|
|
3283
|
+
buffered = buffered.subarray(8);
|
|
3284
|
+
sawSslRequest = true;
|
|
3285
|
+
if (buffered.length < 8) {
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
const startupLength = buffered.readInt32BE(0);
|
|
3291
|
+
if (buffered.length < startupLength) {
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
client.off("data", onFirstData);
|
|
3296
|
+
try {
|
|
3297
|
+
const startupPacket = buffered.subarray(0, startupLength);
|
|
3298
|
+
const remainder = buffered.subarray(startupLength);
|
|
3299
|
+
connectUpstream(startupPacket, client);
|
|
3300
|
+
if (remainder.length > 0) {
|
|
3301
|
+
client.unshift(remainder);
|
|
3302
|
+
}
|
|
3303
|
+
} catch {
|
|
3304
|
+
client.destroy();
|
|
3305
|
+
}
|
|
3306
|
+
});
|
|
3307
|
+
});
|
|
3308
|
+
|
|
3309
|
+
server.listen(listenPort, "0.0.0.0", () => {
|
|
3310
|
+
console.log(JSON.stringify({ event: "listening", listenPort, targetHost, targetConnectHost, targetDatabase }));
|
|
3311
|
+
});
|
|
3312
|
+
`;
|
|
3313
|
+
}
|
|
3314
|
+
function writeProxyScript(projectId) {
|
|
3315
|
+
const scriptPath = join2(STATE_DIR, projectId, "proxy.js");
|
|
3316
|
+
mkdirSync2(dirname(scriptPath), { recursive: true });
|
|
3317
|
+
writeFileSync2(scriptPath, proxyScript());
|
|
3318
|
+
return scriptPath;
|
|
3319
|
+
}
|
|
3320
|
+
function serviceEnvForBranch(servicePrefix, env, dbContainer, branchUser, branchPassword) {
|
|
3321
|
+
const localDbUri = (role) => `postgresql://${encodeURIComponent(role)}:${encodeURIComponent(branchPassword)}@${dbContainer}:5432/postgres?sslmode=disable`;
|
|
3322
|
+
return env.map((entry) => {
|
|
3323
|
+
if (servicePrefix === "rest" && entry.startsWith("PGRST_DB_URI=")) {
|
|
3324
|
+
return `PGRST_DB_URI=${localDbUri("authenticator")}`;
|
|
3325
|
+
}
|
|
3326
|
+
if (servicePrefix === "auth" && entry.startsWith("GOTRUE_DB_DATABASE_URL=")) {
|
|
3327
|
+
return `GOTRUE_DB_DATABASE_URL=${localDbUri("supabase_auth_admin")}`;
|
|
3328
|
+
}
|
|
3329
|
+
if (servicePrefix === "storage" && entry.startsWith("DATABASE_URL=")) {
|
|
3330
|
+
return `DATABASE_URL=${localDbUri("supabase_storage_admin")}`;
|
|
3331
|
+
}
|
|
3332
|
+
if (servicePrefix === "realtime") {
|
|
3333
|
+
if (entry.startsWith("DB_HOST=")) {
|
|
3334
|
+
return `DB_HOST=${dbContainer}`;
|
|
3335
|
+
}
|
|
3336
|
+
if (entry.startsWith("DB_NAME=")) {
|
|
3337
|
+
return "DB_NAME=postgres";
|
|
3338
|
+
}
|
|
3339
|
+
if (entry.startsWith("DB_USER=")) {
|
|
3340
|
+
return "DB_USER=supabase_admin";
|
|
3341
|
+
}
|
|
3342
|
+
if (entry.startsWith("DB_PASSWORD=")) {
|
|
3343
|
+
return `DB_PASSWORD=${branchPassword}`;
|
|
3344
|
+
}
|
|
3345
|
+
if (entry.startsWith("DB_PORT=")) {
|
|
3346
|
+
return "DB_PORT=5432";
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
if (servicePrefix === "pg_meta") {
|
|
3350
|
+
if (entry.startsWith("PG_META_DB_HOST=")) {
|
|
3351
|
+
return `PG_META_DB_HOST=${dbContainer}`;
|
|
3352
|
+
}
|
|
3353
|
+
if (entry.startsWith("PG_META_DB_NAME=")) {
|
|
3354
|
+
return "PG_META_DB_NAME=postgres";
|
|
3355
|
+
}
|
|
3356
|
+
if (entry.startsWith("PG_META_DB_USER=")) {
|
|
3357
|
+
return `PG_META_DB_USER=${branchUser}`;
|
|
3358
|
+
}
|
|
3359
|
+
if (entry.startsWith("PG_META_DB_PASSWORD=")) {
|
|
3360
|
+
return `PG_META_DB_PASSWORD=${branchPassword}`;
|
|
3361
|
+
}
|
|
3362
|
+
if (entry.startsWith("PG_META_DB_PORT=")) {
|
|
3363
|
+
return "PG_META_DB_PORT=5432";
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return entry;
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
function mountArgs(mounts) {
|
|
3370
|
+
const args2 = [];
|
|
3371
|
+
for (const mount of mounts) {
|
|
3372
|
+
if (!mount.Source || !mount.Target) {
|
|
3373
|
+
continue;
|
|
3374
|
+
}
|
|
3375
|
+
const readonly = mount.ReadOnly ? ",readonly" : "";
|
|
3376
|
+
args2.push(
|
|
3377
|
+
"--mount",
|
|
3378
|
+
`type=${mount.Type},source=${mount.Source},target=${mount.Target}${readonly}`
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
return args2;
|
|
3382
|
+
}
|
|
3383
|
+
function portPublishArgs(container) {
|
|
3384
|
+
const args2 = [];
|
|
3385
|
+
const ports = container.NetworkSettings.Ports ?? {};
|
|
3386
|
+
for (const [containerPort, bindings] of Object.entries(ports)) {
|
|
3387
|
+
if (!bindings) {
|
|
3388
|
+
continue;
|
|
3389
|
+
}
|
|
3390
|
+
for (const binding of bindings) {
|
|
3391
|
+
if (!binding.HostPort) {
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
const hostPrefix = binding.HostIp ? `${binding.HostIp}:` : "";
|
|
3395
|
+
args2.push("-p", `${hostPrefix}${binding.HostPort}:${containerPort}`);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
return args2;
|
|
3399
|
+
}
|
|
3400
|
+
function containerAliases(container, network, fallback) {
|
|
3401
|
+
const aliases = container.NetworkSettings.Networks[network]?.Aliases ?? [];
|
|
3402
|
+
const cleanAliases = aliases.filter((alias) => alias && alias !== container.Name.replace(/^\//, ""));
|
|
3403
|
+
return cleanAliases.length > 0 ? cleanAliases : [fallback];
|
|
3404
|
+
}
|
|
3405
|
+
function imageCommandArgs(image, entrypoint, command) {
|
|
3406
|
+
const args2 = [];
|
|
3407
|
+
if (entrypoint && entrypoint.length > 0) {
|
|
3408
|
+
args2.push("--entrypoint", entrypoint[0]);
|
|
3409
|
+
}
|
|
3410
|
+
args2.push(image);
|
|
3411
|
+
if (entrypoint && entrypoint.length > 1) {
|
|
3412
|
+
args2.push(...entrypoint.slice(1));
|
|
3413
|
+
}
|
|
3414
|
+
if (command) {
|
|
3415
|
+
args2.push(...command);
|
|
3416
|
+
}
|
|
3417
|
+
return args2;
|
|
3418
|
+
}
|
|
3419
|
+
function recreateServiceForBranch(servicePrefix, projectId, network, dbContainer, branchUser, branchPassword) {
|
|
3420
|
+
const name = containerName(servicePrefix, projectId);
|
|
3421
|
+
if (!dockerExists(name)) {
|
|
3422
|
+
return void 0;
|
|
3423
|
+
}
|
|
3424
|
+
const originalName = `${name}${ORIGINAL_SUFFIX}`;
|
|
3425
|
+
if (dockerExists(originalName)) {
|
|
3426
|
+
throw new Error(`Refusing to overwrite existing backup container ${originalName}. Run unlink first.`);
|
|
3427
|
+
}
|
|
3428
|
+
const container = inspectContainer(name);
|
|
3429
|
+
const aliases = containerAliases(container, network, servicePrefix.replace("pg_meta", "meta"));
|
|
3430
|
+
const env = serviceEnvForBranch(
|
|
3431
|
+
servicePrefix,
|
|
3432
|
+
container.Config.Env ?? [],
|
|
3433
|
+
dbContainer,
|
|
3434
|
+
branchUser,
|
|
3435
|
+
branchPassword
|
|
3436
|
+
);
|
|
3437
|
+
runDocker(["stop", name]);
|
|
3438
|
+
runDocker(["rename", name, originalName]);
|
|
3439
|
+
const args2 = ["run", "-d", "--name", name, "--network", network];
|
|
3440
|
+
for (const alias of aliases) {
|
|
3441
|
+
args2.push("--network-alias", alias);
|
|
3442
|
+
}
|
|
3443
|
+
for (const envValue of env) {
|
|
3444
|
+
args2.push("-e", envValue);
|
|
3445
|
+
}
|
|
3446
|
+
if (container.Config.User) {
|
|
3447
|
+
args2.push("--user", container.Config.User);
|
|
3448
|
+
}
|
|
3449
|
+
if (container.Config.WorkingDir) {
|
|
3450
|
+
args2.push("--workdir", container.Config.WorkingDir);
|
|
3451
|
+
}
|
|
3452
|
+
args2.push(...mountArgs(container.Mounts));
|
|
3453
|
+
args2.push(
|
|
3454
|
+
...imageCommandArgs(container.Config.Image, container.Config.Entrypoint, container.Config.Cmd)
|
|
3455
|
+
);
|
|
3456
|
+
runDocker(args2);
|
|
3457
|
+
return { name, originalName };
|
|
3458
|
+
}
|
|
3459
|
+
function startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, portBindings) {
|
|
3460
|
+
const scriptPath = writeProxyScript(projectId);
|
|
3461
|
+
const targetDatabase = parsedBranchUrl.pathname.replace(/^\//, "") || "postgres";
|
|
3462
|
+
const targetPort = parsedBranchUrl.port || "5432";
|
|
3463
|
+
const targetConnectHost = branchTargetConnectHost(parsedBranchUrl.hostname);
|
|
3464
|
+
const rejectUnauthorized = shouldVerifyBranchTargetCertificate(parsedBranchUrl.hostname);
|
|
3465
|
+
const args2 = [
|
|
3466
|
+
"run",
|
|
3467
|
+
"-d",
|
|
3468
|
+
"--name",
|
|
3469
|
+
dbContainer,
|
|
3470
|
+
"--network",
|
|
3471
|
+
network,
|
|
3472
|
+
"--network-alias",
|
|
3473
|
+
"db",
|
|
3474
|
+
"--network-alias",
|
|
3475
|
+
"db.supabase.internal"
|
|
3476
|
+
];
|
|
3477
|
+
if (targetConnectHost === DOCKER_HOST_GATEWAY) {
|
|
3478
|
+
args2.push("--add-host", `${DOCKER_HOST_GATEWAY}:host-gateway`);
|
|
3479
|
+
}
|
|
3480
|
+
args2.push(...portBindings);
|
|
3481
|
+
args2.push(
|
|
3482
|
+
"-v",
|
|
3483
|
+
`${scriptPath}:/proxy.js:ro`,
|
|
3484
|
+
"-e",
|
|
3485
|
+
`TARGET_HOST=${parsedBranchUrl.hostname}`,
|
|
3486
|
+
"-e",
|
|
3487
|
+
`TARGET_CONNECT_HOST=${targetConnectHost}`,
|
|
3488
|
+
"-e",
|
|
3489
|
+
`TARGET_PORT=${targetPort}`,
|
|
3490
|
+
"-e",
|
|
3491
|
+
`TARGET_DATABASE=${targetDatabase}`,
|
|
3492
|
+
"-e",
|
|
3493
|
+
`TARGET_REJECT_UNAUTHORIZED=${String(rejectUnauthorized)}`,
|
|
3494
|
+
"-e",
|
|
3495
|
+
"LISTEN_PORT=5432",
|
|
3496
|
+
"node:22-alpine",
|
|
3497
|
+
"node",
|
|
3498
|
+
"/proxy.js"
|
|
3499
|
+
);
|
|
3500
|
+
runDocker(args2);
|
|
3501
|
+
}
|
|
3502
|
+
function dumpLocalStorageMigrations(dbContainer) {
|
|
3503
|
+
return runDocker([
|
|
3504
|
+
"exec",
|
|
3505
|
+
dbContainer,
|
|
3506
|
+
"pg_dump",
|
|
3507
|
+
"-U",
|
|
3508
|
+
"postgres",
|
|
3509
|
+
"-d",
|
|
3510
|
+
"postgres",
|
|
3511
|
+
"--data-only",
|
|
3512
|
+
"--table=storage.migrations",
|
|
3513
|
+
"--column-inserts",
|
|
3514
|
+
"--no-owner",
|
|
3515
|
+
"--no-privileges"
|
|
3516
|
+
]);
|
|
3517
|
+
}
|
|
3518
|
+
function storageMigrationsImportSql(dumpSql) {
|
|
3519
|
+
const tempTable = "pg_temp.ardent_storage_migrations_import";
|
|
3520
|
+
const tempDumpSql = dumpSql.replaceAll("INSERT INTO storage.migrations", `INSERT INTO ${tempTable}`);
|
|
3521
|
+
return `
|
|
3522
|
+
BEGIN;
|
|
3523
|
+
CREATE TEMP TABLE ardent_storage_migrations_import (LIKE storage.migrations INCLUDING DEFAULTS);
|
|
3524
|
+
${tempDumpSql}
|
|
3525
|
+
INSERT INTO storage.migrations
|
|
3526
|
+
SELECT * FROM ${tempTable}
|
|
3527
|
+
ON CONFLICT DO NOTHING;
|
|
3528
|
+
COMMIT;
|
|
3529
|
+
`;
|
|
3530
|
+
}
|
|
3531
|
+
function waitForOriginalDb(containerName2) {
|
|
3532
|
+
const deadline = Date.now() + 3e4;
|
|
3533
|
+
while (Date.now() < deadline) {
|
|
3534
|
+
try {
|
|
3535
|
+
runDocker(["exec", containerName2, "pg_isready", "-U", "postgres", "-d", "postgres"]);
|
|
3536
|
+
return;
|
|
3537
|
+
} catch {
|
|
3538
|
+
execFileSync("sleep", ["1"]);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
throw new Error(`Timed out waiting for ${containerName2} to accept local PostgreSQL connections.`);
|
|
3542
|
+
}
|
|
3543
|
+
function copyLocalStorageMigrationsToBranch(dumpSql, originalDbContainer, dbContainer, network, branchUser, branchPassword) {
|
|
3544
|
+
runDocker(["start", originalDbContainer]);
|
|
3545
|
+
try {
|
|
3546
|
+
waitForOriginalDb(originalDbContainer);
|
|
3547
|
+
const dbContainerIpAddress = containerIpAddress(dbContainer, network);
|
|
3548
|
+
runDockerWithInput(
|
|
3549
|
+
[
|
|
3550
|
+
"exec",
|
|
3551
|
+
"-i",
|
|
3552
|
+
"-e",
|
|
3553
|
+
`PGPASSWORD=${branchPassword}`,
|
|
3554
|
+
originalDbContainer,
|
|
3555
|
+
"psql",
|
|
3556
|
+
"-h",
|
|
3557
|
+
dbContainerIpAddress,
|
|
3558
|
+
"-p",
|
|
3559
|
+
"5432",
|
|
3560
|
+
"-U",
|
|
3561
|
+
branchUser,
|
|
3562
|
+
"-d",
|
|
3563
|
+
"postgres",
|
|
3564
|
+
"-v",
|
|
3565
|
+
"ON_ERROR_STOP=1"
|
|
3566
|
+
],
|
|
3567
|
+
storageMigrationsImportSql(dumpSql)
|
|
3568
|
+
);
|
|
3569
|
+
} finally {
|
|
3570
|
+
runDocker(["stop", originalDbContainer]);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
function supabaseRealtimeCompatibilitySql() {
|
|
3574
|
+
return String.raw`
|
|
3575
|
+
SET lock_timeout = '2s';
|
|
3576
|
+
SET statement_timeout = '5s';
|
|
3577
|
+
|
|
3578
|
+
ALTER SCHEMA realtime OWNER TO supabase_admin;
|
|
3579
|
+
GRANT ALL PRIVILEGES ON SCHEMA realtime TO supabase_realtime_admin;
|
|
3580
|
+
|
|
3581
|
+
CREATE OR REPLACE FUNCTION realtime.list_changes(
|
|
3582
|
+
publication name,
|
|
3583
|
+
slot_name name,
|
|
3584
|
+
max_changes int,
|
|
3585
|
+
max_record_bytes int
|
|
3586
|
+
)
|
|
3587
|
+
RETURNS SETOF realtime.wal_rls
|
|
3588
|
+
LANGUAGE sql
|
|
3589
|
+
AS $$
|
|
3590
|
+
WITH pub AS (
|
|
3591
|
+
SELECT
|
|
3592
|
+
concat_ws(
|
|
3593
|
+
',',
|
|
3594
|
+
CASE WHEN bool_or(pubinsert) THEN 'insert' ELSE NULL END,
|
|
3595
|
+
CASE WHEN bool_or(pubupdate) THEN 'update' ELSE NULL END,
|
|
3596
|
+
CASE WHEN bool_or(pubdelete) THEN 'delete' ELSE NULL END
|
|
3597
|
+
) AS w2j_actions,
|
|
3598
|
+
coalesce(
|
|
3599
|
+
string_agg(
|
|
3600
|
+
realtime.quote_wal2json(format('%I.%I', schemaname, tablename)::regclass),
|
|
3601
|
+
','
|
|
3602
|
+
) FILTER (WHERE ppt.tablename IS NOT NULL AND ppt.tablename NOT LIKE '% %'),
|
|
3603
|
+
''
|
|
3604
|
+
) AS w2j_add_tables
|
|
3605
|
+
FROM pg_publication pp
|
|
3606
|
+
LEFT JOIN pg_publication_tables ppt
|
|
3607
|
+
ON pp.pubname = ppt.pubname
|
|
3608
|
+
WHERE pp.pubname = publication
|
|
3609
|
+
GROUP BY pp.pubname
|
|
3610
|
+
LIMIT 1
|
|
3611
|
+
),
|
|
3612
|
+
w2j AS (
|
|
3613
|
+
SELECT
|
|
3614
|
+
x.*,
|
|
3615
|
+
pub.w2j_add_tables
|
|
3616
|
+
FROM pub,
|
|
3617
|
+
pg_logical_slot_get_changes(
|
|
3618
|
+
slot_name,
|
|
3619
|
+
NULL,
|
|
3620
|
+
max_changes,
|
|
3621
|
+
'include-pk', 'true',
|
|
3622
|
+
'include-transaction', 'false',
|
|
3623
|
+
'include-timestamp', 'true',
|
|
3624
|
+
'include-type-oids', 'true',
|
|
3625
|
+
'format-version', '2',
|
|
3626
|
+
'actions', pub.w2j_actions,
|
|
3627
|
+
'add-tables', pub.w2j_add_tables
|
|
3628
|
+
) x
|
|
3629
|
+
)
|
|
3630
|
+
SELECT
|
|
3631
|
+
xyz.wal,
|
|
3632
|
+
xyz.is_rls_enabled,
|
|
3633
|
+
xyz.subscription_ids,
|
|
3634
|
+
xyz.errors
|
|
3635
|
+
FROM w2j,
|
|
3636
|
+
realtime.apply_rls(
|
|
3637
|
+
wal := w2j.data::jsonb,
|
|
3638
|
+
max_record_bytes := max_record_bytes
|
|
3639
|
+
) xyz(wal, is_rls_enabled, subscription_ids, errors)
|
|
3640
|
+
WHERE w2j.w2j_add_tables <> ''
|
|
3641
|
+
AND xyz.subscription_ids[1] IS NOT NULL
|
|
3642
|
+
$$;
|
|
3643
|
+
|
|
3644
|
+
ALTER FUNCTION realtime.list_changes(name, name, int, int) OWNER TO supabase_realtime_admin;
|
|
3645
|
+
GRANT EXECUTE ON FUNCTION realtime.list_changes(name, name, int, int)
|
|
3646
|
+
TO supabase_admin, supabase_realtime_admin;
|
|
3647
|
+
|
|
3648
|
+
INSERT INTO realtime.schema_migrations (version, inserted_at)
|
|
3649
|
+
VALUES (${REALTIME_LIST_CHANGES_MIGRATION_VERSION}, now())
|
|
3650
|
+
ON CONFLICT (version) DO NOTHING;
|
|
3651
|
+
`;
|
|
3652
|
+
}
|
|
3653
|
+
function applySupabaseRealtimeCompatibility(projectId, originalDbContainer, dbContainer, network, branchPassword) {
|
|
3654
|
+
const realtimeContainer = containerName("realtime", projectId);
|
|
3655
|
+
if (!dockerExists(realtimeContainer)) {
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
runDocker(["start", originalDbContainer]);
|
|
3659
|
+
try {
|
|
3660
|
+
waitForOriginalDb(originalDbContainer);
|
|
3661
|
+
const dbContainerIpAddress = containerIpAddress(dbContainer, network);
|
|
3662
|
+
const deadline = Date.now() + 6e4;
|
|
3663
|
+
let lastError = "";
|
|
3664
|
+
while (Date.now() < deadline) {
|
|
3665
|
+
try {
|
|
3666
|
+
runDockerWithInput(
|
|
3667
|
+
[
|
|
3668
|
+
"exec",
|
|
3669
|
+
"-i",
|
|
3670
|
+
"-e",
|
|
3671
|
+
`PGPASSWORD=${branchPassword}`,
|
|
3672
|
+
originalDbContainer,
|
|
3673
|
+
"psql",
|
|
3674
|
+
"-h",
|
|
3675
|
+
dbContainerIpAddress,
|
|
3676
|
+
"-p",
|
|
3677
|
+
"5432",
|
|
3678
|
+
"-U",
|
|
3679
|
+
"supabase_admin",
|
|
3680
|
+
"-d",
|
|
3681
|
+
"postgres",
|
|
3682
|
+
"-v",
|
|
3683
|
+
"ON_ERROR_STOP=1"
|
|
3684
|
+
],
|
|
3685
|
+
supabaseRealtimeCompatibilitySql(),
|
|
3686
|
+
{ timeoutMs: 7e3 }
|
|
3687
|
+
);
|
|
3688
|
+
runDocker(["restart", realtimeContainer]);
|
|
3689
|
+
return;
|
|
3690
|
+
} catch (error) {
|
|
3691
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3692
|
+
execFileSync("sleep", ["1"]);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
throw new Error(`Timed out applying Supabase Realtime compatibility: ${lastError}`);
|
|
3696
|
+
} finally {
|
|
3697
|
+
runDocker(["stop", originalDbContainer]);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
function kongApiUrl(projectId) {
|
|
3701
|
+
const kong = containerName("kong", projectId);
|
|
3702
|
+
if (!dockerExists(kong)) {
|
|
3703
|
+
return void 0;
|
|
3704
|
+
}
|
|
3705
|
+
const output = runDocker(["port", kong, "8000/tcp"]);
|
|
3706
|
+
const first = output.split("\n")[0];
|
|
3707
|
+
const port = first?.split(":").pop();
|
|
3708
|
+
return port ? `http://127.0.0.1:${port}` : void 0;
|
|
3709
|
+
}
|
|
3710
|
+
async function waitForHealth(projectId) {
|
|
3711
|
+
const apiUrl = kongApiUrl(projectId);
|
|
3712
|
+
if (!apiUrl) {
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
const deadline = Date.now() + 6e4;
|
|
3716
|
+
let lastError = "";
|
|
3717
|
+
while (Date.now() < deadline) {
|
|
3718
|
+
try {
|
|
3719
|
+
const authResponse = await fetch(`${apiUrl}/auth/v1/health`);
|
|
3720
|
+
const restResponse = await fetch(`${apiUrl}/rest/v1/`);
|
|
3721
|
+
if (authResponse.ok && restResponse.ok) {
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
lastError = `auth=${authResponse.status}, rest=${restResponse.status}`;
|
|
3725
|
+
} catch (error) {
|
|
3726
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3727
|
+
}
|
|
3728
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3729
|
+
}
|
|
3730
|
+
throw new Error(`Supabase health checks did not pass: ${lastError}`);
|
|
3731
|
+
}
|
|
3732
|
+
function restoreOriginalContainer(name, originalName) {
|
|
3733
|
+
if (dockerExists(name)) {
|
|
3734
|
+
runDocker(["rm", "-f", name]);
|
|
3735
|
+
}
|
|
3736
|
+
if (dockerExists(originalName)) {
|
|
3737
|
+
runDocker(["rename", originalName, name]);
|
|
3738
|
+
runDocker(["start", name]);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
function rollbackPartialLink(projectId, dbContainer, originalDbContainer, services) {
|
|
3742
|
+
for (const service of services.reverse()) {
|
|
3743
|
+
restoreOriginalContainer(service.name, service.originalName);
|
|
3744
|
+
}
|
|
3745
|
+
restoreOriginalContainer(dbContainer, originalDbContainer);
|
|
3746
|
+
deleteState(projectId);
|
|
3747
|
+
}
|
|
3748
|
+
async function statusSupabaseAction(options = {}) {
|
|
3749
|
+
try {
|
|
3750
|
+
const projectId = detectSupabaseProject(options.project);
|
|
3751
|
+
const state = readState(projectId);
|
|
3752
|
+
console.log(`Supabase project: ${projectId}`);
|
|
3753
|
+
if (!state) {
|
|
3754
|
+
console.log("Status: not linked");
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
console.log("Status: linked");
|
|
3758
|
+
console.log(`Branch: ${state.branchName}`);
|
|
3759
|
+
console.log(`Linked at: ${new Date(state.linkedAt).toLocaleString()}`);
|
|
3760
|
+
console.log(`DB container: ${state.dbContainer}`);
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
|
|
3763
|
+
process.exit(1);
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
async function linkSupabaseAction(branchName, options = {}) {
|
|
3767
|
+
let projectId = "";
|
|
3768
|
+
let dbContainer = "";
|
|
3769
|
+
let originalDbContainer = "";
|
|
3770
|
+
const createdServices = [];
|
|
3771
|
+
try {
|
|
3772
|
+
projectId = detectSupabaseProject(options.project);
|
|
3773
|
+
if (readState(projectId)) {
|
|
3774
|
+
throw new Error(`Supabase project ${projectId} is already linked. Run unlink first.`);
|
|
3775
|
+
}
|
|
3776
|
+
const branch = await resolveBranch(branchName);
|
|
3777
|
+
const parsedBranchUrl = parseBranchUrl(branch.branch_url);
|
|
3778
|
+
const branchUser = decodeURIComponent(parsedBranchUrl.username);
|
|
3779
|
+
const branchPassword = decodeURIComponent(parsedBranchUrl.password);
|
|
3780
|
+
dbContainer = containerName("db", projectId);
|
|
3781
|
+
originalDbContainer = `${dbContainer}${ORIGINAL_SUFFIX}`;
|
|
3782
|
+
const network = networkName(projectId);
|
|
3783
|
+
if (!dockerExists(dbContainer)) {
|
|
3784
|
+
throw new Error(`Local Supabase DB container not found: ${dbContainer}`);
|
|
3785
|
+
}
|
|
3786
|
+
if (dockerExists(originalDbContainer)) {
|
|
3787
|
+
throw new Error(`Backup DB container already exists: ${originalDbContainer}. Run unlink first.`);
|
|
3788
|
+
}
|
|
3789
|
+
console.log(`Linking Supabase project ${projectId} to branch ${branch.name}...`);
|
|
3790
|
+
const storageMigrationsDump = dumpLocalStorageMigrations(dbContainer);
|
|
3791
|
+
const originalDbContainerState = inspectContainer(dbContainer);
|
|
3792
|
+
const dbPortBindings = portPublishArgs(originalDbContainerState);
|
|
3793
|
+
runDocker(["stop", dbContainer]);
|
|
3794
|
+
runDocker(["rename", dbContainer, originalDbContainer]);
|
|
3795
|
+
startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, []);
|
|
3796
|
+
copyLocalStorageMigrationsToBranch(
|
|
3797
|
+
storageMigrationsDump,
|
|
3798
|
+
originalDbContainer,
|
|
3799
|
+
dbContainer,
|
|
3800
|
+
network,
|
|
3801
|
+
branchUser,
|
|
3802
|
+
branchPassword
|
|
3803
|
+
);
|
|
3804
|
+
for (const servicePrefix of SERVICE_SUFFIXES) {
|
|
3805
|
+
const service = recreateServiceForBranch(
|
|
3806
|
+
servicePrefix,
|
|
3807
|
+
projectId,
|
|
3808
|
+
network,
|
|
3809
|
+
dbContainer,
|
|
3810
|
+
branchUser,
|
|
3811
|
+
branchPassword
|
|
3812
|
+
);
|
|
3813
|
+
if (service) {
|
|
3814
|
+
createdServices.push(service);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
applySupabaseRealtimeCompatibility(
|
|
3818
|
+
projectId,
|
|
3819
|
+
originalDbContainer,
|
|
3820
|
+
dbContainer,
|
|
3821
|
+
network,
|
|
3822
|
+
branchPassword
|
|
3823
|
+
);
|
|
3824
|
+
runDocker(["rm", "-f", dbContainer]);
|
|
3825
|
+
startProxyContainer(projectId, network, dbContainer, parsedBranchUrl, dbPortBindings);
|
|
3826
|
+
const kong = containerName("kong", projectId);
|
|
3827
|
+
if (dockerExists(kong)) {
|
|
3828
|
+
runDocker(["restart", kong]);
|
|
3829
|
+
}
|
|
3830
|
+
const state = {
|
|
3831
|
+
version: 1,
|
|
3832
|
+
projectId,
|
|
3833
|
+
network,
|
|
3834
|
+
branchName: branch.name,
|
|
3835
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3836
|
+
dbContainer,
|
|
3837
|
+
originalDbContainer,
|
|
3838
|
+
services: createdServices
|
|
3839
|
+
};
|
|
3840
|
+
if (!options.skipHealthCheck) {
|
|
3841
|
+
await waitForHealth(projectId);
|
|
3842
|
+
}
|
|
3843
|
+
writeState(state);
|
|
3844
|
+
console.log("\u2713 Local Supabase is linked to the branch");
|
|
3845
|
+
trackEvent("CLI: settings supabase link succeeded");
|
|
3846
|
+
} catch (error) {
|
|
3847
|
+
if (projectId) {
|
|
3848
|
+
const state = readState(projectId);
|
|
3849
|
+
if (!state && dbContainer && originalDbContainer) {
|
|
3850
|
+
rollbackPartialLink(projectId, dbContainer, originalDbContainer, createdServices);
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
trackEvent("CLI: settings supabase link failed");
|
|
3854
|
+
console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
|
|
3855
|
+
process.exit(1);
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
async function unlinkSupabaseAction(options = {}) {
|
|
3859
|
+
try {
|
|
3860
|
+
const projectId = detectSupabaseProject(options.project);
|
|
3861
|
+
const state = readState(projectId);
|
|
3862
|
+
if (!state) {
|
|
3863
|
+
console.log(`Supabase project ${projectId} is not linked`);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
console.log(`Unlinking Supabase project ${projectId}...`);
|
|
3867
|
+
restoreOriginalContainer(state.dbContainer, state.originalDbContainer);
|
|
3868
|
+
for (const service of state.services) {
|
|
3869
|
+
restoreOriginalContainer(service.name, service.originalName);
|
|
3870
|
+
}
|
|
3871
|
+
const kong = containerName("kong", projectId);
|
|
3872
|
+
if (dockerExists(kong)) {
|
|
3873
|
+
runDocker(["restart", kong]);
|
|
3874
|
+
}
|
|
3875
|
+
deleteState(projectId);
|
|
3876
|
+
if (!options.skipHealthCheck) {
|
|
3877
|
+
await waitForHealth(projectId);
|
|
3878
|
+
}
|
|
3879
|
+
console.log("\u2713 Local Supabase restored to its local database");
|
|
3880
|
+
trackEvent("CLI: settings supabase unlink succeeded");
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
trackEvent("CLI: settings supabase unlink failed");
|
|
3883
|
+
console.error("\u2717 Failed:", error instanceof Error ? error.message : error);
|
|
3884
|
+
process.exit(1);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
|
|
2865
3888
|
// src/commands/settings/index.ts
|
|
2866
3889
|
var settingsCommand = new Command6("settings").description("Manage settings for the current connector").showHelpAfterError("(run 'ardent settings --help' for valid keys)");
|
|
2867
3890
|
settingsCommand.command("list").description("List current settings for the connector").action(listAction5);
|
|
@@ -2869,6 +3892,11 @@ settingsCommand.command("set <key> <value>").description(
|
|
|
2869
3892
|
"Set a setting. Keys: default_db, branch_sql. For branch_sql, prefix value with @ to read from file."
|
|
2870
3893
|
).action(setAction);
|
|
2871
3894
|
settingsCommand.command("remove <key>").description("Remove a setting. Keys: default_db, branch_sql.").action(removeAction2);
|
|
3895
|
+
var supabaseSettingsCommand = new Command6("supabase").description("Link local Supabase to an Ardent branch");
|
|
3896
|
+
supabaseSettingsCommand.command("status").description("Show local Supabase link status").option("--project <project>", "Supabase CLI project id").action(statusSupabaseAction);
|
|
3897
|
+
supabaseSettingsCommand.command("link [branch]").description("Point local Supabase at an Ardent branch").option("--project <project>", "Supabase CLI project id").option("--skip-health-check", "Skip Supabase API health checks after linking").action(linkSupabaseAction);
|
|
3898
|
+
supabaseSettingsCommand.command("unlink").description("Restore local Supabase to its local database").option("--project <project>", "Supabase CLI project id").option("--skip-health-check", "Skip Supabase API health checks after unlinking").action(unlinkSupabaseAction);
|
|
3899
|
+
settingsCommand.addCommand(supabaseSettingsCommand);
|
|
2872
3900
|
|
|
2873
3901
|
// src/commands/auth/index.ts
|
|
2874
3902
|
import { Command as Command7 } from "commander";
|
|
@@ -3060,16 +4088,16 @@ var logoutCommand = new Command7("logout").description("Logout from Ardent").act
|
|
|
3060
4088
|
var statusCommand = new Command7("status").description("Show status").action(statusAction);
|
|
3061
4089
|
|
|
3062
4090
|
// src/lib/update-check.ts
|
|
3063
|
-
import { existsSync as
|
|
3064
|
-
import { join as
|
|
3065
|
-
import { homedir as
|
|
3066
|
-
var UPDATE_CHECK_FILE =
|
|
4091
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
4092
|
+
import { join as join3 } from "path";
|
|
4093
|
+
import { homedir as homedir3 } from "os";
|
|
4094
|
+
var UPDATE_CHECK_FILE = join3(homedir3(), ".ardent", "update-check.json");
|
|
3067
4095
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3068
4096
|
var PACKAGE_NAME = "ardent-cli";
|
|
3069
4097
|
function loadCache() {
|
|
3070
4098
|
try {
|
|
3071
|
-
if (
|
|
3072
|
-
return JSON.parse(
|
|
4099
|
+
if (existsSync3(UPDATE_CHECK_FILE)) {
|
|
4100
|
+
return JSON.parse(readFileSync6(UPDATE_CHECK_FILE, "utf-8"));
|
|
3073
4101
|
}
|
|
3074
4102
|
} catch {
|
|
3075
4103
|
}
|
|
@@ -3081,7 +4109,7 @@ function saveCache(latestVersion) {
|
|
|
3081
4109
|
latest_version: latestVersion,
|
|
3082
4110
|
checked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3083
4111
|
};
|
|
3084
|
-
|
|
4112
|
+
writeFileSync3(UPDATE_CHECK_FILE, JSON.stringify(data));
|
|
3085
4113
|
} catch {
|
|
3086
4114
|
}
|
|
3087
4115
|
}
|
|
@@ -3168,6 +4196,7 @@ SETTINGS
|
|
|
3168
4196
|
settings list List current settings for the connector
|
|
3169
4197
|
settings set Set a setting (e.g. default_db testdb)
|
|
3170
4198
|
settings remove Remove a setting
|
|
4199
|
+
settings supabase Link local Supabase to the current branch
|
|
3171
4200
|
|
|
3172
4201
|
TEAM
|
|
3173
4202
|
invite <email> Invite a user to your organization
|