@supacloud/admin 0.11.0 → 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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +221 -8
  3. package/package.json +1 -1
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
@@ -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) {
@@ -26039,6 +26057,15 @@ function validatedGetResponseLimit(options) {
26039
26057
  }
26040
26058
  return maxBytes;
26041
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
+ }
26042
26069
  function responseExceedsDeclaredLimit(response, maxBytes) {
26043
26070
  const contentLength = response.headers.get("content-length");
26044
26071
  return contentLength !== null && /^\d+$/u.test(contentLength) && Number(contentLength) > maxBytes;
@@ -26091,6 +26118,13 @@ async function boundedResponseJson(response, maxBytes) {
26091
26118
  throw error;
26092
26119
  }
26093
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
+ }
26094
26128
  async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
26095
26129
  const controller = new AbortController;
26096
26130
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -26142,16 +26176,33 @@ class HttpTransport {
26142
26176
  }
26143
26177
  async get(path, options = {}) {
26144
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;
26145
26184
  try {
26146
- const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
26185
+ response = await fetchWithRetry(`${this.baseUrl}${path}`, {
26147
26186
  method: "GET",
26148
26187
  headers: this.headers()
26149
26188
  });
26150
- const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
26151
- return { ok: res.ok, status: res.status, data };
26152
26189
  } catch (error) {
26153
26190
  return transportFailure(error);
26154
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 };
26155
26206
  }
26156
26207
  async post(path, body, options) {
26157
26208
  const timeoutMs = validatedPostTimeout(options);
@@ -30003,6 +30054,152 @@ function projectGetRead(response, expectedRef) {
30003
30054
  const project = projectDetails(response.data, expectedRef);
30004
30055
  return project ? successfulResult(project) : failedResult("Invalid project response");
30005
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
+
30006
30203
  // src/shared/tools/project-cli-tools.ts
30007
30204
  var PROJECT_SERVICE_NAMES = [
30008
30205
  "postgrest",
@@ -30034,9 +30231,10 @@ var STUDIO_PROJECT_SERVICE_STATUSES = [
30034
30231
  ];
30035
30232
  var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
30036
30233
  var AUTH_SERVICE_HOST_SUFFIX = "-auth";
30037
- var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
30234
+ var SAFE_PROJECT_REF3 = /^[a-z0-9-]{1,20}$/;
30038
30235
  var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
30039
30236
  var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
30237
+ var MAX_RUNTIME_SNAPSHOT_BYTES = 64 * 1024;
30040
30238
  var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
30041
30239
  var PROJECT_CREATE_OPERATION = "project.create";
30042
30240
  var PROJECT_ENVIRONMENTS = ["test", "production"];
@@ -30250,7 +30448,7 @@ function projectServiceStatusOutput(status) {
30250
30448
  function projectServicesResponse(projectRef, response) {
30251
30449
  if (!response.ok)
30252
30450
  return failedProjectServiceHttpResponse(response);
30253
- 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) {
30254
30452
  return failedProjectServiceResponse("Project service inventory response is invalid");
30255
30453
  }
30256
30454
  if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
@@ -30263,6 +30461,15 @@ function projectServicesResponse(projectRef, response) {
30263
30461
  const services = response.data.map(projectServiceStatusOutput);
30264
30462
  return projectToolResponse(JSON.stringify({ project_ref: projectRef, services }, null, 2));
30265
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
+ }
30266
30473
  function supportsProjectServiceAction(service, action) {
30267
30474
  return SUPPORTED_PROJECT_SERVICE_ACTIONS[service].includes(action);
30268
30475
  }
@@ -30336,7 +30543,7 @@ function resolveRef(refFromArgs, defaultRef) {
30336
30543
  }
30337
30544
  function registerAdminProjectCliTools(server, http, options = {}) {
30338
30545
  const fileOperations = options.projectEnvFileOperations;
30339
- 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", {
30340
30547
  action: withDescription(stringEnum([
30341
30548
  "list",
30342
30549
  "create",
@@ -30352,6 +30559,7 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30352
30559
  "logs",
30353
30560
  "tasks",
30354
30561
  "services",
30562
+ "runtime_snapshot",
30355
30563
  "service_control"
30356
30564
  ]), "Action to perform"),
30357
30565
  ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
@@ -30485,6 +30693,10 @@ function registerAdminProjectCliTools(server, http, options = {}) {
30485
30693
  const resolvedRef = resolveRef(ref);
30486
30694
  return projectServicesResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/services`));
30487
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
+ }
30488
30700
  case "service_control": {
30489
30701
  const resolvedRef = resolveRef(ref);
30490
30702
  if (!service)
@@ -30837,7 +31049,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
30837
31049
  // package.json
30838
31050
  var package_default = {
30839
31051
  name: "@supacloud/admin",
30840
- version: "0.11.0",
31052
+ version: "0.12.0",
30841
31053
  description: "Platform administration CLI for SupaCloud operators",
30842
31054
  type: "module",
30843
31055
  main: "./dist/index.js",
@@ -30969,6 +31181,7 @@ EXAMPLES
30969
31181
  supacloud-admin project create --name my-app --domain example.com --env_file /secure/path/.env.project-credentials.test --environment test
30970
31182
  supacloud-admin project list
30971
31183
  supacloud-admin project services --ref abc123
31184
+ supacloud-admin project runtime_snapshot --ref abc123
30972
31185
  supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
30973
31186
  supacloud-admin platform metrics
30974
31187
  supacloud-admin gateway routes --ref abc123
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",