@supacloud/cli 0.22.1 → 0.24.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 +36 -0
  2. package/dist/index.js +319 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -106,6 +106,42 @@ loopback development origins, with the default `:80` likewise omitted. Use
106
106
  `SUPACLOUD_PROJECT_REF` when it cannot be inferred from a managed
107
107
  `<ref>.api.*` application hostname.
108
108
 
109
+ ### Verified release controls
110
+
111
+ `release` is an official CLI entry point for the existing Management API
112
+ logical-backup and PostgREST lifecycle capabilities. It requires the Management
113
+ API context above; it does not promote an application `service_role` key to
114
+ Management authority.
115
+
116
+ ```bash
117
+ supacloud-cli release logical_backup_list --ref abc123
118
+ supacloud-cli release logical_backup_create --ref abc123
119
+ supacloud-cli release logical_backup_restore --ref abc123 \
120
+ --backup_id logical-full_abc123_<backup-id-suffix> \
121
+ --expected_sha256 <64-lowercase-hex> \
122
+ --restore_confirmation RESTORE_PROJECT:abc123:logical-full_abc123_<backup-id-suffix>:<64-lowercase-hex>
123
+ supacloud-cli release postgrest_status --ref abc123
124
+ supacloud-cli release postgrest_restart --ref abc123
125
+ ```
126
+
127
+ Backup creation reports success only after the CLI verifies exactly one new
128
+ logical-backup receipt against the inventory before and after the mutation.
129
+ Logical restore is project-scoped: pause the selected project first, obtain the
130
+ backup ID and SHA-256 from `logical_backup_list`, and supply both the normal
131
+ production `--confirm-production <ref>` value (for production profiles) and
132
+ the exact `--restore_confirmation
133
+ RESTORE_PROJECT:<ref>:<backup_id>:<sha256>`. Before POST, the CLI re-reads
134
+ that same project's inventory and binds the request to the complete verified
135
+ backup identity; it then verifies both the server receipt and a fresh inventory
136
+ read. It never retries a restore. A transport, server, or unreadable-response
137
+ failure is reported as `OUTCOME_UNKNOWN`; read the inventory and investigate
138
+ before any new restore decision.
139
+
140
+ PostgREST restart reports success only after it receives a matching restart
141
+ receipt and reads back `desired=running`, `actual=running`, and
142
+ `health=healthy`. Both mutating controls follow the normal production
143
+ confirmation and read-only protections.
144
+
109
145
  The legacy `.env` fallback is unclassified and therefore does not enable the
110
146
  production confirmation gate. Production automation must select a `prod` or
111
147
  `production` profile, or set `SUPACLOUD_ENV=production` together with a complete
package/dist/index.js CHANGED
@@ -6484,6 +6484,10 @@ var ACTION_POLICY = {
6484
6484
  },
6485
6485
  scheduled_functions: { read: ["list", "get"], write: ["create", "update", "delete"] },
6486
6486
  mutations: { read: ["status"] },
6487
+ release: {
6488
+ read: ["logical_backup_list", "postgrest_status"],
6489
+ write: ["logical_backup_create", "logical_backup_restore", "postgrest_restart"]
6490
+ },
6487
6491
  secrets: { read: ["list"], write: ["upsert", "delete"] },
