@supacloud/admin 0.12.1 → 0.14.1

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
@@ -59,6 +59,30 @@ npx @supacloud/admin --env production ssh upgrade \
59
59
  --confirm-production host:production.example.com:2201
60
60
  ```
61
61
 
62
+ Immutable prebuilt frontend releases use a content-addressed ZIP and an explicit
63
+ compare-and-swap activation. Upload and activation are separate writes, and
64
+ production requires the exact project ref confirmation:
65
+
66
+ ```bash
67
+ supacloud-admin frontend list_releases --ref abc123 --id web
68
+ supacloud-admin frontend get_release --ref abc123 --id web --release_id <sha256>
69
+ supacloud-admin frontend upload_release --ref abc123 --id web \
70
+ --zip_path /secure/site.zip --confirm-production abc123
71
+ supacloud-admin frontend activate_release --ref abc123 --id web \
72
+ --release_id <sha256> --expected_active_release_id absent \
73
+ --expected_activation_id absent --mutation_id <uuid-v4> \
74
+ --confirm-production abc123
75
+ ```
76
+
77
+ The CLI reads the archive through a no-follow file descriptor, verifies its
78
+ identity and SHA-256 before upload, validates bounded Management API receipts,
79
+ and reads the immutable release or active inventory back before reporting
80
+ success. Reuse the same `mutation_id` only when retrying the exact activation.
81
+ Listing and release readback work on every Management API platform. Upload and
82
+ activation mutations require the Linux held-directory-FD implementation; on
83
+ other platforms, Management API returns HTTP 503 before it reads the upload
84
+ body, creates release directories, or writes a mutation journal.
85
+
62
86
  SSH host keys are fail-closed: setting `SUPACLOUD_HOST` and credentials is not
63
87
  enough to enable SSH actions. Obtain the fingerprint through a trusted channel,
64
88
  compare it out of band, then set it explicitly. For example, the discovery
@@ -133,8 +157,9 @@ upload-drop paths for reconciliation. Inspect that evidence before retrying and
133
157
  do not retry blindly while the remote transaction may still be running.
134
158
 
135
159
  `--artifact_transport local` accepts only `--github_proxy direct` or `none` and
136
- clears proxy environment variables on both hosts. The legacy server-download
137
- path remains available as `--artifact_transport remote`.
160
+ clears proxy environment variables on both hosts. The server-download path
161
+ remains available as `--artifact_transport remote`; it verifies and executes
162
+ the target Management release as the runner even for Management-only upgrades.
138
163
 
139
164
  With `--artifact_transport remote` (the default), omitting
140
165
  `--edge_runtime_version` retains the Management and Web Console-only upgrade
@@ -151,14 +176,10 @@ command may finish later and then run its own cleanup. Reconcile the reported
151
176
  helper and trusted-root paths and read back deployed versions before deciding
152
177
  whether to retry.
153
178
 
154
- After a capable Management release is active, an exact rollback can use that
155
- active upgrader with explicit older targets, for example:
156
-
157
- ```bash
158
- npx @supacloud/admin ssh upgrade \
159
- --version 0.50.26 \
160
- --edge_runtime_version 0.16.6
161
- ```
179
+ Targets predating the target-bound systemd-unit helper identity command are
180
+ rejected before mutation. A failed transaction still restores its exact frozen
181
+ prior Management/helper/Web identities; an intentional downgrade to a legacy
182
+ release requires a separately reviewed compatibility procedure.
162
183
 
163
184
  Project commands owned by this CLI:
164
185
 
@@ -249,6 +270,47 @@ The JSON receipt contains only the project ref, requested type, backup ID,
249
270
  database, timestamps, and size. `platform list_backups --ref abc123` applies the
250
271
  same strict, sanitized inventory validation without creating a backup.
251
272
 
273
+ ## Verified logical backups
274
+
275
+ Use the official Admin CLI to list or create project-scoped verified logical
276
+ backups:
277
+
278
+ ```bash
279
+ supacloud-admin platform list_logical_backups --ref abc123
280
+ supacloud-admin platform create_logical_backup --ref abc123
281
+ ```
282
+
283
+ The create command reads the verified inventory before and after the one-shot
284
+ request. It reports success only when the Management response and exactly one
285
+ new inventory identity agree on the project, database, backup ID, kind,
286
+ timestamps, byte count, and SHA-256. The fixed JSON projection never includes
287
+ the server's receipt HMAC, archive path, subprocess output, or response-only
288
+ fields. Do not retry an `OUTCOME_UNKNOWN` create until the inventory has been
289
+ reconciled.
290
+
291
+ Logical restore is destructive and Management also requires the project to be
292
+ paused. Copy an exact identity from `list_logical_backups`, then bind all of its
293
+ recovery-critical fields:
294
+
295
+ ```bash
296
+ supacloud-admin platform restore_logical_backup \
297
+ --ref abc123 \
298
+ --backup_id logical-full_abc123_0123456789abcdef0123456789abcdef \
299
+ --expected_sha256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
300
+ --confirmation RESTORE_PROJECT:abc123:logical-full_abc123_0123456789abcdef0123456789abcdef:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
301
+ ```
302
+
303
+ Both create and restore are classified as remote writes. They require an
304
+ explicit `SUPACLOUD_ENV`, honor `SUPACLOUD_READ_ONLY=true`, and in production
305
+ also require the global exact project confirmation
306
+ `--confirm-production abc123`. A timeout, transport loss, 5xx response, or
307
+ malformed success receipt exits non-zero without reflecting the remote body.
308
+ Logical mutation requests have a bounded 36-minute client deadline so the
309
+ Management request has time to return a terminal response before the client
310
+ gives up.
311
+ Credentials remain environment-only; logical backup commands accept no token,
312
+ password, or service-role flags.
313
+
252
314
  Gateway / Caddy commands (config is injected via the Caddy JSON Admin API; requires admin privileges):
253
315
 
254
316
  - `gateway routes` — list custom gateway routes
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
@@ -12196,7 +12196,7 @@ var require_SFTP = __commonJS((exports, module) => {
12196
12196
  var fs = __require("fs");
12197
12197
  var { constants } = fs;
12198
12198
  var {
12199
- Readable: ReadableStream,
12199
+ Readable: ReadableStream2,
12200
12200
  Writable: WritableStream
12201
12201
  } = __require("stream");
12202
12202
  var { inherits, types: { isDate } } = __require("util");
@@ -14807,7 +14807,7 @@ var require_SFTP = __commonJS((exports, module) => {
14807
14807
  options.highWaterMark = 64 * 1024;
14808
14808
  options.emitClose = false;
14809
14809
  options.autoDestroy = false;
14810
- ReadableStream.call(this, options);
14810
+ ReadableStream2.call(this, options);
14811
14811
  this.path = path;
14812
14812
  this.flags = options.flags === undefined ? "r" : options.flags;
14813
14813
  this.mode = options.mode === undefined ? 438 : options.mode;
@@ -14839,7 +14839,7 @@ var require_SFTP = __commonJS((exports, module) => {
14839
14839
  if (!Buffer.isBuffer(this.handle))
14840
14840
  this.open();
14841
14841
  }
14842
- inherits(ReadStream, ReadableStream);
14842
+ inherits(ReadStream, ReadableStream2);
14843
14843
  ReadStream.prototype.open = function() {
14844
14844
  if (this._opening)
14845
14845
  return;
@@ -15097,7 +15097,7 @@ var require_SFTP = __commonJS((exports, module) => {
15097
15097
  var require_Channel = __commonJS((exports, module) => {
15098
15098
  var {
15099
15099
  Duplex: DuplexStream,
15100
- Readable: ReadableStream,
15100
+ Readable: ReadableStream2,
15101
15101
  Writable: WritableStream
15102
15102
  } = __require("stream");
15103
15103
  var {
@@ -15108,7 +15108,7 @@ var require_Channel = __commonJS((exports, module) => {
15108
15108
  var MAX_WINDOW = 2 * 1024 * 1024;
15109
15109
  var WINDOW_THRESHOLD = MAX_WINDOW / 2;
15110
15110
 
15111
- class ClientStderr extends ReadableStream {
15111
+ class ClientStderr extends ReadableStream2 {
15112
15112
  constructor(channel, streamOpts) {
15113
15113
  super(streamOpts);
15114
15114
  this._channel = channel;
@@ -18848,7 +18848,7 @@ var require_lib3 = __commonJS((exports, module) => {
18848
18848
  });
18849
18849
 
18850
18850
  // src/index.ts
18851
- import { resolve as resolve3 } from "node:path";
18851
+ import { resolve as resolve4 } from "node:path";
18852
18852
  import { fileURLToPath } from "node:url";
18853
18853
  import { realpathSync } from "node:fs";
18854
18854
 
@@ -25802,8 +25802,8 @@ var ACTION_POLICY = {
25802
25802
  write: ["create", "delete", "pause", "restore", "restart", "update_settings"]
25803
25803
  },
25804
25804
  platform: {
25805
- read: ["metrics", "list_backups", "network", "list_orgs", "get_org"],
25806
- write: ["create_backup", "update_network"]
25805
+ read: ["metrics", "list_backups", "list_logical_backups", "network", "list_orgs", "get_org"],
25806
+ write: ["create_backup", "create_logical_backup", "restore_logical_backup", "update_network"]
25807
25807
  },
25808
25808
  gateway: {
25809
25809
  read: ["routes", "get_certificate", "custom_hostname"],
@@ -25821,6 +25821,10 @@ var ACTION_POLICY = {
25821
25821
  "verify_custom_hostname"
25822
25822
  ]
25823
25823
  },
25824
+ frontend: {
25825
+ read: ["list_releases", "get_release"],
25826
+ write: ["upload_release", "activate_release"]
25827
+ },
25824
25828
  ssh: {
25825
25829
  read: [
25826
25830
  "ping",
@@ -25850,6 +25854,9 @@ var SSH_PROJECT_REF_ACTIONS = new Set(["tenant_manage", "tenant_inspect", "tenan
25850
25854
  var PLATFORM_PROJECT_REF_ACTIONS = new Set([
25851
25855
  "list_backups",
25852
25856
  "create_backup",
25857
+ "list_logical_backups",
25858
+ "create_logical_backup",
25859
+ "restore_logical_backup",
25853
25860
  "network",
25854
25861
  "update_network"
25855
25862
  ]);
@@ -25898,7 +25905,7 @@ function requestedProjectRef(moduleName, action, args) {
25898
25905
  if (moduleName === "platform") {
25899
25906
  return PLATFORM_PROJECT_REF_ACTIONS.has(action) ? stringArgument(args, "ref") : null;
25900
25907
  }
25901
- return moduleName === "gateway" ? stringArgument(args, "ref") : null;
25908
+ return ["gateway", "frontend"].includes(moduleName) ? stringArgument(args, "ref") : null;
25902
25909
  }
25903
25910
  function requiresProjectRef(moduleName, action) {
25904
25911
  return !REFLESS_WRITE_ACTIONS.has(`${moduleName}.${action}`);
@@ -26010,7 +26017,7 @@ function validateExecutionPolicyCoverage(tools) {
26010
26017
 
26011
26018
  // src/shared/transports/http.ts
26012
26019
  var DEFAULT_TIMEOUT = 30000;
26013
- var MAX_POST_TIMEOUT_MS = 35 * 60000;
26020
+ var MAX_POST_TIMEOUT_MS = 36 * 60000;
26014
26021
  var MAX_RETRIES = 2;
26015
26022
  var RETRY_BASE_DELAY = 500;
26016
26023
  function isRetryableMethod(method) {
@@ -26206,14 +26213,59 @@ class HttpTransport {
26206
26213
  }
26207
26214
  async post(path, body, options) {
26208
26215
  const timeoutMs = validatedPostTimeout(options);
26216
+ const maxJsonBytes = validatedStrictJsonLimit({ maxJsonBytes: options?.maxJsonBytes });
26217
+ let response;
26209
26218
  try {
26210
- const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
26219
+ response = await fetchWithRetry(`${this.baseUrl}${path}`, {
26211
26220
  method: "POST",
26212
26221
  headers: this.headers(),
26213
26222
  body: body ? JSON.stringify(body) : undefined
26214
26223
  }, timeoutMs);
26215
- const data = await res.json().catch(() => null);
26216
- return { ok: res.ok, status: res.status, data };
26224
+ } catch (error) {
26225
+ return transportFailure(error);
26226
+ }
26227
+ if (maxJsonBytes !== undefined) {
26228
+ try {
26229
+ const data2 = await strictBoundedResponseJson(response, maxJsonBytes);
26230
+ return { ok: response.ok, status: response.status, data: data2 };
26231
+ } catch {
26232
+ return responseBodyFailure(response.status);
26233
+ }
26234
+ }
26235
+ const data = await response.json().catch(() => null);
26236
+ return { ok: response.ok, status: response.status, data };
26237
+ }
26238
+ async postBinary(path, body, options) {
26239
+ if (options.contentType !== "application/zip") {
26240
+ throw new Error("Binary HTTP content type is invalid");
26241
+ }
26242
+ if (!Number.isSafeInteger(options.contentLength) || options.contentLength < 1 || options.contentLength !== body.byteLength) {
26243
+ throw new RangeError("Binary HTTP body length is invalid");
26244
+ }
26245
+ if (!/^[0-9a-f]{64}$/u.test(options.contentSha256)) {
26246
+ throw new Error("Binary HTTP body SHA-256 is invalid");
26247
+ }
26248
+ const maxJsonBytes = validatedStrictJsonLimit({ maxJsonBytes: options.maxJsonBytes });
26249
+ const timeoutMs = validatedPostTimeout(options.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs });
26250
+ try {
26251
+ const request = {
26252
+ method: "POST",
26253
+ headers: {
26254
+ Authorization: `Bearer ${this.token}`,
26255
+ "Content-Type": options.contentType,
26256
+ "Content-Length": String(options.contentLength),
26257
+ "x-supacloud-content-sha256": options.contentSha256
26258
+ },
26259
+ body: body.stream,
26260
+ duplex: "half"
26261
+ };
26262
+ const response = await fetchWithRetry(`${this.baseUrl}${path}`, request, timeoutMs);
26263
+ try {
26264
+ const data = await strictBoundedResponseJson(response, maxJsonBytes);
26265
+ return { ok: response.ok, status: response.status, data };
26266
+ } catch {
26267
+ return responseBodyFailure(response.status);
26268
+ }
26217
26269
  } catch (error) {
26218
26270
  return transportFailure(error);
26219
26271
  }
@@ -27422,6 +27474,7 @@ function upgradeScriptExecution(paths, bundle, request) {
27422
27474
  'install -m 0755 "$RUNNER_ASSET" "$RUNNER"',
27423
27475
  `test "$(sha256sum "$RUNNER"|awk '{print $1}')" = "$(sha256sum "$RUNNER_ASSET"|awk '{print $1}')"`,
27424
27476
  '"$RUNNER" --version | grep -Eq "(^|[^0-9])${MANAGEMENT_VERSION//./\\.}([^0-9]|$)"',
27477
+ `timeout 5s "$RUNNER" --systemd-unit-helper-sha256 | grep -Eq 'SupaCloud systemd-unit helper SHA-256: [0-9a-f]{64}'`,
27425
27478
  `env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" upgrade --yes --target-version ${quoteShell2(request.managementVersion)} --edge-runtime-version ${quoteShell2(request.edgeRuntimeVersion)} --asset-bundle-dir "$BUNDLE"`
