@supacloud/admin 0.12.0 → 0.14.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 +68 -1
- package/dist/index.js +838 -33
- package/dist/sshcrypto-8m50vnmb.node +0 -0
- package/package.json +1 -1
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
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
|
|
@@ -89,7 +113,9 @@ npx @supacloud/admin project service_control --ref abc123 --service gotrue --ser
|
|
|
89
113
|
Each component reports `status` as `ok`, `unknown`, or `error`; a failed probe
|
|
90
114
|
never substitutes a guessed version. Binary evidence is bound to the active
|
|
91
115
|
systemd `ExecStart`, and Web Console evidence comes from its component marker
|
|
92
|
-
plus an explicit `tree_sha256` digest.
|
|
116
|
+
plus an explicit `tree_sha256` digest. An `unknown` component remains a valid
|
|
117
|
+
inventory result, while any `error` component makes the CLI exit non-zero after
|
|
118
|
+
printing the structured report.
|
|
93
119
|
|
|
94
120
|
## Verified platform upgrades
|
|
95
121
|
|
|
@@ -247,6 +273,47 @@ The JSON receipt contains only the project ref, requested type, backup ID,
|
|
|
247
273
|
database, timestamps, and size. `platform list_backups --ref abc123` applies the
|
|
248
274
|
same strict, sanitized inventory validation without creating a backup.
|
|
249
275
|
|
|
276
|
+
## Verified logical backups
|
|
277
|
+
|
|
278
|
+
Use the official Admin CLI to list or create project-scoped verified logical
|
|
279
|
+
backups:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
supacloud-admin platform list_logical_backups --ref abc123
|
|
283
|
+
supacloud-admin platform create_logical_backup --ref abc123
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The create command reads the verified inventory before and after the one-shot
|
|
287
|
+
request. It reports success only when the Management response and exactly one
|
|
288
|
+
new inventory identity agree on the project, database, backup ID, kind,
|
|
289
|
+
timestamps, byte count, and SHA-256. The fixed JSON projection never includes
|
|
290
|
+
the server's receipt HMAC, archive path, subprocess output, or response-only
|
|
291
|
+
fields. Do not retry an `OUTCOME_UNKNOWN` create until the inventory has been
|
|
292
|
+
reconciled.
|
|
293
|
+
|
|
294
|
+
Logical restore is destructive and Management also requires the project to be
|
|
295
|
+
paused. Copy an exact identity from `list_logical_backups`, then bind all of its
|
|
296
|
+
recovery-critical fields:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
supacloud-admin platform restore_logical_backup \
|
|
300
|
+
--ref abc123 \
|
|
301
|
+
--backup_id logical-full_abc123_0123456789abcdef0123456789abcdef \
|
|
302
|
+
--expected_sha256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
|
|
303
|
+
--confirmation RESTORE_PROJECT:abc123:logical-full_abc123_0123456789abcdef0123456789abcdef:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Both create and restore are classified as remote writes. They require an
|
|
307
|
+
explicit `SUPACLOUD_ENV`, honor `SUPACLOUD_READ_ONLY=true`, and in production
|
|
308
|
+
also require the global exact project confirmation
|
|
309
|
+
`--confirm-production abc123`. A timeout, transport loss, 5xx response, or
|
|
310
|
+
malformed success receipt exits non-zero without reflecting the remote body.
|
|
311
|
+
Logical mutation requests have a bounded 36-minute client deadline so the
|
|
312
|
+
Management request has time to return a terminal response before the client
|
|
313
|
+
gives up.
|
|
314
|
+
Credentials remain environment-only; logical backup commands accept no token,
|
|
315
|
+
password, or service-role flags.
|
|
316
|
+
|
|
250
317
|
Gateway / Caddy commands (config is injected via the Caddy JSON Admin API; requires admin privileges):
|
|
251
318
|
|
|
252
319
|
- `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-
|
|
4762
|
+
module.exports = __require("./sshcrypto-8m50vnmb.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:
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
26216
|
-
return
|
|
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
|
}
|
|
@@ -28724,6 +28776,13 @@ async function platformVersions(ssh) {
|
|
|
28724
28776
|
}
|
|
28725
28777
|
};
|
|
28726
28778
|
}
|
|
28779
|
+
function platformVersionsToolResult(report) {
|
|
28780
|
+
const isError = Object.values(report.components).some((component) => component.status === "error");
|
|
28781
|
+
return {
|
|
28782
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
|
|
28783
|
+
...isError ? { isError: true } : {}
|
|
28784
|
+
};
|
|
28785
|
+
}
|
|
28727
28786
|
function registerSshTools(server, ssh) {
|
|
28728
28787
|
server.tool("ssh", `Server management via SSH. Available before & after SupaCloud installation.
|
|
28729
28788
|
Actions: ping, setup, install, upgrade, versions, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
@@ -28910,8 +28969,7 @@ ${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
|
|
|
28910
28969
|
break;
|
|
28911
28970
|
}
|
|
28912
28971
|
case "versions": {
|
|
28913
|
-
|
|
28914
|
-
break;
|
|
28972
|
+
return platformVersionsToolResult(await platformVersions(ssh));
|
|
28915
28973
|
}
|
|
28916
28974
|
case "diagnose": {
|
|
28917
28975
|
const cmds = [
|
|
@@ -29218,6 +29276,238 @@ async function createFullPhysicalBackup(http, projectRef) {
|
|
|
29218
29276
|
return reconciledCreationResponse(projectRef, beforeRead.inventory, creation, afterRead);
|
|
29219
29277
|
}
|
|
29220
29278
|
|
|
29279
|
+
// src/shared/tools/logical-backup-control.ts
|
|
29280
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
29281
|
+
var SAFE_PROJECT_REF2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
29282
|
+
var BACKUP_ID = /^logical-full_[A-Za-z0-9_-]{1,64}_[a-f0-9]{32}$/;
|
|
29283
|
+
var BACKUP_ID_SUFFIX = /^[a-f0-9]{32}$/;
|
|
29284
|
+
var SHA256 = /^[a-f0-9]{64}$/;
|
|
29285
|
+
var SAFE_DATABASE = /^[^\u0000-\u001f\u007f]{1,128}$/;
|
|
29286
|
+
var LOGICAL_BACKUP_REQUEST_TIMEOUT_MS = 36 * 60000;
|
|
29287
|
+
var LOGICAL_BACKUP_INVENTORY_MAX_BYTES = 1024 * 1024;
|
|
29288
|
+
var LOGICAL_BACKUP_MUTATION_MAX_BYTES = 64 * 1024;
|
|
29289
|
+
function logicalBackupResponse(payload) {
|
|
29290
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
29291
|
+
}
|
|
29292
|
+
function logicalBackupSuccess(operation, payload) {
|
|
29293
|
+
return logicalBackupResponse({
|
|
29294
|
+
...payload,
|
|
29295
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
29296
|
+
ok: true,
|
|
29297
|
+
operation
|
|
29298
|
+
});
|
|
29299
|
+
}
|
|
29300
|
+
function logicalBackupFailure(operation, code, httpStatus) {
|
|
29301
|
+
return {
|
|
29302
|
+
...logicalBackupResponse({
|
|
29303
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
29304
|
+
ok: false,
|
|
29305
|
+
operation,
|
|
29306
|
+
error: { code, http_status: httpStatus }
|
|
29307
|
+
}),
|
|
29308
|
+
isError: true
|
|
29309
|
+
};
|
|
29310
|
+
}
|
|
29311
|
+
function isPlainRecord(candidate) {
|
|
29312
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
29313
|
+
return false;
|
|
29314
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
29315
|
+
return prototype === Object.prototype || prototype === null;
|
|
29316
|
+
}
|
|
29317
|
+
function canonicalTimestamp(candidate) {
|
|
29318
|
+
if (typeof candidate !== "string")
|
|
29319
|
+
return false;
|
|
29320
|
+
const timestamp = new Date(candidate);
|
|
29321
|
+
return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === candidate;
|
|
29322
|
+
}
|
|
29323
|
+
function backupBelongsToProject(backupId, projectRef) {
|
|
29324
|
+
const prefix = `logical-full_${projectRef}_`;
|
|
29325
|
+
return BACKUP_ID.test(backupId) && backupId.startsWith(prefix) && BACKUP_ID_SUFFIX.test(backupId.slice(prefix.length));
|
|
29326
|
+
}
|
|
29327
|
+
function validBackupProject(candidate, projectRef) {
|
|
29328
|
+
return typeof candidate.backup_id === "string" && backupBelongsToProject(candidate.backup_id, projectRef) && candidate.project_ref === projectRef;
|
|
29329
|
+
}
|
|
29330
|
+
function validBackupDatabase(candidate) {
|
|
29331
|
+
return typeof candidate.database === "string" && SAFE_DATABASE.test(candidate.database);
|
|
29332
|
+
}
|
|
29333
|
+
function validBackupTimestamps(candidate) {
|
|
29334
|
+
return canonicalTimestamp(candidate.created_at) && canonicalTimestamp(candidate.completed_at) && new Date(candidate.completed_at).valueOf() >= new Date(candidate.created_at).valueOf();
|
|
29335
|
+
}
|
|
29336
|
+
function validBackupEvidence(candidate) {
|
|
29337
|
+
return Number.isSafeInteger(candidate.bytes) && Number(candidate.bytes) > 0 && typeof candidate.sha256 === "string" && SHA256.test(candidate.sha256);
|
|
29338
|
+
}
|
|
29339
|
+
function logicalBackupIdentity(candidate, projectRef) {
|
|
29340
|
+
if (!isPlainRecord(candidate))
|
|
29341
|
+
return null;
|
|
29342
|
+
if (!validBackupProject(candidate, projectRef) || !validBackupDatabase(candidate) || candidate.kind !== "logical-full" || !validBackupTimestamps(candidate) || !validBackupEvidence(candidate)) {
|
|
29343
|
+
return null;
|
|
29344
|
+
}
|
|
29345
|
+
return {
|
|
29346
|
+
backup_id: candidate.backup_id,
|
|
29347
|
+
project_ref: projectRef,
|
|
29348
|
+
database: candidate.database,
|
|
29349
|
+
kind: "logical-full",
|
|
29350
|
+
created_at: candidate.created_at,
|
|
29351
|
+
completed_at: candidate.completed_at,
|
|
29352
|
+
bytes: Number(candidate.bytes),
|
|
29353
|
+
sha256: candidate.sha256
|
|
29354
|
+
};
|
|
29355
|
+
}
|
|
29356
|
+
function logicalBackupInventory(payload, projectRef) {
|
|
29357
|
+
if (!isPlainRecord(payload) || !Array.isArray(payload.backups))
|
|
29358
|
+
return null;
|
|
29359
|
+
const backups = payload.backups.map((candidate) => logicalBackupIdentity(candidate, projectRef));
|
|
29360
|
+
if (backups.some((backup) => backup === null))
|
|
29361
|
+
return null;
|
|
29362
|
+
const inventory = backups;
|
|
29363
|
+
if (new Set(inventory.map((backup) => backup.backup_id)).size !== inventory.length)
|
|
29364
|
+
return null;
|
|
29365
|
+
return inventory;
|
|
29366
|
+
}
|
|
29367
|
+
function assertProjectRef(projectRef) {
|
|
29368
|
+
if (!SAFE_PROJECT_REF2.test(projectRef)) {
|
|
29369
|
+
throw new Error("'ref' is invalid for verified logical backup");
|
|
29370
|
+
}
|
|
29371
|
+
}
|
|
29372
|
+
function logicalBackupEndpoint(projectRef) {
|
|
29373
|
+
assertProjectRef(projectRef);
|
|
29374
|
+
return `/v1/projects/${encodeURIComponent(projectRef)}/database/backups/logical`;
|
|
29375
|
+
}
|
|
29376
|
+
async function readLogicalBackupInventory(http, projectRef) {
|
|
29377
|
+
const response = await http.get(logicalBackupEndpoint(projectRef), {
|
|
29378
|
+
maxJsonBytes: LOGICAL_BACKUP_INVENTORY_MAX_BYTES
|
|
29379
|
+
});
|
|
29380
|
+
return {
|
|
29381
|
+
response,
|
|
29382
|
+
inventory: response.ok ? logicalBackupInventory(response.data, projectRef) ?? undefined : undefined
|
|
29383
|
+
};
|
|
29384
|
+
}
|
|
29385
|
+
function mutationFailureCode2(response) {
|
|
29386
|
+
return response.transportError || response.responseError || response.status === 408 || response.status >= 500 ? "OUTCOME_UNKNOWN" : "HTTP_ERROR";
|
|
29387
|
+
}
|
|
29388
|
+
function identicalBackup(left, right) {
|
|
29389
|
+
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;
|
|
29390
|
+
}
|
|
29391
|
+
function createdBackup(previous, current) {
|
|
29392
|
+
const currentById = new Map(current.map((backup) => [backup.backup_id, backup]));
|
|
29393
|
+
const previousIds = new Set;
|
|
29394
|
+
for (const previousBackup of previous) {
|
|
29395
|
+
previousIds.add(previousBackup.backup_id);
|
|
29396
|
+
const currentBackup = currentById.get(previousBackup.backup_id);
|
|
29397
|
+
if (!currentBackup || !identicalBackup(previousBackup, currentBackup))
|
|
29398
|
+
return null;
|
|
29399
|
+
}
|
|
29400
|
+
const additions = current.filter((backup) => !previousIds.has(backup.backup_id));
|
|
29401
|
+
return additions.length === 1 ? additions[0] : null;
|
|
29402
|
+
}
|
|
29403
|
+
function exactBackupEnvelope(payload, field, projectRef) {
|
|
29404
|
+
return isPlainRecord(payload) ? logicalBackupIdentity(payload[field], projectRef) : null;
|
|
29405
|
+
}
|
|
29406
|
+
function inventoryReadFailure(operation, read) {
|
|
29407
|
+
const { response, inventory } = read;
|
|
29408
|
+
if (!response.ok || response.status !== 200) {
|
|
29409
|
+
const code = response.ok || response.responseError ? "INVALID_RESPONSE" : "HTTP_ERROR";
|
|
29410
|
+
const status = response.transportError ? null : response.status;
|
|
29411
|
+
return logicalBackupFailure(operation, code, status);
|
|
29412
|
+
}
|
|
29413
|
+
return inventory ? null : logicalBackupFailure(operation, "INVALID_RESPONSE", response.status);
|
|
29414
|
+
}
|
|
29415
|
+
function createReceipt(projectRef, previous, mutation, afterRead) {
|
|
29416
|
+
const operation = "platform.create_logical_backup";
|
|
29417
|
+
if (!mutation.ok) {
|
|
29418
|
+
const status = mutation.transportError ? null : mutation.status;
|
|
29419
|
+
return logicalBackupFailure(operation, mutationFailureCode2(mutation), status);
|
|
29420
|
+
}
|
|
29421
|
+
if (mutation.status !== 200)
|
|
29422
|
+
return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
|
|
29423
|
+
const responseBackup = exactBackupEnvelope(mutation.data, "backup", projectRef);
|
|
29424
|
+
const addedBackup = afterRead.response.ok && afterRead.response.status === 200 && afterRead.inventory ? createdBackup(previous, afterRead.inventory) : null;
|
|
29425
|
+
if (!responseBackup || !addedBackup || !identicalBackup(responseBackup, addedBackup)) {
|
|
29426
|
+
return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
|
|
29427
|
+
}
|
|
29428
|
+
return logicalBackupSuccess(operation, { project_ref: projectRef, backup: addedBackup });
|
|
29429
|
+
}
|
|
29430
|
+
async function listVerifiedLogicalBackups(http, projectRef) {
|
|
29431
|
+
const operation = "platform.list_logical_backups";
|
|
29432
|
+
const read = await readLogicalBackupInventory(http, projectRef);
|
|
29433
|
+
const failure = inventoryReadFailure(operation, read);
|
|
29434
|
+
return failure ?? logicalBackupSuccess(operation, {
|
|
29435
|
+
project_ref: projectRef,
|
|
29436
|
+
backups: read.inventory
|
|
29437
|
+
});
|
|
29438
|
+
}
|
|
29439
|
+
async function createVerifiedLogicalBackup(http, projectRef) {
|
|
29440
|
+
const operation = "platform.create_logical_backup";
|
|
29441
|
+
const beforeRead = await readLogicalBackupInventory(http, projectRef);
|
|
29442
|
+
const failure = inventoryReadFailure(operation, beforeRead);
|
|
29443
|
+
if (failure)
|
|
29444
|
+
return failure;
|
|
29445
|
+
const mutation = await http.post(logicalBackupEndpoint(projectRef), {}, {
|
|
29446
|
+
timeoutMs: LOGICAL_BACKUP_REQUEST_TIMEOUT_MS,
|
|
29447
|
+
maxJsonBytes: LOGICAL_BACKUP_MUTATION_MAX_BYTES
|
|
29448
|
+
});
|
|
29449
|
+
const afterRead = await readLogicalBackupInventory(http, projectRef);
|
|
29450
|
+
return createReceipt(projectRef, beforeRead.inventory, mutation, afterRead);
|
|
29451
|
+
}
|
|
29452
|
+
function expectedRestoreConfirmation(request) {
|
|
29453
|
+
return [
|
|
29454
|
+
"RESTORE_PROJECT",
|
|
29455
|
+
request.projectRef,
|
|
29456
|
+
request.backupId,
|
|
29457
|
+
request.expectedSha256
|
|
29458
|
+
].join(":");
|
|
29459
|
+
}
|
|
29460
|
+
function assertRestoreRequest(request) {
|
|
29461
|
+
assertProjectRef(request.projectRef);
|
|
29462
|
+
if (!backupBelongsToProject(request.backupId, request.projectRef)) {
|
|
29463
|
+
throw new Error("'backup_id' must belong to the requested project");
|
|
29464
|
+
}
|
|
29465
|
+
if (!SHA256.test(request.expectedSha256)) {
|
|
29466
|
+
throw new Error("'expected_sha256' must be exactly 64 lowercase hexadecimal characters");
|
|
29467
|
+
}
|
|
29468
|
+
if (request.confirmation !== expectedRestoreConfirmation(request)) {
|
|
29469
|
+
throw new Error("'confirmation' must exactly bind the project ref, backup ID, and SHA-256");
|
|
29470
|
+
}
|
|
29471
|
+
}
|
|
29472
|
+
function restoreReceipt(request, requestedBackup, mutation) {
|
|
29473
|
+
const operation = "platform.restore_logical_backup";
|
|
29474
|
+
if (!mutation.ok) {
|
|
29475
|
+
const status = mutation.transportError ? null : mutation.status;
|
|
29476
|
+
return logicalBackupFailure(operation, mutationFailureCode2(mutation), status);
|
|
29477
|
+
}
|
|
29478
|
+
if (mutation.status !== 200)
|
|
29479
|
+
return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
|
|
29480
|
+
const restoredBackup = exactBackupEnvelope(mutation.data, "restored_backup", request.projectRef);
|
|
29481
|
+
if (!restoredBackup || !identicalBackup(restoredBackup, requestedBackup)) {
|
|
29482
|
+
return logicalBackupFailure(operation, "OUTCOME_UNKNOWN", mutation.status);
|
|
29483
|
+
}
|
|
29484
|
+
return logicalBackupSuccess(operation, {
|
|
29485
|
+
project_ref: request.projectRef,
|
|
29486
|
+
restored_backup: restoredBackup
|
|
29487
|
+
});
|
|
29488
|
+
}
|
|
29489
|
+
async function restoreVerifiedLogicalBackup(http, request) {
|
|
29490
|
+
const operation = "platform.restore_logical_backup";
|
|
29491
|
+
assertRestoreRequest(request);
|
|
29492
|
+
const beforeRead = await readLogicalBackupInventory(http, request.projectRef);
|
|
29493
|
+
const failure = inventoryReadFailure(operation, beforeRead);
|
|
29494
|
+
if (failure)
|
|
29495
|
+
return failure;
|
|
29496
|
+
const requestedBackup = beforeRead.inventory.find((backup) => backup.backup_id === request.backupId);
|
|
29497
|
+
if (!requestedBackup || requestedBackup.sha256 !== request.expectedSha256) {
|
|
29498
|
+
return logicalBackupFailure(operation, "INVALID_RESPONSE", beforeRead.response.status);
|
|
29499
|
+
}
|
|
29500
|
+
const mutation = await http.post(`${logicalBackupEndpoint(request.projectRef)}/restore`, {
|
|
29501
|
+
backup_id: request.backupId,
|
|
29502
|
+
expected_sha256: request.expectedSha256,
|
|
29503
|
+
confirmation: request.confirmation
|
|
29504
|
+
}, {
|
|
29505
|
+
timeoutMs: LOGICAL_BACKUP_REQUEST_TIMEOUT_MS,
|
|
29506
|
+
maxJsonBytes: LOGICAL_BACKUP_MUTATION_MAX_BYTES
|
|
29507
|
+
});
|
|
29508
|
+
return restoreReceipt(request, requestedBackup, mutation);
|
|
29509
|
+
}
|
|
29510
|
+
|
|
29221
29511
|
// src/shared/tools/advanced-tools.ts
|
|
29222
29512
|
function registerAdvancedTools(server, http) {
|
|
29223
29513
|
server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
|
|
@@ -29361,11 +29651,14 @@ Actions: list, upsert, delete`, {
|
|
|
29361
29651
|
return { content: [{ type: "text", text }] };
|
|
29362
29652
|
});
|
|
29363
29653
|
server.tool("platform", `Platform monitoring, backups, network, and organizations.
|
|
29364
|
-
Actions: metrics, list_backups, create_backup, network, update_network, list_orgs, get_org`, {
|
|
29654
|
+
Actions: metrics, list_backups, create_backup, list_logical_backups, create_logical_backup, restore_logical_backup, network, update_network, list_orgs, get_org`, {
|
|
29365
29655
|
action: withDescription(stringEnum([
|
|
29366
29656
|
"metrics",
|
|
29367
29657
|
"list_backups",
|
|
29368
29658
|
"create_backup",
|
|
29659
|
+
"list_logical_backups",
|
|
29660
|
+
"create_logical_backup",
|
|
29661
|
+
"restore_logical_backup",
|
|
29369
29662
|
"network",
|
|
29370
29663
|
"update_network",
|
|
29371
29664
|
"list_orgs",
|
|
@@ -29373,10 +29666,22 @@ Actions: metrics, list_backups, create_backup, network, update_network, list_org
|
|
|
29373
29666
|
]), "Action"),
|
|
29374
29667
|
ref: optional(Type.String(), "[*] Project ref for project-scoped platform actions"),
|
|
29375
29668
|
backup_type: optional(stringEnum(["full"]), "[create_backup] Explicit physical backup type"),
|
|
29669
|
+
backup_id: optional(Type.String(), "[restore_logical_backup] Verified logical backup ID"),
|
|
29670
|
+
expected_sha256: optional(Type.String(), "[restore_logical_backup] Exact lowercase backup SHA-256"),
|
|
29671
|
+
confirmation: optional(Type.String(), "[restore_logical_backup] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation"),
|
|
29376
29672
|
slug: optional(Type.String(), "[get_org] Organization slug"),
|
|
29377
29673
|
allowed_cidrs: optional(Type.Array(Type.String()), "[update_network] Allowed CIDRs")
|
|
29378
29674
|
}, async (args) => {
|
|
29379
|
-
const {
|
|
29675
|
+
const {
|
|
29676
|
+
action,
|
|
29677
|
+
ref,
|
|
29678
|
+
backup_type,
|
|
29679
|
+
backup_id,
|
|
29680
|
+
expected_sha256,
|
|
29681
|
+
confirmation,
|
|
29682
|
+
slug,
|
|
29683
|
+
allowed_cidrs
|
|
29684
|
+
} = args;
|
|
29380
29685
|
const need = (f, v) => {
|
|
29381
29686
|
if (!v)
|
|
29382
29687
|
throw new Error(`'${f}' required for '${action}'`);
|
|
@@ -29394,6 +29699,23 @@ Actions: metrics, list_backups, create_backup, network, update_network, list_org
|
|
|
29394
29699
|
need("backup_type", backup_type);
|
|
29395
29700
|
return createFullPhysicalBackup(http, ref);
|
|
29396
29701
|
}
|
|
29702
|
+
case "list_logical_backups":
|
|
29703
|
+
need("ref", ref);
|
|
29704
|
+
return listVerifiedLogicalBackups(http, ref);
|
|
29705
|
+
case "create_logical_backup":
|
|
29706
|
+
need("ref", ref);
|
|
29707
|
+
return createVerifiedLogicalBackup(http, ref);
|
|
29708
|
+
case "restore_logical_backup":
|
|
29709
|
+
need("ref", ref);
|
|
29710
|
+
need("backup_id", backup_id);
|
|
29711
|
+
need("expected_sha256", expected_sha256);
|
|
29712
|
+
need("confirmation", confirmation);
|
|
29713
|
+
return restoreVerifiedLogicalBackup(http, {
|
|
29714
|
+
projectRef: ref,
|
|
29715
|
+
backupId: backup_id,
|
|
29716
|
+
expectedSha256: expected_sha256,
|
|
29717
|
+
confirmation
|
|
29718
|
+
});
|
|
29397
29719
|
case "network":
|
|
29398
29720
|
need("ref", ref);
|
|
29399
29721
|
text = JSON.stringify((await http.get(`/v1/projects/${ref}/network-restrictions`)).data, null, 2);
|
|
@@ -29898,7 +30220,7 @@ function matchingText(candidate, maxLength, pattern) {
|
|
|
29898
30220
|
const candidateText = boundedText(candidate, maxLength);
|
|
29899
30221
|
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
29900
30222
|
}
|
|
29901
|
-
function
|
|
30223
|
+
function canonicalTimestamp2(candidate) {
|
|
29902
30224
|
const timestamp = boundedText(candidate, 64);
|
|
29903
30225
|
if (!timestamp)
|
|
29904
30226
|
return null;
|
|
@@ -29913,7 +30235,7 @@ function projectedSummary(project) {
|
|
|
29913
30235
|
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29914
30236
|
name: boundedText(project.name, 100),
|
|
29915
30237
|
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
29916
|
-
created_at:
|
|
30238
|
+
created_at: canonicalTimestamp2(project.created_at),
|
|
29917
30239
|
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
29918
30240
|
};
|
|
29919
30241
|
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
@@ -30057,7 +30379,7 @@ function projectGetRead(response, expectedRef) {
|
|
|
30057
30379
|
// src/shared/tools/project-runtime-snapshot.ts
|
|
30058
30380
|
var RUNTIME_SNAPSHOT_SCHEMA = "supacloud.runtime-snapshot.v1";
|
|
30059
30381
|
var ATTESTED_REVISION_PATTERN = /^hmac-sha256:[a-f0-9]{64}$/;
|
|
30060
|
-
var
|
|
30382
|
+
var SAFE_PROJECT_REF3 = /^[a-z0-9-]{1,20}$/;
|
|
30061
30383
|
var SNAPSHOT_KEYS = ["schema", "project_ref", "captured_at", "secrets", "postgrest"];
|
|
30062
30384
|
var SECRETS_KEYS = [
|
|
30063
30385
|
"desired_revision",
|
|
@@ -30194,7 +30516,7 @@ function hasCausalLoadTimestamps(snapshot) {
|
|
|
30194
30516
|
return [snapshot.secrets.loaded_at, snapshot.postgrest.loaded_at].every((loadedAt) => loadedAt === null || Date.parse(loadedAt) <= capturedAt);
|
|
30195
30517
|
}
|
|
30196
30518
|
function parseProjectRuntimeSnapshot(payload, requestedProjectRef2) {
|
|
30197
|
-
if (!
|
|
30519
|
+
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))
|
|
30198
30520
|
return null;
|
|
30199
30521
|
const snapshot = payload;
|
|
30200
30522
|
return hasCausalLoadTimestamps(snapshot) ? sanitizedSnapshot(snapshot) : null;
|
|
@@ -30231,11 +30553,11 @@ var STUDIO_PROJECT_SERVICE_STATUSES = [
|
|
|
30231
30553
|
];
|
|
30232
30554
|
var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
|
|
30233
30555
|
var AUTH_SERVICE_HOST_SUFFIX = "-auth";
|
|
30234
|
-
var
|
|
30556
|
+
var SAFE_PROJECT_REF4 = /^[a-z0-9-]{1,20}$/;
|
|
30235
30557
|
var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
|
|
30236
30558
|
var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
|
|
30237
30559
|
var MAX_RUNTIME_SNAPSHOT_BYTES = 64 * 1024;
|
|
30238
|
-
var
|
|
30560
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA2 = "supacloud.cli.release-control.v1";
|
|
30239
30561
|
var PROJECT_CREATE_OPERATION = "project.create";
|
|
30240
30562
|
var PROJECT_ENVIRONMENTS = ["test", "production"];
|
|
30241
30563
|
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -30263,7 +30585,7 @@ function failedProjectServiceResponse(message) {
|
|
|
30263
30585
|
function projectCreateReceipt(payload) {
|
|
30264
30586
|
return {
|
|
30265
30587
|
content: [{ type: "text", text: JSON.stringify({
|
|
30266
|
-
schema:
|
|
30588
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA2,
|
|
30267
30589
|
...payload
|
|
30268
30590
|
}, null, 2) }]
|
|
30269
30591
|
};
|
|
@@ -30448,7 +30770,7 @@ function projectServiceStatusOutput(status) {
|
|
|
30448
30770
|
function projectServicesResponse(projectRef, response) {
|
|
30449
30771
|
if (!response.ok)
|
|
30450
30772
|
return failedProjectServiceHttpResponse(response);
|
|
30451
|
-
if (!
|
|
30773
|
+
if (!SAFE_PROJECT_REF4.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
|
|
30452
30774
|
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
30453
30775
|
}
|
|
30454
30776
|
if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
|
|
@@ -31046,10 +31368,484 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
31046
31368
|
return { content: [{ type: "text", text }] };
|
|
31047
31369
|
});
|
|
31048
31370
|
}
|
|
31371
|
+
|
|
31372
|
+
// src/shared/tools/frontend-release-control.ts
|
|
31373
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
31374
|
+
import { constants as fsConstants2 } from "node:fs";
|
|
31375
|
+
import { open as open2 } from "node:fs/promises";
|
|
31376
|
+
import { resolve as resolve3 } from "node:path";
|
|
31377
|
+
var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,20}$/u;
|
|
31378
|
+
var DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
31379
|
+
var RELEASE_ID_PATTERN = /^[0-9a-f]{64}$/u;
|
|
31380
|
+
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;
|
|
31381
|
+
var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
31382
|
+
var ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
|
|
31383
|
+
var ARCHIVE_CHUNK_BYTES = 64 * 1024;
|
|
31384
|
+
var RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
31385
|
+
var UPLOAD_REQUEST_TIMEOUT_MS = 10 * 60000;
|
|
31386
|
+
var RELEASE_LIST_LIMIT_MAX = 100;
|
|
31387
|
+
function toolResponse(payload) {
|
|
31388
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
31389
|
+
}
|
|
31390
|
+
function releaseFailure(operation, code, status) {
|
|
31391
|
+
return {
|
|
31392
|
+
isError: true,
|
|
31393
|
+
content: [{
|
|
31394
|
+
type: "text",
|
|
31395
|
+
text: JSON.stringify({ ok: false, operation, error: { code, http_status: status } })
|
|
31396
|
+
}]
|
|
31397
|
+
};
|
|
31398
|
+
}
|
|
31399
|
+
function exactKeys(candidate, keys) {
|
|
31400
|
+
const actual = Object.keys(candidate).sort();
|
|
31401
|
+
const expected = [...keys].sort();
|
|
31402
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
31403
|
+
}
|
|
31404
|
+
function releaseRecord(candidate) {
|
|
31405
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
31406
|
+
return null;
|
|
31407
|
+
const record = candidate;
|
|
31408
|
+
const keys = [
|
|
31409
|
+
"schema",
|
|
31410
|
+
"project_ref",
|
|
31411
|
+
"deployment_id",
|
|
31412
|
+
"release_id",
|
|
31413
|
+
"sha256",
|
|
31414
|
+
"tree_sha256",
|
|
31415
|
+
"size_bytes",
|
|
31416
|
+
"file_count",
|
|
31417
|
+
"created_at",
|
|
31418
|
+
"kind"
|
|
31419
|
+
];
|
|
31420
|
+
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")
|
|
31421
|
+
return null;
|
|
31422
|
+
return {
|
|
31423
|
+
project_ref: record.project_ref,
|
|
31424
|
+
deployment_id: record.deployment_id,
|
|
31425
|
+
release_id: record.release_id,
|
|
31426
|
+
sha256: record.release_id,
|
|
31427
|
+
tree_sha256: record.tree_sha256,
|
|
31428
|
+
size_bytes: Number(record.size_bytes),
|
|
31429
|
+
file_count: Number(record.file_count),
|
|
31430
|
+
created_at: record.created_at,
|
|
31431
|
+
kind: "prebuilt_static"
|
|
31432
|
+
};
|
|
31433
|
+
}
|
|
31434
|
+
function releaseEnvelope(candidate, expected) {
|
|
31435
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
31436
|
+
return null;
|
|
31437
|
+
const envelope = candidate;
|
|
31438
|
+
const release = exactKeys(envelope, ["project_ref", "deployment_id", "release"]) ? releaseRecord(envelope.release) : null;
|
|
31439
|
+
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)
|
|
31440
|
+
return null;
|
|
31441
|
+
return release;
|
|
31442
|
+
}
|
|
31443
|
+
function canonicalTimestamp3(candidate) {
|
|
31444
|
+
if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
|
|
31445
|
+
return false;
|
|
31446
|
+
const milliseconds = Date.parse(candidate);
|
|
31447
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
31448
|
+
}
|
|
31449
|
+
function nullableIdentity(candidate, pattern) {
|
|
31450
|
+
if (candidate === null)
|
|
31451
|
+
return null;
|
|
31452
|
+
return typeof candidate === "string" && pattern.test(candidate) ? candidate : undefined;
|
|
31453
|
+
}
|
|
31454
|
+
function releaseInventory(candidate, expected) {
|
|
31455
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
31456
|
+
return null;
|
|
31457
|
+
const record = candidate;
|
|
31458
|
+
const keys = [
|
|
31459
|
+
"project_ref",
|
|
31460
|
+
"deployment_id",
|
|
31461
|
+
"active_release_id",
|
|
31462
|
+
"active_activation_id",
|
|
31463
|
+
"releases",
|
|
31464
|
+
"next_cursor"
|
|
31465
|
+
];
|
|
31466
|
+
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))
|
|
31467
|
+
return null;
|
|
31468
|
+
const activeReleaseId = nullableIdentity(record.active_release_id, RELEASE_ID_PATTERN);
|
|
31469
|
+
const activeActivationId = nullableIdentity(record.active_activation_id, MUTATION_ID_PATTERN);
|
|
31470
|
+
const nextCursor = nullableIdentity(record.next_cursor, RELEASE_ID_PATTERN);
|
|
31471
|
+
const releases = record.releases.map(releaseRecord);
|
|
31472
|
+
if (activeReleaseId === undefined || activeActivationId === undefined || nextCursor === undefined || activeReleaseId === null !== (activeActivationId === null) || releases.some((release) => release === null))
|
|
31473
|
+
return null;
|
|
31474
|
+
const verified = releases;
|
|
31475
|
+
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))
|
|
31476
|
+
return null;
|
|
31477
|
+
return {
|
|
31478
|
+
project_ref: record.project_ref,
|
|
31479
|
+
deployment_id: record.deployment_id,
|
|
31480
|
+
active_release_id: activeReleaseId,
|
|
31481
|
+
active_activation_id: activeActivationId,
|
|
31482
|
+
releases: verified,
|
|
31483
|
+
next_cursor: nextCursor
|
|
31484
|
+
};
|
|
31485
|
+
}
|
|
31486
|
+
function releaseEndpoint(projectRef, deploymentId) {
|
|
31487
|
+
if (!PROJECT_REF_PATTERN2.test(projectRef))
|
|
31488
|
+
throw new Error("'ref' is invalid for frontend releases");
|
|
31489
|
+
if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
|
|
31490
|
+
throw new Error("'id' is invalid for frontend releases");
|
|
31491
|
+
return `/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
|
|
31492
|
+
}
|
|
31493
|
+
function releasePath(projectRef, deploymentId, releaseId) {
|
|
31494
|
+
if (!RELEASE_ID_PATTERN.test(releaseId))
|
|
31495
|
+
throw new Error("'release_id' must be a SHA-256 digest");
|
|
31496
|
+
return `${releaseEndpoint(projectRef, deploymentId)}/${releaseId}`;
|
|
31497
|
+
}
|
|
31498
|
+
function sameArchiveIdentity(left, right) {
|
|
31499
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
31500
|
+
}
|
|
31501
|
+
async function archiveSha256(handle, sizeBytes) {
|
|
31502
|
+
const hash2 = createHash3("sha256");
|
|
31503
|
+
const chunk = new Uint8Array(Math.min(sizeBytes, ARCHIVE_CHUNK_BYTES));
|
|
31504
|
+
for (let offset = 0;offset < sizeBytes; ) {
|
|
31505
|
+
const requested = Math.min(chunk.byteLength, sizeBytes - offset);
|
|
31506
|
+
const { bytesRead } = await handle.read(chunk, 0, requested, offset);
|
|
31507
|
+
if (bytesRead < 1)
|
|
31508
|
+
throw new Error("Frontend release archive changed while it was hashed");
|
|
31509
|
+
hash2.update(chunk.subarray(0, bytesRead));
|
|
31510
|
+
offset += bytesRead;
|
|
31511
|
+
}
|
|
31512
|
+
return hash2.digest("hex");
|
|
31513
|
+
}
|
|
31514
|
+
async function verifiedArchive(path2) {
|
|
31515
|
+
const archivePath = resolve3(path2);
|
|
31516
|
+
const handle = await open2(archivePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
31517
|
+
try {
|
|
31518
|
+
const before = await handle.stat({ bigint: true });
|
|
31519
|
+
const sizeBytes = Number(before.size);
|
|
31520
|
+
if (!before.isFile() || sizeBytes < 1 || sizeBytes > ARCHIVE_MAX_BYTES) {
|
|
31521
|
+
throw new Error(`Frontend release archive must be a 1-${ARCHIVE_MAX_BYTES} byte regular file`);
|
|
31522
|
+
}
|
|
31523
|
+
const sha2562 = await archiveSha256(handle, sizeBytes);
|
|
31524
|
+
const after = await handle.stat({ bigint: true });
|
|
31525
|
+
if (!sameArchiveIdentity(before, after)) {
|
|
31526
|
+
throw new Error("Frontend release archive identity changed while it was hashed");
|
|
31527
|
+
}
|
|
31528
|
+
return { handle, sizeBytes, sha256: sha2562 };
|
|
31529
|
+
} catch (error) {
|
|
31530
|
+
await handle.close();
|
|
31531
|
+
throw error;
|
|
31532
|
+
}
|
|
31533
|
+
}
|
|
31534
|
+
function archiveStream(archive) {
|
|
31535
|
+
let offset = 0;
|
|
31536
|
+
return new ReadableStream({
|
|
31537
|
+
async pull(controller) {
|
|
31538
|
+
if (offset === archive.sizeBytes) {
|
|
31539
|
+
controller.close();
|
|
31540
|
+
return;
|
|
31541
|
+
}
|
|
31542
|
+
const length = Math.min(ARCHIVE_CHUNK_BYTES, archive.sizeBytes - offset);
|
|
31543
|
+
const chunk = new Uint8Array(length);
|
|
31544
|
+
const { bytesRead } = await archive.handle.read(chunk, 0, length, offset);
|
|
31545
|
+
if (bytesRead < 1)
|
|
31546
|
+
throw new Error("Frontend release archive changed while it was uploaded");
|
|
31547
|
+
offset += bytesRead;
|
|
31548
|
+
controller.enqueue(bytesRead === length ? chunk : chunk.subarray(0, bytesRead));
|
|
31549
|
+
}
|
|
31550
|
+
});
|
|
31551
|
+
}
|
|
31552
|
+
function releaseReadFailure(operation, response) {
|
|
31553
|
+
return releaseFailure(operation, response.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", response.status);
|
|
31554
|
+
}
|
|
31555
|
+
async function listFrontendReleases(http, projectRef, deploymentId, cursor, limit = 50) {
|
|
31556
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > RELEASE_LIST_LIMIT_MAX) {
|
|
31557
|
+
throw new Error("'limit' must be 1-100");
|
|
31558
|
+
}
|
|
31559
|
+
if (cursor !== undefined && !RELEASE_ID_PATTERN.test(cursor))
|
|
31560
|
+
throw new Error("'cursor' is invalid");
|
|
31561
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
31562
|
+
if (cursor)
|
|
31563
|
+
query.set("cursor", cursor);
|
|
31564
|
+
const response = await http.get(`${releaseEndpoint(projectRef, deploymentId)}?${query}`, {
|
|
31565
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
31566
|
+
});
|
|
31567
|
+
const inventory = response.ok ? releaseInventory(response.data, { projectRef, deploymentId }) : null;
|
|
31568
|
+
if (!inventory || inventory.releases.length > limit) {
|
|
31569
|
+
return releaseReadFailure("frontend.list_releases", response);
|
|
31570
|
+
}
|
|
31571
|
+
return toolResponse(inventory);
|
|
31572
|
+
}
|
|
31573
|
+
async function getFrontendRelease(http, projectRef, deploymentId, releaseId) {
|
|
31574
|
+
const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
|
|
31575
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
31576
|
+
});
|
|
31577
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
|
|
31578
|
+
if (!release) {
|
|
31579
|
+
return releaseReadFailure("frontend.get_release", response);
|
|
31580
|
+
}
|
|
31581
|
+
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
|
|
31582
|
+
}
|
|
31583
|
+
async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath) {
|
|
31584
|
+
const endpoint = releaseEndpoint(projectRef, deploymentId);
|
|
31585
|
+
const archive = await verifiedArchive(archivePath);
|
|
31586
|
+
let response;
|
|
31587
|
+
try {
|
|
31588
|
+
response = await http.postBinary(endpoint, {
|
|
31589
|
+
stream: archiveStream(archive),
|
|
31590
|
+
byteLength: archive.sizeBytes
|
|
31591
|
+
}, {
|
|
31592
|
+
contentType: "application/zip",
|
|
31593
|
+
contentLength: archive.sizeBytes,
|
|
31594
|
+
contentSha256: archive.sha256,
|
|
31595
|
+
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
31596
|
+
timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS
|
|
31597
|
+
});
|
|
31598
|
+
} finally {
|
|
31599
|
+
await archive.handle.close();
|
|
31600
|
+
}
|
|
31601
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
|
|
31602
|
+
if (!release) {
|
|
31603
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
31604
|
+
return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
|
|
31605
|
+
}
|
|
31606
|
+
return uploadReadback(http, projectRef, deploymentId, archive.sha256, response.status);
|
|
31607
|
+
}
|
|
31608
|
+
const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
31609
|
+
const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef, deploymentId, releaseId: release.release_id }) : null;
|
|
31610
|
+
if (!verified || verified.tree_sha256 !== release.tree_sha256) {
|
|
31611
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
|
|
31612
|
+
}
|
|
31613
|
+
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release: verified });
|
|
31614
|
+
}
|
|
31615
|
+
async function uploadReadback(http, projectRef, deploymentId, releaseId, uploadStatus) {
|
|
31616
|
+
const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
|
|
31617
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
31618
|
+
});
|
|
31619
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
|
|
31620
|
+
if (!release) {
|
|
31621
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
|
|
31622
|
+
}
|
|
31623
|
+
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
|
|
31624
|
+
}
|
|
31625
|
+
function stableJson(candidate) {
|
|
31626
|
+
if (candidate === null || typeof candidate !== "object")
|
|
31627
|
+
return JSON.stringify(candidate);
|
|
31628
|
+
if (Array.isArray(candidate))
|
|
31629
|
+
return `[${candidate.map(stableJson).join(",")}]`;
|
|
31630
|
+
const record = candidate;
|
|
31631
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
31632
|
+
}
|
|
31633
|
+
function activationFingerprint(identity) {
|
|
31634
|
+
return createHash3("sha256").update(stableJson({
|
|
31635
|
+
project_ref: identity.projectRef,
|
|
31636
|
+
deployment_id: identity.deploymentId,
|
|
31637
|
+
release_id: identity.releaseId,
|
|
31638
|
+
expected_active_release_id: identity.expectedActiveReleaseId,
|
|
31639
|
+
activation_id: identity.mutationId,
|
|
31640
|
+
expected_activation_id: identity.expectedActivationId
|
|
31641
|
+
})).digest("hex");
|
|
31642
|
+
}
|
|
31643
|
+
function activationResourceKey(deploymentId) {
|
|
31644
|
+
return `v1/frontend_release/${Buffer.from(deploymentId, "utf8").toString("base64url")}`;
|
|
31645
|
+
}
|
|
31646
|
+
function publicMutationReceipt(candidate, expected) {
|
|
31647
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
31648
|
+
return null;
|
|
31649
|
+
const envelope = candidate;
|
|
31650
|
+
const mutation = envelope.mutation;
|
|
31651
|
+
const expectedKeys = [
|
|
31652
|
+
"project_ref",
|
|
31653
|
+
"mutation_id",
|
|
31654
|
+
"operation",
|
|
31655
|
+
"resource_key",
|
|
31656
|
+
"request_fingerprint",
|
|
31657
|
+
"principal",
|
|
31658
|
+
"status",
|
|
31659
|
+
"checkpoint",
|
|
31660
|
+
"receipt",
|
|
31661
|
+
"response_status",
|
|
31662
|
+
"failure_code",
|
|
31663
|
+
"lease",
|
|
31664
|
+
"completed_at",
|
|
31665
|
+
"created_at",
|
|
31666
|
+
"updated_at"
|
|
31667
|
+
];
|
|
31668
|
+
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"))
|
|
31669
|
+
return null;
|
|
31670
|
+
return {
|
|
31671
|
+
operation: mutation.operation,
|
|
31672
|
+
status: mutation.status,
|
|
31673
|
+
responseStatus: mutation.response_status,
|
|
31674
|
+
failureCode: mutation.failure_code
|
|
31675
|
+
};
|
|
31676
|
+
}
|
|
31677
|
+
function activationReceipt(candidate, input) {
|
|
31678
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
31679
|
+
return null;
|
|
31680
|
+
const record = candidate;
|
|
31681
|
+
const release = releaseRecord(record.release);
|
|
31682
|
+
const mutation = record.mutation;
|
|
31683
|
+
if (!release || !mutation || !exactKeys(record, [
|
|
31684
|
+
"project_ref",
|
|
31685
|
+
"deployment_id",
|
|
31686
|
+
"active_release_id",
|
|
31687
|
+
"activation_id",
|
|
31688
|
+
"release",
|
|
31689
|
+
"mutation"
|
|
31690
|
+
]) || !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")
|
|
31691
|
+
return null;
|
|
31692
|
+
return release;
|
|
31693
|
+
}
|
|
31694
|
+
async function activeReleaseReadback(http, identity) {
|
|
31695
|
+
const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
|
|
31696
|
+
const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
|
|
31697
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
31698
|
+
});
|
|
31699
|
+
const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
|
|
31700
|
+
projectRef: identity.projectRef,
|
|
31701
|
+
deploymentId: identity.deploymentId
|
|
31702
|
+
}) : null;
|
|
31703
|
+
if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
|
|
31704
|
+
return { release: null, status: inventoryRead.status };
|
|
31705
|
+
}
|
|
31706
|
+
const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
31707
|
+
const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
|
|
31708
|
+
projectRef: identity.projectRef,
|
|
31709
|
+
deploymentId: identity.deploymentId,
|
|
31710
|
+
releaseId: identity.releaseId
|
|
31711
|
+
}) : null;
|
|
31712
|
+
return { release, status: releaseRead.status };
|
|
31713
|
+
}
|
|
31714
|
+
async function activateFrontendRelease(http, input) {
|
|
31715
|
+
const mutationId = input.mutationId;
|
|
31716
|
+
if (!MUTATION_ID_PATTERN.test(mutationId))
|
|
31717
|
+
throw new Error("'mutation_id' must be a UUIDv4");
|
|
31718
|
+
if (input.expectedActiveReleaseId !== "absent" && !RELEASE_ID_PATTERN.test(input.expectedActiveReleaseId)) {
|
|
31719
|
+
throw new Error("'expected_active_release_id' is invalid");
|
|
31720
|
+
}
|
|
31721
|
+
if (input.expectedActivationId !== "absent" && !MUTATION_ID_PATTERN.test(input.expectedActivationId)) {
|
|
31722
|
+
throw new Error("'expected_activation_id' is invalid");
|
|
31723
|
+
}
|
|
31724
|
+
const endpoint = `${releasePath(input.projectRef, input.deploymentId, input.releaseId)}/activate`;
|
|
31725
|
+
const response = await http.post(endpoint, {
|
|
31726
|
+
expected_active_release_id: input.expectedActiveReleaseId,
|
|
31727
|
+
expected_activation_id: input.expectedActivationId,
|
|
31728
|
+
mutation_id: mutationId
|
|
31729
|
+
}, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
31730
|
+
const release = activationReceipt(response.data, { ...input, mutationId });
|
|
31731
|
+
if (!response.ok || !release) {
|
|
31732
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
31733
|
+
return releaseFailure("frontend.activate_release", "HTTP_ERROR", response.status);
|
|
31734
|
+
}
|
|
31735
|
+
return activationReadback(http, input, mutationId, response.status);
|
|
31736
|
+
}
|
|
31737
|
+
const readback = await activeReleaseReadback(http, { ...input, mutationId });
|
|
31738
|
+
if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
|
|
31739
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
|
|
31740
|
+
}
|
|
31741
|
+
return toolResponse({
|
|
31742
|
+
project_ref: input.projectRef,
|
|
31743
|
+
deployment_id: input.deploymentId,
|
|
31744
|
+
active_release_id: input.releaseId,
|
|
31745
|
+
activation_id: mutationId,
|
|
31746
|
+
release: readback.release
|
|
31747
|
+
});
|
|
31748
|
+
}
|
|
31749
|
+
async function activationReadback(http, input, mutationId, activationStatus) {
|
|
31750
|
+
const mutationRead = await http.get(`/v1/projects/${encodeURIComponent(input.projectRef)}/mutations/${mutationId}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
31751
|
+
const mutationEnvelope = mutationRead.data;
|
|
31752
|
+
const mutation = mutationRead.ok ? publicMutationReceipt(mutationRead.data, {
|
|
31753
|
+
projectRef: input.projectRef,
|
|
31754
|
+
mutationId,
|
|
31755
|
+
resourceKey: activationResourceKey(input.deploymentId),
|
|
31756
|
+
requestFingerprint: activationFingerprint({ ...input, mutationId })
|
|
31757
|
+
}) : null;
|
|
31758
|
+
if (!mutation || mutationEnvelope?.project_ref !== input.projectRef || mutation.operation !== "frontend.release.activate" || mutation.status !== "succeeded" || mutation.responseStatus !== 200 || mutation.failureCode !== null) {
|
|
31759
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
|
|
31760
|
+
}
|
|
31761
|
+
const readback = await activeReleaseReadback(http, { ...input, mutationId });
|
|
31762
|
+
if (!readback.release) {
|
|
31763
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
|
|
31764
|
+
}
|
|
31765
|
+
return toolResponse({
|
|
31766
|
+
project_ref: input.projectRef,
|
|
31767
|
+
deployment_id: input.deploymentId,
|
|
31768
|
+
active_release_id: input.releaseId,
|
|
31769
|
+
activation_id: mutationId,
|
|
31770
|
+
release: readback.release
|
|
31771
|
+
});
|
|
31772
|
+
}
|
|
31773
|
+
|
|
31774
|
+
// src/shared/tools/frontend-tools.ts
|
|
31775
|
+
function registerFrontendTools(server, http) {
|
|
31776
|
+
server.tool("frontend", `Immutable prebuilt frontend release control.
|
|
31777
|
+
Actions: list_releases, get_release, upload_release, activate_release`, {
|
|
31778
|
+
action: withDescription(stringEnum([
|
|
31779
|
+
"list_releases",
|
|
31780
|
+
"get_release",
|
|
31781
|
+
"upload_release",
|
|
31782
|
+
"activate_release"
|
|
31783
|
+
]), "Action"),
|
|
31784
|
+
ref: optional(Type.String(), "Project ref"),
|
|
31785
|
+
id: optional(Type.String(), "Deployment ID"),
|
|
31786
|
+
zip_path: optional(Type.String(), "[upload_release] Local ZIP file path"),
|
|
31787
|
+
release_id: optional(Type.String(), "[get_release/activate_release] SHA-256 release ID"),
|
|
31788
|
+
expected_active_release_id: optional(Type.String(), "[activate_release] Current release SHA-256 or absent"),
|
|
31789
|
+
expected_activation_id: optional(Type.String(), "[activate_release] Current activation UUIDv4 or absent"),
|
|
31790
|
+
mutation_id: optional(Type.String(), "[activate_release] Required retry-stable UUIDv4"),
|
|
31791
|
+
cursor: optional(Type.String(), "[list_releases] Last release SHA-256 cursor"),
|
|
31792
|
+
limit: optional(Type.Number(), "[list_releases] Page size, 1-100 (default 50)")
|
|
31793
|
+
}, async (args) => {
|
|
31794
|
+
const {
|
|
31795
|
+
action,
|
|
31796
|
+
ref,
|
|
31797
|
+
id,
|
|
31798
|
+
zip_path,
|
|
31799
|
+
release_id,
|
|
31800
|
+
expected_active_release_id,
|
|
31801
|
+
expected_activation_id,
|
|
31802
|
+
mutation_id,
|
|
31803
|
+
cursor,
|
|
31804
|
+
limit
|
|
31805
|
+
} = args;
|
|
31806
|
+
const need = (f, v) => {
|
|
31807
|
+
if (!v)
|
|
31808
|
+
throw new Error(`'${f}' required for '${action}'`);
|
|
31809
|
+
};
|
|
31810
|
+
switch (action) {
|
|
31811
|
+
case "list_releases":
|
|
31812
|
+
need("ref", ref);
|
|
31813
|
+
need("id", id);
|
|
31814
|
+
return listFrontendReleases(http, ref, id, cursor, limit);
|
|
31815
|
+
case "get_release":
|
|
31816
|
+
need("ref", ref);
|
|
31817
|
+
need("id", id);
|
|
31818
|
+
need("release_id", release_id);
|
|
31819
|
+
return getFrontendRelease(http, ref, id, release_id);
|
|
31820
|
+
case "upload_release":
|
|
31821
|
+
need("ref", ref);
|
|
31822
|
+
need("id", id);
|
|
31823
|
+
need("zip_path", zip_path);
|
|
31824
|
+
return uploadFrontendRelease(http, ref, id, zip_path);
|
|
31825
|
+
case "activate_release":
|
|
31826
|
+
need("ref", ref);
|
|
31827
|
+
need("id", id);
|
|
31828
|
+
need("release_id", release_id);
|
|
31829
|
+
need("expected_active_release_id", expected_active_release_id);
|
|
31830
|
+
need("expected_activation_id", expected_activation_id);
|
|
31831
|
+
need("mutation_id", mutation_id);
|
|
31832
|
+
return activateFrontendRelease(http, {
|
|
31833
|
+
projectRef: ref,
|
|
31834
|
+
deploymentId: id,
|
|
31835
|
+
releaseId: release_id,
|
|
31836
|
+
expectedActiveReleaseId: expected_active_release_id,
|
|
31837
|
+
expectedActivationId: expected_activation_id,
|
|
31838
|
+
mutationId: mutation_id
|
|
31839
|
+
});
|
|
31840
|
+
default:
|
|
31841
|
+
throw new Error("Unknown immutable frontend release action");
|
|
31842
|
+
}
|
|
31843
|
+
});
|
|
31844
|
+
}
|
|
31049
31845
|
// package.json
|
|
31050
31846
|
var package_default = {
|
|
31051
31847
|
name: "@supacloud/admin",
|
|
31052
|
-
version: "0.
|
|
31848
|
+
version: "0.14.0",
|
|
31053
31849
|
description: "Platform administration CLI for SupaCloud operators",
|
|
31054
31850
|
type: "module",
|
|
31055
31851
|
main: "./dist/index.js",
|
|
@@ -31119,6 +31915,7 @@ function unavailableAdminSchemas() {
|
|
|
31119
31915
|
project: captureTools((server) => registerAdminProjectCliTools(server, schemaOnlyHttp)).project.schema,
|
|
31120
31916
|
platform: captureTools((server) => registerAdvancedTools(server, schemaOnlyHttp)).platform.schema,
|
|
31121
31917
|
gateway: captureTools((server) => registerGatewayTools(server, schemaOnlyHttp)).gateway.schema,
|
|
31918
|
+
frontend: captureTools((server) => registerFrontendTools(server, schemaOnlyHttp)).frontend.schema,
|
|
31122
31919
|
ssh: captureTools((server) => registerSshTools(server, {})).ssh.schema
|
|
31123
31920
|
};
|
|
31124
31921
|
}
|
|
@@ -31128,6 +31925,7 @@ function registerUnavailableAdminTools(tools) {
|
|
|
31128
31925
|
tools.platform = unavailableTool(schemas.platform, "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
|
|
31129
31926
|
tools.ssh = unavailableTool(schemas.ssh, "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT.");
|
|
31130
31927
|
tools.gateway = unavailableTool(schemas.gateway, "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges).");
|
|
31928
|
+
tools.frontend = unavailableTool(schemas.frontend, "⚠️ Frontend commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
|
|
31131
31929
|
}
|
|
31132
31930
|
function printHelp(context = resolveSupaCloudContext()) {
|
|
31133
31931
|
console.error(`
|
|
@@ -31184,10 +31982,16 @@ EXAMPLES
|
|
|
31184
31982
|
supacloud-admin project runtime_snapshot --ref abc123
|
|
31185
31983
|
supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
31186
31984
|
supacloud-admin platform metrics
|
|
31985
|
+
supacloud-admin platform list_logical_backups --ref abc123
|
|
31986
|
+
supacloud-admin platform create_logical_backup --ref abc123
|
|
31987
|
+
supacloud-admin platform restore_logical_backup --ref abc123 --backup_id BACKUP_ID --expected_sha256 SHA256 --confirmation RESTORE_PROJECT:abc123:BACKUP_ID:SHA256
|
|
31187
31988
|
supacloud-admin gateway routes --ref abc123
|
|
31188
31989
|
supacloud-admin gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
|
|
31189
31990
|
supacloud-admin gateway config --ref abc123 --rate_limit_tier pro
|
|
31190
31991
|
supacloud-admin gateway rebuild --ref abc123 --clean
|
|
31992
|
+
supacloud-admin frontend list_releases --ref abc123 --id web
|
|
31993
|
+
supacloud-admin frontend upload_release --ref abc123 --id web --zip_path /secure/site.zip
|
|
31994
|
+
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>
|
|
31191
31995
|
`);
|
|
31192
31996
|
}
|
|
31193
31997
|
function authorizedToolMap(tools, context, confirmProduction) {
|
|
@@ -31275,6 +32079,7 @@ function createAdminTools(context = resolveSupaCloudContext(), confirmProduction
|
|
|
31275
32079
|
});
|
|
31276
32080
|
Object.assign(tools, captureTools((server) => registerAdminProjectCliTools(server, http)));
|
|
31277
32081
|
Object.assign(tools, captureTools((server) => registerGatewayTools(server, http)));
|
|
32082
|
+
Object.assign(tools, captureTools((server) => registerFrontendTools(server, http)));
|
|
31278
32083
|
const advancedTools = captureTools((server) => registerAdvancedTools(server, http));
|
|
31279
32084
|
if (advancedTools.platform) {
|
|
31280
32085
|
tools.platform = advancedTools.platform;
|
|
@@ -31325,17 +32130,17 @@ async function main() {
|
|
|
31325
32130
|
}
|
|
31326
32131
|
const cliTools = createAdminTools(context, globalOptions.confirmProduction);
|
|
31327
32132
|
if (args.length === 1 && cliTools[args[0]]) {
|
|
31328
|
-
const
|
|
31329
|
-
if (
|
|
31330
|
-
for (const chunk of
|
|
32133
|
+
const toolResponse2 = await cliTools[args[0]].callback({});
|
|
32134
|
+
if (toolResponse2?.content && Array.isArray(toolResponse2.content)) {
|
|
32135
|
+
for (const chunk of toolResponse2.content) {
|
|
31331
32136
|
if (chunk.type === "text") {
|
|
31332
32137
|
console.log(chunk.text);
|
|
31333
32138
|
}
|
|
31334
32139
|
}
|
|
31335
32140
|
} else {
|
|
31336
|
-
console.log(JSON.stringify(
|
|
32141
|
+
console.log(JSON.stringify(toolResponse2, null, 2));
|
|
31337
32142
|
}
|
|
31338
|
-
if (cliToolResultIsError(
|
|
32143
|
+
if (cliToolResultIsError(toolResponse2))
|
|
31339
32144
|
process.exitCode = 1;
|
|
31340
32145
|
return;
|
|
31341
32146
|
}
|
|
@@ -31345,7 +32150,7 @@ function isDirectRun() {
|
|
|
31345
32150
|
if (!process.argv[1])
|
|
31346
32151
|
return false;
|
|
31347
32152
|
try {
|
|
31348
|
-
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(
|
|
32153
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve4(process.argv[1]));
|
|
31349
32154
|
} catch {
|
|
31350
32155
|
return false;
|
|
31351
32156
|
}
|
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|