@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 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. After a capable Management
141
- release is active, an exact rollback can use that active upgrader with explicit
142
- older targets, for example:
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-8m50vnmb.node");
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 Error(`SSH command timed out after ${timeoutMs}ms`));
25159
+ reject(new SshCommandOutcomeUnknownError(`SSH command timed out after ${timeoutMs}ms; remote outcome is unknown`));
25152
25160
  }, timeoutMs);
25153
- conn.exec(command, (err, stream) => {
25154
- if (err) {
25155
- clearTimeout(timer);
25156
- connectionReusable = false;
25157
- return reject(err);
25158
- }
25159
- stream.on("close", (code) => {
25160
- clearTimeout(timer);
25161
- resolve({
25162
- success: code === 0,
25163
- stdout: stdout.finalize(),
25164
- stderr: stderr.finalize(),
25165
- code,
25166
- stdoutTruncated: stdout.truncated,
25167
- stderrTruncated: stderr.truncated
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 DIRECT_UPGRADE_SSH_TIMEOUT_MS = 720000;
27726
- var PROXIED_UPGRADE_SSH_TIMEOUT_MS = 1320000;
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 helperPrepared = false;
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
- if (helperPrepared) {
28039
- try {
28040
- await removeRemoteUpgradeHelper(ssh, helperPath);
28041
- } catch (error) {
28042
- cleanupFailed = true;
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
- text = ok(await http.get("/v1/projects"));
30031
- break;
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
- text = ok(await http.get(`/v1/projects/${resolveRef(ref)}`));
30071
- break;
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.10.1",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
Binary file