6488
6492
  frontend: {
6489
6493
  read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
@@ -6568,6 +6572,7 @@ function validateExecutionPolicyCoverage(tools) {
6568
6572
 
6569
6573
  // src/shared/transports/http.ts
6570
6574
  var DEFAULT_TIMEOUT = 30000;
6575
+ var MAX_POST_TIMEOUT_MS = 36 * 60000;
6571
6576
  var RELEASE_MUTATION_RESPONSE_TIMEOUT = 5000;
6572
6577
  var RELEASE_MUTATION_RESPONSE_MAX_BYTES = 64 * 1024;
6573
6578
  var MAX_RETRIES = 2;
@@ -6581,6 +6586,29 @@ function validatedGetResponseLimit(options) {
6581
6586
  }
6582
6587
  return maxBytes;
6583
6588
  }
6589
+ function validatedJsonResponseLimit(maxBytes) {
6590
+ if (maxBytes === undefined)
6591
+ return;
6592
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
6593
+ throw new RangeError("HTTP JSON response limit must be a positive safe integer");
6594
+ }
6595
+ return maxBytes;
6596
+ }
6597
+ function validatedPostTimeout(options) {
6598
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT;
6599
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_POST_TIMEOUT_MS) {
6600
+ throw new RangeError(`HTTP request timeout must be between 1 and ${MAX_POST_TIMEOUT_MS} ms`);
6601
+ }
6602
+ return timeoutMs;
6603
+ }
6604
+ function validatedResponseTimeout(timeoutMs) {
6605
+ if (timeoutMs === undefined)
6606
+ return;
6607
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_POST_TIMEOUT_MS) {
6608
+ throw new RangeError(`HTTP response timeout must be between 1 and ${MAX_POST_TIMEOUT_MS} ms`);
6609
+ }
6610
+ return timeoutMs;
6611
+ }
6584
6612
  function isRetryableMethod(method) {
6585
6613
  const normalizedMethod = (method ?? "GET").toUpperCase();
6586
6614
  return normalizedMethod === "GET" || normalizedMethod === "HEAD";
@@ -6609,9 +6637,9 @@ function responseReadFailure(status) {
6609
6637
  responseReadError: true
6610
6638
  };
6611
6639
  }
