@supacloud/admin 0.10.2 → 0.12.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
@@ -80,6 +80,7 @@ npx @supacloud/admin project create --name my-app --domain example.com \
80
80
  --env_file /secure/path/.env.project-credentials.test --environment test
81
81
  npx @supacloud/admin project list
82
82
  npx @supacloud/admin project services --ref abc123
83
+ npx @supacloud/admin project runtime_snapshot --ref abc123
83
84
  npx @supacloud/admin project service_control --ref abc123 --service gotrue --service_action stop
84
85
  ```
85
86
 
@@ -167,6 +168,7 @@ Project commands owned by this CLI:
167
168
  - `project restart`
168
169
  - `project update_settings`
169
170
  - `project services` — read-only project service inventory
171
+ - `project runtime_snapshot` — strict read-only runtime revision and PostgREST attestation snapshot
170
172
  - `project service_control` — constrained project service lifecycle control
171
173
 
172
174
  `project create` never prints project credentials. Pass an absolute
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
@@ -25788,7 +25788,17 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
25788
25788
  // src/shared/execution-policy.ts
25789
25789
  var ACTION_POLICY = {
25790
25790
  project: {
25791
- read: ["list", "get", "settings", "api_keys", "health", "logs", "tasks", "services"],
25791
+ read: [
25792
+ "list",
25793
+ "get",
25794
+ "settings",
25795
+ "api_keys",
25796
+ "health",
25797
+ "logs",
25798
+ "tasks",
25799
+ "services",
25800
+ "runtime_snapshot"
25801
+ ],
25792
25802
  write: ["create", "delete", "pause", "restore", "restart", "update_settings"]
25793
25803
  },
25794
25804
  platform: {
@@ -26023,6 +26033,14 @@ function transportFailure(error) {
26023
26033
  transportError: true
26024
26034
  };
26025
26035
  }
26036
+ function responseBodyFailure(status) {
26037
+ return {
26038
+ ok: false,
26039
+ status,
26040
+ data: { error: "Invalid Response", code: "INVALID_RESPONSE" },
26041
+ responseError: true
26042
+ };
26043
+ }
26026
26044
  function validatedPostTimeout(options) {
26027
26045
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT;
26028
26046
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_POST_TIMEOUT_MS) {
@@ -26030,12 +26048,90 @@ function validatedPostTimeout(options) {
26030
26048
  }
26031
26049
  return timeoutMs;
26032
26050
  }
26051
+ function validatedGetResponseLimit(options) {
26052
+ const maxBytes = options.maxResponseBytes;
26053
+ if (maxBytes === undefined)
26054
+ return;
26055
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
26056
+ throw new RangeError("HTTP response limit must be a positive safe integer");
26057
+ }
26058
+ return maxBytes;
26059
+ }
26060
+ function validatedStrictJsonLimit(options) {
26061
+ const maxBytes = options.maxJsonBytes;
26062
+ if (maxBytes === undefined)
26063
+ return;
26064
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
26065
+ throw new RangeError("HTTP JSON response byte limit must be a positive safe integer");
26066
+ }
26067
+ return maxBytes;
26068
+ }
26069
+ function responseExceedsDeclaredLimit(response, maxBytes) {
26070
+ const contentLength = response.headers.get("content-length");
26071
+ return contentLength !== null && /^\d+$/u.test(contentLength) && Number(contentLength) > maxBytes;
26072
+ }
26073
+ function joinedResponseBytes(chunks, totalBytes) {
26074
+ const responseBytes = new Uint8Array(totalBytes);
26075
+ let offset = 0;
26076
+ for (const chunk of chunks) {
26077
+ responseBytes.set(chunk, offset);
26078
+ offset += chunk.byteLength;
26079
+ }
26080
+ return responseBytes;
26081
+ }
26082
+ async function boundedResponseBytes(response, maxBytes) {
26083
+ if (responseExceedsDeclaredLimit(response, maxBytes)) {
26084
+ response.body?.cancel().catch(() => {
26085
+ return;
26086
+ });
26087
+ return null;
26088
+ }
26089
+ if (!response.body)
26090
+ return new Uint8Array;
26091
+ const reader = response.body.getReader();
26092
+ const chunks = [];
26093
+ let totalBytes = 0;
26094
+ while (true) {
26095
+ const { done, value } = await reader.read();
26096
+ if (done)
26097
+ return joinedResponseBytes(chunks, totalBytes);
26098
+ totalBytes += value.byteLength;
26099
+ if (totalBytes > maxBytes) {
26100
+ reader.cancel().catch(() => {
26101
+ return;
26102
+ });
26103
+ return null;
26104
+ }
26105
+ chunks.push(value);
26106
+ }
26107
+ }
26108
+ async function boundedResponseJson(response, maxBytes) {
26109
+ const responseBytes = await boundedResponseBytes(response, maxBytes);
26110
+ if (responseBytes === null)
26111
+ return null;
26112
+ try {
26113
+ const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
26114
+ return JSON.parse(responseText);
26115
+ } catch (error) {
26116
+ if (error instanceof SyntaxError || error instanceof TypeError)
26117
+ return null;
26118
+ throw error;
26119
+ }
26120
+ }
26121
+ async function strictBoundedResponseJson(response, maxBytes) {
26122
+ const responseBytes = await boundedResponseBytes(response, maxBytes);
26123
+ if (responseBytes === null)
26124
+ throw new Error("HTTP JSON response exceeded its byte limit");
26125
+ const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
26126
+ return JSON.parse(responseText);
26127
+ }
26033
26128
  async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
26034
26129
  const controller = new AbortController;
26035
26130
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
26036
26131
  try {
26037
26132
  return await fetch(url, {
26038
26133
  ...options,
26134
+ redirect: "error",
26039
26135
  signal: controller.signal
26040
26136
  });
26041
26137
  } finally {
@@ -26078,17 +26174,35 @@ class HttpTransport {
26078
26174
  "Content-Type": "application/json"
26079
26175
  };
26080
26176
  }
26081
- async get(path) {
26177
+ async get(path, options = {}) {
26178
+ const maxResponseBytes = validatedGetResponseLimit(options);
26179
+ const maxJsonBytes = validatedStrictJsonLimit(options);
26180
+ if (maxResponseBytes !== undefined && maxJsonBytes !== undefined) {
26181
+ throw new RangeError("HTTP response limit options are mutually exclusive");
26182
+ }
26183
+ let response;
26082
26184
  try {
26083
- const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
26185
+ response = await fetchWithRetry(`${this.baseUrl}${path}`, {
26084
26186
  method: "GET",
26085
26187
  headers: this.headers()
26086
26188
  });
26087
- const data = await res.json().catch(() => null);
26088
- return { ok: res.ok, status: res.status, data };
26089
26189
  } catch (error) {
26090
26190
  return transportFailure(error);
26091
26191
  }
26192
+ if (maxJsonBytes !== undefined) {
26193
+ try {
26194
+ const data2 = await strictBoundedResponseJson(response, maxJsonBytes);
26195
+ return { ok: response.ok, status: response.status, data: data2 };
26196
+ } catch {
26197
+ return responseBodyFailure(response.status);
26198
+ }
26199
+ }
26200
+ if (maxResponseBytes !== undefined) {
26201
+ const data2 = await boundedResponseJson(response, maxResponseBytes);
26202
+ return { ok: response.ok, status: response.status, data: data2 };
26203
+ }
26204
+ const data = await response.json().catch(() => null);
26205
+ return { ok: response.ok, status: response.status, data };
26092
26206
  }
26093
26207
  async post(path, body, options) {
26094
26208
  const timeoutMs = validatedPostTimeout(options);
@@ -29717,6 +29831,375 @@ function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expec
29717
29831
  return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
29718
29832
  }
29719
29833
 
29834
+ // ../cli/src/shared/tools/project-read-projection.ts
29835
+ var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
29836
+ var PROJECT_REF_PATTERN = /^[a-z0-9-]{1,20}$/;
29837
+ var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
29838
+ var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
29839
+ var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
29840
+ var DNS_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
29841
+ var DATABASE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
29842
+ var MAX_PROJECTS = 1e4;
29843
+ var PROJECT_SUMMARY_KEYS = new Set([
29844
+ "id",
29845
+ "ref",
29846
+ "organization_id",
29847
+ "organization_slug",
29848
+ "name",
29849
+ "region",
29850
+ "created_at",
29851
+ "status"
29852
+ ]);
29853
+ var PROJECT_DETAILS_KEYS = new Set([
29854
+ ...PROJECT_SUMMARY_KEYS,
29855
+ "database",
29856
+ "api",
29857
+ "studio",
29858
+ "config",
29859
+ "anon_key",
29860
+ "services"
29861
+ ]);
29862
+ var PROJECT_DATABASE_KEYS = new Set([
29863
+ "host",
29864
+ "version",
29865
+ "postgres_engine",
29866
+ "release_channel"
29867
+ ]);
29868
+ var PROJECT_ENDPOINT_KEYS = new Set(["url"]);
29869
+ function plainRecord(candidate) {
29870
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
29871
+ return null;
29872
+ const prototype = Object.getPrototypeOf(candidate);
29873
+ return prototype === Object.prototype || prototype === null ? candidate : null;
29874
+ }
29875
+ function hasOnlyKeys(record, allowedKeys) {
29876
+ return Object.keys(record).every((key) => allowedKeys.has(key));
29877
+ }
29878
+ function hasWellFormedUnicode(text) {
29879
+ for (let index = 0;index < text.length; index++) {
29880
+ const codeUnit = text.charCodeAt(index);
29881
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
29882
+ if (index + 1 >= text.length)
29883
+ return false;
29884
+ const lowSurrogate = text.charCodeAt(index + 1);
29885
+ if (lowSurrogate < 56320 || lowSurrogate > 57343)
29886
+ return false;
29887
+ index++;
29888
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
29889
+ return false;
29890
+ }
29891
+ }
29892
+ return true;
29893
+ }
29894
+ function boundedText(candidate, maxLength) {
29895
+ return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
29896
+ }
29897
+ function matchingText(candidate, maxLength, pattern) {
29898
+ const candidateText = boundedText(candidate, maxLength);
29899
+ return candidateText && pattern.test(candidateText) ? candidateText : null;
29900
+ }
29901
+ function canonicalTimestamp(candidate) {
29902
+ const timestamp = boundedText(candidate, 64);
29903
+ if (!timestamp)
29904
+ return null;
29905
+ const milliseconds = Date.parse(timestamp);
29906
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === timestamp ? timestamp : null;
29907
+ }
29908
+ function projectedSummary(project) {
29909
+ const summary = {
29910
+ id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
29911
+ ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN),
29912
+ organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
29913
+ organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
29914
+ name: boundedText(project.name, 100),
29915
+ region: matchingText(project.region, 64, REGION_PATTERN),
29916
+ created_at: canonicalTimestamp(project.created_at),
29917
+ status: matchingText(project.status, 64, STATUS_PATTERN)
29918
+ };
29919
+ return Object.values(summary).every((field) => field !== null) ? summary : null;
29920
+ }
29921
+ function projectSummary(candidate) {
29922
+ const project = plainRecord(candidate);
29923
+ return project && hasOnlyKeys(project, PROJECT_SUMMARY_KEYS) ? projectedSummary(project) : null;
29924
+ }
29925
+ function databaseHost(candidate) {
29926
+ const host = boundedText(candidate, 255);
29927
+ if (!host)
29928
+ return null;
29929
+ if (host.startsWith("[") && host.endsWith("]")) {
29930
+ try {
29931
+ const parsedHost = new URL(`http://${host}`);
29932
+ return parsedHost.host === host ? host : null;
29933
+ } catch (error) {
29934
+ if (error instanceof TypeError)
29935
+ return null;
29936
+ throw error;
29937
+ }
29938
+ }
29939
+ const ipv4Parts = host.split(".");
29940
+ if (ipv4Parts.length === 4 && ipv4Parts.every((part) => /^\d{1,3}$/u.test(part))) {
29941
+ return ipv4Parts.every((part) => Number(part) <= 255) ? host : null;
29942
+ }
29943
+ return ipv4Parts.every((label) => DNS_LABEL_PATTERN.test(label)) ? host : null;
29944
+ }
29945
+ function projectDatabase(candidate) {
29946
+ const database = plainRecord(candidate);
29947
+ if (!database || !hasOnlyKeys(database, PROJECT_DATABASE_KEYS))
29948
+ return null;
29949
+ const host = databaseHost(database.host);
29950
+ const version = matchingText(database.version, 64, DATABASE_VERSION_PATTERN);
29951
+ const postgresEngine = matchingText(database.postgres_engine, 64, DATABASE_VERSION_PATTERN);
29952
+ const releaseChannel = matchingText(database.release_channel, 64, DATABASE_VERSION_PATTERN);
29953
+ return host && version && postgresEngine && releaseChannel ? { host, version, postgres_engine: postgresEngine, release_channel: releaseChannel } : null;
29954
+ }
29955
+ function rawUrlHasNoPath(candidate) {
29956
+ if (candidate.trim() !== candidate || candidate.includes("\\"))
29957
+ return false;
29958
+ const schemeEnd = candidate.indexOf("://");
29959
+ const pathStart = candidate.indexOf("/", schemeEnd + 3);
29960
+ return pathStart === -1;
29961
+ }
29962
+ function projectEndpoint(candidate) {
29963
+ const endpoint = plainRecord(candidate);
29964
+ if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
29965
+ return null;
29966
+ const endpointUrl = boundedText(endpoint.url, 2048);
29967
+ if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
29968
+ return null;
29969
+ try {
29970
+ const url = new URL(endpointUrl);
29971
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
29972
+ return null;
29973
+ return { url: url.origin };
29974
+ } catch (error) {
29975
+ if (error instanceof TypeError)
29976
+ return null;
29977
+ throw error;
29978
+ }
29979
+ }
29980
+ function discardedDetailFieldsAreValid(project) {
29981
+ if (project.config !== undefined && plainRecord(project.config) === null)
29982
+ return false;
29983
+ if (project.anon_key !== undefined && boundedText(project.anon_key, 16384) === null)
29984
+ return false;
29985
+ return project.services === undefined || Array.isArray(project.services);
29986
+ }
29987
+ function projectDetails(candidate, expectedRef) {
29988
+ const project = plainRecord(candidate);
29989
+ if (!project || !hasOnlyKeys(project, PROJECT_DETAILS_KEYS))
29990
+ return null;
29991
+ const summary = projectedSummary(project);
29992
+ const database = projectDatabase(project.database);
29993
+ const api = project.api === undefined ? undefined : projectEndpoint(project.api);
29994
+ const studio = project.studio === undefined ? undefined : projectEndpoint(project.studio);
29995
+ if (!summary || summary.ref !== expectedRef || !database || !discardedDetailFieldsAreValid(project) || project.api !== undefined && !api || project.studio !== undefined && !studio)
29996
+ return null;
29997
+ return {
29998
+ ...summary,
29999
+ database,
30000
+ ...api ? { api } : {},
30001
+ ...studio ? { studio } : {}
30002
+ };
30003
+ }
30004
+ function payloadWithinLimit(candidate) {
30005
+ try {
30006
+ const serializedPayload = JSON.stringify(candidate);
30007
+ return serializedPayload !== undefined && new TextEncoder().encode(serializedPayload).byteLength <= PROJECT_READ_RESPONSE_MAX_BYTES;
30008
+ } catch {
30009
+ return false;
30010
+ }
30011
+ }
30012
+ function safeProjectList(candidate) {
30013
+ if (!payloadWithinLimit(candidate) || !Array.isArray(candidate) || candidate.length > MAX_PROJECTS)
30014
+ return null;
30015
+ const safeProjects = [];
30016
+ const ids = new Set;
30017
+ const refs = new Set;
30018
+ for (const projectCandidate of candidate) {
30019
+ const project = projectSummary(projectCandidate);
30020
+ if (!project || ids.has(project.id) || refs.has(project.ref))
30021
+ return null;
30022
+ ids.add(project.id);
30023
+ refs.add(project.ref);
30024
+ safeProjects.push(project);
30025
+ }
30026
+ return safeProjects;
30027
+ }
30028
+ function validHttpStatus(status) {
30029
+ return Number.isSafeInteger(status) && status >= 100 && status <= 599;
30030
+ }
30031
+ function successfulResponse(response) {
30032
+ return response.ok === true && validHttpStatus(response.status) && response.status >= 200 && response.status <= 299;
30033
+ }
30034
+ function failedResult(message) {
30035
+ return { text: `❌ ${message}`, isError: true };
30036
+ }
30037
+ function failedHttpResult(label, status) {
30038
+ return failedResult(validHttpStatus(status) ? `${label} request failed (${status})` : `${label} request failed`);
30039
+ }
30040
+ function successfulResult(payload) {
30041
+ return { text: JSON.stringify(payload, null, 2), isError: false };
30042
+ }
30043
+ function projectListRead(response) {
30044
+ if (!successfulResponse(response))
30045
+ return failedHttpResult("Project list", response.status);
30046
+ const projects = safeProjectList(response.data);
30047
+ return projects ? successfulResult(projects) : failedResult("Invalid project list response");
30048
+ }
30049
+ function projectGetRead(response, expectedRef) {
30050
+ if (!successfulResponse(response))
30051
+ return failedHttpResult("Project get", response.status);
30052
+ if (!payloadWithinLimit(response.data))
30053
+ return failedResult("Invalid project response");
30054
+ const project = projectDetails(response.data, expectedRef);
30055
+ return project ? successfulResult(project) : failedResult("Invalid project response");
30056
+ }
30057
+ // src/shared/tools/project-runtime-snapshot.ts
30058
+ var RUNTIME_SNAPSHOT_SCHEMA = "supacloud.runtime-snapshot.v1";
30059
+ var ATTESTED_REVISION_PATTERN = /^hmac-sha256:[a-f0-9]{64}$/;
30060
+ var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
30061
+ var SNAPSHOT_KEYS = ["schema", "project_ref", "captured_at", "secrets", "postgrest"];
30062
+ var SECRETS_KEYS = [
30063
+ "desired_revision",
30064
+ "loaded_revision",
30065
+ "load_state",
30066
+ "load_source",
30067
+ "matches_desired",
30068
+ "loaded_at"
30069
+ ];
30070
+ var POSTGREST_KEYS = [
30071
+ "desired_revision",
30072
+ "loaded_revision",
30073
+ "attestation_state",
30074
+ "matches_desired",
30075
+ "desired",
30076
+ "actual",
30077
+ "health",
30078
+ "port",
30079
+ "unit",
30080
+ "loaded_at"
30081
+ ];
30082
+ var SECRET_LOAD_STATES = ["current", "stale", "not_loaded", "unverified", "unreachable"];
30083
+ var SECRET_LOAD_SOURCES = ["management_api", "stale_cache", "file_fallback"];
30084
+ var POSTGREST_ATTESTATION_STATES = [
30085
+ "loaded",
30086
+ "stale",
30087
+ "drifted",
30088
+ "unverified_legacy",
30089
+ "stopped",
30090
+ "unreachable"
30091
+ ];
30092
+ var POSTGREST_DESIRED_STATES = ["running", "stopped"];
30093
+ var POSTGREST_ACTUAL_STATES = ["running", "stopped", "starting", "error"];
30094
+ var POSTGREST_HEALTH_STATES = ["healthy", "unhealthy", "unknown"];
30095
+ function isRecord2(candidate) {
30096
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
30097
+ }
30098
+ function hasExactKeys(candidate, expectedKeys) {
30099
+ const actualKeys = Object.keys(candidate).sort();
30100
+ const sortedExpectedKeys = [...expectedKeys].sort();
30101
+ return actualKeys.length === sortedExpectedKeys.length && actualKeys.every((key, index) => key === sortedExpectedKeys[index]);
30102
+ }
30103
+ function isEnumMember(candidate, members) {
30104
+ return typeof candidate === "string" && members.includes(candidate);
30105
+ }
30106
+ function isIsoTimestampOrNull(candidate) {
30107
+ if (candidate === null)
30108
+ return true;
30109
+ if (typeof candidate !== "string")
30110
+ return false;
30111
+ const timestamp = Date.parse(candidate);
30112
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === candidate;
30113
+ }
30114
+ function isRevisionOrNull(candidate) {
30115
+ return candidate === null || typeof candidate === "string" && ATTESTED_REVISION_PATTERN.test(candidate);
30116
+ }
30117
+ function isBooleanOrNull(candidate) {
30118
+ return candidate === null || typeof candidate === "boolean";
30119
+ }
30120
+ function isUnloadedSecretsState(candidate) {
30121
+ return candidate.loaded_revision === null && candidate.load_source === null && candidate.matches_desired === null && candidate.loaded_at === null;
30122
+ }
30123
+ function isLoadedSecretsState(candidate) {
30124
+ if (candidate.load_state === "current") {
30125
+ return candidate.loaded_revision === candidate.desired_revision && candidate.load_source === "management_api" && candidate.matches_desired === true && candidate.loaded_at !== null;
30126
+ }
30127
+ return candidate.load_state === "stale" && candidate.loaded_revision !== null && candidate.loaded_revision !== candidate.desired_revision && candidate.load_source === "management_api" && candidate.matches_desired === false && candidate.loaded_at !== null;
30128
+ }
30129
+ function isUnverifiedSecretsState(candidate) {
30130
+ if (candidate.load_state !== "unverified" || candidate.matches_desired !== null || candidate.loaded_at === null)
30131
+ return false;
30132
+ if (candidate.loaded_revision === candidate.desired_revision) {
30133
+ return candidate.load_source === "management_api";
30134
+ }
30135
+ return candidate.loaded_revision === null && candidate.load_source !== null;
30136
+ }
30137
+ function hasValidSecretsState(candidate) {
30138
+ if (candidate.load_state === "not_loaded" || candidate.load_state === "unreachable") {
30139
+ return isUnloadedSecretsState(candidate);
30140
+ }
30141
+ return isLoadedSecretsState(candidate) || isUnverifiedSecretsState(candidate);
30142
+ }
30143
+ function isRuntimeSecretsSnapshot(payload) {
30144
+ if (!isRecord2(payload) || !hasExactKeys(payload, SECRETS_KEYS))
30145
+ return false;
30146
+ if (!isRevisionOrNull(payload.loaded_revision) || typeof payload.desired_revision !== "string" || !ATTESTED_REVISION_PATTERN.test(payload.desired_revision) || !isEnumMember(payload.load_state, SECRET_LOAD_STATES) || !(payload.load_source === null || isEnumMember(payload.load_source, SECRET_LOAD_SOURCES)) || !isBooleanOrNull(payload.matches_desired) || !isIsoTimestampOrNull(payload.loaded_at))
30147
+ return false;
30148
+ return hasValidSecretsState(payload);
30149
+ }
30150
+ function hasConsistentRevisionMatch(candidate) {
30151
+ if (candidate.loaded_revision === null)
30152
+ return candidate.matches_desired === null;
30153
+ return candidate.matches_desired === (candidate.loaded_revision === candidate.desired_revision);
30154
+ }
30155
+ function hasActivePostgrestProjection(candidate) {
30156
+ return candidate.actual === "running" && candidate.health === "healthy" || candidate.actual === "error" && candidate.health === "unhealthy";
30157
+ }
30158
+ function hasValidPostgrestState(candidate) {
30159
+ if (!hasConsistentRevisionMatch(candidate))
30160
+ return false;
30161
+ if (candidate.attestation_state === "loaded") {
30162
+ return candidate.matches_desired === true && candidate.actual === "running" && candidate.health === "healthy" && candidate.loaded_at !== null;
30163
+ }
30164
+ if (candidate.attestation_state === "stale") {
30165
+ return candidate.loaded_revision !== null && candidate.matches_desired === false && candidate.loaded_at !== null && hasActivePostgrestProjection(candidate);
30166
+ }
30167
+ if (candidate.attestation_state === "unverified_legacy")
30168
+ return candidate.loaded_revision === null;
30169
+ if (candidate.attestation_state === "stopped") {
30170
+ return candidate.loaded_revision === null && candidate.actual === "stopped" && candidate.health === "unknown" && candidate.loaded_at === null;
30171
+ }
30172
+ if (candidate.attestation_state === "unreachable")
30173
+ return candidate.loaded_revision === null;
30174
+ return candidate.attestation_state === "drifted";
30175
+ }
30176
+ function isPostgrestRuntimeSnapshot(payload, projectRef) {
30177
+ if (!isRecord2(payload) || !hasExactKeys(payload, POSTGREST_KEYS))
30178
+ return false;
30179
+ if (typeof payload.desired_revision !== "string" || !ATTESTED_REVISION_PATTERN.test(payload.desired_revision) || !isRevisionOrNull(payload.loaded_revision) || !isEnumMember(payload.attestation_state, POSTGREST_ATTESTATION_STATES) || !isBooleanOrNull(payload.matches_desired) || !isEnumMember(payload.desired, POSTGREST_DESIRED_STATES) || !isEnumMember(payload.actual, POSTGREST_ACTUAL_STATES) || !isEnumMember(payload.health, POSTGREST_HEALTH_STATES) || !Number.isSafeInteger(payload.port) || Number(payload.port) < 1 || Number(payload.port) > 65535 || payload.unit !== `supacloud-pgrst@${projectRef}` || !isIsoTimestampOrNull(payload.loaded_at))
30180
+ return false;
30181
+ return hasValidPostgrestState(payload);
30182
+ }
30183
+ function sanitizedSnapshot(snapshot) {
30184
+ return {
30185
+ schema: snapshot.schema,
30186
+ project_ref: snapshot.project_ref,
30187
+ captured_at: snapshot.captured_at,
30188
+ secrets: { ...snapshot.secrets },
30189
+ postgrest: { ...snapshot.postgrest }
30190
+ };
30191
+ }
30192
+ function hasCausalLoadTimestamps(snapshot) {
30193
+ const capturedAt = Date.parse(snapshot.captured_at);
30194
+ return [snapshot.secrets.loaded_at, snapshot.postgrest.loaded_at].every((loadedAt) => loadedAt === null || Date.parse(loadedAt) <= capturedAt);
30195
+ }
30196
+ function parseProjectRuntimeSnapshot(payload, requestedProjectRef2) {
30197
+ if (!SAFE_PROJECT_REF2.test(requestedProjectRef2) || !isRecord2(payload) || !hasExactKeys(payload, SNAPSHOT_KEYS) || payload.schema !== RUNTIME_SNAPSHOT_SCHEMA || payload.project_ref !== requestedProjectRef2 || !isIsoTimestampOrNull(payload.captured_at) || payload.captured_at === null || !isRuntimeSecretsSnapshot(payload.secrets) || !isPostgrestRuntimeSnapshot(payload.postgrest, requestedProjectRef2))
30198
+ return null;
30199
+ const snapshot = payload;
30200
+ return hasCausalLoadTimestamps(snapshot) ? sanitizedSnapshot(snapshot) : null;
30201
+ }
30202
+
29720
30203
  // src/shared/tools/project-cli-tools.ts