27426
27479
  ];
27427
27480
  }
@@ -28008,30 +28061,27 @@ function assertExactStableVersion2(value, fieldName) {
28008
28061
  }
28009
28062
  return value;
28010
28063
  }
28011
- function upgradeEnvAssignments(request) {
28064
+ function upgradeTransactionEnvAssignments(request) {
28012
28065
  return [
28013
- request.version ? `SUPACLOUD_UPGRADE_TAG=${quoteEnvValue(request.version)}` : "",
28066
+ 'SUPACLOUD_UPGRADE_TAG="$TARGET_MANAGEMENT_VERSION"',
28014
28067
  request.edgeRuntimeVersion ? `SUPACLOUD_EDGE_RUNTIME_UPGRADE_TAG=${quoteEnvValue(request.edgeRuntimeVersion)}` : "",
28015
28068
  request.githubProxy ? `SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : ""
28016
28069
  ].filter(Boolean).join(" ");
28017
28070
  }
28018
28071
  function componentBootstrapCommands(request) {
28019
- if (!request.edgeRuntimeVersion)
28020
- return [`UPGRADE_RUNNER=${quoteEnvValue("/usr/local/bin/supacloud")}`];
28021
28072
  const managementVersion = request.version || "latest";
28022
28073
  return [
28023
- `ACTIVE_VERSION=$(/usr/local/bin/supacloud --version 2>&1 | grep -Eo '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1 || true)`,
28024
- `UPGRADE_RUNNER=${quoteEnvValue("/usr/local/bin/supacloud")}`,
28025
- `if ! supacloud_version_at_least "$ACTIVE_VERSION" ${quoteEnvValue(MINIMUM_COMPONENT_UPGRADE_VERSION)}; then`,
28026
- ` case "$(uname -m)" in x86_64|amd64) MANAGEMENT_ASSET=supacloud-linux-amd64 ;; aarch64|arm64) MANAGEMENT_ASSET=supacloud-linux-arm64 ;; *) echo 'Unsupported Management architecture' >&2; exit 1 ;; esac`,
28027
- ` STAGED_MANAGEMENT=$(mktemp /tmp/supacloud-management-upgrade.XXXXXX)`,
28028
- ` MANAGEMENT_RELEASE=$(supacloud_fetch_component_release management-api ${quoteEnvValue(managementVersion)} "$MANAGEMENT_ASSET" web-console-build.tar.gz)`,
28029
- ` supacloud_download_release_asset "$MANAGEMENT_RELEASE" "$MANAGEMENT_ASSET" "$STAGED_MANAGEMENT" binary`,
28030
- ` chmod 0755 "$STAGED_MANAGEMENT"`,
28031
- ` STAGED_VERSION=$("$STAGED_MANAGEMENT" --version 2>&1 | grep -Eo '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1 || true)`,
28032
- ` supacloud_version_at_least "$STAGED_VERSION" ${quoteEnvValue(MINIMUM_COMPONENT_UPGRADE_VERSION)} || { echo 'Target Management release lacks Edge Runtime transaction capability' >&2; exit 1; }`,
28033
- ` UPGRADE_RUNNER="$STAGED_MANAGEMENT"`,
28034
- "fi"
28074
+ `case "$(uname -m)" in x86_64|amd64) MANAGEMENT_ASSET=supacloud-linux-amd64 ;; aarch64|arm64) MANAGEMENT_ASSET=supacloud-linux-arm64 ;; *) echo 'Unsupported Management architecture' >&2; exit 1 ;; esac`,
28075
+ `STAGED_MANAGEMENT=$(mktemp /tmp/supacloud-management-upgrade.XXXXXX)`,
28076
+ `MANAGEMENT_RELEASE=$(supacloud_fetch_component_release management-api ${quoteEnvValue(managementVersion)} "$MANAGEMENT_ASSET" web-console-build.tar.gz)`,
28077
+ `supacloud_download_release_asset "$MANAGEMENT_RELEASE" "$MANAGEMENT_ASSET" "$STAGED_MANAGEMENT" binary`,
28078
+ `chmod 0755 "$STAGED_MANAGEMENT"`,
28079
+ `TARGET_MANAGEMENT_VERSION=$(jq -er '.tag_name | capture("^management-api-v(?<version>(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*))$").version' <<< "$MANAGEMENT_RELEASE")`,
28080
+ `STAGED_VERSION=$("$STAGED_MANAGEMENT" --version 2>&1 | grep -Eo '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1 || true)`,
28081
+ `test "$STAGED_VERSION" = "$TARGET_MANAGEMENT_VERSION" || { echo 'Target Management binary version does not match its verified release' >&2; exit 1; }`,
28082
+ `supacloud_version_at_least "$STAGED_VERSION" ${quoteEnvValue(MINIMUM_COMPONENT_UPGRADE_VERSION)} || { echo 'Target Management release lacks component transaction capability' >&2; exit 1; }`,
28083
+ `"$STAGED_MANAGEMENT" --systemd-unit-helper-sha256 >/dev/null 2>&1 || { echo 'Target Management release lacks target-bound systemd-unit helper delivery' >&2; exit 1; }`,
28084
+ `UPGRADE_RUNNER="$STAGED_MANAGEMENT"`
28035
28085
  ];
28036
28086
  }
28037
28087
  function componentPreflightCommands(request) {
@@ -28069,7 +28119,7 @@ function trustedRootPreflightCommands(helperPath) {
28069
28119
  ];
28070
28120
  }
28071
28121
  function buildRootUpgradeScript(request) {
28072
- const envAssignments = upgradeEnvAssignments(request);
28122
+ const envAssignments = upgradeTransactionEnvAssignments(request);
28073
28123
  return [
28074
28124
  "set -euo pipefail",
28075
28125
  "umask 077",
@@ -28090,7 +28140,7 @@ function buildRootUpgradeScript(request) {
28090
28140
  "if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
28091
28141
  "supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
28092
28142
  ...componentBootstrapCommands(request),
28093
- `${envAssignments ? `env ${envAssignments} ` : ""}"$UPGRADE_RUNNER" upgrade --yes`
28143
+ `env ${envAssignments} "$UPGRADE_RUNNER" upgrade --yes`
28094
28144
  ].join(`
28095
28145
  `);
28096
28146
  }
@@ -29224,6 +29274,238 @@ async function createFullPhysicalBackup(http, projectRef) {
29224
29274
  return reconciledCreationResponse(projectRef, beforeRead.inventory, creation, afterRead);
29225
29275
  }
29226
29276
 
29277
+ // src/shared/tools/logical-backup-control.ts
29278
+ var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
29279
+ var SAFE_PROJECT_REF2 = /^[A-Za-z0-9_-]{1,64}$/;
29280
+ var BACKUP_ID = /^logical-full_[A-Za-z0-9_-]{1,64}_[a-f0-9]{32}$/;
29281
+ var BACKUP_ID_SUFFIX = /^[a-f0-9]{32}$/;
29282
+ var SHA256 = /^[a-f0-9]{64}$/;
29283
+ var SAFE_DATABASE = /^[^\u0000-\u001f\u007f]{1,128}$/;
29284
+ var LOGICAL_BACKUP_REQUEST_TIMEOUT_MS = 36 * 60000;
29285
+ var LOGICAL_BACKUP_INVENTORY_MAX_BYTES = 1024 * 1024;
29286
+ var LOGICAL_BACKUP_MUTATION_MAX_BYTES = 64 * 1024;
29287
+ function logicalBackupResponse(payload) {
29288
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
29289
+ }
29290
+ function logicalBackupSuccess(operation, payload) {
29291
+ return logicalBackupResponse({
29292
+ ...payload,
29293
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
29294
+ ok: true,
29295
+ operation
29296
+ });
29297
+ }
29298
+ function logicalBackupFailure(operation, code, httpStatus) {
29299
+ return {
29300
+ ...logicalBackupResponse({
29301
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
29302
+ ok: false,
29303
+ operation,
29304
+ error: { code, http_status: httpStatus }
29305
+ }),
29306
+ isError: true
29307
+ };
29308
+ }
29309
+ function isPlainRecord(candidate) {
29310
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
29311
+ return false;
29312
+ const prototype = Object.getPrototypeOf(candidate);
29313
+ return prototype === Object.prototype || prototype === null;
29314
+ }
29315
+ function canonicalTimestamp(candidate) {
29316
+ if (typeof candidate !== "string")
29317
+ return false;
29318
+ const timestamp = new Date(candidate);
29319
+ return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === candidate;
29320
+ }
29321
+ function backupBelongsToProject(backupId, projectRef) {
29322
+ const prefix = `logical-full_${projectRef}_`;
29323
+ return BACKUP_ID.test(backupId) && backupId.startsWith(prefix) && BACKUP_ID_SUFFIX.test(backupId.slice(prefix.length));
29324
+ }
29325
+ function validBackupProject(candidate, projectRef) {
29326
+ return typeof candidate.backup_id === "string" && backupBelongsToProject(candidate.backup_id, projectRef) && candidate.project_ref === projectRef;
29327
+ }
29328
+ function validBackupDatabase(candidate) {
29329
+ return typeof candidate.database === "string" && SAFE_DATABASE.test(candidate.database);
29330
+ }
29331
+ function validBackupTimestamps(candidate) {
29332
+ return canonicalTimestamp(candidate.created_at) && canonicalTimestamp(candidate.completed_at) && new Date(candidate.completed_at).valueOf() >= new Date(candidate.created_at).valueOf();
29333
+ }
29334
+ function validBackupEvidence(candidate) {
29335
+ return Number.isSafeInteger(candidate.bytes) && Number(candidate.bytes) > 0 && typeof candidate.sha256 === "string" && SHA256.test(candidate.sha256);
29336
+ }
29337
+ function logicalBackupIdentity(candidate, projectRef) {
29338
+ if (!isPlainRecord(candidate))
29339
+ return null;
29340
+ if (!validBackupProject(candidate, projectRef) || !validBackupDatabase(candidate) || candidate.kind !== "logical-full" || !validBackupTimestamps(candidate) || !validBackupEvidence(candidate)) {
29341
+ return null;
29342
+ }
29343
+ return {
29344
+ backup_id: candidate.backup_id,
29345
+ project_ref: projectRef,
29346
+ database: candidate.database,
29347
+ kind: "logical-full",
29348
+ created_at: candidate.created_at,
29349
+ completed_at: candidate.completed_at,
29350
+ bytes: Number(candidate.bytes),
29351
+ sha256: candidate.sha256
29352
+ };
29353
+ }
29354
+ function logicalBackupInventory(payload, projectRef) {
29355
+ if (!isPlainRecord(payload) || !Array.isArray(payload.backups))
29356
+ return null;
29357
+ const backups = payload.backups.map((candidate) => logicalBackupIdentity(candidate, projectRef));
29358
+ if (backups.some((backup) => backup === null))
29359
+ return null;
29360
+ const inventory = backups;
29361
+ if (new Set(inventory.map((backup) => backup.backup_id)).size !== inventory.length)
29362
+ return null;
29363
+ return inventory;
29364
+ }
29365
+ function assertProjectRef(projectRef) {
29366
+ if (!SAFE_PROJECT_REF2.test(projectRef)) {
29367
+ throw new Error("'ref' is invalid for verified logical backup");
29368
+ }
29369
+ }
29370
+ function logicalBackupEndpoint(projectRef) {
29371
+ assertProjectRef(projectRef);
29372
+ return `/v1/projects/${encodeURIComponent(projectRef)}/database/backups/logical`;
29373
+ }
29374
+ async function readLogicalBackupInventory(http, projectRef) {
29375
+ const response = await http.get(logicalBackupEndpoint(projectRef), {
29376
+ maxJsonBytes: LOGICAL_BACKUP_INVENTORY_MAX_BYTES
29377
+ });
29378
+ return {
29379
+ response,
29380
+ inventory: response.ok ? logicalBackupInventory(response.data, projectRef) ?? undefined : undefined
29381
+ };
29382
+ }
29383
+ function mutationFailureCode2(response) {
29384
+ return response.transportError || response.responseError || response.status === 408 || response.status >= 500 ? "OUTCOME_UNKNOWN" : "HTTP_ERROR";
29385
+ }
29386
+ function identicalBackup(left, right) {
29387
+ return left.backup_id === right.backup_id && left.project_ref === right.project_ref && left.database === right.database && left.kind === right.kind && left.created_at === right.created_at && left.completed_at === right.completed_at && left.bytes === right.bytes && left.sha256 === right.sha256;
29388
+ }
29389
+ function createdBackup(previous, current) {
29390
+ const currentById = new Map(current.map((backup) => [backup.backup_id, backup]));
29391
+ const previousIds = new Set;
29392
+ for (const previousBackup of previous) {
29393
+ previousIds.add(previousBackup.backup_id);
29394
+ const currentBackup = currentById.get(previousBackup.backup_id);
29395
+ if (!currentBackup || !identicalBackup(previousBackup, currentBackup))
29396
+ return null;
29397
+ }
29398
+ const additions = current.filter((backup) => !previousIds.has(backup.backup_id));
29399
+ return additions.length === 1 ? additions[0] : null;
29400
+ }
29401
+ function exactBackupEnvelope(payload, field, projectRef) {
29402
+ return isPlainRecord(payload) ? logicalBackupIdentity(payload[field], projectRef) : null;
29403
+ }
29404
+ function inventoryReadFailure(operation, read) {
29405
+ const { response, inventory } = read;
29406
+ if (!response.ok || response.status !== 200) {
29407
+ const code = response.ok || response.responseError ? "INVALID_RESPONSE" : "HTTP_ERROR";
29408
+ const status = response.transportError ? null : response.status;
29409
+ return logicalBackupFailure(operation, code, status);
29410
+ }
29411
+ return inventory ? null : logicalBackupFailure(operation, "INVALID_RESPONSE", response.status);
29412
+ }
29413
+ function createReceipt(projectRef, previous, mutation, afterRead) {
29414
+ const operation = "platform.create_logical_backup";
29415
+ if (!mutation.ok) {
29416
+ const status = mutation.transportError ? null : mutation.status;
29417
+ return logicalBackupFailure(operation, mutationFailureCode2(mutation), status);
29418
+ }
29419
+ if (mutation.status !== 200)
29420
+ return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
29421
+ const responseBackup = exactBackupEnvelope(mutation.data, "backup", projectRef);
29422
+ const addedBackup = afterRead.response.ok && afterRead.response.status === 200 && afterRead.inventory ? createdBackup(previous, afterRead.inventory) : null;
29423
+ if (!responseBackup || !addedBackup || !identicalBackup(responseBackup, addedBackup)) {
29424
+ return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
29425
+ }
29426
+ return logicalBackupSuccess(operation, { project_ref: projectRef, backup: addedBackup });
29427
+ }
29428
+ async function listVerifiedLogicalBackups(http, projectRef) {
29429
+ const operation = "platform.list_logical_backups";
29430
+ const read = await readLogicalBackupInventory(http, projectRef);
29431
+ const failure = inventoryReadFailure(operation, read);
29432
+ return failure ?? logicalBackupSuccess(operation, {
29433
+ project_ref: projectRef,
29434
+ backups: read.inventory
29435
+ });
29436
+ }
29437
+ async function createVerifiedLogicalBackup(http, projectRef) {
29438
+ const operation = "platform.create_logical_backup";
29439
+ const beforeRead = await readLogicalBackupInventory(http, projectRef);
29440
+ const failure = inventoryReadFailure(operation, beforeRead);
29441
+ if (failure)
29442
+ return failure;
29443
+ const mutation = await http.post(logicalBackupEndpoint(projectRef), {}, {
29444
+ timeoutMs: LOGICAL_BACKUP_REQUEST_TIMEOUT_MS,
29445
+ maxJsonBytes: LOGICAL_BACKUP_MUTATION_MAX_BYTES
29446
+ });
29447
+ const afterRead = await readLogicalBackupInventory(http, projectRef);
29448
+ return createReceipt(projectRef, beforeRead.inventory, mutation, afterRead);
29449
+ }
29450
+ function expectedRestoreConfirmation(request) {
29451
+ return [
29452
+ "RESTORE_PROJECT",
29453
+ request.projectRef,
29454
+ request.backupId,
29455
+ request.expectedSha256
29456
+ ].join(":");
29457
+ }
29458
+ function assertRestoreRequest(request) {
29459
+ assertProjectRef(request.projectRef);
29460
+ if (!backupBelongsToProject(request.backupId, request.projectRef)) {
29461
+ throw new Error("'backup_id' must belong to the requested project");
29462
+ }
29463
+ if (!SHA256.test(request.expectedSha256)) {
29464
+ throw new Error("'expected_sha256' must be exactly 64 lowercase hexadecimal characters");
29465
+ }
29466
+ if (request.confirmation !== expectedRestoreConfirmation(request)) {
29467
+ throw new Error("'confirmation' must exactly bind the project ref, backup ID, and SHA-256");
29468
+ }
29469
+ }
29470
+ function restoreReceipt(request, requestedBackup, mutation) {
29471
+ const operation = "platform.restore_logical_backup";
29472
+ if (!mutation.ok) {
29473
+ const status = mutation.transportError ? null : mutation.status;
29474
+ return logicalBackupFailure(operation, mutationFailureCode2(mutation), status);
29475
+ }
29476
+ if (mutation.status !== 200)
29477
+ return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
29478
+ const restoredBackup = exactBackupEnvelope(mutation.data, "restored_backup", request.projectRef);
29479
+ if (!restoredBackup || !identicalBackup(restoredBackup, requestedBackup)) {
29480
+ return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
29481
+ }
29482
+ return logicalBackupSuccess(operation, {
29483
+ project_ref: request.projectRef,
29484
+ restored_backup: restoredBackup
29485
+ });
29486
+ }
29487
+ async function restoreVerifiedLogicalBackup(http, request) {
29488
+ const operation = "platform.restore_logical_backup";
29489
+ assertRestoreRequest(request);
29490
+ const beforeRead = await readLogicalBackupInventory(http, request.projectRef);
29491
+ const failure = inventoryReadFailure(operation, beforeRead);
29492
+ if (failure)
29493
+ return failure;
29494
+ const requestedBackup = beforeRead.inventory.find((backup) => backup.backup_id === request.backupId);
29495
+ if (!requestedBackup || requestedBackup.sha256 !== request.expectedSha256) {
29496
+ return logicalBackupFailure(operation, "INVALID_RESPONSE", beforeRead.response.status);
29497
+ }
29498
+ const mutation = await http.post(`${logicalBackupEndpoint(request.projectRef)}/restore`, {
29499
+ backup_id: request.backupId,
29500
+ expected_sha256: request.expectedSha256,
29501
+ confirmation: request.confirmation
29502
+ }, {
29503
+ timeoutMs: LOGICAL_BACKUP_REQUEST_TIMEOUT_MS,
29504
+ maxJsonBytes: LOGICAL_BACKUP_MUTATION_MAX_BYTES
29505
+ });
29506
+ return restoreReceipt(request, requestedBackup, mutation);
29507
+ }
29508
+
29227
29509
  // src/shared/tools/advanced-tools.ts
29228
29510
  function registerAdvancedTools(server, http) {
29229
29511
  server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
@@ -29367,11 +29649,14 @@ Actions: list, upsert, delete`, {
29367
29649
  return { content: [{ type: "text", text }] };
29368
29650
  });
29369
29651
  server.tool("platform", `Platform monitoring, backups, network, and organizations.
29370
- Actions: metrics, list_backups, create_backup, network, update_network, list_orgs, get_org`, {
29652
+ Actions: metrics, list_backups, create_backup, list_logical_backups, create_logical_backup, restore_logical_backup, network, update_network, list_orgs, get_org`, {
29371
29653
  action: withDescription(stringEnum([
29372
29654
  "metrics",
29373
29655
  "list_backups",
29374
29656
  "create_backup",
29657
+ "list_logical_backups",
29658
+ "create_logical_backup",
29659
+ "restore_logical_backup",
29375
29660
  "network",
29376
29661
  "update_network",
29377
29662
  "list_orgs",
@@ -29379,10 +29664,22 @@ Actions: metrics, list_backups, create_backup, network, update_network, list_org
29379
29664
  ]), "Action"),
29380
29665
  ref: optional(Type.String(), "[*] Project ref for project-scoped platform actions"),
29381
29666
  backup_type: optional(stringEnum(["full"]), "[create_backup] Explicit physical backup type"),
29667
+ backup_id: optional(Type.String(), "[restore_logical_backup] Verified logical backup ID"),
29668
+ expected_sha256: optional(Type.String(), "[restore_logical_backup] Exact lowercase backup SHA-256"),
29669
+ confirmation: optional(Type.String(), "[restore_logical_backup] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation"),
29382
29670
  slug: optional(Type.String(), "[get_org] Organization slug"),
29383
29671
  allowed_cidrs: optional(Type.Array(Type.String()), "[update_network] Allowed CIDRs")
29384
29672
  }, async (args) => {
29385
- const { action, ref, backup_type, slug, allowed_cidrs } = args;
29673
+ const {
29674
+ action,
29675
+ ref,
29676
+ backup_type,
29677
+ backup_id,
29678
+ expected_sha256,
29679
+ confirmation,
29680
+ slug,
29681
+ allowed_cidrs
29682
+ } = args;
29386
29683
  const need = (f, v) => {
29387
29684
  if (!v)
29388
29685
  throw new Error(`'${f}' required for '${action}'`);
@@ -29400,6 +29697,23 @@ Actions: metrics, list_backups, create_backup, network, update_network, list_org
29400
29697
  need("backup_type", backup_type);
29401
29698
  return createFullPhysicalBackup(http, ref);
29402
29699
  }
29700
+ case "list_logical_backups":
29701
+ need("ref", ref);
29702
+ return listVerifiedLogicalBackups(http, ref);
29703
+ case "create_logical_backup":
29704
+ need("ref", ref);
29705
+ return createVerifiedLogicalBackup(http, ref);
29706
+ case "restore_logical_backup":
29707
+ need("ref", ref);
29708
+ need("backup_id", backup_id);
29709
+ need("expected_sha256", expected_sha256);
29710
+ need("confirmation", confirmation);
29711
+ return restoreVerifiedLogicalBackup(http, {
29712
+ projectRef: ref,
29713
+ backupId: backup_id,
29714
+ expectedSha256: expected_sha256,
29715
+ confirmation
29716
+ });
29403
29717
  case "network":
29404
29718
  need("ref", ref);
29405
29719
  text = JSON.stringify((await http.get(`/v1/projects/${ref}/network-restrictions`)).data, null, 2);
@@ -29904,7 +30218,7 @@ function matchingText(candidate, maxLength, pattern) {
29904
30218
  const candidateText = boundedText(candidate, maxLength);
29905
30219
  return candidateText && pattern.test(candidateText) ? candidateText : null;
29906
30220
  }
29907
- function canonicalTimestamp(candidate) {
30221
+ function canonicalTimestamp2(candidate) {
29908
30222
  const timestamp = boundedText(candidate, 64);
29909
30223
  if (!timestamp)
29910
30224
  return null;
@@ -29919,7 +30233,7 @@ function projectedSummary(project) {
29919
30233
  organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
29920
30234
  name: boundedText(project.name, 100),
29921
30235
  region: matchingText(project.region, 64, REGION_PATTERN),
29922
- created_at: canonicalTimestamp(project.created_at),
30236
+ created_at: canonicalTimestamp2(project.created_at),
29923
30237
  status: matchingText(project.status, 64, STATUS_PATTERN)
29924
30238
  };
29925
30239
  return Object.values(summary).every((field) => field !== null) ? summary : null;
@@ -30063,7 +30377,7 @@ function projectGetRead(response, expectedRef) {
30063
30377
  // src/shared/tools/project-runtime-snapshot.ts
30064
30378
  var RUNTIME_SNAPSHOT_SCHEMA = "supacloud.runtime-snapshot.v1";
30065
30379
  var ATTESTED_REVISION_PATTERN = /^hmac-sha256:[a-f0-9]{64}$/;
30066
- var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
30380
+ var SAFE_PROJECT_REF3 = /^[a-z0-9-]{1,20}$/;
30067
30381
  var SNAPSHOT_KEYS = ["schema", "project_ref", "captured_at", "secrets", "postgrest"];
30068
30382
  var SECRETS_KEYS = [
30069
30383
  "desired_revision",
@@ -30200,7 +30514,7 @@ function hasCausalLoadTimestamps(snapshot) {
30200
30514
  return [snapshot.secrets.loaded_at, snapshot.postgrest.loaded_at].every((loadedAt) => loadedAt === null || Date.parse(loadedAt) <= capturedAt);
30201
30515
  }
30202
30516
  function parseProjectRuntimeSnapshot(payload, requestedProjectRef2) {
30203
- 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))
30517
+ if (!SAFE_PROJECT_REF3.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))
30204
30518
  return null;
30205
30519
  const snapshot = payload;
30206
30520
  return hasCausalLoadTimestamps(snapshot) ? sanitizedSnapshot(snapshot) : null;
@@ -30237,11 +30551,11 @@ var STUDIO_PROJECT_SERVICE_STATUSES = [
30237
30551
  ];
30238
30552
  var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
30239
30553
  var AUTH_SERVICE_HOST_SUFFIX = "-auth";
30240
- var SAFE_PROJECT_REF3 = /^[a-z0-9-]{1,20}$/;
30554
+ var SAFE_PROJECT_REF4 = /^[a-z0-9-]{1,20}$/;
30241
30555
  var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
30242
30556
  var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
30243
30557
  var MAX_RUNTIME_SNAPSHOT_BYTES = 64 * 1024;
30244
- var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
30558
+ var RELEASE_CONTROL_RESPONSE_SCHEMA2 = "supacloud.cli.release-control.v1";
30245
30559
  var PROJECT_CREATE_OPERATION = "project.create";
30246
30560
  var PROJECT_ENVIRONMENTS = ["test", "production"];
30247
30561
  var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -30269,7 +30583,7 @@ function failedProjectServiceResponse(message) {
30269
30583
  function projectCreateReceipt(payload) {
30270
30584
  return {
30271
30585
  content: [{ type: "text", text: JSON.stringify({
30272
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
30586
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA2,
30273
30587
  ...payload
30274
30588
  }, null, 2) }]
30275
30589
  };
@@ -30454,7 +30768,7 @@ function projectServiceStatusOutput(status) {
30454
30768
  function projectServicesResponse(projectRef, response) {
30455
30769
  if (!response.ok)
30456
30770
  return failedProjectServiceHttpResponse(response);
30457
- if (!SAFE_PROJECT_REF3.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
30771
+ if (!SAFE_PROJECT_REF4.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
30458
30772
  return failedProjectServiceResponse("Project service inventory response is invalid");
30459
30773
  }
30460
30774
  if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
@@ -31052,10 +31366,484 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
31052
31366
  return { content: [{ type: "text", text }] };
31053
31367
  });
31054
31368
  }
31369
+
31370
+ // src/shared/tools/frontend-release-control.ts
31371
+ import { createHash as createHash3 } from "node:crypto";
31372
+ import { constants as fsConstants2 } from "node:fs";
31373
+ import { open as open2 } from "node:fs/promises";
31374
+ import { resolve as resolve3 } from "node:path";
31375
+ var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,20}$/u;
31376
+ var DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
31377
+ var RELEASE_ID_PATTERN = /^[0-9a-f]{64}$/u;
31378
+ var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
31379
+ var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
31380
+ var ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
31381
+ var ARCHIVE_CHUNK_BYTES = 64 * 1024;
31382
+ var RESPONSE_MAX_BYTES = 1024 * 1024;
31383
+ var UPLOAD_REQUEST_TIMEOUT_MS = 10 * 60000;
31384
+ var RELEASE_LIST_LIMIT_MAX = 100;
31385
+ function toolResponse(payload) {
31386
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
31387
+ }
31388
+ function releaseFailure(operation, code, status) {
31389
+ return {
31390
+ isError: true,
31391
+ content: [{
31392
+ type: "text",
31393
+ text: JSON.stringify({ ok: false, operation, error: { code, http_status: status } })
31394
+ }]
31395
+ };
31396
+ }
31397
+ function exactKeys(candidate, keys) {
31398
+ const actual = Object.keys(candidate).sort();
31399
+ const expected = [...keys].sort();
31400
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
31401
+ }
31402
+ function releaseRecord(candidate) {
31403
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
31404
+ return null;
31405
+ const record = candidate;
31406
+ const keys = [
31407
+ "schema",
31408
+ "project_ref",
31409
+ "deployment_id",
31410
+ "release_id",
31411
+ "sha256",
31412
+ "tree_sha256",
31413
+ "size_bytes",
31414
+ "file_count",
31415
+ "created_at",
31416
+ "kind"
31417
+ ];
31418
+ if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN2.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || typeof record.release_id !== "string" || !RELEASE_ID_PATTERN.test(record.release_id) || record.sha256 !== record.release_id || typeof record.tree_sha256 !== "string" || !RELEASE_ID_PATTERN.test(record.tree_sha256) || !Number.isSafeInteger(record.size_bytes) || Number(record.size_bytes) < 1 || !Number.isSafeInteger(record.file_count) || Number(record.file_count) < 1 || !canonicalTimestamp3(record.created_at) || record.kind !== "prebuilt_static")
31419
+ return null;
31420
+ return {
31421
+ project_ref: record.project_ref,
31422
+ deployment_id: record.deployment_id,
31423
+ release_id: record.release_id,
31424
+ sha256: record.release_id,
31425
+ tree_sha256: record.tree_sha256,
31426
+ size_bytes: Number(record.size_bytes),
31427
+ file_count: Number(record.file_count),
31428
+ created_at: record.created_at,
31429
+ kind: "prebuilt_static"
31430
+ };
31431
+ }
31432
+ function releaseEnvelope(candidate, expected) {
31433
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
31434
+ return null;
31435
+ const envelope = candidate;
31436
+ const release = exactKeys(envelope, ["project_ref", "deployment_id", "release"]) ? releaseRecord(envelope.release) : null;
31437
+ if (!release || envelope.project_ref !== expected.projectRef || envelope.deployment_id !== expected.deploymentId || release.project_ref !== expected.projectRef || release.deployment_id !== expected.deploymentId || release.release_id !== expected.releaseId)
31438
+ return null;
31439
+ return release;
31440
+ }
31441
+ function canonicalTimestamp3(candidate) {
31442
+ if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
31443
+ return false;
31444
+ const milliseconds = Date.parse(candidate);
31445
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
31446
+ }
31447
+ function nullableIdentity(candidate, pattern) {
31448
+ if (candidate === null)
31449
+ return null;
31450
+ return typeof candidate === "string" && pattern.test(candidate) ? candidate : undefined;
31451
+ }
31452
+ function releaseInventory(candidate, expected) {
31453
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
31454
+ return null;
31455
+ const record = candidate;
31456
+ const keys = [
31457
+ "project_ref",
31458
+ "deployment_id",
31459
+ "active_release_id",
31460
+ "active_activation_id",
31461
+ "releases",
31462
+ "next_cursor"
31463
+ ];
31464
+ if (!exactKeys(record, keys) || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN2.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || !Array.isArray(record.releases))
31465
+ return null;
31466
+ const activeReleaseId = nullableIdentity(record.active_release_id, RELEASE_ID_PATTERN);
31467
+ const activeActivationId = nullableIdentity(record.active_activation_id, MUTATION_ID_PATTERN);
31468
+ const nextCursor = nullableIdentity(record.next_cursor, RELEASE_ID_PATTERN);
31469
+ const releases = record.releases.map(releaseRecord);
31470
+ if (activeReleaseId === undefined || activeActivationId === undefined || nextCursor === undefined || activeReleaseId === null !== (activeActivationId === null) || releases.some((release) => release === null))
31471
+ return null;
31472
+ const verified = releases;
31473
+ if (new Set(verified.map((release) => release.release_id)).size !== verified.length || verified.some((release) => release.project_ref !== record.project_ref || release.deployment_id !== record.deployment_id) || expected && (record.project_ref !== expected.projectRef || record.deployment_id !== expected.deploymentId))
31474
+ return null;
31475
+ return {
31476
+ project_ref: record.project_ref,
31477
+ deployment_id: record.deployment_id,
31478
+ active_release_id: activeReleaseId,
31479
+ active_activation_id: activeActivationId,
31480
+ releases: verified,
31481
+ next_cursor: nextCursor
31482
+ };
31483
+ }
31484
+ function releaseEndpoint(projectRef, deploymentId) {
31485
+ if (!PROJECT_REF_PATTERN2.test(projectRef))
31486
+ throw new Error("'ref' is invalid for frontend releases");
31487
+ if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
31488
+ throw new Error("'id' is invalid for frontend releases");
31489
+ return `/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
31490
+ }
31491
+ function releasePath(projectRef, deploymentId, releaseId) {
31492
+ if (!RELEASE_ID_PATTERN.test(releaseId))
31493
+ throw new Error("'release_id' must be a SHA-256 digest");
31494
+ return `${releaseEndpoint(projectRef, deploymentId)}/${releaseId}`;
31495
+ }
31496
+ function sameArchiveIdentity(left, right) {
31497
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
31498
+ }
31499
+ async function archiveSha256(handle, sizeBytes) {
31500
+ const hash2 = createHash3("sha256");
31501
+ const chunk = new Uint8Array(Math.min(sizeBytes, ARCHIVE_CHUNK_BYTES));
31502
+ for (let offset = 0;offset < sizeBytes; ) {
31503
+ const requested = Math.min(chunk.byteLength, sizeBytes - offset);
31504
+ const { bytesRead } = await handle.read(chunk, 0, requested, offset);
31505
+ if (bytesRead < 1)
31506
+ throw new Error("Frontend release archive changed while it was hashed");
31507
+ hash2.update(chunk.subarray(0, bytesRead));
31508
+ offset += bytesRead;
31509
+ }
31510
+ return hash2.digest("hex");
31511
+ }
31512
+ async function verifiedArchive(path2) {
31513
+ const archivePath = resolve3(path2);
31514
+ const handle = await open2(archivePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
31515
+ try {
31516
+ const before = await handle.stat({ bigint: true });
31517
+ const sizeBytes = Number(before.size);
31518
+ if (!before.isFile() || sizeBytes < 1 || sizeBytes > ARCHIVE_MAX_BYTES) {
31519
+ throw new Error(`Frontend release archive must be a 1-${ARCHIVE_MAX_BYTES} byte regular file`);
31520
+ }
31521
+ const sha2562 = await archiveSha256(handle, sizeBytes);
31522
+ const after = await handle.stat({ bigint: true });
31523
+ if (!sameArchiveIdentity(before, after)) {
31524
+ throw new Error("Frontend release archive identity changed while it was hashed");
31525
+ }
31526
+ return { handle, sizeBytes, sha256: sha2562 };
31527
+ } catch (error) {
31528
+ await handle.close();
31529
+ throw error;
31530
+ }
31531
+ }
31532
+ function archiveStream(archive) {
31533
+ let offset = 0;
31534
+ return new ReadableStream({
31535
+ async pull(controller) {
31536
+ if (offset === archive.sizeBytes) {
31537
+ controller.close();
31538
+ return;
31539
+ }
31540
+ const length = Math.min(ARCHIVE_CHUNK_BYTES, archive.sizeBytes - offset);
31541
+ const chunk = new Uint8Array(length);
31542
+ const { bytesRead } = await archive.handle.read(chunk, 0, length, offset);
31543
+ if (bytesRead < 1)
31544
+ throw new Error("Frontend release archive changed while it was uploaded");
31545
+ offset += bytesRead;
31546
+ controller.enqueue(bytesRead === length ? chunk : chunk.subarray(0, bytesRead));
31547
+ }
31548
+ });
31549
+ }
31550
+ function releaseReadFailure(operation, response) {
31551
+ return releaseFailure(operation, response.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", response.status);
31552
+ }
31553
+ async function listFrontendReleases(http, projectRef, deploymentId, cursor, limit = 50) {
31554
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > RELEASE_LIST_LIMIT_MAX) {
31555
+ throw new Error("'limit' must be 1-100");
31556
+ }
31557
+ if (cursor !== undefined && !RELEASE_ID_PATTERN.test(cursor))
31558
+ throw new Error("'cursor' is invalid");
31559
+ const query = new URLSearchParams({ limit: String(limit) });
31560
+ if (cursor)
31561
+ query.set("cursor", cursor);
31562
+ const response = await http.get(`${releaseEndpoint(projectRef, deploymentId)}?${query}`, {
31563
+ maxJsonBytes: RESPONSE_MAX_BYTES
31564
+ });
31565
+ const inventory = response.ok ? releaseInventory(response.data, { projectRef, deploymentId }) : null;
31566
+ if (!inventory || inventory.releases.length > limit) {
31567
+ return releaseReadFailure("frontend.list_releases", response);
31568
+ }
31569
+ return toolResponse(inventory);
31570
+ }
31571
+ async function getFrontendRelease(http, projectRef, deploymentId, releaseId) {
31572
+ const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
31573
+ maxJsonBytes: RESPONSE_MAX_BYTES
31574
+ });
31575
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
31576
+ if (!release) {
31577
+ return releaseReadFailure("frontend.get_release", response);
31578
+ }
31579
+ return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
31580
+ }
31581
+ async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath) {
31582
+ const endpoint = releaseEndpoint(projectRef, deploymentId);
31583
+ const archive = await verifiedArchive(archivePath);
31584
+ let response;
31585
+ try {
31586
+ response = await http.postBinary(endpoint, {
31587
+ stream: archiveStream(archive),
31588
+ byteLength: archive.sizeBytes
31589
+ }, {
31590
+ contentType: "application/zip",
31591
+ contentLength: archive.sizeBytes,
31592
+ contentSha256: archive.sha256,
31593
+ maxJsonBytes: RESPONSE_MAX_BYTES,
31594
+ timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS
31595
+ });
31596
+ } finally {
31597
+ await archive.handle.close();
31598
+ }
31599
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
31600
+ if (!release) {
31601
+ if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
31602
+ return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
31603
+ }
31604
+ return uploadReadback(http, projectRef, deploymentId, archive.sha256, response.status);
31605
+ }
31606
+ const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
31607
+ const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef, deploymentId, releaseId: release.release_id }) : null;
31608
+ if (!verified || verified.tree_sha256 !== release.tree_sha256) {
31609
+ return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
31610
+ }
31611
+ return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release: verified });
31612
+ }
31613
+ async function uploadReadback(http, projectRef, deploymentId, releaseId, uploadStatus) {
31614
+ const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
31615
+ maxJsonBytes: RESPONSE_MAX_BYTES
31616
+ });
31617
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
31618
+ if (!release) {
31619
+ return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
31620
+ }
31621
+ return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
31622
+ }
31623
+ function stableJson(candidate) {
31624
+ if (candidate === null || typeof candidate !== "object")
31625
+ return JSON.stringify(candidate);
31626
+ if (Array.isArray(candidate))
31627
+ return `[${candidate.map(stableJson).join(",")}]`;
31628
+ const record = candidate;
31629
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
31630
+ }
31631
+ function activationFingerprint(identity) {
31632
+ return createHash3("sha256").update(stableJson({
31633
+ project_ref: identity.projectRef,
31634
+ deployment_id: identity.deploymentId,
31635
+ release_id: identity.releaseId,
31636
+ expected_active_release_id: identity.expectedActiveReleaseId,
31637
+ activation_id: identity.mutationId,
31638
+ expected_activation_id: identity.expectedActivationId
31639
+ })).digest("hex");
31640
+ }
31641
+ function activationResourceKey(deploymentId) {
31642
+ return `v1/frontend_release/${Buffer.from(deploymentId, "utf8").toString("base64url")}`;
31643
+ }
31644
+ function publicMutationReceipt(candidate, expected) {
31645
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
31646
+ return null;
31647
+ const envelope = candidate;
31648
+ const mutation = envelope.mutation;
31649
+ const expectedKeys = [
31650
+ "project_ref",
31651
+ "mutation_id",
31652
+ "operation",
31653
+ "resource_key",
31654
+ "request_fingerprint",
31655
+ "principal",
31656
+ "status",
31657
+ "checkpoint",
31658
+ "receipt",
31659
+ "response_status",
31660
+ "failure_code",
31661
+ "lease",
31662
+ "completed_at",
31663
+ "created_at",
31664
+ "updated_at"
31665
+ ];
31666
+ if (!mutation || !exactKeys(envelope, ["project_ref", "mutation"]) || !exactKeys(mutation, expectedKeys) || envelope.project_ref !== expected.projectRef || mutation.project_ref !== expected.projectRef || mutation.mutation_id !== expected.mutationId || mutation.resource_key !== expected.resourceKey || mutation.request_fingerprint !== expected.requestFingerprint || typeof mutation.operation !== "string" || typeof mutation.status !== "string" || !(mutation.response_status === null || Number.isSafeInteger(mutation.response_status)) || !(mutation.failure_code === null || typeof mutation.failure_code === "string"))
31667
+ return null;
31668
+ return {
31669
+ operation: mutation.operation,
31670
+ status: mutation.status,
31671
+ responseStatus: mutation.response_status,
31672
+ failureCode: mutation.failure_code
31673
+ };
31674
+ }
31675
+ function activationReceipt(candidate, input) {
31676
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
31677
+ return null;
31678
+ const record = candidate;
31679
+ const release = releaseRecord(record.release);
31680
+ const mutation = record.mutation;
31681
+ if (!release || !mutation || !exactKeys(record, [
31682
+ "project_ref",
31683
+ "deployment_id",
31684
+ "active_release_id",
31685
+ "activation_id",
31686
+ "release",
31687
+ "mutation"
31688
+ ]) || !exactKeys(mutation, ["mutation_id", "status", "replayed"]) || record.project_ref !== input.projectRef || record.deployment_id !== input.deploymentId || record.active_release_id !== input.releaseId || record.activation_id !== input.mutationId || release.project_ref !== input.projectRef || release.deployment_id !== input.deploymentId || release.release_id !== input.releaseId || mutation.mutation_id !== input.mutationId || mutation.status !== "succeeded" || typeof mutation.replayed !== "boolean")
31689
+ return null;
31690
+ return release;
31691
+ }
31692
+ async function activeReleaseReadback(http, identity) {
31693
+ const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
31694
+ const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
31695
+ maxJsonBytes: RESPONSE_MAX_BYTES
31696
+ });
31697
+ const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
31698
+ projectRef: identity.projectRef,
31699
+ deploymentId: identity.deploymentId
31700
+ }) : null;
31701
+ if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
31702
+ return { release: null, status: inventoryRead.status };
31703
+ }
31704
+ const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
31705
+ const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
31706
+ projectRef: identity.projectRef,
31707
+ deploymentId: identity.deploymentId,
31708
+ releaseId: identity.releaseId
31709
+ }) : null;
31710
+ return { release, status: releaseRead.status };
31711
+ }
31712
+ async function activateFrontendRelease(http, input) {
31713
+ const mutationId = input.mutationId;
31714
+ if (!MUTATION_ID_PATTERN.test(mutationId))
31715
+ throw new Error("'mutation_id' must be a UUIDv4");
31716
+ if (input.expectedActiveReleaseId !== "absent" && !RELEASE_ID_PATTERN.test(input.expectedActiveReleaseId)) {
31717
+ throw new Error("'expected_active_release_id' is invalid");
31718
+ }
31719
+ if (input.expectedActivationId !== "absent" && !MUTATION_ID_PATTERN.test(input.expectedActivationId)) {
31720
+ throw new Error("'expected_activation_id' is invalid");
31721
+ }
31722
+ const endpoint = `${releasePath(input.projectRef, input.deploymentId, input.releaseId)}/activate`;
31723
+ const response = await http.post(endpoint, {
31724
+ expected_active_release_id: input.expectedActiveReleaseId,
31725
+ expected_activation_id: input.expectedActivationId,
31726
+ mutation_id: mutationId
31727
+ }, { maxJsonBytes: RESPONSE_MAX_BYTES });
31728
+ const release = activationReceipt(response.data, { ...input, mutationId });
31729
+ if (!response.ok || !release) {
31730
+ if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
31731
+ return releaseFailure("frontend.activate_release", "HTTP_ERROR", response.status);
31732
+ }
31733
+ return activationReadback(http, input, mutationId, response.status);
31734
+ }
31735
+ const readback = await activeReleaseReadback(http, { ...input, mutationId });
31736
+ if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
31737
+ return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
31738
+ }
31739
+ return toolResponse({
31740
+ project_ref: input.projectRef,
31741
+ deployment_id: input.deploymentId,
31742
+ active_release_id: input.releaseId,
31743
+ activation_id: mutationId,
31744
+ release: readback.release
31745
+ });
31746
+ }
31747
+ async function activationReadback(http, input, mutationId, activationStatus) {
31748
+ const mutationRead = await http.get(`/v1/projects/${encodeURIComponent(input.projectRef)}/mutations/${mutationId}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
31749
+ const mutationEnvelope = mutationRead.data;
31750
+ const mutation = mutationRead.ok ? publicMutationReceipt(mutationRead.data, {
31751
+ projectRef: input.projectRef,
31752
+ mutationId,
31753
+ resourceKey: activationResourceKey(input.deploymentId),
31754
+ requestFingerprint: activationFingerprint({ ...input, mutationId })
31755
+ }) : null;
31756
+ if (!mutation || mutationEnvelope?.project_ref !== input.projectRef || mutation.operation !== "frontend.release.activate" || mutation.status !== "succeeded" || mutation.responseStatus !== 200 || mutation.failureCode !== null) {
31757
+ return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
31758
+ }
31759
+ const readback = await activeReleaseReadback(http, { ...input, mutationId });
31760
+ if (!readback.release) {
31761
+ return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
31762
+ }
31763
+ return toolResponse({
31764
+ project_ref: input.projectRef,
31765
+ deployment_id: input.deploymentId,
31766
+ active_release_id: input.releaseId,
31767
+ activation_id: mutationId,
31768
+ release: readback.release
31769
+ });
31770
+ }
31771
+
31772
+ // src/shared/tools/frontend-tools.ts
31773
+ function registerFrontendTools(server, http) {
31774
+ server.tool("frontend", `Immutable prebuilt frontend release control.
31775
+ Actions: list_releases, get_release, upload_release, activate_release`, {
31776
+ action: withDescription(stringEnum([
31777
+ "list_releases",
31778
+ "get_release",
31779
+ "upload_release",
31780
+ "activate_release"
31781
+ ]), "Action"),
31782
+ ref: optional(Type.String(), "Project ref"),
31783
+ id: optional(Type.String(), "Deployment ID"),
31784
+ zip_path: optional(Type.String(), "[upload_release] Local ZIP file path"),
31785
+ release_id: optional(Type.String(), "[get_release/activate_release] SHA-256 release ID"),
31786
+ expected_active_release_id: optional(Type.String(), "[activate_release] Current release SHA-256 or absent"),
31787
+ expected_activation_id: optional(Type.String(), "[activate_release] Current activation UUIDv4 or absent"),
31788
+ mutation_id: optional(Type.String(), "[activate_release] Required retry-stable UUIDv4"),
31789
+ cursor: optional(Type.String(), "[list_releases] Last release SHA-256 cursor"),
31790
+ limit: optional(Type.Number(), "[list_releases] Page size, 1-100 (default 50)")
31791
+ }, async (args) => {
31792
+ const {
31793
+ action,
31794
+ ref,
31795
+ id,
31796
+ zip_path,
31797
+ release_id,
31798
+ expected_active_release_id,
31799
+ expected_activation_id,
31800
+ mutation_id,
31801
+ cursor,
31802
+ limit
31803
+ } = args;
31804
+ const need = (f, v) => {
31805
+ if (!v)
31806
+ throw new Error(`'${f}' required for '${action}'`);
31807
+ };
31808
+ switch (action) {
31809
+ case "list_releases":
31810
+ need("ref", ref);
31811
+ need("id", id);
31812
+ return listFrontendReleases(http, ref, id, cursor, limit);
31813
+ case "get_release":
31814
+ need("ref", ref);
31815
+ need("id", id);
31816
+ need("release_id", release_id);
31817
+ return getFrontendRelease(http, ref, id, release_id);
31818
+ case "upload_release":
31819
+ need("ref", ref);
31820
+ need("id", id);
31821
+ need("zip_path", zip_path);
31822
+ return uploadFrontendRelease(http, ref, id, zip_path);
31823
+ case "activate_release":
31824
+ need("ref", ref);
31825
+ need("id", id);
31826
+ need("release_id", release_id);
31827
+ need("expected_active_release_id", expected_active_release_id);
31828
+ need("expected_activation_id", expected_activation_id);
31829
+ need("mutation_id", mutation_id);
31830
+ return activateFrontendRelease(http, {
31831
+ projectRef: ref,
31832
+ deploymentId: id,
31833
+ releaseId: release_id,
31834
+ expectedActiveReleaseId: expected_active_release_id,
31835
+ expectedActivationId: expected_activation_id,
31836
+ mutationId: mutation_id
31837
+ });
31838
+ default:
31839
+ throw new Error("Unknown immutable frontend release action");
31840
+ }
31841
+ });
31842
+ }
31055
31843
  // package.json
31056
31844
  var package_default = {
31057
31845
  name: "@supacloud/admin",
31058
- version: "0.12.1",
31846
+ version: "0.14.1",
31059
31847
  description: "Platform administration CLI for SupaCloud operators",
31060
31848
  type: "module",
31061
31849
  main: "./dist/index.js",
@@ -31125,6 +31913,7 @@ function unavailableAdminSchemas() {
31125
31913
  project: captureTools((server) => registerAdminProjectCliTools(server, schemaOnlyHttp)).project.schema,
31126
31914
  platform: captureTools((server) => registerAdvancedTools(server, schemaOnlyHttp)).platform.schema,
31127
31915
  gateway: captureTools((server) => registerGatewayTools(server, schemaOnlyHttp)).gateway.schema,
31916
+ frontend: captureTools((server) => registerFrontendTools(server, schemaOnlyHttp)).frontend.schema,
31128
31917
  ssh: captureTools((server) => registerSshTools(server, {})).ssh.schema
31129
31918
  };
31130
31919
  }
@@ -31134,6 +31923,7 @@ function registerUnavailableAdminTools(tools) {
31134
31923
  tools.platform = unavailableTool(schemas.platform, "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
31135
31924
  tools.ssh = unavailableTool(schemas.ssh, "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT.");
31136
31925
  tools.gateway = unavailableTool(schemas.gateway, "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges).");
31926
+ tools.frontend = unavailableTool(schemas.frontend, "⚠️ Frontend commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
31137
31927
  }
31138
31928
  function printHelp(context = resolveSupaCloudContext()) {
31139
31929
  console.error(`
@@ -31190,10 +31980,16 @@ EXAMPLES
31190
31980
  supacloud-admin project runtime_snapshot --ref abc123
31191
31981
  supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
31192
31982
  supacloud-admin platform metrics
31983
+ supacloud-admin platform list_logical_backups --ref abc123
31984
+ supacloud-admin platform create_logical_backup --ref abc123
31985
+ supacloud-admin platform restore_logical_backup --ref abc123 --backup_id BACKUP_ID --expected_sha256 SHA256 --confirmation RESTORE_PROJECT:abc123:BACKUP_ID:SHA256
31193
31986
  supacloud-admin gateway routes --ref abc123
31194
31987
  supacloud-admin gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
31195
31988
  supacloud-admin gateway config --ref abc123 --rate_limit_tier pro
31196
31989
  supacloud-admin gateway rebuild --ref abc123 --clean
31990
+ supacloud-admin frontend list_releases --ref abc123 --id web
31991
+ supacloud-admin frontend upload_release --ref abc123 --id web --zip_path /secure/site.zip
31992
+ supacloud-admin frontend activate_release --ref abc123 --id web --release_id <sha256> --expected_active_release_id absent --expected_activation_id absent --mutation_id <uuid-v4>
31197
31993
  `);
31198
31994
  }
31199
31995
  function authorizedToolMap(tools, context, confirmProduction) {
@@ -31281,6 +32077,7 @@ function createAdminTools(context = resolveSupaCloudContext(), confirmProduction
31281
32077
  });
31282
32078
  Object.assign(tools, captureTools((server) => registerAdminProjectCliTools(server, http)));
31283
32079
  Object.assign(tools, captureTools((server) => registerGatewayTools(server, http)));
32080
+ Object.assign(tools, captureTools((server) => registerFrontendTools(server, http)));
31284
32081
  const advancedTools = captureTools((server) => registerAdvancedTools(server, http));
31285
32082
  if (advancedTools.platform) {
31286
32083
  tools.platform = advancedTools.platform;
@@ -31331,17 +32128,17 @@ async function main() {
31331
32128
  }
31332
32129
  const cliTools = createAdminTools(context, globalOptions.confirmProduction);
31333
32130
  if (args.length === 1 && cliTools[args[0]]) {
31334
- const toolResponse = await cliTools[args[0]].callback({});
31335
- if (toolResponse?.content && Array.isArray(toolResponse.content)) {
31336
- for (const chunk of toolResponse.content) {
32131
+ const toolResponse2 = await cliTools[args[0]].callback({});
32132
+ if (toolResponse2?.content && Array.isArray(toolResponse2.content)) {
32133
+ for (const chunk of toolResponse2.content) {
31337
32134
  if (chunk.type === "text") {
31338
32135
  console.log(chunk.text);
31339
32136
  }
31340
32137
  }
31341
32138
  } else {
31342
- console.log(JSON.stringify(toolResponse, null, 2));
32139
+ console.log(JSON.stringify(toolResponse2, null, 2));
31343
32140
  }
31344
- if (cliToolResultIsError(toolResponse))
32141
+ if (cliToolResultIsError(toolResponse2))
31345
32142
  process.exitCode = 1;
31346
32143
  return;
31347
32144
  }
@@ -31351,7 +32148,7 @@ function isDirectRun() {
31351
32148
  if (!process.argv[1])
31352
32149
  return false;
31353
32150
  try {
31354
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve3(process.argv[1]));
32151
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve4(process.argv[1]));
31355
32152
  } catch {
31356
32153
  return false;
31357
32154
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.12.1",
3
+ "version": "0.14.1",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
Binary file