@supacloud/admin 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/index.js +411 -46
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
- package/package.json +1 -1
- package/dist/sshcrypto-8m50vnmb.node +0 -0
package/README.md
CHANGED
|
@@ -137,9 +137,19 @@ With `--artifact_transport remote` (the default), omitting
|
|
|
137
137
|
`--edge_runtime_version` retains the Management and Web Console-only upgrade
|
|
138
138
|
behavior and reports that Edge Runtime was not upgraded. Local transport
|
|
139
139
|
requires exact Management and Edge Runtime versions. Caddy and GoTrue are
|
|
140
|
-
outside this transaction and are not replaced.
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
outside this transaction and are not replaced.
|
|
141
|
+
|
|
142
|
+
The remote transport allows the same 30-minute transaction window plus bounded
|
|
143
|
+
verifier/bootstrap downloads: 42 minutes for direct GitHub access, or 52
|
|
144
|
+
minutes when each download may try direct GitHub before an explicit proxy. If
|
|
145
|
+
the SSH command times out or its stream fails after dispatch, Admin reports
|
|
146
|
+
`OUTCOME_UNKNOWN` and does not issue client-side helper cleanup. The remote
|
|
147
|
+
command may finish later and then run its own cleanup. Reconcile the reported
|
|
148
|
+
helper and trusted-root paths and read back deployed versions before deciding
|
|
149
|
+
whether to retry.
|
|
150
|
+
|
|
151
|
+
After a capable Management release is active, an exact rollback can use that
|
|
152
|
+
active upgrader with explicit older targets, for example:
|
|
143
153
|
|
|
144
154
|
```bash
|
|
145
155
|
npx @supacloud/admin ssh upgrade \
|
package/dist/index.js
CHANGED
|
@@ -4759,7 +4759,7 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
4759
4759
|
|
|
4760
4760
|
// node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
|
|
4761
4761
|
var require_sshcrypto = __commonJS((exports, module) => {
|
|
4762
|
-
module.exports = __require("./sshcrypto-
|
|
4762
|
+
module.exports = __require("./sshcrypto-vd2k5hq9.node");
|
|
4763
4763
|
});
|
|
4764
4764
|
|
|
4765
4765
|
// node_modules/ssh2/lib/protocol/crypto/poly1305.js
|
|
@@ -24931,6 +24931,14 @@ function schemaProperties(schema) {
|
|
|
24931
24931
|
var import_ssh2 = __toESM(require_lib3(), 1);
|
|
24932
24932
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
24933
24933
|
import { readFileSync } from "node:fs";
|
|
24934
|
+
|
|
24935
|
+
class SshCommandOutcomeUnknownError extends Error {
|
|
24936
|
+
code = "OUTCOME_UNKNOWN";
|
|
24937
|
+
constructor(message) {
|
|
24938
|
+
super(message);
|
|
24939
|
+
this.name = "SshCommandOutcomeUnknownError";
|
|
24940
|
+
}
|
|
24941
|
+
}
|
|
24934
24942
|
var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
24935
24943
|
var MAX_CONFIGURABLE_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
24936
24944
|
var DEFAULT_UPLOAD_TIMEOUT_MS = 10 * 60000;
|
|
@@ -25148,34 +25156,45 @@ class SshTransport {
|
|
|
25148
25156
|
const stderr = new BoundedOutputCollector(outputLimit);
|
|
25149
25157
|
const timer = setTimeout(() => {
|
|
25150
25158
|
connectionReusable = false;
|
|
25151
|
-
reject(new
|
|
25159
|
+
reject(new SshCommandOutcomeUnknownError(`SSH command timed out after ${timeoutMs}ms; remote outcome is unknown`));
|
|
25152
25160
|
}, timeoutMs);
|
|
25153
|
-
|
|
25154
|
-
|
|
25155
|
-
|
|
25156
|
-
|
|
25157
|
-
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
25161
|
-
|
|
25162
|
-
|
|
25163
|
-
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25161
|
+
try {
|
|
25162
|
+
conn.exec(command, (err, stream) => {
|
|
25163
|
+
if (err) {
|
|
25164
|
+
clearTimeout(timer);
|
|
25165
|
+
connectionReusable = false;
|
|
25166
|
+
return reject(err);
|
|
25167
|
+
}
|
|
25168
|
+
stream.on("close", (code) => {
|
|
25169
|
+
clearTimeout(timer);
|
|
25170
|
+
if (code === undefined) {
|
|
25171
|
+
connectionReusable = false;
|
|
25172
|
+
reject(new SshCommandOutcomeUnknownError("SSH command stream closed without a terminal status; remote outcome is unknown"));
|
|
25173
|
+
return;
|
|
25174
|
+
}
|
|
25175
|
+
resolve({
|
|
25176
|
+
success: code === 0,
|
|
25177
|
+
stdout: stdout.finalize(),
|
|
25178
|
+
stderr: stderr.finalize(),
|
|
25179
|
+
code: code ?? 128,
|
|
25180
|
+
stdoutTruncated: stdout.truncated,
|
|
25181
|
+
stderrTruncated: stderr.truncated
|
|
25182
|
+
});
|
|
25183
|
+
}).on("error", () => {
|
|
25184
|
+
clearTimeout(timer);
|
|
25185
|
+
connectionReusable = false;
|
|
25186
|
+
reject(new SshCommandOutcomeUnknownError("SSH command stream failed after dispatch; remote outcome is unknown"));
|
|
25187
|
+
}).on("data", (data) => {
|
|
25188
|
+
stdout.append(data);
|
|
25189
|
+
}).stderr.on("data", (data) => {
|
|
25190
|
+
stderr.append(data);
|
|
25168
25191
|
});
|
|
25169
|
-
}).on("error", (streamError) => {
|
|
25170
|
-
clearTimeout(timer);
|
|
25171
|
-
connectionReusable = false;
|
|
25172
|
-
reject(streamError);
|
|
25173
|
-
}).on("data", (data) => {
|
|
25174
|
-
stdout.append(data);
|
|
25175
|
-
}).stderr.on("data", (data) => {
|
|
25176
|
-
stderr.append(data);
|
|
25177
25192
|
});
|
|
25178
|
-
})
|
|
25193
|
+
} catch (error) {
|
|
25194
|
+
clearTimeout(timer);
|
|
25195
|
+
connectionReusable = false;
|
|
25196
|
+
reject(error);
|
|
25197
|
+
}
|
|
25179
25198
|
});
|
|
25180
25199
|
} finally {
|
|
25181
25200
|
if (connectionReusable)
|
|
@@ -26011,12 +26030,74 @@ function validatedPostTimeout(options) {
|
|
|
26011
26030
|
}
|
|
26012
26031
|
return timeoutMs;
|
|
26013
26032
|
}
|
|
26033
|
+
function validatedGetResponseLimit(options) {
|
|
26034
|
+
const maxBytes = options.maxResponseBytes;
|
|
26035
|
+
if (maxBytes === undefined)
|
|
26036
|
+
return;
|
|
26037
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
26038
|
+
throw new RangeError("HTTP response limit must be a positive safe integer");
|
|
26039
|
+
}
|
|
26040
|
+
return maxBytes;
|
|
26041
|
+
}
|
|
26042
|
+
function responseExceedsDeclaredLimit(response, maxBytes) {
|
|
26043
|
+
const contentLength = response.headers.get("content-length");
|
|
26044
|
+
return contentLength !== null && /^\d+$/u.test(contentLength) && Number(contentLength) > maxBytes;
|
|
26045
|
+
}
|
|
26046
|
+
function joinedResponseBytes(chunks, totalBytes) {
|
|
26047
|
+
const responseBytes = new Uint8Array(totalBytes);
|
|
26048
|
+
let offset = 0;
|
|
26049
|
+
for (const chunk of chunks) {
|
|
26050
|
+
responseBytes.set(chunk, offset);
|
|
26051
|
+
offset += chunk.byteLength;
|
|
26052
|
+
}
|
|
26053
|
+
return responseBytes;
|
|
26054
|
+
}
|
|
26055
|
+
async function boundedResponseBytes(response, maxBytes) {
|
|
26056
|
+
if (responseExceedsDeclaredLimit(response, maxBytes)) {
|
|
26057
|
+
response.body?.cancel().catch(() => {
|
|
26058
|
+
return;
|
|
26059
|
+
});
|
|
26060
|
+
return null;
|
|
26061
|
+
}
|
|
26062
|
+
if (!response.body)
|
|
26063
|
+
return new Uint8Array;
|
|
26064
|
+
const reader = response.body.getReader();
|
|
26065
|
+
const chunks = [];
|
|
26066
|
+
let totalBytes = 0;
|
|
26067
|
+
while (true) {
|
|
26068
|
+
const { done, value } = await reader.read();
|
|
26069
|
+
if (done)
|
|
26070
|
+
return joinedResponseBytes(chunks, totalBytes);
|
|
26071
|
+
totalBytes += value.byteLength;
|
|
26072
|
+
if (totalBytes > maxBytes) {
|
|
26073
|
+
reader.cancel().catch(() => {
|
|
26074
|
+
return;
|
|
26075
|
+
});
|
|
26076
|
+
return null;
|
|
26077
|
+
}
|
|
26078
|
+
chunks.push(value);
|
|
26079
|
+
}
|
|
26080
|
+
}
|
|
26081
|
+
async function boundedResponseJson(response, maxBytes) {
|
|
26082
|
+
const responseBytes = await boundedResponseBytes(response, maxBytes);
|
|
26083
|
+
if (responseBytes === null)
|
|
26084
|
+
return null;
|
|
26085
|
+
try {
|
|
26086
|
+
const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
|
|
26087
|
+
return JSON.parse(responseText);
|
|
26088
|
+
} catch (error) {
|
|
26089
|
+
if (error instanceof SyntaxError || error instanceof TypeError)
|
|
26090
|
+
return null;
|
|
26091
|
+
throw error;
|
|
26092
|
+
}
|
|
26093
|
+
}
|
|
26014
26094
|
async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
|
|
26015
26095
|
const controller = new AbortController;
|
|
26016
26096
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
26017
26097
|
try {
|
|
26018
26098
|
return await fetch(url, {
|
|
26019
26099
|
...options,
|
|
26100
|
+
redirect: "error",
|
|
26020
26101
|
signal: controller.signal
|
|
26021
26102
|
});
|
|
26022
26103
|
} finally {
|
|
@@ -26059,13 +26140,14 @@ class HttpTransport {
|
|
|
26059
26140
|
"Content-Type": "application/json"
|
|
26060
26141
|
};
|
|
26061
26142
|
}
|
|
26062
|
-
async get(path) {
|
|
26143
|
+
async get(path, options = {}) {
|
|
26144
|
+
const maxResponseBytes = validatedGetResponseLimit(options);
|
|
26063
26145
|
try {
|
|
26064
26146
|
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
26065
26147
|
method: "GET",
|
|
26066
26148
|
headers: this.headers()
|
|
26067
26149
|
});
|
|
26068
|
-
const data = await res.json().catch(() => null);
|
|
26150
|
+
const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
|
|
26069
26151
|
return { ok: res.ok, status: res.status, data };
|
|
26070
26152
|
} catch (error) {
|
|
26071
26153
|
return transportFailure(error);
|
|
@@ -27722,8 +27804,10 @@ var SAFE_HOSTNAME = /^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9
|
|
|
27722
27804
|
var SAFE_SYSTEMD_UNIT = /^[a-zA-Z0-9][a-zA-Z0-9_.@:-]{0,127}$/;
|
|
27723
27805
|
var SAFE_DB_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/;
|
|
27724
27806
|
var MINIMUM_COMPONENT_UPGRADE_VERSION = "0.50.27";
|
|
27725
|
-
var
|
|
27726
|
-
var
|
|
27807
|
+
var DIRECT_BOOTSTRAP_TRANSFER_BUDGET_MS = 12 * 60000;
|
|
27808
|
+
var PROXIED_BOOTSTRAP_TRANSFER_BUDGET_MS = 22 * 60000;
|
|
27809
|
+
var DIRECT_UPGRADE_SSH_TIMEOUT_MS = UPGRADE_OBSERVATION_TIMEOUT_MS + DIRECT_BOOTSTRAP_TRANSFER_BUDGET_MS;
|
|
27810
|
+
var PROXIED_UPGRADE_SSH_TIMEOUT_MS = UPGRADE_OBSERVATION_TIMEOUT_MS + PROXIED_BOOTSTRAP_TRANSFER_BUDGET_MS;
|
|
27727
27811
|
var PLATFORM_PROBE_TIMEOUT_MS = 1e4;
|
|
27728
27812
|
var PLATFORM_HASH_TIMEOUT_MS = 30000;
|
|
27729
27813
|
var WEB_CONSOLE_PROBE_TIMEOUT_MS = 60000;
|
|
@@ -27996,11 +28080,58 @@ async function removeRemoteUpgradeHelper(ssh, helperPath) {
|
|
|
27996
28080
|
throw new Error(`Failed to remove remote upgrade helper (exit ${cleanup.code}): ${cleanup.stderr.slice(-300)}`);
|
|
27997
28081
|
}
|
|
27998
28082
|
}
|
|
28083
|
+
|
|
28084
|
+
class RemoteUpgradeOutcomeUnknownError extends AggregateError {
|
|
28085
|
+
code = "OUTCOME_UNKNOWN";
|
|
28086
|
+
}
|
|
28087
|
+
function boundedUpgradeError(error) {
|
|
28088
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28089
|
+
const diagnostic = redactSshOutput(message).replace(/[\r\n\t]+/g, " ").trim().slice(0, 500);
|
|
28090
|
+
return new Error(diagnostic || "Remote operation ended without a diagnostic");
|
|
28091
|
+
}
|
|
28092
|
+
function remoteUpgradeEvidence(helperPath) {
|
|
28093
|
+
const trustedRootPath = join2(dirname(helperPath), SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
28094
|
+
return `helper=${helperPath} trusted_root=${trustedRootPath}`;
|
|
28095
|
+
}
|
|
28096
|
+
function remoteUpgradeReconciliationFailure(message, failures, helperPath) {
|
|
28097
|
+
return new RemoteUpgradeOutcomeUnknownError(failures.map(boundedUpgradeError), `${message} Reconcile ${remoteUpgradeEvidence(helperPath)}; do not retry blindly`);
|
|
28098
|
+
}
|
|
28099
|
+
function remoteUpgradeOutcomeUnknown(transportError, helperPath) {
|
|
28100
|
+
return remoteUpgradeReconciliationFailure("OUTCOME_UNKNOWN: Remote upgrade transport ended after dispatch; client cleanup was suppressed " + "because the remote command may still be running. Verify deployed versions before retrying.", [transportError], helperPath);
|
|
28101
|
+
}
|
|
27999
28102
|
function remoteUpgradeFailure(execution) {
|
|
28000
28103
|
const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
|
|
28001
28104
|
return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
|
|
28002
28105
|
}
|
|
28106
|
+
function remoteHelperSetupOutcomeUnknown(outcome) {
|
|
28107
|
+
const cleanupStatus = outcome.cleanupFailed ? "the helper cleanup did not complete" : "the cleanup command completed, but setup may finish later";
|
|
28108
|
+
const failures = outcome.cleanupFailed ? [outcome.executionError, outcome.cleanupError] : [outcome.executionError];
|
|
28109
|
+
return remoteUpgradeReconciliationFailure(`OUTCOME_UNKNOWN: Remote helper setup ended without terminal status; ${cleanupStatus}. ` + "The upgrade command was not dispatched.", failures, outcome.helperPath);
|
|
28110
|
+
}
|
|
28111
|
+
function completedUpgradeCleanupOutcomeUnknown(outcome) {
|
|
28112
|
+
if (!outcome.execution) {
|
|
28113
|
+
return remoteUpgradeReconciliationFailure("OUTCOME_UNKNOWN: Upgrade execution returned no result, and helper cleanup outcome is unknown.", [outcome.cleanupError], outcome.helperPath);
|
|
28114
|
+
}
|
|
28115
|
+
const message = outcome.execution.success ? "OUTCOME_UNKNOWN: Remote upgrade succeeded, but helper cleanup outcome is unknown." : "OUTCOME_UNKNOWN: Remote upgrade failed with a terminal result, and helper cleanup outcome is unknown.";
|
|
28116
|
+
const failures = outcome.execution.success ? [outcome.cleanupError] : [remoteUpgradeFailure(outcome.execution), outcome.cleanupError];
|
|
28117
|
+
return remoteUpgradeReconciliationFailure(message, failures, outcome.helperPath);
|
|
28118
|
+
}
|
|
28119
|
+
function remoteHelperCleanupOutcomeUnknown(outcome) {
|
|
28120
|
+
if (!outcome.upgradeExecutionRequested) {
|
|
28121
|
+
return remoteUpgradeReconciliationFailure("OUTCOME_UNKNOWN: Remote helper setup failed, and helper cleanup outcome is unknown. " + "The upgrade command was not dispatched.", [outcome.executionError, outcome.cleanupError], outcome.helperPath);
|
|
28122
|
+
}
|
|
28123
|
+
if (outcome.executionFailed) {
|
|
28124
|
+
return remoteUpgradeReconciliationFailure("OUTCOME_UNKNOWN: Upgrade execution request failed, and helper cleanup outcome is unknown.", [outcome.executionError, outcome.cleanupError], outcome.helperPath);
|
|
28125
|
+
}
|
|
28126
|
+
return completedUpgradeCleanupOutcomeUnknown(outcome);
|
|
28127
|
+
}
|
|
28003
28128
|
function officialUpgradeOutcome(outcome) {
|
|
28129
|
+
if (!outcome.upgradeExecutionRequested && outcome.executionError instanceof SshCommandOutcomeUnknownError) {
|
|
28130
|
+
throw remoteHelperSetupOutcomeUnknown(outcome);
|
|
28131
|
+
}
|
|
28132
|
+
if (outcome.cleanupFailed && outcome.cleanupError instanceof SshCommandOutcomeUnknownError) {
|
|
28133
|
+
throw remoteHelperCleanupOutcomeUnknown(outcome);
|
|
28134
|
+
}
|
|
28004
28135
|
if (outcome.executionFailed && outcome.cleanupFailed) {
|
|
28005
28136
|
throw new AggregateError([outcome.executionError, outcome.cleanupError], "Upgrade execution failed and helper cleanup did not complete");
|
|
28006
28137
|
}
|
|
@@ -28021,34 +28152,37 @@ async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
|
|
|
28021
28152
|
let execution;
|
|
28022
28153
|
let executionFailed = false;
|
|
28023
28154
|
let executionError;
|
|
28024
|
-
let
|
|
28155
|
+
let upgradeExecutionRequested = false;
|
|
28025
28156
|
try {
|
|
28026
28157
|
await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
|
|
28027
|
-
helperPrepared = true;
|
|
28028
28158
|
await ssh.uploadText(helperPath, release_assets_default, 384);
|
|
28029
28159
|
const trustedRootPath = join2(dirname(helperPath), SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
28030
28160
|
await ssh.uploadText(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, 384);
|
|
28161
|
+
upgradeExecutionRequested = true;
|
|
28031
28162
|
execution = await ssh.exec(command, timeoutMs);
|
|
28032
28163
|
} catch (error) {
|
|
28164
|
+
if (upgradeExecutionRequested && error instanceof SshCommandOutcomeUnknownError) {
|
|
28165
|
+
throw remoteUpgradeOutcomeUnknown(error, helperPath);
|
|
28166
|
+
}
|
|
28033
28167
|
executionFailed = true;
|
|
28034
28168
|
executionError = error;
|
|
28035
28169
|
}
|
|
28036
28170
|
let cleanupFailed = false;
|
|
28037
28171
|
let cleanupError;
|
|
28038
|
-
|
|
28039
|
-
|
|
28040
|
-
|
|
28041
|
-
|
|
28042
|
-
|
|
28043
|
-
cleanupError = error;
|
|
28044
|
-
}
|
|
28172
|
+
try {
|
|
28173
|
+
await removeRemoteUpgradeHelper(ssh, helperPath);
|
|
28174
|
+
} catch (error) {
|
|
28175
|
+
cleanupFailed = true;
|
|
28176
|
+
cleanupError = error;
|
|
28045
28177
|
}
|
|
28046
28178
|
return officialUpgradeOutcome({
|
|
28047
28179
|
execution,
|
|
28048
28180
|
executionFailed,
|
|
28049
28181
|
executionError,
|
|
28050
28182
|
cleanupFailed,
|
|
28051
|
-
cleanupError
|
|
28183
|
+
cleanupError,
|
|
28184
|
+
helperPath,
|
|
28185
|
+
upgradeExecutionRequested
|
|
28052
28186
|
});
|
|
28053
28187
|
}
|
|
28054
28188
|
function assertSafeGithubProxy(value) {
|
|
@@ -29646,6 +29780,229 @@ function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expec
|
|
|
29646
29780
|
return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
|
|
29647
29781
|
}
|
|
29648
29782
|
|
|
29783
|
+
// ../cli/src/shared/tools/project-read-projection.ts
|
|
29784
|
+
var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
|
|
29785
|
+
var PROJECT_REF_PATTERN = /^[a-z0-9-]{1,20}$/;
|
|
29786
|
+
var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
29787
|
+
var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
29788
|
+
var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
29789
|
+
var DNS_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
|
|
29790
|
+
var DATABASE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
29791
|
+
var MAX_PROJECTS = 1e4;
|
|
29792
|
+
var PROJECT_SUMMARY_KEYS = new Set([
|
|
29793
|
+
"id",
|
|
29794
|
+
"ref",
|
|
29795
|
+
"organization_id",
|
|
29796
|
+
"organization_slug",
|
|
29797
|
+
"name",
|
|
29798
|
+
"region",
|
|
29799
|
+
"created_at",
|
|
29800
|
+
"status"
|
|
29801
|
+
]);
|
|
29802
|
+
var PROJECT_DETAILS_KEYS = new Set([
|
|
29803
|
+
...PROJECT_SUMMARY_KEYS,
|
|
29804
|
+
"database",
|
|
29805
|
+
"api",
|
|
29806
|
+
"studio",
|
|
29807
|
+
"config",
|
|
29808
|
+
"anon_key",
|
|
29809
|
+
"services"
|
|
29810
|
+
]);
|
|
29811
|
+
var PROJECT_DATABASE_KEYS = new Set([
|
|
29812
|
+
"host",
|
|
29813
|
+
"version",
|
|
29814
|
+
"postgres_engine",
|
|
29815
|
+
"release_channel"
|
|
29816
|
+
]);
|
|
29817
|
+
var PROJECT_ENDPOINT_KEYS = new Set(["url"]);
|
|
29818
|
+
function plainRecord(candidate) {
|
|
29819
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
29820
|
+
return null;
|
|
29821
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
29822
|
+
return prototype === Object.prototype || prototype === null ? candidate : null;
|
|
29823
|
+
}
|
|
29824
|
+
function hasOnlyKeys(record, allowedKeys) {
|
|
29825
|
+
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
29826
|
+
}
|
|
29827
|
+
function hasWellFormedUnicode(text) {
|
|
29828
|
+
for (let index = 0;index < text.length; index++) {
|
|
29829
|
+
const codeUnit = text.charCodeAt(index);
|
|
29830
|
+
if (codeUnit >= 55296 && codeUnit <= 56319) {
|
|
29831
|
+
if (index + 1 >= text.length)
|
|
29832
|
+
return false;
|
|
29833
|
+
const lowSurrogate = text.charCodeAt(index + 1);
|
|
29834
|
+
if (lowSurrogate < 56320 || lowSurrogate > 57343)
|
|
29835
|
+
return false;
|
|
29836
|
+
index++;
|
|
29837
|
+
} else if (codeUnit >= 56320 && codeUnit <= 57343) {
|
|
29838
|
+
return false;
|
|
29839
|
+
}
|
|
29840
|
+
}
|
|
29841
|
+
return true;
|
|
29842
|
+
}
|
|
29843
|
+
function boundedText(candidate, maxLength) {
|
|
29844
|
+
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
|
|
29845
|
+
}
|
|
29846
|
+
function matchingText(candidate, maxLength, pattern) {
|
|
29847
|
+
const candidateText = boundedText(candidate, maxLength);
|
|
29848
|
+
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
29849
|
+
}
|
|
29850
|
+
function canonicalTimestamp(candidate) {
|
|
29851
|
+
const timestamp = boundedText(candidate, 64);
|
|
29852
|
+
if (!timestamp)
|
|
29853
|
+
return null;
|
|
29854
|
+
const milliseconds = Date.parse(timestamp);
|
|
29855
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === timestamp ? timestamp : null;
|
|
29856
|
+
}
|
|
29857
|
+
function projectedSummary(project) {
|
|
29858
|
+
const summary = {
|
|
29859
|
+
id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29860
|
+
ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN),
|
|
29861
|
+
organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29862
|
+
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29863
|
+
name: boundedText(project.name, 100),
|
|
29864
|
+
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
29865
|
+
created_at: canonicalTimestamp(project.created_at),
|
|
29866
|
+
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
29867
|
+
};
|
|
29868
|
+
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
29869
|
+
}
|
|
29870
|
+
function projectSummary(candidate) {
|
|
29871
|
+
const project = plainRecord(candidate);
|
|
29872
|
+
return project && hasOnlyKeys(project, PROJECT_SUMMARY_KEYS) ? projectedSummary(project) : null;
|
|
29873
|
+
}
|
|
29874
|
+
function databaseHost(candidate) {
|
|
29875
|
+
const host = boundedText(candidate, 255);
|
|
29876
|
+
if (!host)
|
|
29877
|
+
return null;
|
|
29878
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
29879
|
+
try {
|
|
29880
|
+
const parsedHost = new URL(`http://${host}`);
|
|
29881
|
+
return parsedHost.host === host ? host : null;
|
|
29882
|
+
} catch (error) {
|
|
29883
|
+
if (error instanceof TypeError)
|
|
29884
|
+
return null;
|
|
29885
|
+
throw error;
|
|
29886
|
+
}
|
|
29887
|
+
}
|
|
29888
|
+
const ipv4Parts = host.split(".");
|
|
29889
|
+
if (ipv4Parts.length === 4 && ipv4Parts.every((part) => /^\d{1,3}$/u.test(part))) {
|
|
29890
|
+
return ipv4Parts.every((part) => Number(part) <= 255) ? host : null;
|
|
29891
|
+
}
|
|
29892
|
+
return ipv4Parts.every((label) => DNS_LABEL_PATTERN.test(label)) ? host : null;
|
|
29893
|
+
}
|
|
29894
|
+
function projectDatabase(candidate) {
|
|
29895
|
+
const database = plainRecord(candidate);
|
|
29896
|
+
if (!database || !hasOnlyKeys(database, PROJECT_DATABASE_KEYS))
|
|
29897
|
+
return null;
|
|
29898
|
+
const host = databaseHost(database.host);
|
|
29899
|
+
const version = matchingText(database.version, 64, DATABASE_VERSION_PATTERN);
|
|
29900
|
+
const postgresEngine = matchingText(database.postgres_engine, 64, DATABASE_VERSION_PATTERN);
|
|
29901
|
+
const releaseChannel = matchingText(database.release_channel, 64, DATABASE_VERSION_PATTERN);
|
|
29902
|
+
return host && version && postgresEngine && releaseChannel ? { host, version, postgres_engine: postgresEngine, release_channel: releaseChannel } : null;
|
|
29903
|
+
}
|
|
29904
|
+
function rawUrlHasNoPath(candidate) {
|
|
29905
|
+
if (candidate.trim() !== candidate || candidate.includes("\\"))
|
|
29906
|
+
return false;
|
|
29907
|
+
const schemeEnd = candidate.indexOf("://");
|
|
29908
|
+
const pathStart = candidate.indexOf("/", schemeEnd + 3);
|
|
29909
|
+
return pathStart === -1;
|
|
29910
|
+
}
|
|
29911
|
+
function projectEndpoint(candidate) {
|
|
29912
|
+
const endpoint = plainRecord(candidate);
|
|
29913
|
+
if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
|
|
29914
|
+
return null;
|
|
29915
|
+
const endpointUrl = boundedText(endpoint.url, 2048);
|
|
29916
|
+
if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
|
|
29917
|
+
return null;
|
|
29918
|
+
try {
|
|
29919
|
+
const url = new URL(endpointUrl);
|
|
29920
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
29921
|
+
return null;
|
|
29922
|
+
return { url: url.origin };
|
|
29923
|
+
} catch (error) {
|
|
29924
|
+
if (error instanceof TypeError)
|
|
29925
|
+
return null;
|
|
29926
|
+
throw error;
|
|
29927
|
+
}
|
|
29928
|
+
}
|
|
29929
|
+
function discardedDetailFieldsAreValid(project) {
|
|
29930
|
+
if (project.config !== undefined && plainRecord(project.config) === null)
|
|
29931
|
+
return false;
|
|
29932
|
+
if (project.anon_key !== undefined && boundedText(project.anon_key, 16384) === null)
|
|
29933
|
+
return false;
|
|
29934
|
+
return project.services === undefined || Array.isArray(project.services);
|
|
29935
|
+
}
|
|
29936
|
+
function projectDetails(candidate, expectedRef) {
|
|
29937
|
+
const project = plainRecord(candidate);
|
|
29938
|
+
if (!project || !hasOnlyKeys(project, PROJECT_DETAILS_KEYS))
|
|
29939
|
+
return null;
|
|
29940
|
+
const summary = projectedSummary(project);
|
|
29941
|
+
const database = projectDatabase(project.database);
|
|
29942
|
+
const api = project.api === undefined ? undefined : projectEndpoint(project.api);
|
|
29943
|
+
const studio = project.studio === undefined ? undefined : projectEndpoint(project.studio);
|
|
29944
|
+
if (!summary || summary.ref !== expectedRef || !database || !discardedDetailFieldsAreValid(project) || project.api !== undefined && !api || project.studio !== undefined && !studio)
|
|
29945
|
+
return null;
|
|
29946
|
+
return {
|
|
29947
|
+
...summary,
|
|
29948
|
+
database,
|
|
29949
|
+
...api ? { api } : {},
|
|
29950
|
+
...studio ? { studio } : {}
|
|
29951
|
+
};
|
|
29952
|
+
}
|
|
29953
|
+
function payloadWithinLimit(candidate) {
|
|
29954
|
+
try {
|
|
29955
|
+
const serializedPayload = JSON.stringify(candidate);
|
|
29956
|
+
return serializedPayload !== undefined && new TextEncoder().encode(serializedPayload).byteLength <= PROJECT_READ_RESPONSE_MAX_BYTES;
|
|
29957
|
+
} catch {
|
|
29958
|
+
return false;
|
|
29959
|
+
}
|
|
29960
|
+
}
|
|
29961
|
+
function safeProjectList(candidate) {
|
|
29962
|
+
if (!payloadWithinLimit(candidate) || !Array.isArray(candidate) || candidate.length > MAX_PROJECTS)
|
|
29963
|
+
return null;
|
|
29964
|
+
const safeProjects = [];
|
|
29965
|
+
const ids = new Set;
|
|
29966
|
+
const refs = new Set;
|
|
29967
|
+
for (const projectCandidate of candidate) {
|
|
29968
|
+
const project = projectSummary(projectCandidate);
|
|
29969
|
+
if (!project || ids.has(project.id) || refs.has(project.ref))
|
|
29970
|
+
return null;
|
|
29971
|
+
ids.add(project.id);
|
|
29972
|
+
refs.add(project.ref);
|
|
29973
|
+
safeProjects.push(project);
|
|
29974
|
+
}
|
|
29975
|
+
return safeProjects;
|
|
29976
|
+
}
|
|
29977
|
+
function validHttpStatus(status) {
|
|
29978
|
+
return Number.isSafeInteger(status) && status >= 100 && status <= 599;
|
|
29979
|
+
}
|
|
29980
|
+
function successfulResponse(response) {
|
|
29981
|
+
return response.ok === true && validHttpStatus(response.status) && response.status >= 200 && response.status <= 299;
|
|
29982
|
+
}
|
|
29983
|
+
function failedResult(message) {
|
|
29984
|
+
return { text: `❌ ${message}`, isError: true };
|
|
29985
|
+
}
|
|
29986
|
+
function failedHttpResult(label, status) {
|
|
29987
|
+
return failedResult(validHttpStatus(status) ? `${label} request failed (${status})` : `${label} request failed`);
|
|
29988
|
+
}
|
|
29989
|
+
function successfulResult(payload) {
|
|
29990
|
+
return { text: JSON.stringify(payload, null, 2), isError: false };
|
|
29991
|
+
}
|
|
29992
|
+
function projectListRead(response) {
|
|
29993
|
+
if (!successfulResponse(response))
|
|
29994
|
+
return failedHttpResult("Project list", response.status);
|
|
29995
|
+
const projects = safeProjectList(response.data);
|
|
29996
|
+
return projects ? successfulResult(projects) : failedResult("Invalid project list response");
|
|
29997
|
+
}
|
|
29998
|
+
function projectGetRead(response, expectedRef) {
|
|
29999
|
+
if (!successfulResponse(response))
|
|
30000
|
+
return failedHttpResult("Project get", response.status);
|
|
30001
|
+
if (!payloadWithinLimit(response.data))
|
|
30002
|
+
return failedResult("Invalid project response");
|
|
30003
|
+
const project = projectDetails(response.data, expectedRef);
|
|
30004
|
+
return project ? successfulResult(project) : failedResult("Invalid project response");
|
|
30005
|
+
}
|
|
29649
30006
|
// src/shared/tools/project-cli-tools.ts
|
|
29650
30007
|
var PROJECT_SERVICE_NAMES = [
|
|
29651
30008
|
"postgrest",
|
|
@@ -29695,6 +30052,10 @@ var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
|
|
|
29695
30052
|
function projectToolResponse(text) {
|
|
29696
30053
|
return { content: [{ type: "text", text }] };
|
|
29697
30054
|
}
|
|
30055
|
+
function projectReadResponse(readResult) {
|
|
30056
|
+
const response = projectToolResponse(readResult.text);
|
|
30057
|
+
return readResult.isError ? { ...response, isError: true } : response;
|
|
30058
|
+
}
|
|
29698
30059
|
function failedProjectServiceResponse(message) {
|
|
29699
30060
|
return {
|
|
29700
30061
|
content: [{ type: "text", text: `❌ ${message}` }],
|
|
@@ -30027,8 +30388,9 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30027
30388
|
let text;
|
|
30028
30389
|
switch (action) {
|
|
30029
30390
|
case "list":
|
|
30030
|
-
|
|
30031
|
-
|
|
30391
|
+
return projectReadResponse(projectListRead(await http.get("/v1/projects", {
|
|
30392
|
+
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
30393
|
+
})));
|
|
30032
30394
|
case "create": {
|
|
30033
30395
|
if (!name)
|
|
30034
30396
|
throw new Error("'name' is required for create");
|
|
@@ -30066,9 +30428,12 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30066
30428
|
createRequest.credential_delivery = "response";
|
|
30067
30429
|
return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
|
|
30068
30430
|
}
|
|
30069
|
-
case "get":
|
|
30070
|
-
|
|
30071
|
-
|
|
30431
|
+
case "get": {
|
|
30432
|
+
const resolvedRef = resolveRef(ref);
|
|
30433
|
+
return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
|
|
30434
|
+
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
30435
|
+
}), resolvedRef));
|
|
30436
|
+
}
|
|
30072
30437
|
case "delete": {
|
|
30073
30438
|
const resolvedRef = resolveRef(ref);
|
|
30074
30439
|
text = simple(await http.delete(`/v1/projects/${resolvedRef}`), `Project ${resolvedRef} deleted`);
|
|
@@ -30472,7 +30837,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
30472
30837
|
// package.json
|
|
30473
30838
|
var package_default = {
|
|
30474
30839
|
name: "@supacloud/admin",
|
|
30475
|
-
version: "0.
|
|
30840
|
+
version: "0.11.0",
|
|
30476
30841
|
description: "Platform administration CLI for SupaCloud operators",
|
|
30477
30842
|
type: "module",
|
|
30478
30843
|
main: "./dist/index.js",
|
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|