@supacloud/cli 0.33.0 → 0.34.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 +26 -0
- package/dist/index.js +557 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,32 @@ 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
|
+
### Immutable frontend releases
|
|
110
|
+
|
|
111
|
+
The `frontend` command keeps the existing deployment, Git, and legacy ZIP
|
|
112
|
+
actions and also exposes the immutable prebuilt release workflow:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
supacloud-cli frontend list_releases --ref abc123 --id web
|
|
116
|
+
supacloud-cli frontend get_release --ref abc123 --id web --release_id <sha256>
|
|
117
|
+
supacloud-cli frontend upload_release --ref abc123 --id web --zip_path ./dist.zip
|
|
118
|
+
supacloud-cli frontend activate_release --ref abc123 --id web \
|
|
119
|
+
--release_id <sha256> \
|
|
120
|
+
--expected_active_release_id absent \
|
|
121
|
+
--expected_activation_id absent \
|
|
122
|
+
--mutation_id <retry-stable-uuid-v4>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`upload_release` hashes and streams an existing regular ZIP file without
|
|
126
|
+
buffering the full archive. The Management API binds the upload to that SHA-256,
|
|
127
|
+
and the CLI reads the immutable release back before reporting success.
|
|
128
|
+
`activate_release` uses both the observed active release and activation IDs as
|
|
129
|
+
optimistic concurrency tokens, then verifies the authoritative active release.
|
|
130
|
+
Use the values returned by `list_releases`; `absent` is valid only when no
|
|
131
|
+
release has been activated. Production uploads and activations require the
|
|
132
|
+
normal exact `--confirm-production <ref>` value, and
|
|
133
|
+
`SUPACLOUD_READ_ONLY=true` blocks both mutations.
|
|
134
|
+
|
|
109
135
|
### Verified release controls
|
|
110
136
|
|
|
111
137
|
`release` is an official CLI entry point for verified Management API controls
|
package/dist/index.js
CHANGED
|
@@ -6514,8 +6514,8 @@ var ACTION_POLICY = {
|
|
|
6514
6514
|
},
|
|
6515
6515
|
secrets: { read: ["list"], write: ["upsert", "delete"] },
|
|
6516
6516
|
frontend: {
|
|
6517
|
-
read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
|
|
6518
|
-
write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env"]
|
|
6517
|
+
read: ["list", "get", "build_logs", "list_frameworks", "list_records", "list_releases", "get_release"],
|
|
6518
|
+
write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env", "upload_release", "activate_release"]
|
|
6519
6519
|
},
|
|
6520
6520
|
task_events: { read: ["inspect_webhook"], write: ["register_webhook", "unregister_webhook"] },
|
|
6521
6521
|
diagnostics: { read: ["list_checks", "get_run"], write: ["run_checks", "repair"] },
|
|
@@ -6875,6 +6875,7 @@ class HttpTransport {
|
|
|
6875
6875
|
async post(path, body, options) {
|
|
6876
6876
|
const timeoutMs = validatedPostTimeout(options);
|
|
6877
6877
|
const maxJsonBytes = validatedJsonResponseLimit(options?.maxJsonBytes);
|
|
6878
|
+
const responseTimeoutMs = validatedResponseTimeout(options?.responseTimeoutMs);
|
|
6878
6879
|
try {
|
|
6879
6880
|
if (maxJsonBytes === undefined) {
|
|
6880
6881
|
return await this.mutationWithResponseReader("POST", path, serializedRequestBody(body), responseJsonOrNull, timeoutMs);
|
|
@@ -6884,7 +6885,7 @@ class HttpTransport {
|
|
|
6884
6885
|
headers: this.headers(),
|
|
6885
6886
|
body: serializedRequestBody(body)
|
|
6886
6887
|
}, timeoutMs);
|
|
6887
|
-
const data = await boundedResponseJson(response, maxJsonBytes);
|
|
6888
|
+
const data = await boundedResponseJson(response, maxJsonBytes, responseTimeoutMs);
|
|
6888
6889
|
return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
|
|
6889
6890
|
} catch (error) {
|
|
6890
6891
|
return transportFailure(error);
|
|
@@ -6894,6 +6895,39 @@ class HttpTransport {
|
|
|
6894
6895
|
const timeoutMs = validatedPostTimeout(options);
|
|
6895
6896
|
return this.mutationWithResponseReader("POST", path, serializedRequestBody(body), releaseMutationResponseJson, timeoutMs);
|
|
6896
6897
|
}
|
|
6898
|
+
async postBinary(path, body, options) {
|
|
6899
|
+
if (options.contentType !== "application/zip") {
|
|
6900
|
+
throw new Error("Binary HTTP content type is invalid");
|
|
6901
|
+
}
|
|
6902
|
+
if (!Number.isSafeInteger(options.contentLength) || options.contentLength < 1 || options.contentLength !== body.byteLength) {
|
|
6903
|
+
throw new RangeError("Binary HTTP body length is invalid");
|
|
6904
|
+
}
|
|
6905
|
+
if (!/^[0-9a-f]{64}$/u.test(options.contentSha256)) {
|
|
6906
|
+
throw new Error("Binary HTTP body SHA-256 is invalid");
|
|
6907
|
+
}
|
|
6908
|
+
const maxJsonBytes = validatedJsonResponseLimit(options.maxJsonBytes);
|
|
6909
|
+
const timeoutMs = validatedPostTimeout(options);
|
|
6910
|
+
const responseTimeoutMs = validatedResponseTimeout(options.responseTimeoutMs);
|
|
6911
|
+
try {
|
|
6912
|
+
const request = {
|
|
6913
|
+
method: "POST",
|
|
6914
|
+
headers: {
|
|
6915
|
+
Authorization: `Bearer ${this.token}`,
|
|
6916
|
+
"Content-Type": options.contentType,
|
|
6917
|
+
"Content-Length": String(options.contentLength),
|
|
6918
|
+
"x-supacloud-content-sha256": options.contentSha256,
|
|
6919
|
+
...this.apiKey ? { apikey: this.apiKey } : {}
|
|
6920
|
+
},
|
|
6921
|
+
body: body.stream,
|
|
6922
|
+
duplex: "half"
|
|
6923
|
+
};
|
|
6924
|
+
const response = await fetchWithRetry(`${this.baseUrl}${path}`, request, timeoutMs);
|
|
6925
|
+
const data = await boundedResponseJson(response, maxJsonBytes, responseTimeoutMs);
|
|
6926
|
+
return data === null ? responseReadFailure(response.status) : { ok: response.ok, status: response.status, data };
|
|
6927
|
+
} catch (error) {
|
|
6928
|
+
return transportFailure(error);
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6897
6931
|
async patchReleaseMutation(path, body) {
|
|
6898
6932
|
return this.mutationWithResponseReader("PATCH", path, serializedRequestBody(body), releaseMutationResponseJson);
|
|
6899
6933
|
}
|
|
@@ -11034,9 +11068,418 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
11034
11068
|
// src/shared/tools/frontend-tools.ts
|
|
11035
11069
|
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
11036
11070
|
import { basename as basename3 } from "node:path";
|
|
11071
|
+
|
|
11072
|
+
// src/shared/tools/frontend-release-control.ts
|
|
11073
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
11074
|
+
import { constants as fsConstants2 } from "node:fs";
|
|
11075
|
+
import { open } from "node:fs/promises";
|
|
11076
|
+
import { resolve as resolve3 } from "node:path";
|
|
11077
|
+
var PROJECT_REF_PATTERN3 = /^[A-Za-z0-9_-]{1,20}$/u;
|
|
11078
|
+
var DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
11079
|
+
var RELEASE_ID_PATTERN = /^[0-9a-f]{64}$/u;
|
|
11080
|
+
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;
|
|
11081
|
+
var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
11082
|
+
var ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
|
|
11083
|
+
var ARCHIVE_CHUNK_BYTES = 64 * 1024;
|
|
11084
|
+
var RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
11085
|
+
var UPLOAD_REQUEST_TIMEOUT_MS = 10 * 60000;
|
|
11086
|
+
var MUTATION_RESPONSE_TIMEOUT_MS = 5000;
|
|
11087
|
+
var RELEASE_LIST_LIMIT_MAX = 100;
|
|
11088
|
+
function toolResponse(payload) {
|
|
11089
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
11090
|
+
}
|
|
11091
|
+
function releaseFailure(operation, code, status) {
|
|
11092
|
+
return {
|
|
11093
|
+
isError: true,
|
|
11094
|
+
content: [{
|
|
11095
|
+
type: "text",
|
|
11096
|
+
text: JSON.stringify({ ok: false, operation, error: { code, http_status: status } })
|
|
11097
|
+
}]
|
|
11098
|
+
};
|
|
11099
|
+
}
|
|
11100
|
+
function exactKeys(candidate, keys) {
|
|
11101
|
+
const actual = Object.keys(candidate).sort();
|
|
11102
|
+
const expected = [...keys].sort();
|
|
11103
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
11104
|
+
}
|
|
11105
|
+
function releaseRecord(candidate) {
|
|
11106
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11107
|
+
return null;
|
|
11108
|
+
const record = candidate;
|
|
11109
|
+
const keys = [
|
|
11110
|
+
"schema",
|
|
11111
|
+
"project_ref",
|
|
11112
|
+
"deployment_id",
|
|
11113
|
+
"release_id",
|
|
11114
|
+
"sha256",
|
|
11115
|
+
"tree_sha256",
|
|
11116
|
+
"size_bytes",
|
|
11117
|
+
"file_count",
|
|
11118
|
+
"created_at",
|
|
11119
|
+
"kind"
|
|
11120
|
+
];
|
|
11121
|
+
if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.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 || !canonicalTimestamp(record.created_at) || record.kind !== "prebuilt_static")
|
|
11122
|
+
return null;
|
|
11123
|
+
return {
|
|
11124
|
+
project_ref: record.project_ref,
|
|
11125
|
+
deployment_id: record.deployment_id,
|
|
11126
|
+
release_id: record.release_id,
|
|
11127
|
+
sha256: record.release_id,
|
|
11128
|
+
tree_sha256: record.tree_sha256,
|
|
11129
|
+
size_bytes: Number(record.size_bytes),
|
|
11130
|
+
file_count: Number(record.file_count),
|
|
11131
|
+
created_at: record.created_at,
|
|
11132
|
+
kind: "prebuilt_static"
|
|
11133
|
+
};
|
|
11134
|
+
}
|
|
11135
|
+
function releaseEnvelope(candidate, expected) {
|
|
11136
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11137
|
+
return null;
|
|
11138
|
+
const envelope = candidate;
|
|
11139
|
+
const release = exactKeys(envelope, ["project_ref", "deployment_id", "release"]) ? releaseRecord(envelope.release) : null;
|
|
11140
|
+
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)
|
|
11141
|
+
return null;
|
|
11142
|
+
return release;
|
|
11143
|
+
}
|
|
11144
|
+
function canonicalTimestamp(candidate) {
|
|
11145
|
+
if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
|
|
11146
|
+
return false;
|
|
11147
|
+
const milliseconds = Date.parse(candidate);
|
|
11148
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
11149
|
+
}
|
|
11150
|
+
function nullableIdentity(candidate, pattern) {
|
|
11151
|
+
if (candidate === null)
|
|
11152
|
+
return null;
|
|
11153
|
+
return typeof candidate === "string" && pattern.test(candidate) ? candidate : undefined;
|
|
11154
|
+
}
|
|
11155
|
+
function releaseInventory(candidate, expected) {
|
|
11156
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11157
|
+
return null;
|
|
11158
|
+
const record = candidate;
|
|
11159
|
+
const keys = [
|
|
11160
|
+
"project_ref",
|
|
11161
|
+
"deployment_id",
|
|
11162
|
+
"active_release_id",
|
|
11163
|
+
"active_activation_id",
|
|
11164
|
+
"releases",
|
|
11165
|
+
"next_cursor"
|
|
11166
|
+
];
|
|
11167
|
+
if (!exactKeys(record, keys) || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || !Array.isArray(record.releases))
|
|
11168
|
+
return null;
|
|
11169
|
+
const activeReleaseId = nullableIdentity(record.active_release_id, RELEASE_ID_PATTERN);
|
|
11170
|
+
const activeActivationId = nullableIdentity(record.active_activation_id, MUTATION_ID_PATTERN);
|
|
11171
|
+
const nextCursor = nullableIdentity(record.next_cursor, RELEASE_ID_PATTERN);
|
|
11172
|
+
const releases = record.releases.map(releaseRecord);
|
|
11173
|
+
if (activeReleaseId === undefined || activeActivationId === undefined || nextCursor === undefined || activeReleaseId === null !== (activeActivationId === null) || releases.some((release) => release === null))
|
|
11174
|
+
return null;
|
|
11175
|
+
const verified = releases;
|
|
11176
|
+
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))
|
|
11177
|
+
return null;
|
|
11178
|
+
return {
|
|
11179
|
+
project_ref: record.project_ref,
|
|
11180
|
+
deployment_id: record.deployment_id,
|
|
11181
|
+
active_release_id: activeReleaseId,
|
|
11182
|
+
active_activation_id: activeActivationId,
|
|
11183
|
+
releases: verified,
|
|
11184
|
+
next_cursor: nextCursor
|
|
11185
|
+
};
|
|
11186
|
+
}
|
|
11187
|
+
function releaseEndpoint(projectRef2, deploymentId) {
|
|
11188
|
+
if (!PROJECT_REF_PATTERN3.test(projectRef2))
|
|
11189
|
+
throw new Error("'ref' is invalid for frontend releases");
|
|
11190
|
+
if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
|
|
11191
|
+
throw new Error("'id' is invalid for frontend releases");
|
|
11192
|
+
return `/v1/projects/${encodeURIComponent(projectRef2)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
|
|
11193
|
+
}
|
|
11194
|
+
function releasePath(projectRef2, deploymentId, releaseId) {
|
|
11195
|
+
if (!RELEASE_ID_PATTERN.test(releaseId))
|
|
11196
|
+
throw new Error("'release_id' must be a SHA-256 digest");
|
|
11197
|
+
return `${releaseEndpoint(projectRef2, deploymentId)}/${releaseId}`;
|
|
11198
|
+
}
|
|
11199
|
+
function sameArchiveIdentity(left, right) {
|
|
11200
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
11201
|
+
}
|
|
11202
|
+
async function archiveSha256(handle, sizeBytes) {
|
|
11203
|
+
const hash2 = createHash3("sha256");
|
|
11204
|
+
const chunk = new Uint8Array(Math.min(sizeBytes, ARCHIVE_CHUNK_BYTES));
|
|
11205
|
+
for (let offset = 0;offset < sizeBytes; ) {
|
|
11206
|
+
const requested = Math.min(chunk.byteLength, sizeBytes - offset);
|
|
11207
|
+
const { bytesRead } = await handle.read(chunk, 0, requested, offset);
|
|
11208
|
+
if (bytesRead < 1)
|
|
11209
|
+
throw new Error("Frontend release archive changed while it was hashed");
|
|
11210
|
+
hash2.update(chunk.subarray(0, bytesRead));
|
|
11211
|
+
offset += bytesRead;
|
|
11212
|
+
}
|
|
11213
|
+
return hash2.digest("hex");
|
|
11214
|
+
}
|
|
11215
|
+
async function verifiedArchive(path) {
|
|
11216
|
+
const archivePath = resolve3(path);
|
|
11217
|
+
const handle = await open(archivePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
11218
|
+
try {
|
|
11219
|
+
const before = await handle.stat({ bigint: true });
|
|
11220
|
+
const sizeBytes = Number(before.size);
|
|
11221
|
+
if (!before.isFile() || sizeBytes < 1 || sizeBytes > ARCHIVE_MAX_BYTES) {
|
|
11222
|
+
throw new Error(`Frontend release archive must be a 1-${ARCHIVE_MAX_BYTES} byte regular file`);
|
|
11223
|
+
}
|
|
11224
|
+
const sha256 = await archiveSha256(handle, sizeBytes);
|
|
11225
|
+
const after = await handle.stat({ bigint: true });
|
|
11226
|
+
if (!sameArchiveIdentity(before, after)) {
|
|
11227
|
+
throw new Error("Frontend release archive identity changed while it was hashed");
|
|
11228
|
+
}
|
|
11229
|
+
return { handle, sizeBytes, sha256 };
|
|
11230
|
+
} catch (error) {
|
|
11231
|
+
await handle.close();
|
|
11232
|
+
throw error;
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
function archiveStream(archive) {
|
|
11236
|
+
let offset = 0;
|
|
11237
|
+
return new ReadableStream({
|
|
11238
|
+
async pull(controller) {
|
|
11239
|
+
if (offset === archive.sizeBytes) {
|
|
11240
|
+
controller.close();
|
|
11241
|
+
return;
|
|
11242
|
+
}
|
|
11243
|
+
const length = Math.min(ARCHIVE_CHUNK_BYTES, archive.sizeBytes - offset);
|
|
11244
|
+
const chunk = new Uint8Array(length);
|
|
11245
|
+
const { bytesRead } = await archive.handle.read(chunk, 0, length, offset);
|
|
11246
|
+
if (bytesRead < 1)
|
|
11247
|
+
throw new Error("Frontend release archive changed while it was uploaded");
|
|
11248
|
+
offset += bytesRead;
|
|
11249
|
+
controller.enqueue(bytesRead === length ? chunk : chunk.subarray(0, bytesRead));
|
|
11250
|
+
}
|
|
11251
|
+
});
|
|
11252
|
+
}
|
|
11253
|
+
function releaseReadFailure(operation, response) {
|
|
11254
|
+
return releaseFailure(operation, response.ok ? "INVALID_RESPONSE" : "HTTP_ERROR", response.status);
|
|
11255
|
+
}
|
|
11256
|
+
async function listFrontendReleases(http, projectRef2, deploymentId, cursor, limit = 50) {
|
|
11257
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > RELEASE_LIST_LIMIT_MAX) {
|
|
11258
|
+
throw new Error("'limit' must be 1-100");
|
|
11259
|
+
}
|
|
11260
|
+
if (cursor !== undefined && !RELEASE_ID_PATTERN.test(cursor))
|
|
11261
|
+
throw new Error("'cursor' is invalid");
|
|
11262
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
11263
|
+
if (cursor)
|
|
11264
|
+
query.set("cursor", cursor);
|
|
11265
|
+
const response = await http.get(`${releaseEndpoint(projectRef2, deploymentId)}?${query}`, {
|
|
11266
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11267
|
+
});
|
|
11268
|
+
const inventory = response.ok ? releaseInventory(response.data, { projectRef: projectRef2, deploymentId }) : null;
|
|
11269
|
+
if (!inventory || inventory.releases.length > limit) {
|
|
11270
|
+
return releaseReadFailure("frontend.list_releases", response);
|
|
11271
|
+
}
|
|
11272
|
+
return toolResponse(inventory);
|
|
11273
|
+
}
|
|
11274
|
+
async function getFrontendRelease(http, projectRef2, deploymentId, releaseId) {
|
|
11275
|
+
const response = await http.get(releasePath(projectRef2, deploymentId, releaseId), {
|
|
11276
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11277
|
+
});
|
|
11278
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId }) : null;
|
|
11279
|
+
if (!release) {
|
|
11280
|
+
return releaseReadFailure("frontend.get_release", response);
|
|
11281
|
+
}
|
|
11282
|
+
return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release });
|
|
11283
|
+
}
|
|
11284
|
+
async function uploadFrontendRelease(http, projectRef2, deploymentId, archivePath) {
|
|
11285
|
+
const endpoint = releaseEndpoint(projectRef2, deploymentId);
|
|
11286
|
+
const archive = await verifiedArchive(archivePath);
|
|
11287
|
+
let response;
|
|
11288
|
+
try {
|
|
11289
|
+
response = await http.postBinary(endpoint, {
|
|
11290
|
+
stream: archiveStream(archive),
|
|
11291
|
+
byteLength: archive.sizeBytes
|
|
11292
|
+
}, {
|
|
11293
|
+
contentType: "application/zip",
|
|
11294
|
+
contentLength: archive.sizeBytes,
|
|
11295
|
+
contentSha256: archive.sha256,
|
|
11296
|
+
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
11297
|
+
timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
|
|
11298
|
+
responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
|
|
11299
|
+
});
|
|
11300
|
+
} finally {
|
|
11301
|
+
await archive.handle.close();
|
|
11302
|
+
}
|
|
11303
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId: archive.sha256 }) : null;
|
|
11304
|
+
if (!release) {
|
|
11305
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
11306
|
+
return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
|
|
11307
|
+
}
|
|
11308
|
+
return uploadReadback(http, projectRef2, deploymentId, archive.sha256, response.status);
|
|
11309
|
+
}
|
|
11310
|
+
const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
11311
|
+
const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef: projectRef2, deploymentId, releaseId: release.release_id }) : null;
|
|
11312
|
+
if (!verified || verified.tree_sha256 !== release.tree_sha256) {
|
|
11313
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
|
|
11314
|
+
}
|
|
11315
|
+
return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release: verified });
|
|
11316
|
+
}
|
|
11317
|
+
async function uploadReadback(http, projectRef2, deploymentId, releaseId, uploadStatus) {
|
|
11318
|
+
const response = await http.get(releasePath(projectRef2, deploymentId, releaseId), {
|
|
11319
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11320
|
+
});
|
|
11321
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef: projectRef2, deploymentId, releaseId }) : null;
|
|
11322
|
+
if (!release) {
|
|
11323
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
|
|
11324
|
+
}
|
|
11325
|
+
return toolResponse({ project_ref: projectRef2, deployment_id: deploymentId, release });
|
|
11326
|
+
}
|
|
11327
|
+
function stableJson(candidate) {
|
|
11328
|
+
if (candidate === null || typeof candidate !== "object")
|
|
11329
|
+
return JSON.stringify(candidate);
|
|
11330
|
+
if (Array.isArray(candidate))
|
|
11331
|
+
return `[${candidate.map(stableJson).join(",")}]`;
|
|
11332
|
+
const record = candidate;
|
|
11333
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
11334
|
+
}
|
|
11335
|
+
function activationFingerprint(identity) {
|
|
11336
|
+
return createHash3("sha256").update(stableJson({
|
|
11337
|
+
project_ref: identity.projectRef,
|
|
11338
|
+
deployment_id: identity.deploymentId,
|
|
11339
|
+
release_id: identity.releaseId,
|
|
11340
|
+
expected_active_release_id: identity.expectedActiveReleaseId,
|
|
11341
|
+
activation_id: identity.mutationId,
|
|
11342
|
+
expected_activation_id: identity.expectedActivationId
|
|
11343
|
+
})).digest("hex");
|
|
11344
|
+
}
|
|
11345
|
+
function activationResourceKey(deploymentId) {
|
|
11346
|
+
return `v1/frontend_release/${Buffer.from(deploymentId, "utf8").toString("base64url")}`;
|
|
11347
|
+
}
|
|
11348
|
+
function publicMutationReceipt(candidate, expected) {
|
|
11349
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11350
|
+
return null;
|
|
11351
|
+
const envelope = candidate;
|
|
11352
|
+
const mutation = envelope.mutation;
|
|
11353
|
+
const expectedKeys = [
|
|
11354
|
+
"project_ref",
|
|
11355
|
+
"mutation_id",
|
|
11356
|
+
"operation",
|
|
11357
|
+
"resource_key",
|
|
11358
|
+
"request_fingerprint",
|
|
11359
|
+
"principal",
|
|
11360
|
+
"status",
|
|
11361
|
+
"checkpoint",
|
|
11362
|
+
"receipt",
|
|
11363
|
+
"response_status",
|
|
11364
|
+
"failure_code",
|
|
11365
|
+
"lease",
|
|
11366
|
+
"completed_at",
|
|
11367
|
+
"created_at",
|
|
11368
|
+
"updated_at"
|
|
11369
|
+
];
|
|
11370
|
+
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"))
|
|
11371
|
+
return null;
|
|
11372
|
+
return {
|
|
11373
|
+
operation: mutation.operation,
|
|
11374
|
+
status: mutation.status,
|
|
11375
|
+
responseStatus: mutation.response_status,
|
|
11376
|
+
failureCode: mutation.failure_code
|
|
11377
|
+
};
|
|
11378
|
+
}
|
|
11379
|
+
function activationReceipt(candidate, input) {
|
|
11380
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
11381
|
+
return null;
|
|
11382
|
+
const record = candidate;
|
|
11383
|
+
const release = releaseRecord(record.release);
|
|
11384
|
+
const mutation = record.mutation;
|
|
11385
|
+
if (!release || !mutation || !exactKeys(record, [
|
|
11386
|
+
"project_ref",
|
|
11387
|
+
"deployment_id",
|
|
11388
|
+
"active_release_id",
|
|
11389
|
+
"activation_id",
|
|
11390
|
+
"release",
|
|
11391
|
+
"mutation"
|
|
11392
|
+
]) || !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")
|
|
11393
|
+
return null;
|
|
11394
|
+
return release;
|
|
11395
|
+
}
|
|
11396
|
+
async function activeReleaseReadback(http, identity) {
|
|
11397
|
+
const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
|
|
11398
|
+
const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
|
|
11399
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
11400
|
+
});
|
|
11401
|
+
const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
|
|
11402
|
+
projectRef: identity.projectRef,
|
|
11403
|
+
deploymentId: identity.deploymentId
|
|
11404
|
+
}) : null;
|
|
11405
|
+
if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
|
|
11406
|
+
return { release: null, status: inventoryRead.status };
|
|
11407
|
+
}
|
|
11408
|
+
const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
11409
|
+
const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
|
|
11410
|
+
projectRef: identity.projectRef,
|
|
11411
|
+
deploymentId: identity.deploymentId,
|
|
11412
|
+
releaseId: identity.releaseId
|
|
11413
|
+
}) : null;
|
|
11414
|
+
return { release, status: releaseRead.status };
|
|
11415
|
+
}
|
|
11416
|
+
async function activateFrontendRelease(http, input) {
|
|
11417
|
+
const mutationId = input.mutationId;
|
|
11418
|
+
if (!MUTATION_ID_PATTERN.test(mutationId))
|
|
11419
|
+
throw new Error("'mutation_id' must be a UUIDv4");
|
|
11420
|
+
if (input.expectedActiveReleaseId !== "absent" && !RELEASE_ID_PATTERN.test(input.expectedActiveReleaseId)) {
|
|
11421
|
+
throw new Error("'expected_active_release_id' is invalid");
|
|
11422
|
+
}
|
|
11423
|
+
if (input.expectedActivationId !== "absent" && !MUTATION_ID_PATTERN.test(input.expectedActivationId)) {
|
|
11424
|
+
throw new Error("'expected_activation_id' is invalid");
|
|
11425
|
+
}
|
|
11426
|
+
const endpoint = `${releasePath(input.projectRef, input.deploymentId, input.releaseId)}/activate`;
|
|
11427
|
+
const response = await http.post(endpoint, {
|
|
11428
|
+
expected_active_release_id: input.expectedActiveReleaseId,
|
|
11429
|
+
expected_activation_id: input.expectedActivationId,
|
|
11430
|
+
mutation_id: mutationId
|
|
11431
|
+
}, {
|
|
11432
|
+
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
11433
|
+
responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
|
|
11434
|
+
});
|
|
11435
|
+
const release = activationReceipt(response.data, { ...input, mutationId });
|
|
11436
|
+
if (!response.ok || !release) {
|
|
11437
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
11438
|
+
return releaseFailure("frontend.activate_release", "HTTP_ERROR", response.status);
|
|
11439
|
+
}
|
|
11440
|
+
return activationReadback(http, input, mutationId, response.status);
|
|
11441
|
+
}
|
|
11442
|
+
const readback = await activeReleaseReadback(http, { ...input, mutationId });
|
|
11443
|
+
if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
|
|
11444
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
|
|
11445
|
+
}
|
|
11446
|
+
return toolResponse({
|
|
11447
|
+
project_ref: input.projectRef,
|
|
11448
|
+
deployment_id: input.deploymentId,
|
|
11449
|
+
active_release_id: input.releaseId,
|
|
11450
|
+
activation_id: mutationId,
|
|
11451
|
+
release: readback.release
|
|
11452
|
+
});
|
|
11453
|
+
}
|
|
11454
|
+
async function activationReadback(http, input, mutationId, activationStatus) {
|
|
11455
|
+
const mutationRead = await http.get(`/v1/projects/${encodeURIComponent(input.projectRef)}/mutations/${mutationId}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
11456
|
+
const mutationEnvelope = mutationRead.data;
|
|
11457
|
+
const mutation = mutationRead.ok ? publicMutationReceipt(mutationRead.data, {
|
|
11458
|
+
projectRef: input.projectRef,
|
|
11459
|
+
mutationId,
|
|
11460
|
+
resourceKey: activationResourceKey(input.deploymentId),
|
|
11461
|
+
requestFingerprint: activationFingerprint({ ...input, mutationId })
|
|
11462
|
+
}) : null;
|
|
11463
|
+
if (!mutation || mutationEnvelope?.project_ref !== input.projectRef || mutation.operation !== "frontend.release.activate" || mutation.status !== "succeeded" || mutation.responseStatus !== 200 || mutation.failureCode !== null) {
|
|
11464
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
|
|
11465
|
+
}
|
|
11466
|
+
const readback = await activeReleaseReadback(http, { ...input, mutationId });
|
|
11467
|
+
if (!readback.release) {
|
|
11468
|
+
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", activationStatus);
|
|
11469
|
+
}
|
|
11470
|
+
return toolResponse({
|
|
11471
|
+
project_ref: input.projectRef,
|
|
11472
|
+
deployment_id: input.deploymentId,
|
|
11473
|
+
active_release_id: input.releaseId,
|
|
11474
|
+
activation_id: mutationId,
|
|
11475
|
+
release: readback.release
|
|
11476
|
+
});
|
|
11477
|
+
}
|
|
11478
|
+
|
|
11479
|
+
// src/shared/tools/frontend-tools.ts
|
|
11037
11480
|
function registerFrontendTools(server, http) {
|
|
11038
|
-
server.tool("frontend", `Frontend hosting
|
|
11039
|
-
Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records`, {
|
|
11481
|
+
server.tool("frontend", `Frontend hosting and immutable prebuilt releases. Supports: static, react, vue, svelte, sveltekit, sveltekit-static, nextjs, nuxt, astro.
|
|
11482
|
+
Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy, build_logs, add_domain, remove_domain, set_env, list_frameworks, list_records, list_releases, get_release, upload_release, activate_release`, {
|
|
11040
11483
|
action: withDescription(stringEnum([
|
|
11041
11484
|
"list",
|
|
11042
11485
|
"get",
|
|
@@ -11051,7 +11494,11 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11051
11494
|
"remove_domain",
|
|
11052
11495
|
"set_env",
|
|
11053
11496
|
"list_frameworks",
|
|
11054
|
-
"list_records"
|
|
11497
|
+
"list_records",
|
|
11498
|
+
"list_releases",
|
|
11499
|
+
"get_release",
|
|
11500
|
+
"upload_release",
|
|
11501
|
+
"activate_release"
|
|
11055
11502
|
]), "Action"),
|
|
11056
11503
|
ref: optional(Type.String(), "Project ref"),
|
|
11057
11504
|
id: optional(Type.String(), "Deployment ID"),
|
|
@@ -11066,9 +11513,37 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11066
11513
|
env_vars: optional(Type.Record(Type.String(), Type.String()), "[create/update/set_env] Environment variables"),
|
|
11067
11514
|
git_url: optional(Type.String(), "[deploy_git] Git repository URL"),
|
|
11068
11515
|
branch: optional(Type.String(), "[deploy_git] Branch (default: main)"),
|
|
11069
|
-
zip_path: optional(Type.String(), "[deploy_upload] Local
|
|
11516
|
+
zip_path: optional(Type.String(), "[deploy_upload/upload_release] Local ZIP file path"),
|
|
11517
|
+
release_id: optional(Type.String(), "[get_release/activate_release] SHA-256 release ID"),
|
|
11518
|
+
expected_active_release_id: optional(Type.String(), "[activate_release] Current release SHA-256 or absent"),
|
|
11519
|
+
expected_activation_id: optional(Type.String(), "[activate_release] Current activation UUIDv4 or absent"),
|
|
11520
|
+
mutation_id: optional(Type.String(), "[activate_release] Required retry-stable UUIDv4"),
|
|
11521
|
+
cursor: optional(Type.String(), "[list_releases] Last release SHA-256 cursor"),
|
|
11522
|
+
limit: optional(Type.Number(), "[list_releases] Page size, 1-100 (default 50)")
|
|
11070
11523
|
}, async (args) => {
|
|
11071
|
-
const {
|
|
11524
|
+
const {
|
|
11525
|
+
action,
|
|
11526
|
+
ref,
|
|
11527
|
+
id,
|
|
11528
|
+
name,
|
|
11529
|
+
framework,
|
|
11530
|
+
domain,
|
|
11531
|
+
build_command,
|
|
11532
|
+
output_dir,
|
|
11533
|
+
install_command,
|
|
11534
|
+
node_version,
|
|
11535
|
+
health_check_path,
|
|
11536
|
+
env_vars,
|
|
11537
|
+
git_url,
|
|
11538
|
+
branch,
|
|
11539
|
+
zip_path,
|
|
11540
|
+
release_id,
|
|
11541
|
+
expected_active_release_id,
|
|
11542
|
+
expected_activation_id,
|
|
11543
|
+
mutation_id,
|
|
11544
|
+
cursor,
|
|
11545
|
+
limit
|
|
11546
|
+
} = args;
|
|
11072
11547
|
const need = (f, v) => {
|
|
11073
11548
|
if (!v)
|
|
11074
11549
|
throw new Error(`'${f}' required for '${action}'`);
|
|
@@ -11175,6 +11650,35 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11175
11650
|
need("id", id);
|
|
11176
11651
|
text = ok(await http.get(`/v1/projects/${ref}/frontend/deployments/${id}/records`));
|
|
11177
11652
|
break;
|
|
11653
|
+
case "list_releases":
|
|
11654
|
+
need("ref", ref);
|
|
11655
|
+
need("id", id);
|
|
11656
|
+
return listFrontendReleases(http, ref, id, cursor, limit);
|
|
11657
|
+
case "get_release":
|
|
11658
|
+
need("ref", ref);
|
|
11659
|
+
need("id", id);
|
|
11660
|
+
need("release_id", release_id);
|
|
11661
|
+
return getFrontendRelease(http, ref, id, release_id);
|
|
11662
|
+
case "upload_release":
|
|
11663
|
+
need("ref", ref);
|
|
11664
|
+
need("id", id);
|
|
11665
|
+
need("zip_path", zip_path);
|
|
11666
|
+
return uploadFrontendRelease(http, ref, id, zip_path);
|
|
11667
|
+
case "activate_release":
|
|
11668
|
+
need("ref", ref);
|
|
11669
|
+
need("id", id);
|
|
11670
|
+
need("release_id", release_id);
|
|
11671
|
+
need("expected_active_release_id", expected_active_release_id);
|
|
11672
|
+
need("expected_activation_id", expected_activation_id);
|
|
11673
|
+
need("mutation_id", mutation_id);
|
|
11674
|
+
return activateFrontendRelease(http, {
|
|
11675
|
+
projectRef: ref,
|
|
11676
|
+
deploymentId: id,
|
|
11677
|
+
releaseId: release_id,
|
|
11678
|
+
expectedActiveReleaseId: expected_active_release_id,
|
|
11679
|
+
expectedActivationId: expected_activation_id,
|
|
11680
|
+
mutationId: mutation_id
|
|
11681
|
+
});
|
|
11178
11682
|
default:
|
|
11179
11683
|
text = `❌ Unknown action`;
|
|
11180
11684
|
}
|
|
@@ -11184,7 +11688,7 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
|
|
|
11184
11688
|
|
|
11185
11689
|
// src/shared/tools/project-read-projection.ts
|
|
11186
11690
|
var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
|
|
11187
|
-
var
|
|
11691
|
+
var PROJECT_REF_PATTERN4 = /^[a-z0-9-]{1,20}$/;
|
|
11188
11692
|
var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
11189
11693
|
var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
11190
11694
|
var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
@@ -11248,7 +11752,7 @@ function matchingText(candidate, maxLength, pattern) {
|
|
|
11248
11752
|
const candidateText = boundedText2(candidate, maxLength);
|
|
11249
11753
|
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
11250
11754
|
}
|
|
11251
|
-
function
|
|
11755
|
+
function canonicalTimestamp2(candidate) {
|
|
11252
11756
|
const timestamp = boundedText2(candidate, 64);
|
|
11253
11757
|
if (!timestamp)
|
|
11254
11758
|
return null;
|
|
@@ -11258,12 +11762,12 @@ function canonicalTimestamp(candidate) {
|
|
|
11258
11762
|
function projectedSummary(project) {
|
|
11259
11763
|
const summary = {
|
|
11260
11764
|
id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
11261
|
-
ref: matchingText(project.ref, 20,
|
|
11765
|
+
ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN4),
|
|
11262
11766
|
organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
11263
11767
|
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
11264
11768
|
name: boundedText2(project.name, 100),
|
|
11265
11769
|
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
11266
|
-
created_at:
|
|
11770
|
+
created_at: canonicalTimestamp2(project.created_at),
|
|
11267
11771
|
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
11268
11772
|
};
|
|
11269
11773
|
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
@@ -11382,7 +11886,7 @@ function projectGetRead(response, expectedRef) {
|
|
|
11382
11886
|
// src/shared/tools/project-endpoint-read.ts
|
|
11383
11887
|
var PROJECT_ENDPOINT_RESPONSE_MAX_BYTES = 256 * 1024;
|
|
11384
11888
|
var PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
11385
|
-
var
|
|
11889
|
+
var PROJECT_REF_PATTERN5 = /^[a-z0-9-]{1,20}$/;
|
|
11386
11890
|
var PROJECT_ENDPOINTS_SCHEMA = "supacloud.project-endpoints.v1";
|
|
11387
11891
|
var PROJECT_ENDPOINT_SOURCES = new Set([
|
|
11388
11892
|
"explicit_api_domain",
|
|
@@ -11452,7 +11956,7 @@ function projectEndpoint2(candidate) {
|
|
|
11452
11956
|
}
|
|
11453
11957
|
function projectEndpointProjection(candidate) {
|
|
11454
11958
|
const projection = plainRecord2(candidate);
|
|
11455
|
-
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !
|
|
11959
|
+
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN5.test(projection.project_ref))
|
|
11456
11960
|
return null;
|
|
11457
11961
|
const endpoints = plainRecord2(projection.endpoints);
|
|
11458
11962
|
if (!endpoints || !hasOnlyKeys2(endpoints, ENDPOINTS_KEYS))
|
|
@@ -12531,7 +13035,7 @@ function registerBranchTools(server, http, options = {}) {
|
|
|
12531
13035
|
// src/shared/tools/supabase-cli-tools.ts
|
|
12532
13036
|
import { spawn } from "node:child_process";
|
|
12533
13037
|
import { chmodSync, existsSync as existsSync5, mkdirSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12534
|
-
import { dirname, isAbsolute, join as join3, resolve as
|
|
13038
|
+
import { dirname, isAbsolute, join as join3, resolve as resolve4 } from "node:path";
|
|
12535
13039
|
var SENSITIVE_ENV_KEY = /(?:^|_)(?:PASSWORD|PASS|SECRET|TOKEN|KEY|CREDENTIALS?|AUTHORIZATION|AUTH|SESSION|COOKIE|BEARER|DB_URI|DB_URL|DSN|DATABASE_URL|DATABASE_URI|CONNECTION_STRING|CONNECTION_URI)(?:_|$)/i;
|
|
12536
13040
|
var VALID_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
12537
13041
|
var VALID_MIGRATION_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,100}$/;
|
|
@@ -12575,12 +13079,12 @@ function schemaArguments(schema) {
|
|
|
12575
13079
|
function workdirArguments(workdir) {
|
|
12576
13080
|
if (!workdir)
|
|
12577
13081
|
throw new Error("A workdir is required");
|
|
12578
|
-
return ["--workdir",
|
|
13082
|
+
return ["--workdir", resolve4(workdir)];
|
|
12579
13083
|
}
|
|
12580
13084
|
function resolveOutputPath(workdir, file, label) {
|
|
12581
13085
|
if (!file || /[\r\n\0]/.test(file))
|
|
12582
13086
|
throw new Error(`${label} file is required`);
|
|
12583
|
-
return isAbsolute(file) ?
|
|
13087
|
+
return isAbsolute(file) ? resolve4(file) : resolve4(workdir, file);
|
|
12584
13088
|
}
|
|
12585
13089
|
function databaseTargetArguments(databaseUrl) {
|
|
12586
13090
|
return databaseUrl ? ["--db-url", requirePostgresUrl(databaseUrl)] : ["--local"];
|
|
@@ -12665,7 +13169,7 @@ function actionArguments(request, workdir) {
|
|
|
12665
13169
|
function buildOfficialSupabaseArgs(request) {
|
|
12666
13170
|
if (request.action === "version")
|
|
12667
13171
|
return ["--version"];
|
|
12668
|
-
const workdir = request.workdir ?
|
|
13172
|
+
const workdir = request.workdir ? resolve4(request.workdir) : undefined;
|
|
12669
13173
|
if (!workdir)
|
|
12670
13174
|
throw new Error("A workdir is required");
|
|
12671
13175
|
return [...actionArguments(request, workdir), ...workdirArguments(workdir)];
|
|
@@ -12711,13 +13215,13 @@ function resolveOfficialSupabaseCommand(workdir, environment = process.env) {
|
|
|
12711
13215
|
}
|
|
12712
13216
|
return ["npx", "--yes", `supabase@${version}`];
|
|
12713
13217
|
}
|
|
12714
|
-
const localPackageEntry = join3(
|
|
13218
|
+
const localPackageEntry = join3(resolve4(workdir), "node_modules", "supabase", "dist", "supabase.js");
|
|
12715
13219
|
if (existsSync5(localPackageEntry))
|
|
12716
13220
|
return [process.execPath, localPackageEntry];
|
|
12717
13221
|
return ["supabase"];
|
|
12718
13222
|
}
|
|
12719
13223
|
function resolveExistingWorkdir(workdirInput, fallback) {
|
|
12720
|
-
const workdir =
|
|
13224
|
+
const workdir = resolve4(workdirInput || fallback);
|
|
12721
13225
|
if (!existsSync5(workdir) || !statSync3(workdir).isDirectory()) {
|
|
12722
13226
|
throw new Error(`Supabase workdir not found: ${workdir}`);
|
|
12723
13227
|
}
|
|
@@ -12831,7 +13335,7 @@ async function executeMigrationPush(request, runtime) {
|
|
|
12831
13335
|
if (!projectRef2)
|
|
12832
13336
|
return missingProjectRefResult();
|
|
12833
13337
|
const workdir = resolveExistingWorkdir(request.workdir, runtime.fallbackWorkdir);
|
|
12834
|
-
const migrationDirectory =
|
|
13338
|
+
const migrationDirectory = resolve4(workdir, request.dir || "supabase/migrations");
|
|
12835
13339
|
const migrationResponse = await pushMigrations({
|
|
12836
13340
|
action: "push_migrations",
|
|
12837
13341
|
ref: projectRef2,
|
|
@@ -12915,9 +13419,9 @@ function registerSupabaseCliTools(server, options = {}) {
|
|
|
12915
13419
|
// src/shared/tools/lite-cli-tools.ts
|
|
12916
13420
|
import { spawn as spawn2 } from "node:child_process";
|
|
12917
13421
|
import { existsSync as existsSync6, statSync as statSync4 } from "node:fs";
|
|
12918
|
-
import { join as join4, resolve as
|
|
13422
|
+
import { join as join4, resolve as resolve5 } from "node:path";
|
|
12919
13423
|
function requireWorkdir(workdir, fallback) {
|
|
12920
|
-
const resolved =
|
|
13424
|
+
const resolved = resolve5(workdir || fallback);
|
|
12921
13425
|
if (!existsSync6(resolved) || !statSync4(resolved).isDirectory()) {
|
|
12922
13426
|
throw new Error(`Lite workdir not found: ${resolved}`);
|
|
12923
13427
|
}
|
|
@@ -13003,7 +13507,7 @@ function resolveLiteCommand(workdir, environment = process.env) {
|
|
|
13003
13507
|
throw new Error("Invalid SUPACLOUD_LITE_CLI_BIN");
|
|
13004
13508
|
return [explicitBinary];
|
|
13005
13509
|
}
|
|
13006
|
-
const localPackageEntry = join4(
|
|
13510
|
+
const localPackageEntry = join4(resolve5(workdir), "node_modules", "@supacloud", "lite", "dist", "launcher.cjs");
|
|
13007
13511
|
if (existsSync6(localPackageEntry))
|
|
13008
13512
|
return [process.execPath, localPackageEntry];
|
|
13009
13513
|
return ["supacloud-lite"];
|
|
@@ -13151,7 +13655,7 @@ import {
|
|
|
13151
13655
|
rmSync as rmSync2
|
|
13152
13656
|
} from "node:fs";
|
|
13153
13657
|
import { homedir as homedir2 } from "node:os";
|
|
13154
|
-
import { dirname as dirname2, join as join5, relative as relative2, resolve as
|
|
13658
|
+
import { dirname as dirname2, join as join5, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
|
|
13155
13659
|
import { fileURLToPath } from "node:url";
|
|
13156
13660
|
var SKILL_NAME = "supacloud-cli";
|
|
13157
13661
|
function regularFiles(rootDirectory, currentDirectory = rootDirectory) {
|
|
@@ -13224,8 +13728,8 @@ function replaceSkill(sourceDirectory, targetRoot, destinationDirectory, backupD
|
|
|
13224
13728
|
}
|
|
13225
13729
|
}
|
|
13226
13730
|
function skillSummary(request, action, files, backupDirectory) {
|
|
13227
|
-
const sourceDirectory =
|
|
13228
|
-
const targetRoot =
|
|
13731
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13732
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13229
13733
|
return {
|
|
13230
13734
|
name: SKILL_NAME,
|
|
13231
13735
|
sourceDirectory,
|
|
@@ -13239,8 +13743,8 @@ function skillSummary(request, action, files, backupDirectory) {
|
|
|
13239
13743
|
};
|
|
13240
13744
|
}
|
|
13241
13745
|
function installSkill(request) {
|
|
13242
|
-
const sourceDirectory =
|
|
13243
|
-
const targetRoot =
|
|
13746
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13747
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13244
13748
|
const destinationDirectory = join5(targetRoot, SKILL_NAME);
|
|
13245
13749
|
if (!existsSync7(join5(sourceDirectory, "SKILL.md"))) {
|
|
13246
13750
|
throw new Error(`Bundled SupaCloud CLI skill not found: ${sourceDirectory}`);
|
|
@@ -13257,16 +13761,16 @@ function installSkill(request) {
|
|
|
13257
13761
|
return installReplacementSkill(request, files);
|
|
13258
13762
|
}
|
|
13259
13763
|
function installNewSkill(request, files) {
|
|
13260
|
-
const sourceDirectory =
|
|
13261
|
-
const targetRoot =
|
|
13764
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13765
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13262
13766
|
if (request.mode === "write") {
|
|
13263
13767
|
createSkill(sourceDirectory, targetRoot, join5(targetRoot, SKILL_NAME));
|
|
13264
13768
|
}
|
|
13265
13769
|
return skillSummary(request, "create", files, null);
|
|
13266
13770
|
}
|
|
13267
13771
|
function installReplacementSkill(request, files) {
|
|
13268
|
-
const sourceDirectory =
|
|
13269
|
-
const targetRoot =
|
|
13772
|
+
const sourceDirectory = resolve6(request.sourceDirectory);
|
|
13773
|
+
const targetRoot = resolve6(request.targetRoot);
|
|
13270
13774
|
const destinationDirectory = join5(targetRoot, SKILL_NAME);
|
|
13271
13775
|
const backupDirectory = availableBackupDirectory(destinationDirectory, request.now);
|
|
13272
13776
|
if (request.mode === "write") {
|
|
@@ -13276,13 +13780,13 @@ function installReplacementSkill(request, files) {
|
|
|
13276
13780
|
}
|
|
13277
13781
|
function resolveDefaultCodexSkillRoot(environment = process.env, homeDirectory = homedir2()) {
|
|
13278
13782
|
const codexHome = environment.CODEX_HOME?.trim();
|
|
13279
|
-
return join5(
|
|
13783
|
+
return join5(resolve6(codexHome || join5(homeDirectory, ".codex")), "skills");
|
|
13280
13784
|
}
|
|
13281
13785
|
function resolveBundledSkillDirectory(moduleUrl = import.meta.url) {
|
|
13282
13786
|
const moduleDirectory = dirname2(fileURLToPath(moduleUrl));
|
|
13283
13787
|
const candidates = [
|
|
13284
|
-
|
|
13285
|
-
|
|
13788
|
+
resolve6(moduleDirectory, "../../../skills", SKILL_NAME),
|
|
13789
|
+
resolve6(moduleDirectory, "../skills", SKILL_NAME)
|
|
13286
13790
|
];
|
|
13287
13791
|
const skillDirectory = candidates.find((candidate) => existsSync7(join5(candidate, "SKILL.md")));
|
|
13288
13792
|
if (!skillDirectory)
|
|
@@ -13322,7 +13826,7 @@ function registerAiTools(server) {
|
|
|
13322
13826
|
// src/shared/tools/scheduled-function-tools.ts
|
|
13323
13827
|
import { randomUUID } from "node:crypto";
|
|
13324
13828
|
import { readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
|
|
13325
|
-
import { resolve as
|
|
13829
|
+
import { resolve as resolve7 } from "node:path";
|
|
13326
13830
|
import { isDeepStrictEqual } from "node:util";
|
|
13327
13831
|
var HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
|
|
13328
13832
|
var ENVIRONMENT_NAME_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
@@ -13406,7 +13910,7 @@ function validScheduledFunctionCron(expression) {
|
|
|
13406
13910
|
function readScheduleBodyFile(bodyPathInput) {
|
|
13407
13911
|
if (!bodyPathInput.trim())
|
|
13408
13912
|
throw new Error("'body_file' must be a path");
|
|
13409
|
-
const bodyPath =
|
|
13913
|
+
const bodyPath = resolve7(bodyPathInput);
|
|
13410
13914
|
const bodyStat = statSync5(bodyPath);
|
|
13411
13915
|
if (!bodyStat.isFile() || bodyStat.size > MAX_BODY_FILE_BYTES) {
|
|
13412
13916
|
throw new Error("Scheduled Function body file must be a regular file no larger than 1 MiB");
|
|
@@ -13752,14 +14256,14 @@ var SCHEDULE_TOOL_SCHEMA = {
|
|
|
13752
14256
|
};
|
|
13753
14257
|
|
|
13754
14258
|
// src/shared/mutation-protocol.ts
|
|
13755
|
-
var
|
|
14259
|
+
var MUTATION_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
13756
14260
|
var FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
|
|
13757
14261
|
var OPERATION_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
|
|
13758
14262
|
var RESOURCE_KEY_PATTERN = /^v1\/(?:[a-z0-9][a-z0-9._-]{0,63})\/([A-Za-z0-9_-]{2,171})$/;
|
|
13759
14263
|
var RESOURCE_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
13760
14264
|
var FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
13761
14265
|
var LEASE_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,254}$/;
|
|
13762
|
-
var
|
|
14266
|
+
var TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
13763
14267
|
var MAX_STATUS_RESPONSE_BYTES = 196608;
|
|
13764
14268
|
var MAX_RESOURCE_ID_BYTES = 128;
|
|
13765
14269
|
var FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -13792,7 +14296,7 @@ var MUTATION_KEYS = [
|
|
|
13792
14296
|
var PRINCIPAL_KEYS = ["type", "id"];
|
|
13793
14297
|
var LEASE_KEYS = ["owner", "expires_at", "fencing_epoch"];
|
|
13794
14298
|
function isMutationId(candidate) {
|
|
13795
|
-
return typeof candidate === "string" &&
|
|
14299
|
+
return typeof candidate === "string" && MUTATION_ID_PATTERN2.test(candidate);
|
|
13796
14300
|
}
|
|
13797
14301
|
function objectRecord4(candidate) {
|
|
13798
14302
|
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
@@ -13807,14 +14311,14 @@ function emptyProjection(candidate) {
|
|
|
13807
14311
|
const record = objectRecord4(candidate);
|
|
13808
14312
|
return record && Object.keys(record).length === 0 ? record : null;
|
|
13809
14313
|
}
|
|
13810
|
-
function
|
|
13811
|
-
if (typeof candidate !== "string" || !
|
|
14314
|
+
function canonicalTimestamp3(candidate) {
|
|
14315
|
+
if (typeof candidate !== "string" || !TIMESTAMP_PATTERN2.test(candidate))
|
|
13812
14316
|
return false;
|
|
13813
14317
|
const milliseconds = Date.parse(candidate);
|
|
13814
14318
|
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
13815
14319
|
}
|
|
13816
14320
|
function nullableTimestamp(candidate) {
|
|
13817
|
-
return candidate === null ||
|
|
14321
|
+
return candidate === null || canonicalTimestamp3(candidate);
|
|
13818
14322
|
}
|
|
13819
14323
|
function safePrincipal(candidate) {
|
|
13820
14324
|
const principal = exactRecord(candidate, PRINCIPAL_KEYS);
|
|
@@ -13897,7 +14401,7 @@ function validMutationTerminalFields(mutation) {
|
|
|
13897
14401
|
return false;
|
|
13898
14402
|
if (mutation.failure_code !== null && (typeof mutation.failure_code !== "string" || !FAILURE_CODE_PATTERN.test(mutation.failure_code)))
|
|
13899
14403
|
return false;
|
|
13900
|
-
return nullableTimestamp(mutation.completed_at) &&
|
|
14404
|
+
return nullableTimestamp(mutation.completed_at) && canonicalTimestamp3(mutation.created_at) && canonicalTimestamp3(mutation.updated_at);
|
|
13901
14405
|
}
|
|
13902
14406
|
function safeMutationStatus(candidate) {
|
|
13903
14407
|
const mutation = exactRecord(candidate, MUTATION_KEYS);
|
|
@@ -14005,7 +14509,7 @@ var RELEASE_CANARY_CLAIM_MAX_LENGTH = 2048;
|
|
|
14005
14509
|
function isRecord3(value) {
|
|
14006
14510
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14007
14511
|
}
|
|
14008
|
-
function
|
|
14512
|
+
function canonicalTimestamp4(value) {
|
|
14009
14513
|
if (typeof value !== "string")
|
|
14010
14514
|
return false;
|
|
14011
14515
|
const parsed = new Date(value);
|
|
@@ -14018,7 +14522,7 @@ function backupBelongsToProject(backupId, projectRef2) {
|
|
|
14018
14522
|
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef2}_`);
|
|
14019
14523
|
}
|
|
14020
14524
|
function verifiedBackup(value, projectRef2) {
|
|
14021
|
-
if (!isRecord3(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !
|
|
14525
|
+
if (!isRecord3(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp4(value.created_at) || !canonicalTimestamp4(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))
|
|
14022
14526
|
return null;
|
|
14023
14527
|
return {
|
|
14024
14528
|
backup_id: value.backup_id,
|
|
@@ -14365,7 +14869,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
14365
14869
|
// package.json
|
|
14366
14870
|
var package_default = {
|
|
14367
14871
|
name: "@supacloud/cli",
|
|
14368
|
-
version: "0.
|
|
14872
|
+
version: "0.34.0",
|
|
14369
14873
|
description: "Project-scoped CLI for SupaCloud users",
|
|
14370
14874
|
type: "module",
|
|
14371
14875
|
main: "./dist/index.js",
|
|
@@ -14742,6 +15246,11 @@ function createCliTools(context, confirmProduction) {
|
|
|
14742
15246
|
if (branchHelpTool) {
|
|
14743
15247
|
tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
|
|
14744
15248
|
}
|
|
15249
|
+
const frontendContextCallback = tools.frontend.callback;
|
|
15250
|
+
const frontendHelpTool = captureTools((server) => registerFrontendTools(server, {})).frontend;
|
|
15251
|
+
if (frontendHelpTool) {
|
|
15252
|
+
tools.frontend = { schema: frontendHelpTool.schema, callback: frontendContextCallback };
|
|
15253
|
+
}
|
|
14745
15254
|
};
|
|
14746
15255
|
if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
|
|
14747
15256
|
registerContextAwareHelp();
|