@supacloud/admin 0.10.2 → 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/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
@@ -26030,12 +26030,74 @@ function validatedPostTimeout(options) {
26030
26030
  }
26031
26031
  return timeoutMs;
26032
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
+ }
26033
26094
  async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
26034
26095
  const controller = new AbortController;
26035
26096
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
26036
26097
  try {
26037
26098
  return await fetch(url, {
26038
26099
  ...options,
26100
+ redirect: "error",
26039
26101
  signal: controller.signal
26040
26102
  });
26041
26103
  } finally {
@@ -26078,13 +26140,14 @@ class HttpTransport {
26078
26140
  "Content-Type": "application/json"
26079
26141
  };
26080
26142
  }
26081
- async get(path) {
26143
+ async get(path, options = {}) {
26144
+ const maxResponseBytes = validatedGetResponseLimit(options);
26082
26145
  try {
26083
26146
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
26084
26147
  method: "GET",
26085
26148
  headers: this.headers()
26086
26149
  });
26087
- const data = await res.json().catch(() => null);
26150
+ const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
26088
26151
  return { ok: res.ok, status: res.status, data };
26089
26152
  } catch (error) {
26090
26153
  return transportFailure(error);
@@ -29717,6 +29780,229 @@ function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expec
29717
29780
  return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
29718
29781
  }
29719
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
+ }
29720
30006
  // src/shared/tools/project-cli-tools.ts
29721
30007
  var PROJECT_SERVICE_NAMES = [
29722
30008
  "postgrest",
@@ -29766,6 +30052,10 @@ var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
29766
30052
  function projectToolResponse(text) {
29767
30053
  return { content: [{ type: "text", text }] };
29768
30054
  }
30055
+ function projectReadResponse(readResult) {
30056
+ const response = projectToolResponse(readResult.text);
30057
+ return readResult.isError ? { ...response, isError: true } : response;
30058
+ }
29769
30059
  function failedProjectServiceResponse(message) {
29770
30060
  return {
29771
30061
  content: [{ type: "text", text: `❌ ${message}` }],
@@ -30098,8 +30388,9 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30098
30388
  let text;
30099
30389
  switch (action) {
30100
30390
  case "list":
30101
- text = ok(await http.get("/v1/projects"));
30102
- break;
30391
+ return projectReadResponse(projectListRead(await http.get("/v1/projects", {
30392
+ maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
30393
+ })));
30103
30394
  case "create": {
30104
30395
  if (!name)
30105
30396
  throw new Error("'name' is required for create");
@@ -30137,9 +30428,12 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30137
30428
  createRequest.credential_delivery = "response";
30138
30429
  return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
30139
30430
  }
30140
- case "get":
30141
- text = ok(await http.get(`/v1/projects/${resolveRef(ref)}`));
30142
- 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
+ }
30143
30437
  case "delete": {
30144
30438
  const resolvedRef = resolveRef(ref);
30145
30439
  text = simple(await http.delete(`/v1/projects/${resolvedRef}`), `Project ${resolvedRef} deleted`);
@@ -30543,7 +30837,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
30543
30837
  // package.json
30544
30838
  var package_default = {
30545
30839
  name: "@supacloud/admin",
30546
- version: "0.10.2",
30840
+ version: "0.11.0",
30547
30841
  description: "Platform administration CLI for SupaCloud operators",
30548
30842
  type: "module",
30549
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.2",
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