29721
30204
  var PROJECT_SERVICE_NAMES = [
29722
30205
  "postgrest",
@@ -29748,9 +30231,10 @@ var STUDIO_PROJECT_SERVICE_STATUSES = [
29748
30231
  ];
29749
30232
  var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
29750
30233
  var AUTH_SERVICE_HOST_SUFFIX = "-auth";
29751
- var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
30234
+ var SAFE_PROJECT_REF3 = /^[a-z0-9-]{1,20}$/;
29752
30235
  var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
29753
30236
  var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
30237
+ var MAX_RUNTIME_SNAPSHOT_BYTES = 64 * 1024;
29754
30238
  var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
29755
30239
  var PROJECT_CREATE_OPERATION = "project.create";
29756
30240
  var PROJECT_ENVIRONMENTS = ["test", "production"];
@@ -29766,6 +30250,10 @@ var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
29766
30250
  function projectToolResponse(text) {
29767
30251
  return { content: [{ type: "text", text }] };
29768
30252
  }
30253
+ function projectReadResponse(readResult) {
30254
+ const response = projectToolResponse(readResult.text);
30255
+ return readResult.isError ? { ...response, isError: true } : response;
30256
+ }
29769
30257
  function failedProjectServiceResponse(message) {
29770
30258
  return {
29771
30259
  content: [{ type: "text", text: `❌ ${message}` }],
@@ -29960,7 +30448,7 @@ function projectServiceStatusOutput(status) {
29960
30448
  function projectServicesResponse(projectRef, response) {
29961
30449
  if (!response.ok)
29962
30450
  return failedProjectServiceHttpResponse(response);
29963
- if (!SAFE_PROJECT_REF2.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
30451
+ if (!SAFE_PROJECT_REF3.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
29964
30452
  return failedProjectServiceResponse("Project service inventory response is invalid");
29965
30453
  }
29966
30454
  if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
@@ -29973,6 +30461,15 @@ function projectServicesResponse(projectRef, response) {
29973
30461
  const services = response.data.map(projectServiceStatusOutput);
29974
30462
  return projectToolResponse(JSON.stringify({ project_ref: projectRef, services }, null, 2));
29975
30463
  }
30464
+ function projectRuntimeSnapshotResponse(projectRef, response) {
30465
+ if (response.responseError) {
30466
+ return failedProjectServiceResponse("Project runtime snapshot response is invalid");
30467
+ }
30468
+ if (!response.ok)
30469
+ return failedProjectServiceHttpResponse(response);
30470
+ const snapshot = parseProjectRuntimeSnapshot(response.data, projectRef);
30471
+ return snapshot ? projectToolResponse(JSON.stringify(snapshot, null, 2)) : failedProjectServiceResponse("Project runtime snapshot response is invalid");
30472
+ }
29976
30473
  function supportsProjectServiceAction(service, action) {
29977
30474
  return SUPPORTED_PROJECT_SERVICE_ACTIONS[service].includes(action);
29978
30475
  }
@@ -30046,7 +30543,7 @@ function resolveRef(refFromArgs, defaultRef) {
30046
30543
  }
30047
30544
  function registerAdminProjectCliTools(server, http, options = {}) {
30048
30545
  const fileOperations = options.projectEnvFileOperations;
30049
- server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, service_control", {
30546
+ server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, runtime_snapshot, service_control", {
30050
30547
  action: withDescription(stringEnum([
30051
30548
  "list",
30052
30549
  "create",
@@ -30062,6 +30559,7 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30062
30559
  "logs",
30063
30560
  "tasks",
30064
30561
  "services",
30562
+ "runtime_snapshot",
30065
30563
  "service_control"
30066
30564
  ]), "Action to perform"),
30067
30565
  ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
@@ -30098,8 +30596,9 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30098
30596
  let text;
30099
30597
  switch (action) {
30100
30598
  case "list":
30101
- text = ok(await http.get("/v1/projects"));
30102
- break;
30599
+ return projectReadResponse(projectListRead(await http.get("/v1/projects", {
30600
+ maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
30601
+ })));
30103
30602
  case "create": {
30104
30603
  if (!name)
30105
30604
  throw new Error("'name' is required for create");
@@ -30137,9 +30636,12 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30137
30636
  createRequest.credential_delivery = "response";
30138
30637
  return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
30139
30638
  }
30140
- case "get":
30141
- text = ok(await http.get(`/v1/projects/${resolveRef(ref)}`));
30142
- break;
30639
+ case "get": {
30640
+ const resolvedRef = resolveRef(ref);
30641
+ return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
30642
+ maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
30643
+ }), resolvedRef));
30644
+ }
30143
30645
  case "delete": {
30144
30646
  const resolvedRef = resolveRef(ref);
30145
30647
  text = simple(await http.delete(`/v1/projects/${resolvedRef}`), `Project ${resolvedRef} deleted`);
@@ -30191,6 +30693,10 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30191
30693
  const resolvedRef = resolveRef(ref);
30192
30694
  return projectServicesResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/services`));
30193
30695
  }
30696
+ case "runtime_snapshot": {
30697
+ const resolvedRef = resolveRef(ref);
30698
+ return projectRuntimeSnapshotResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/runtime-snapshot`, { maxJsonBytes: MAX_RUNTIME_SNAPSHOT_BYTES }));
30699
+ }
30194
30700
  case "service_control": {
30195
30701
  const resolvedRef = resolveRef(ref);
30196
30702
  if (!service)
@@ -30543,7 +31049,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
30543
31049
  // package.json
30544
31050
  var package_default = {
30545
31051
  name: "@supacloud/admin",
30546
- version: "0.10.2",
31052
+ version: "0.12.0",
30547
31053
  description: "Platform administration CLI for SupaCloud operators",
30548
31054
  type: "module",
30549
31055
  main: "./dist/index.js",
@@ -30675,6 +31181,7 @@ EXAMPLES
30675
31181
  supacloud-admin project create --name my-app --domain example.com --env_file /secure/path/.env.project-credentials.test --environment test
30676
31182
  supacloud-admin project list
30677
31183
  supacloud-admin project services --ref abc123
31184
+ supacloud-admin project runtime_snapshot --ref abc123
30678
31185
  supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
30679
31186
  supacloud-admin platform metrics
30680
31187
  supacloud-admin gateway routes --ref abc123
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.12.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
Binary file