@supacloud/cli 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/index.js +275 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,28 @@ 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. Restore remains an admin-only operation and is not
|
|
115
|
+
exposed by this command group.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
supacloud-cli release logical_backup_list --ref abc123
|
|
119
|
+
supacloud-cli release logical_backup_create --ref abc123
|
|
120
|
+
supacloud-cli release postgrest_status --ref abc123
|
|
121
|
+
supacloud-cli release postgrest_restart --ref abc123
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Backup creation reports success only after the CLI verifies exactly one new
|
|
125
|
+
logical-backup receipt against the inventory before and after the mutation.
|
|
126
|
+
PostgREST restart reports success only after it receives a matching restart
|
|
127
|
+
receipt and reads back `desired=running`, `actual=running`, and
|
|
128
|
+
`health=healthy`. Both mutating controls follow the normal production
|
|
129
|
+
confirmation and read-only protections.
|
|
130
|
+
|
|
109
131
|
The legacy `.env` fallback is unclassified and therefore does not enable the
|
|
110
132
|
production confirmation gate. Production automation must select a `prod` or
|
|
111
133
|
`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", "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(),
|
|
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
|
-
},
|
|
6775
|
+
}, responseTimeoutMs);
|
|
6748
6776
|
});
|
|
6749
6777
|
try {
|
|
6750
6778
|
return await Promise.race([
|
|
6751
|
-
responseBytesFromReader(reader,
|
|
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
|
-
|
|
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
|
-
|
|
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,210 @@ 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 endpoint(projectRef) {
|
|
12281
|
+
if (!validProjectRef(projectRef))
|
|
12282
|
+
throw new Error("'ref' is invalid for release controls");
|
|
12283
|
+
return `/v1/projects/${encodeURIComponent(projectRef)}`;
|
|
12284
|
+
}
|
|
12285
|
+
function httpFailure(operation, response) {
|
|
12286
|
+
if (response.responseReadError) {
|
|
12287
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", response.status);
|
|
12288
|
+
}
|
|
12289
|
+
return releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
|
|
12290
|
+
}
|
|
12291
|
+
function mutationFailure(operation, response) {
|
|
12292
|
+
if (response.responseReadError || response.transportError || response.status === 408 || response.status >= 500) {
|
|
12293
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
|
|
12294
|
+
}
|
|
12295
|
+
return releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
12296
|
+
}
|
|
12297
|
+
async function readInventory(http, projectRef) {
|
|
12298
|
+
const response = await http.get(`${endpoint(projectRef)}/database/backups/logical`, {
|
|
12299
|
+
maxJsonBytes: INVENTORY_MAX_BYTES,
|
|
12300
|
+
responseTimeoutMs: RELEASE_READ_RESPONSE_TIMEOUT_MS
|
|
12301
|
+
});
|
|
12302
|
+
return { response, inventory: response.ok && response.status === 200 ? backupInventory(response.data, projectRef) : null };
|
|
12303
|
+
}
|
|
12304
|
+
function readInventoryFailure(operation, read) {
|
|
12305
|
+
if (!read.response.ok)
|
|
12306
|
+
return httpFailure(operation, read.response);
|
|
12307
|
+
if (read.response.status !== 200 || !read.inventory) {
|
|
12308
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
12309
|
+
}
|
|
12310
|
+
return null;
|
|
12311
|
+
}
|
|
12312
|
+
function postgrestStatus(value) {
|
|
12313
|
+
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)))
|
|
12314
|
+
return null;
|
|
12315
|
+
return {
|
|
12316
|
+
desired: value.desired,
|
|
12317
|
+
actual: value.actual,
|
|
12318
|
+
health: value.health
|
|
12319
|
+
};
|
|
12320
|
+
}
|
|
12321
|
+
async function readPostgrestStatus(http, projectRef) {
|
|
12322
|
+
const response = await http.get(`${endpoint(projectRef)}/services/postgrest/status`, {
|
|
12323
|
+
maxJsonBytes: MUTATION_MAX_BYTES,
|
|
12324
|
+
responseTimeoutMs: RELEASE_READ_RESPONSE_TIMEOUT_MS
|
|
12325
|
+
});
|
|
12326
|
+
return { response, status: response.ok && response.status === 200 ? postgrestStatus(response.data) : null };
|
|
12327
|
+
}
|
|
12328
|
+
function readPostgrestFailure(operation, read) {
|
|
12329
|
+
if (!read.response.ok)
|
|
12330
|
+
return httpFailure(operation, read.response);
|
|
12331
|
+
return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
12332
|
+
}
|
|
12333
|
+
function isRestartReceipt(value) {
|
|
12334
|
+
return isRecord(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
12335
|
+
}
|
|
12336
|
+
function registerReleaseTools(server, http, options = {}) {
|
|
12337
|
+
server.tool("release", "Verified release controls using a Management API credential. Actions: logical_backup_list, logical_backup_create, postgrest_status, postgrest_restart", {
|
|
12338
|
+
action: withDescription(stringEnum([
|
|
12339
|
+
"logical_backup_list",
|
|
12340
|
+
"logical_backup_create",
|
|
12341
|
+
"postgrest_status",
|
|
12342
|
+
"postgrest_restart"
|
|
12343
|
+
]), "Release control action"),
|
|
12344
|
+
ref: optional(Type.String(), options.projectRef ? "Optional override when not auto-linked" : "Project ref")
|
|
12345
|
+
}, async ({ action, ref }) => {
|
|
12346
|
+
const projectRef = typeof ref === "string" && ref || options.projectRef;
|
|
12347
|
+
if (!projectRef)
|
|
12348
|
+
throw new Error("'ref' is required for release controls");
|
|
12349
|
+
if (!validProjectRef(projectRef))
|
|
12350
|
+
throw new Error("'ref' is invalid for release controls");
|
|
12351
|
+
if (action === "logical_backup_list") {
|
|
12352
|
+
const read2 = await readInventory(http, projectRef);
|
|
12353
|
+
const failure = readInventoryFailure("release.logical_backup.list", read2);
|
|
12354
|
+
return failure ?? releaseControlSuccess("release.logical_backup.list", {
|
|
12355
|
+
project_ref: projectRef,
|
|
12356
|
+
backups: read2.inventory.map(publicBackup)
|
|
12357
|
+
});
|
|
12358
|
+
}
|
|
12359
|
+
if (action === "logical_backup_create") {
|
|
12360
|
+
const before = await readInventory(http, projectRef);
|
|
12361
|
+
const beforeFailure = readInventoryFailure("release.logical_backup.create", before);
|
|
12362
|
+
if (beforeFailure)
|
|
12363
|
+
return beforeFailure;
|
|
12364
|
+
const mutation2 = await http.postReleaseMutation(`${endpoint(projectRef)}/database/backups/logical`, {}, {
|
|
12365
|
+
timeoutMs: BACKUP_TIMEOUT_MS
|
|
12366
|
+
});
|
|
12367
|
+
const after = await readInventory(http, projectRef);
|
|
12368
|
+
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12369
|
+
return mutationFailure("release.logical_backup.create", mutation2);
|
|
12370
|
+
}
|
|
12371
|
+
const responseBackup = isRecord(mutation2.data) ? verifiedBackup(mutation2.data.backup, projectRef) : null;
|
|
12372
|
+
const afterFailure = readInventoryFailure("release.logical_backup.create", after);
|
|
12373
|
+
const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
|
|
12374
|
+
if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
|
|
12375
|
+
return releaseControlFailure("release.logical_backup.create", "OUTCOME_UNKNOWN", mutation2.status);
|
|
12376
|
+
}
|
|
12377
|
+
return releaseControlSuccess("release.logical_backup.create", {
|
|
12378
|
+
project_ref: projectRef,
|
|
12379
|
+
backup: publicBackup(addedBackup)
|
|
12380
|
+
});
|
|
12381
|
+
}
|
|
12382
|
+
if (action === "postgrest_status") {
|
|
12383
|
+
const read2 = await readPostgrestStatus(http, projectRef);
|
|
12384
|
+
const failure = readPostgrestFailure("release.postgrest.status", read2);
|
|
12385
|
+
return failure ?? releaseControlSuccess("release.postgrest.status", {
|
|
12386
|
+
project_ref: projectRef,
|
|
12387
|
+
postgrest: read2.status
|
|
12388
|
+
});
|
|
12389
|
+
}
|
|
12390
|
+
if (action !== "postgrest_restart")
|
|
12391
|
+
throw new Error("Unknown release control action");
|
|
12392
|
+
const mutation = await http.postReleaseMutation(`${endpoint(projectRef)}/services/postgrest/restart`);
|
|
12393
|
+
const read = await readPostgrestStatus(http, projectRef);
|
|
12394
|
+
if (!mutation.ok || mutation.status !== 200) {
|
|
12395
|
+
return mutationFailure("release.postgrest.restart", mutation);
|
|
12396
|
+
}
|
|
12397
|
+
const readFailure = readPostgrestFailure("release.postgrest.restart", read);
|
|
12398
|
+
if (!isRestartReceipt(mutation.data) || readFailure || read.status.desired !== "running" || read.status.actual !== "running" || read.status.health !== "healthy") {
|
|
12399
|
+
return releaseControlFailure("release.postgrest.restart", "OUTCOME_UNKNOWN", mutation.status);
|
|
12400
|
+
}
|
|
12401
|
+
return releaseControlSuccess("release.postgrest.restart", {
|
|
12402
|
+
project_ref: projectRef,
|
|
12403
|
+
postgrest: read.status
|
|
12404
|
+
});
|
|
12405
|
+
});
|
|
12406
|
+
}
|
|
12158
12407
|
// package.json
|
|
12159
12408
|
var package_default = {
|
|
12160
12409
|
name: "@supacloud/cli",
|
|
12161
|
-
version: "0.
|
|
12410
|
+
version: "0.23.0",
|
|
12162
12411
|
description: "Project-scoped CLI for SupaCloud users",
|
|
12163
12412
|
type: "module",
|
|
12164
12413
|
main: "./dist/index.js",
|
|
@@ -12402,6 +12651,9 @@ EXAMPLES
|
|
|
12402
12651
|
${preferredCommand} project get
|
|
12403
12652
|
${preferredCommand} project logs --log_type database
|
|
12404
12653
|
${preferredCommand} project task_stats
|
|
12654
|
+
${preferredCommand} release logical_backup_create --ref abc123
|
|
12655
|
+
${preferredCommand} release postgrest_status --ref abc123
|
|
12656
|
+
${preferredCommand} release postgrest_restart --ref abc123
|
|
12405
12657
|
${preferredCommand} queue stats --queue emails
|
|
12406
12658
|
${preferredCommand} queue dlq --queue emails --limit 20
|
|
12407
12659
|
${preferredCommand} frontend list --ref abc123
|
|
@@ -12494,7 +12746,7 @@ function createCliTools(context, confirmProduction) {
|
|
|
12494
12746
|
]
|
|
12495
12747
|
})
|
|
12496
12748
|
};
|
|
12497
|
-
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch"]) {
|
|
12749
|
+
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch", "release"]) {
|
|
12498
12750
|
tools[name] = {
|
|
12499
12751
|
schema: { action: genericActionSchema },
|
|
12500
12752
|
callback: async () => ({
|
|
@@ -12579,6 +12831,9 @@ function createCliTools(context, confirmProduction) {
|
|
|
12579
12831
|
readOnly: context.readOnly
|
|
12580
12832
|
})));
|
|
12581
12833
|
assign(captureTools((server) => registerMutationTools(server, http)));
|
|
12834
|
+
assign(captureTools((server) => registerReleaseTools(server, http, {
|
|
12835
|
+
projectRef: context.projectRef || undefined
|
|
12836
|
+
})));
|
|
12582
12837
|
assign(captureTools((server) => registerFrontendTools(server, http)));
|
|
12583
12838
|
assign(captureTools((server) => registerGatewayTools(server, http, {
|
|
12584
12839
|
projectRef: context.projectRef || undefined
|