6612
- async function fetchWithTimeout(url, options) {
6640
+ async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
6613
6641
  const controller = new AbortController;
6614
- const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
6642
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
6615
6643
  try {
6616
6644
  return await fetch(url, {
6617
6645
  ...options,
@@ -6622,11 +6650,11 @@ async function fetchWithTimeout(url, options) {
6622
6650
  clearTimeout(timeout);
6623
6651
  }
6624
6652
  }
6625
- async function fetchWithRetry(url, options) {
6653
+ async function fetchWithRetry(url, options, timeoutMs = DEFAULT_TIMEOUT) {
6626
6654
  const retries = isRetryableMethod(options.method) ? MAX_RETRIES : 0;
6627
6655
  for (let attempt = 0;attempt <= retries; attempt++) {
6628
6656
  try {
6629
- const res = await fetchWithTimeout(url, options);
6657
+ const res = await fetchWithTimeout(url, options, timeoutMs);
6630
6658
  if (res.status >= 500 && res.status < 600 && attempt < retries) {
6631
6659
  const delay = RETRY_BASE_DELAY * Math.pow(2, attempt);
6632
6660
  await new Promise((r) => setTimeout(r, delay));
@@ -6685,7 +6713,7 @@ async function responseBytesFromReader(reader, maxBytes, declaredBytes) {
6685
6713
  chunks.push(value);
6686
6714
  }
6687
6715
  }
6688
- async function responseBytesWithinLimit(response, maxBytes) {
6716
+ async function responseBytesWithinLimit(response, maxBytes, responseTimeoutMs) {
6689
6717
  if (declaredResponseTooLarge(response, maxBytes)) {
6690
6718
  await response.body?.cancel();
6691
6719
  return null;
@@ -6693,7 +6721,7 @@ async function responseBytesWithinLimit(response, maxBytes) {
6693
6721
  if (!response.body)
6694
6722
  return new Uint8Array;
6695
6723
  const reader = response.body.getReader();
6696
- const bodyRead = await responseBytesFromReader(reader, maxBytes, null);
6724
+ const bodyRead = responseTimeoutMs === undefined ? await responseBytesFromReader(reader, maxBytes, null) : await responseBytesBeforeDeadline(reader, maxBytes, null, responseTimeoutMs);
6697
6725
  return bodyRead.ok ? bodyRead.bytes : null;
6698
6726
  }
6699
6727
  function parsedUtf8Json(responseBytes) {
@@ -6706,8 +6734,8 @@ function parsedUtf8Json(responseBytes) {
6706
6734
  throw error;
6707
6735
  }
6708
6736
  }
6709
- async function boundedResponseJson(response, maxBytes) {
6710
- const responseBytes = await responseBytesWithinLimit(response, maxBytes);
6737
+ async function boundedResponseJson(response, maxBytes, responseTimeoutMs) {
6738
+ const responseBytes = await responseBytesWithinLimit(response, maxBytes, responseTimeoutMs);
6711
6739
  if (responseBytes === null)
6712
6740
  return null;
6713
6741
  const parsed = parsedUtf8Json(responseBytes);
@@ -6736,19 +6764,19 @@ async function releaseMutationResponseBytes(response) {
6736
6764
  if (!response.body) {
6737
6765
  return declaredBytes === null || declaredBytes === 0 ? { ok: true, bytes: new Uint8Array } : { ok: false };
6738
6766
  }
6739
- return responseBytesBeforeDeadline(response.body.getReader(), declaredBytes);
6767
+ return responseBytesBeforeDeadline(response.body.getReader(), RELEASE_MUTATION_RESPONSE_MAX_BYTES, declaredBytes, RELEASE_MUTATION_RESPONSE_TIMEOUT);
6740
6768
  }
6741
- async function responseBytesBeforeDeadline(reader, declaredBytes) {
6769
+ async function responseBytesBeforeDeadline(reader, maxBytes, declaredBytes, responseTimeoutMs) {
6742
6770
  let deadlineTimer;
6743
6771
  const deadline = new Promise((resolve2) => {
6744
6772
  deadlineTimer = setTimeout(() => {
6745
6773
  cancelResponseReader(reader);
6746
6774
  resolve2({ ok: false });
6747
- }, RELEASE_MUTATION_RESPONSE_TIMEOUT);
6775
+ }, responseTimeoutMs);
6748
6776
  });
6749
6777
  try {
6750
6778
  return await Promise.race([
6751
- responseBytesFromReader(reader, RELEASE_MUTATION_RESPONSE_MAX_BYTES, declaredBytes),
6779
+ responseBytesFromReader(reader, maxBytes, declaredBytes),
6752
6780
  deadline
6753
6781
  ]);
6754
6782
  } catch {
@@ -6782,13 +6810,13 @@ class HttpTransport {
6782
6810
  "Content-Type": "application/json"
6783
6811
  };
6784
6812
  }
6785
- async mutationWithResponseReader(method, path, serializedBody, responseReader) {
6813
+ async mutationWithResponseReader(method, path, serializedBody, responseReader, timeoutMs = DEFAULT_TIMEOUT) {
6786
6814
  try {
6787
6815
  const response = await fetchWithRetry(`${this.baseUrl}${path}`, {
6788
6816
  method,
6789
6817
  headers: this.headers(),
6790
6818
  body: serializedBody
6791
- });
6819
+ }, timeoutMs);
6792
6820
  const responseBody = await responseReader(response);
6793
6821
  return responseBody.ok ? { ok: response.ok, status: response.status, data: responseBody.parsedJson } : responseReadFailure(response.status);
6794
6822
  } catch (error) {
@@ -6797,26 +6825,47 @@ class HttpTransport {
6797
6825
  }
6798
6826
  async get(path, options = {}) {
6799
6827
  const maxResponseBytes = validatedGetResponseLimit(options);
6828
+ const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
6829
+ const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
6830
+ if (maxResponseBytes !== undefined && maxJsonBytes !== undefined) {
6831
+ throw new RangeError("HTTP response limit options are mutually exclusive");
6832
+ }
6800
6833
  try {
6801
6834
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
6802
6835
  method: "GET",
6803
6836
  headers: this.headers()
6804
6837
  });
6838
+ if (maxJsonBytes !== undefined) {
6839
+ const data2 = await boundedResponseJson(res, maxJsonBytes, responseTimeoutMs);
6840
+ return data2 === null ? responseReadFailure(res.status) : { ok: res.ok, status: res.status, data: data2 };
6841
+ }
6805
6842
  const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
6806
6843
  return { ok: res.ok, status: res.status, data };
6807
6844
  } catch (error) {
6808
6845
  return transportFailure(error);
6809
6846
  }
6810
6847
  }
6811
- async post(path, body) {
6848
+ async post(path, body, options) {
6849
+ const timeoutMs = validatedPostTimeout(options);
6850
+ const maxJsonBytes = validatedJsonResponseLimit(options?.maxJsonBytes);
6812
6851
  try {
6813
- return await this.mutationWithResponseReader("POST", path, serializedRequestBody(body), responseJsonOrNull);
6852
+ if (maxJsonBytes === undefined) {
6853
+ return await this.mutationWithResponseReader("POST", path, serializedRequestBody(body), responseJsonOrNull, timeoutMs);
6854
+ }
6855
+ const response = await fetchWithRetry(`${this.baseUrl}${path}`, {
6856
+ method: "POST",
6857
+ headers: this.headers(),
6858
+ body: serializedRequestBody(body)
6859
+ }, timeoutMs);
6860
+ const data = await boundedResponseJson(response, maxJsonBytes);
6861
+ return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
6814
6862
  } catch (error) {
6815
6863
  return transportFailure(error);
6816
6864
  }
6817
6865
  }
6818
- async postReleaseMutation(path, body) {
6819
- return this.mutationWithResponseReader("POST", path, serializedRequestBody(body), releaseMutationResponseJson);
6866
+ async postReleaseMutation(path, body, options) {
6867
+ const timeoutMs = validatedPostTimeout(options);
6868
+ return this.mutationWithResponseReader("POST", path, serializedRequestBody(body), releaseMutationResponseJson, timeoutMs);
6820
6869
  }
6821
6870
  async patchReleaseMutation(path, body) {
6822
6871
  return this.mutationWithResponseReader("PATCH", path, serializedRequestBody(body), releaseMutationResponseJson);
@@ -12155,10 +12204,253 @@ var MUTATION_TOOL_SCHEMA = {
12155
12204
  ref: withDescription(Type.String(), "[status] Project ref"),
12156
12205
  mutation_id: withDescription(Type.String(), "[status] Client mutation UUID")
12157
12206
  };
12207
+
12208
+ // src/shared/tools/release-tools.ts
12209
+ var SAFE_PROJECT_REF = /^[A-Za-z0-9_-]{1,64}$/;
12210
+ var BACKUP_ID = /^logical-full_[A-Za-z0-9_-]{1,64}_[a-f0-9]{32}$/;
12211
+ var SHA256 = /^[a-f0-9]{64}$/;
12212
+ var SAFE_DATABASE = /^[^\u0000-\u001f\u007f]{1,128}$/;
12213
+ var INVENTORY_MAX_BYTES = 1024 * 1024;
12214
+ var MUTATION_MAX_BYTES = 64 * 1024;
12215
+ var BACKUP_TIMEOUT_MS = 36 * 60000;
12216
+ var RELEASE_READ_RESPONSE_TIMEOUT_MS = 5000;
12217
+ function isRecord(value) {
12218
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12219
+ }
12220
+ function canonicalTimestamp3(value) {
12221
+ if (typeof value !== "string")
12222
+ return false;
12223
+ const parsed = new Date(value);
12224
+ return Number.isFinite(parsed.valueOf()) && parsed.toISOString() === value;
12225
+ }
12226
+ function validProjectRef(ref) {
12227
+ return SAFE_PROJECT_REF.test(ref);
12228
+ }
12229
+ function backupBelongsToProject(backupId, projectRef) {
12230
+ return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef}_`);
12231
+ }
12232
+ function verifiedBackup(value, projectRef) {
12233
+ if (!isRecord(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef) || value.project_ref !== projectRef || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp3(value.created_at) || !canonicalTimestamp3(value.completed_at) || new Date(value.completed_at).valueOf() < new Date(value.created_at).valueOf() || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sha256 !== "string" || !SHA256.test(value.sha256))
12234
+ return null;
12235
+ return {
12236
+ backup_id: value.backup_id,
12237
+ project_ref: projectRef,
12238
+ database: value.database,
12239
+ kind: "logical-full",
12240
+ created_at: value.created_at,
12241
+ completed_at: value.completed_at,
12242
+ bytes: value.bytes,
12243
+ sha256: value.sha256
12244
+ };
12245
+ }
12246
+ function backupInventory(value, projectRef) {
12247
+ if (!isRecord(value) || !Array.isArray(value.backups))
12248
+ return null;
12249
+ const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef));
12250
+ if (backups.some((backup) => backup === null))
12251
+ return null;
12252
+ const inventory = backups;
12253
+ return new Set(inventory.map((backup) => backup.backup_id)).size === inventory.length ? inventory : null;
12254
+ }
12255
+ function publicBackup(backup) {
12256
+ return {
12257
+ backup_id: backup.backup_id,
12258
+ project_ref: backup.project_ref,
12259
+ kind: backup.kind,
12260
+ created_at: backup.created_at,
12261
+ completed_at: backup.completed_at,
12262
+ bytes: backup.bytes,
12263
+ sha256: backup.sha256
12264
+ };
12265
+ }
12266
+ function equalBackup(left, right) {
12267
+ 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;
12268
+ }
12269
+ function newlyCreatedBackup(before, after) {
12270
+ const afterById = new Map(after.map((backup) => [backup.backup_id, backup]));
12271
+ for (const previous of before) {
12272
+ const current = afterById.get(previous.backup_id);
12273
+ if (!current || !equalBackup(previous, current))
12274
+ return null;
12275
+ }
12276
+ const known = new Set(before.map((backup) => backup.backup_id));
12277
+ const additions = after.filter((backup) => !known.has(backup.backup_id));
12278
+ return additions.length === 1 ? additions[0] : null;
12279
+ }
12280
+ function restoreRequest(projectRef, backupId, expectedSha256, restoreConfirmation) {
12281
+ if (typeof backupId !== "string" || !backupBelongsToProject(backupId, projectRef)) {
12282
+ throw new Error("'backup_id' must identify a logical-full backup for 'ref'");
12283
+ }
12284
+ if (typeof expectedSha256 !== "string" || !SHA256.test(expectedSha256)) {
12285
+ throw new Error("'expected_sha256' must be a lowercase SHA-256 digest");
12286
+ }
12287
+ const confirmation = `RESTORE_PROJECT:${projectRef}:${backupId}:${expectedSha256}`;
12288
+ if (restoreConfirmation !== confirmation) {
12289
+ throw new Error("'restore_confirmation' must exactly confirm the selected logical backup restore");
12290
+ }
12291
+ return { backup_id: backupId, expected_sha256: expectedSha256, confirmation };
12292
+ }
12293
+ function endpoint(projectRef) {
12294
+ if (!validProjectRef(projectRef))
12295
+ throw new Error("'ref' is invalid for release controls");
12296
+ return `/v1/projects/${encodeURIComponent(projectRef)}`;
12297
+ }
12298
+ function httpFailure(operation, response) {
12299
+ if (response.responseReadError) {
12300
+ return releaseControlFailure(operation, "INVALID_RESPONSE", response.status);
12301
+ }
12302
+ return releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
12303
+ }
12304
+ function mutationFailure(operation, response) {
12305
+ if (response.responseReadError || response.transportError || response.status === 408 || response.status >= 500) {
12306
+ return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
12307
+ }
12308
+ return releaseControlFailure(operation, "HTTP_ERROR", response.status);
12309
+ }
12310
+ async function readInventory(http, projectRef) {
12311
+ const response = await http.get(`${endpoint(projectRef)}/database/backups/logical`, {
12312
+ maxJsonBytes: INVENTORY_MAX_BYTES,
12313
+ responseTimeoutMs: RELEASE_READ_RESPONSE_TIMEOUT_MS
12314
+ });
12315
+ return { response, inventory: response.ok && response.status === 200 ? backupInventory(response.data, projectRef) : null };
12316
+ }
12317
+ function readInventoryFailure(operation, read) {
12318
+ if (!read.response.ok)
12319
+ return httpFailure(operation, read.response);
12320
+ if (read.response.status !== 200 || !read.inventory) {
12321
+ return releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
12322
+ }
12323
+ return null;
12324
+ }
12325
+ function postgrestStatus(value) {
12326
+ if (!isRecord(value) || value.component !== "postgrest" || !["running", "stopped"].includes(String(value.desired)) || !["running", "stopped", "starting", "error"].includes(String(value.actual)) || !["healthy", "unhealthy", "unknown"].includes(String(value.health)))
12327
+ return null;
12328
+ return {
12329
+ desired: value.desired,
12330
+ actual: value.actual,
12331
+ health: value.health
12332
+ };
12333
+ }
12334
+ async function readPostgrestStatus(http, projectRef) {
12335
+ const response = await http.get(`${endpoint(projectRef)}/services/postgrest/status`, {
12336
+ maxJsonBytes: MUTATION_MAX_BYTES,
12337
+ responseTimeoutMs: RELEASE_READ_RESPONSE_TIMEOUT_MS
12338
+ });
12339
+ return { response, status: response.ok && response.status === 200 ? postgrestStatus(response.data) : null };
12340
+ }
12341
+ function readPostgrestFailure(operation, read) {
12342
+ if (!read.response.ok)
12343
+ return httpFailure(operation, read.response);
12344
+ return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
12345
+ }
12346
+ function isRestartReceipt(value) {
12347
+ return isRecord(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
12348
+ }
12349
+ function registerReleaseTools(server, http, options = {}) {
12350
+ server.tool("release", "Verified release controls using a Management API credential. Actions: logical_backup_list, logical_backup_create, logical_backup_restore, postgrest_status, postgrest_restart", {
12351
+ action: withDescription(stringEnum([
12352
+ "logical_backup_list",
12353
+ "logical_backup_create",
12354
+ "logical_backup_restore",
12355
+ "postgrest_status",
12356
+ "postgrest_restart"
12357
+ ]), "Release control action"),
12358
+ ref: optional(Type.String(), options.projectRef ? "Optional override when not auto-linked" : "Project ref"),
12359
+ backup_id: optional(Type.String(), "[logical_backup_restore] Exact verified logical-full backup ID from the selected project inventory"),
12360
+ expected_sha256: optional(Type.String(), "[logical_backup_restore] Exact lowercase SHA-256 from the selected project inventory"),
12361
+ restore_confirmation: optional(Type.String(), "[logical_backup_restore] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation")
12362
+ }, async ({ action, ref, backup_id, expected_sha256, restore_confirmation }) => {
12363
+ const projectRef = typeof ref === "string" && ref || options.projectRef;
12364
+ if (!projectRef)
12365
+ throw new Error("'ref' is required for release controls");
12366
+ if (!validProjectRef(projectRef))
12367
+ throw new Error("'ref' is invalid for release controls");
12368
+ if (action === "logical_backup_list") {
12369
+ const read2 = await readInventory(http, projectRef);
12370
+ const failure = readInventoryFailure("release.logical_backup.list", read2);
12371
+ return failure ?? releaseControlSuccess("release.logical_backup.list", {
12372
+ project_ref: projectRef,
12373
+ backups: read2.inventory.map(publicBackup)
12374
+ });
12375
+ }
12376
+ if (action === "logical_backup_create") {
12377
+ const before = await readInventory(http, projectRef);
12378
+ const beforeFailure = readInventoryFailure("release.logical_backup.create", before);
12379
+ if (beforeFailure)
12380
+ return beforeFailure;
12381
+ const mutation2 = await http.postReleaseMutation(`${endpoint(projectRef)}/database/backups/logical`, {}, {
12382
+ timeoutMs: BACKUP_TIMEOUT_MS
12383
+ });
12384
+ const after = await readInventory(http, projectRef);
12385
+ if (!mutation2.ok || mutation2.status !== 200) {
12386
+ return mutationFailure("release.logical_backup.create", mutation2);
12387
+ }
12388
+ const responseBackup = isRecord(mutation2.data) ? verifiedBackup(mutation2.data.backup, projectRef) : null;
12389
+ const afterFailure = readInventoryFailure("release.logical_backup.create", after);
12390
+ const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
12391
+ if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
12392
+ return releaseControlFailure("release.logical_backup.create", "OUTCOME_UNKNOWN", mutation2.status);
12393
+ }
12394
+ return releaseControlSuccess("release.logical_backup.create", {
12395
+ project_ref: projectRef,
12396
+ backup: publicBackup(addedBackup)
12397
+ });
12398
+ }
12399
+ if (action === "logical_backup_restore") {
12400
+ const request = restoreRequest(projectRef, backup_id, expected_sha256, restore_confirmation);
12401
+ const before = await readInventory(http, projectRef);
12402
+ const beforeFailure = readInventoryFailure("release.logical_backup.restore", before);
12403
+ if (beforeFailure)
12404
+ return beforeFailure;
12405
+ const selectedBackup = before.inventory.find((backup) => backup.backup_id === request.backup_id && backup.sha256 === request.expected_sha256);
12406
+ if (!selectedBackup) {
12407
+ return releaseControlFailure("release.logical_backup.restore", "MUTATION_NOT_SUCCEEDED", null);
12408
+ }
12409
+ const mutation2 = await http.postReleaseMutation(`${endpoint(projectRef)}/database/backups/logical/restore`, request, { timeoutMs: BACKUP_TIMEOUT_MS });
12410
+ if (!mutation2.ok || mutation2.status !== 200) {
12411
+ return mutationFailure("release.logical_backup.restore", mutation2);
12412
+ }
12413
+ const responseBackup = isRecord(mutation2.data) ? verifiedBackup(mutation2.data.restored_backup, projectRef) : null;
12414
+ const after = await readInventory(http, projectRef);
12415
+ const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
12416
+ const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
12417
+ if (!responseBackup || !equalBackup(responseBackup, selectedBackup) || afterFailure || !restoredInventoryBackup || !equalBackup(restoredInventoryBackup, selectedBackup)) {
12418
+ return releaseControlFailure("release.logical_backup.restore", "OUTCOME_UNKNOWN", mutation2.status);
12419
+ }
12420
+ return releaseControlSuccess("release.logical_backup.restore", {
12421
+ project_ref: projectRef,
12422
+ backup: publicBackup(selectedBackup)
12423
+ });
12424
+ }
12425
+ if (action === "postgrest_status") {
12426
+ const read2 = await readPostgrestStatus(http, projectRef);
12427
+ const failure = readPostgrestFailure("release.postgrest.status", read2);
12428
+ return failure ?? releaseControlSuccess("release.postgrest.status", {
12429
+ project_ref: projectRef,
12430
+ postgrest: read2.status
12431
+ });
12432
+ }
12433
+ if (action !== "postgrest_restart")
12434
+ throw new Error("Unknown release control action");
12435
+ const mutation = await http.postReleaseMutation(`${endpoint(projectRef)}/services/postgrest/restart`);
12436
+ const read = await readPostgrestStatus(http, projectRef);
12437
+ if (!mutation.ok || mutation.status !== 200) {
12438
+ return mutationFailure("release.postgrest.restart", mutation);
12439
+ }
12440
+ const readFailure = readPostgrestFailure("release.postgrest.restart", read);
12441
+ if (!isRestartReceipt(mutation.data) || readFailure || read.status.desired !== "running" || read.status.actual !== "running" || read.status.health !== "healthy") {
12442
+ return releaseControlFailure("release.postgrest.restart", "OUTCOME_UNKNOWN", mutation.status);
12443
+ }
12444
+ return releaseControlSuccess("release.postgrest.restart", {
12445
+ project_ref: projectRef,
12446
+ postgrest: read.status
12447
+ });
12448
+ });
12449
+ }
12158
12450
  // package.json
12159
12451
  var package_default = {
12160
12452
  name: "@supacloud/cli",
12161
- version: "0.22.1",
12453
+ version: "0.24.0",
12162
12454
  description: "Project-scoped CLI for SupaCloud users",
12163
12455
  type: "module",
12164
12456
  main: "./dist/index.js",
@@ -12402,6 +12694,10 @@ EXAMPLES
12402
12694
  ${preferredCommand} project get
12403
12695
  ${preferredCommand} project logs --log_type database
12404
12696
  ${preferredCommand} project task_stats
12697
+ ${preferredCommand} release logical_backup_create --ref abc123
12698
+ ${preferredCommand} release logical_backup_restore --ref abc123 --backup_id <backup_id> --expected_sha256 <sha256> --restore_confirmation RESTORE_PROJECT:abc123:<backup_id>:<sha256>
12699
+ ${preferredCommand} release postgrest_status --ref abc123
12700
+ ${preferredCommand} release postgrest_restart --ref abc123
12405
12701
  ${preferredCommand} queue stats --queue emails
12406
12702
  ${preferredCommand} queue dlq --queue emails --limit 20
12407
12703
  ${preferredCommand} frontend list --ref abc123
@@ -12494,7 +12790,7 @@ function createCliTools(context, confirmProduction) {
12494
12790
  ]
12495
12791
  })
12496
12792
  };
12497
- for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch"]) {
12793
+ for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch", "release"]) {
12498
12794
  tools[name] = {
12499
12795
  schema: { action: genericActionSchema },
12500
12796
  callback: async () => ({
@@ -12579,6 +12875,9 @@ function createCliTools(context, confirmProduction) {
12579
12875
  readOnly: context.readOnly
12580
12876
  })));
12581
12877
  assign(captureTools((server) => registerMutationTools(server, http)));
12878
+ assign(captureTools((server) => registerReleaseTools(server, http, {
12879
+ projectRef: context.projectRef || undefined
12880
+ })));
12582
12881
  assign(captureTools((server) => registerFrontendTools(server, http)));
12583
12882
  assign(captureTools((server) => registerGatewayTools(server, http, {
12584
12883
  projectRef: context.projectRef || undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",