@supacloud/cli 0.16.0 → 0.18.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 +70 -10
- package/dist/index.js +587 -155
- package/package.json +1 -1
- package/skills/supacloud-cli/references/command-map.md +1 -1
package/README.md
CHANGED
|
@@ -155,10 +155,11 @@ supacloud-cli frontend list --ref abc123
|
|
|
155
155
|
supacloud-cli branch create --name feature-orders --data_mode schema_only
|
|
156
156
|
supacloud-cli branch promotion_plan --branch_ref preview123
|
|
157
157
|
supacloud-cli branch promote --branch_ref preview123 --plan_checksum <sha256>
|
|
158
|
-
supacloud-cli edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
|
|
159
|
-
supacloud-cli edge_functions
|
|
160
|
-
supacloud-cli edge_functions
|
|
161
|
-
supacloud-cli edge_functions
|
|
158
|
+
supacloud-cli edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent
|
|
159
|
+
supacloud-cli edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
|
|
160
|
+
supacloud-cli edge_functions deploy_bundle --ref abc123 --slug hello --files '{"index.ts":"export default { fetch: () => new Response(\"ok\") }"}' --expected-active-version 7
|
|
161
|
+
supacloud-cli edge_functions source --ref abc123 --slug hello --version 7 --output ./hello-v7.ts
|
|
162
|
+
supacloud-cli edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 8
|
|
162
163
|
supacloud-cli scheduled_functions list --ref abc123
|
|
163
164
|
supacloud-cli secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
|
|
164
165
|
supacloud-cli storage list_buckets --ref abc123
|
|
@@ -166,8 +167,10 @@ supacloud-cli storage get_bucket --ref abc123 --bucket reports
|
|
|
166
167
|
supacloud-cli storage create_bucket --ref abc123 --bucket reports --public false \
|
|
167
168
|
--file_size_limit 10485760 --allowed_mime_types "application/pdf,image/png"
|
|
168
169
|
supacloud-cli storage update_bucket --ref abc123 --bucket reports \
|
|
170
|
+
--expected_revision <revision-from-get_bucket> \
|
|
169
171
|
--allowed_mime_types '["application/pdf"]'
|
|
170
|
-
supacloud-cli storage delete_bucket --ref abc123 --bucket reports
|
|
172
|
+
supacloud-cli storage delete_bucket --ref abc123 --bucket reports \
|
|
173
|
+
--expected_revision <revision-from-get_bucket> --require_empty true
|
|
171
174
|
```
|
|
172
175
|
|
|
173
176
|
`database migration_inventory` reads the canonical migration ledger through the
|
|
@@ -178,14 +181,44 @@ drift, and statement-count mismatches instead of treating them as an empty
|
|
|
178
181
|
ledger. `database list_migrations` remains available with its legacy SQL-backed,
|
|
179
182
|
human-readable behavior.
|
|
180
183
|
|
|
184
|
+
Bucket list/get output includes the metadata `revision`. Update and delete reject
|
|
185
|
+
stale revisions with HTTP 409. Delete additionally requires `require_empty=true`;
|
|
186
|
+
it never empties a bucket. Mutation receipts bind `project_ref`, `bucket_id`,
|
|
187
|
+
`previous_revision`, and `new_revision` (`null` after delete).
|
|
188
|
+
|
|
189
|
+
`database push_migrations` rejects a remote row that reuses a local migration
|
|
190
|
+
name with another version, or a local version with another name, before either
|
|
191
|
+
dry-run reporting or apply can continue. This keeps the preview consistent with
|
|
192
|
+
the server conflict that would otherwise occur after deployment starts.
|
|
193
|
+
|
|
181
194
|
`edge_functions deploy --path` bundles local TypeScript and dependencies with
|
|
182
195
|
Bun and runs a local syntax check before upload. The Management API validates and
|
|
183
196
|
normalizes the final server-side artifact against the multi-tenant Edge Runtime
|
|
184
197
|
module policy consistently for CLI, Web Console, and direct API deployments.
|
|
198
|
+
For an artifact already built and validated by release automation, use
|
|
199
|
+
`deploy --prebundled-path <file> --expected-sha256 <64-lowercase-hex>`. The CLI
|
|
200
|
+
holds the opened regular file while reading it, rejects metadata drift, invalid
|
|
201
|
+
UTF-8, or a caller-hash mismatch before HTTP, and never passes the artifact in
|
|
202
|
+
the process argument list. The Management API validates the hash and runtime
|
|
203
|
+
policy again, rejects any normalization that would change the code, and stores
|
|
204
|
+
the submitted bytes unchanged as both immutable source and runtime artifact.
|
|
205
|
+
`--prebundled-path` is mutually exclusive with `--path`, `--code`, and
|
|
206
|
+
`--minify`.
|
|
185
207
|
`deploy_bundle --files` accepts a JSON object in shell usage.
|
|
186
208
|
Use `source --output <file>` for large Functions so terminal or automation output
|
|
187
209
|
limits cannot truncate the original TS/JS source code. The destination must not
|
|
188
|
-
already exist.
|
|
210
|
+
already exist. Add the positive version observed from `list` as
|
|
211
|
+
`source --version <N>` to read the immutable release instead of the moving active
|
|
212
|
+
pointer; this remains correct across an active-version A→B→A transition.
|
|
213
|
+
|
|
214
|
+
`deploy`, `deploy_bundle`, and `activate` require
|
|
215
|
+
`--expected-active-version <N|absent>`. Read the current non-negative integer
|
|
216
|
+
version from `edge_functions list`; use `0` for a listed legacy Function and
|
|
217
|
+
`absent` only when creating a slug that does not yet exist. A stale value returns
|
|
218
|
+
HTTP 409 without building, preheating, or activating another version. List output remains a JSON array with string
|
|
219
|
+
`slug` and numeric `version` fields, while source output is exactly
|
|
220
|
+
`{ "code": "..." }`. Release automation must use `source --version <N>` for a
|
|
221
|
+
version-bound backup.
|
|
189
222
|
|
|
190
223
|
`edge_functions activate` restores an existing immutable Function version and
|
|
191
224
|
returns a machine-readable receipt containing the activated version and JWT
|
|
@@ -195,28 +228,55 @@ server response body.
|
|
|
195
228
|
Mutation receipts use schema `supacloud.cli.release-control.v1`. An
|
|
196
229
|
`OUTCOME_UNKNOWN` error means the server may have committed the mutation before
|
|
197
230
|
the response was lost or failed validation; read back current state before any
|
|
198
|
-
retry.
|
|
231
|
+
retry. For Function deploy, bundle deploy, and activation, the CLI applies a
|
|
232
|
+
separate 5-second, 64 KiB response-body boundary after receiving HTTP headers.
|
|
233
|
+
A stalled, oversized, truncated, unreadable, or malformed body is always
|
|
234
|
+
`OUTCOME_UNKNOWN` and its content is never included in CLI output.
|
|
235
|
+
Version `0` is reserved as the active-version CAS token for legacy Functions. It
|
|
236
|
+
can be passed only as `--expected-active-version`; immutable source reads and
|
|
237
|
+
activation targets still require a positive version.
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"schema": "supacloud.cli.release-control.v1",
|
|
242
|
+
"ok": true,
|
|
243
|
+
"operation": "edge_functions.deploy_bundle",
|
|
244
|
+
"project_ref": "abc123",
|
|
245
|
+
"slug": "hello",
|
|
246
|
+
"previous_active_version": "7",
|
|
247
|
+
"active_version": "8",
|
|
248
|
+
"version": "8",
|
|
249
|
+
"verify_jwt": true
|
|
250
|
+
}
|
|
251
|
+
```
|
|
199
252
|
|
|
200
253
|
Scheduled Function lifecycle operations are also project-scoped:
|
|
201
254
|
|
|
202
255
|
```bash
|
|
203
256
|
supacloud-cli scheduled_functions create --ref abc123 --name nightly \
|
|
204
257
|
--slug cleanup --cron "0 2 * * *" --method POST
|
|
258
|
+
supacloud-cli scheduled_functions get --ref abc123 --schedule_id <id>
|
|
205
259
|
supacloud-cli scheduled_functions update --ref abc123 --schedule_id <id> \
|
|
206
|
-
--cron "0 3 * * *"
|
|
207
|
-
supacloud-cli scheduled_functions delete --ref abc123 --schedule_id <id>
|
|
260
|
+
--expected_updated_at <updated_at-from-list> --cron "0 3 * * *"
|
|
261
|
+
supacloud-cli scheduled_functions delete --ref abc123 --schedule_id <id> \
|
|
262
|
+
--expected_updated_at <updated_at-from-list>
|
|
208
263
|
```
|
|
209
264
|
|
|
210
265
|
Schedule IDs are canonical UUIDv4 values returned by create/list. Cron values
|
|
211
266
|
use bounded numeric five-field syntax with wildcards, lists, ranges, and steps;
|
|
212
267
|
out-of-range endpoints and steps are rejected before HTTP dispatch.
|
|
268
|
+
Update and delete require the exact canonical UTC `updated_at` returned by list.
|
|
269
|
+
A stale revision fails with HTTP 409 and performs no mutation; read the list
|
|
270
|
+
again before deciding whether to issue a new write.
|
|
213
271
|
|
|
214
272
|
Use `--body_file ./payload.json` for a JSON-object request body. Header values
|
|
215
273
|
must come from environment variables: pass a JSON name mapping such as
|
|
216
274
|
`--header_env '{"x-schedule-token":"SCHEDULE_TOKEN"}'`. Platform-owned
|
|
217
275
|
`authorization`, `apikey`, and `x-project-ref` headers cannot be overridden. Receipts never
|
|
218
276
|
include header values or body content; list and mutation receipts report only
|
|
219
|
-
whether the body is empty and the configured header names.
|
|
277
|
+
whether the body is empty and the configured header names. Update receipts bind
|
|
278
|
+
`previous_updated_at` to the requested revision and return a newer `updated_at`;
|
|
279
|
+
delete receipts return the matched revision as `deleted_updated_at`.
|
|
220
280
|
|
|
221
281
|
For secret writes, `--from-env` accepts a comma-separated list of environment
|
|
222
282
|
variable names. The CLI reads each non-empty value from its own process
|
package/dist/index.js
CHANGED
|
@@ -6491,7 +6491,7 @@ var ACTION_POLICY = {
|
|
|
6491
6491
|
local: ["check"],
|
|
6492
6492
|
write: ["deploy", "deploy_bundle", "config", "activate", "delete"]
|
|
6493
6493
|
},
|
|
6494
|
-
scheduled_functions: { read: ["list"], write: ["create", "update", "delete"] },
|
|
6494
|
+
scheduled_functions: { read: ["list", "get"], write: ["create", "update", "delete"] },
|
|
6495
6495
|
secrets: { read: ["list"], write: ["upsert", "delete"] },
|
|
6496
6496
|
frontend: {
|
|
6497
6497
|
read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
|
|
@@ -6576,6 +6576,8 @@ function validateExecutionPolicyCoverage(tools) {
|
|
|
6576
6576
|
|
|
6577
6577
|
// src/shared/transports/http.ts
|
|
6578
6578
|
var DEFAULT_TIMEOUT = 30000;
|
|
6579
|
+
var RELEASE_MUTATION_RESPONSE_TIMEOUT = 5000;
|
|
6580
|
+
var RELEASE_MUTATION_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
6579
6581
|
var MAX_RETRIES = 2;
|
|
6580
6582
|
var RETRY_BASE_DELAY = 500;
|
|
6581
6583
|
function isRetryableMethod(method) {
|
|
@@ -6598,6 +6600,14 @@ function transportFailure(error) {
|
|
|
6598
6600
|
transportError: true
|
|
6599
6601
|
};
|
|
6600
6602
|
}
|
|
6603
|
+
function responseReadFailure(status) {
|
|
6604
|
+
return {
|
|
6605
|
+
ok: false,
|
|
6606
|
+
status,
|
|
6607
|
+
data: { error: "Response body unavailable", code: "RESPONSE_READ_ERROR" },
|
|
6608
|
+
responseReadError: true
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6601
6611
|
async function fetchWithTimeout(url, options) {
|
|
6602
6612
|
const controller = new AbortController;
|
|
6603
6613
|
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
@@ -6647,41 +6657,114 @@ function joinedResponseBytes(chunks, totalBytes) {
|
|
|
6647
6657
|
}
|
|
6648
6658
|
return responseBytes;
|
|
6649
6659
|
}
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6660
|
+
function cancelResponseReader(reader) {
|
|
6661
|
+
reader.cancel().catch(() => {
|
|
6662
|
+
return;
|
|
6663
|
+
});
|
|
6664
|
+
}
|
|
6665
|
+
function serializedRequestBody(body) {
|
|
6666
|
+
return body ? JSON.stringify(body) : undefined;
|
|
6667
|
+
}
|
|
6668
|
+
async function responseBytesFromReader(reader, maxBytes, declaredBytes) {
|
|
6658
6669
|
const chunks = [];
|
|
6659
6670
|
let totalBytes = 0;
|
|
6660
6671
|
while (true) {
|
|
6661
6672
|
const { done, value } = await reader.read();
|
|
6662
|
-
if (done)
|
|
6663
|
-
|
|
6673
|
+
if (done) {
|
|
6674
|
+
if (declaredBytes !== null && totalBytes !== declaredBytes)
|
|
6675
|
+
return { ok: false };
|
|
6676
|
+
return { ok: true, bytes: joinedResponseBytes(chunks, totalBytes) };
|
|
6677
|
+
}
|
|
6664
6678
|
totalBytes += value.byteLength;
|
|
6665
6679
|
if (totalBytes > maxBytes) {
|
|
6666
|
-
|
|
6667
|
-
return
|
|
6680
|
+
cancelResponseReader(reader);
|
|
6681
|
+
return { ok: false };
|
|
6668
6682
|
}
|
|
6669
6683
|
chunks.push(value);
|
|
6670
6684
|
}
|
|
6671
6685
|
}
|
|
6686
|
+
async function responseBytesWithinLimit(response, maxBytes) {
|
|
6687
|
+
if (declaredResponseTooLarge(response, maxBytes)) {
|
|
6688
|
+
await response.body?.cancel();
|
|
6689
|
+
return null;
|
|
6690
|
+
}
|
|
6691
|
+
if (!response.body)
|
|
6692
|
+
return new Uint8Array;
|
|
6693
|
+
const reader = response.body.getReader();
|
|
6694
|
+
const bodyRead = await responseBytesFromReader(reader, maxBytes, null);
|
|
6695
|
+
return bodyRead.ok ? bodyRead.bytes : null;
|
|
6696
|
+
}
|
|
6672
6697
|
function parsedUtf8Json(responseBytes) {
|
|
6673
6698
|
try {
|
|
6674
6699
|
const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
|
|
6675
|
-
return JSON.parse(responseText);
|
|
6700
|
+
return { ok: true, parsedJson: JSON.parse(responseText) };
|
|
6676
6701
|
} catch (error) {
|
|
6677
6702
|
if (error instanceof SyntaxError || error instanceof TypeError)
|
|
6678
|
-
return
|
|
6703
|
+
return { ok: false };
|
|
6679
6704
|
throw error;
|
|
6680
6705
|
}
|
|
6681
6706
|
}
|
|
6682
6707
|
async function boundedResponseJson(response, maxBytes) {
|
|
6683
6708
|
const responseBytes = await responseBytesWithinLimit(response, maxBytes);
|
|
6684
|
-
|
|
6709
|
+
if (responseBytes === null)
|
|
6710
|
+
return null;
|
|
6711
|
+
const parsed = parsedUtf8Json(responseBytes);
|
|
6712
|
+
return parsed.ok ? parsed.parsedJson : null;
|
|
6713
|
+
}
|
|
6714
|
+
function declaredIdentityResponseBytes(response) {
|
|
6715
|
+
const contentEncoding = response.headers.get("content-encoding");
|
|
6716
|
+
if (contentEncoding !== null && contentEncoding.toLowerCase() !== "identity")
|
|
6717
|
+
return null;
|
|
6718
|
+
const contentLength = response.headers.get("content-length");
|
|
6719
|
+
if (contentLength === null)
|
|
6720
|
+
return null;
|
|
6721
|
+
if (!/^\d+$/.test(contentLength))
|
|
6722
|
+
return "invalid";
|
|
6723
|
+
const declaredBytes = Number(contentLength);
|
|
6724
|
+
return Number.isSafeInteger(declaredBytes) ? declaredBytes : "invalid";
|
|
6725
|
+
}
|
|
6726
|
+
async function releaseMutationResponseBytes(response) {
|
|
6727
|
+
const declaredBytes = declaredIdentityResponseBytes(response);
|
|
6728
|
+
if (declaredBytes === "invalid" || declaredBytes !== null && declaredBytes > RELEASE_MUTATION_RESPONSE_MAX_BYTES) {
|
|
6729
|
+
response.body?.cancel().catch(() => {
|
|
6730
|
+
return;
|
|
6731
|
+
});
|
|
6732
|
+
return { ok: false };
|
|
6733
|
+
}
|
|
6734
|
+
if (!response.body) {
|
|
6735
|
+
return declaredBytes === null || declaredBytes === 0 ? { ok: true, bytes: new Uint8Array } : { ok: false };
|
|
6736
|
+
}
|
|
6737
|
+
return responseBytesBeforeDeadline(response.body.getReader(), declaredBytes);
|
|
6738
|
+
}
|
|
6739
|
+
async function responseBytesBeforeDeadline(reader, declaredBytes) {
|
|
6740
|
+
let deadlineTimer;
|
|
6741
|
+
const deadline = new Promise((resolve2) => {
|
|
6742
|
+
deadlineTimer = setTimeout(() => {
|
|
6743
|
+
cancelResponseReader(reader);
|
|
6744
|
+
resolve2({ ok: false });
|
|
6745
|
+
}, RELEASE_MUTATION_RESPONSE_TIMEOUT);
|
|
6746
|
+
});
|
|
6747
|
+
try {
|
|
6748
|
+
return await Promise.race([
|
|
6749
|
+
responseBytesFromReader(reader, RELEASE_MUTATION_RESPONSE_MAX_BYTES, declaredBytes),
|
|
6750
|
+
deadline
|
|
6751
|
+
]);
|
|
6752
|
+
} catch {
|
|
6753
|
+
cancelResponseReader(reader);
|
|
6754
|
+
return { ok: false };
|
|
6755
|
+
} finally {
|
|
6756
|
+
clearTimeout(deadlineTimer);
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
async function releaseMutationResponseJson(response) {
|
|
6760
|
+
const responseBytes = await releaseMutationResponseBytes(response);
|
|
6761
|
+
if (!responseBytes.ok)
|
|
6762
|
+
return { ok: false };
|
|
6763
|
+
const parsed = parsedUtf8Json(responseBytes.bytes);
|
|
6764
|
+
return parsed.ok ? { ok: true, parsedJson: parsed.parsedJson } : { ok: false };
|
|
6765
|
+
}
|
|
6766
|
+
async function responseJsonOrNull(response) {
|
|
6767
|
+
return { ok: true, parsedJson: await response.json().catch(() => null) };
|
|
6685
6768
|
}
|
|
6686
6769
|
|
|
6687
6770
|
class HttpTransport {
|
|
@@ -6697,6 +6780,19 @@ class HttpTransport {
|
|
|
6697
6780
|
"Content-Type": "application/json"
|
|
6698
6781
|
};
|
|
6699
6782
|
}
|
|
6783
|
+
async postWithResponseReader(path, serializedBody, responseReader) {
|
|
6784
|
+
try {
|
|
6785
|
+
const response = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
6786
|
+
method: "POST",
|
|
6787
|
+
headers: this.headers(),
|
|
6788
|
+
body: serializedBody
|
|
6789
|
+
});
|
|
6790
|
+
const responseBody = await responseReader(response);
|
|
6791
|
+
return responseBody.ok ? { ok: response.ok, status: response.status, data: responseBody.parsedJson } : responseReadFailure(response.status);
|
|
6792
|
+
} catch (error) {
|
|
6793
|
+
return transportFailure(error);
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6700
6796
|
async get(path, options = {}) {
|
|
6701
6797
|
try {
|
|
6702
6798
|
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
@@ -6711,17 +6807,14 @@ class HttpTransport {
|
|
|
6711
6807
|
}
|
|
6712
6808
|
async post(path, body) {
|
|
6713
6809
|
try {
|
|
6714
|
-
|
|
6715
|
-
method: "POST",
|
|
6716
|
-
headers: this.headers(),
|
|
6717
|
-
body: body ? JSON.stringify(body) : undefined
|
|
6718
|
-
});
|
|
6719
|
-
const data = await res.json().catch(() => null);
|
|
6720
|
-
return { ok: res.ok, status: res.status, data };
|
|
6810
|
+
return await this.postWithResponseReader(path, serializedRequestBody(body), responseJsonOrNull);
|
|
6721
6811
|
} catch (error) {
|
|
6722
6812
|
return transportFailure(error);
|
|
6723
6813
|
}
|
|
6724
6814
|
}
|
|
6815
|
+
async postReleaseMutation(path, body) {
|
|
6816
|
+
return this.postWithResponseReader(path, serializedRequestBody(body), releaseMutationResponseJson);
|
|
6817
|
+
}
|
|
6725
6818
|
async postMultipart(path, formData) {
|
|
6726
6819
|
try {
|
|
6727
6820
|
const headers = { Authorization: `Bearer ${this.token}` };
|
|
@@ -6964,6 +7057,28 @@ function migrationRows(data) {
|
|
|
6964
7057
|
function migrationIdentityKey(version, name) {
|
|
6965
7058
|
return `${version}\x00${name}`;
|
|
6966
7059
|
}
|
|
7060
|
+
function migrationIdentities(data) {
|
|
7061
|
+
return migrationRows(data).flatMap((row) => row.version == null || row.name == null ? [] : [{ version: String(row.version), name: String(row.name) }]);
|
|
7062
|
+
}
|
|
7063
|
+
function identityConflicts(local, remote) {
|
|
7064
|
+
const reusesVersionOrName = local.version === remote.version || local.name === remote.name;
|
|
7065
|
+
const exactlyMatches = local.version === remote.version && local.name === remote.name;
|
|
7066
|
+
return reusesVersionOrName && !exactlyMatches;
|
|
7067
|
+
}
|
|
7068
|
+
function migrationIdentityConflicts(data, migrationFiles) {
|
|
7069
|
+
const remoteMigrations = migrationIdentities(data);
|
|
7070
|
+
return migrationFiles.flatMap((localMigration) => remoteMigrations.filter((remoteMigration) => identityConflicts(localMigration, remoteMigration)).map((remoteMigration) => ({ file: localMigration.file, local: localMigration, remote: remoteMigration })));
|
|
7071
|
+
}
|
|
7072
|
+
function assertNoMigrationIdentityConflicts(data, migrationFiles) {
|
|
7073
|
+
const conflicts = migrationIdentityConflicts(data, migrationFiles);
|
|
7074
|
+
if (!conflicts.length)
|
|
7075
|
+
return;
|
|
7076
|
+
throw new Error([
|
|
7077
|
+
"Migration identity conflicts:",
|
|
7078
|
+
...conflicts.map(({ file, local, remote }) => `- ${file} (${local.version}) conflicts with remote ${remote.name} (${remote.version})`)
|
|
7079
|
+
].join(`
|
|
7080
|
+
`));
|
|
7081
|
+
}
|
|
6967
7082
|
function nameBoundMigrationMarkerKeys(data) {
|
|
6968
7083
|
const keys = new Set;
|
|
6969
7084
|
for (const row of migrationRows(data)) {
|
|
@@ -7227,6 +7342,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7227
7342
|
text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
|
|
7228
7343
|
break;
|
|
7229
7344
|
}
|
|
7345
|
+
assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
|
|
7230
7346
|
if (args.dry_run) {
|
|
7231
7347
|
const appliedKeys = appliedMigrationKeys(migrationsResult.data);
|
|
7232
7348
|
const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
|
|
@@ -7863,7 +7979,7 @@ function releaseControlFailure(operation, code, httpStatus) {
|
|
|
7863
7979
|
});
|
|
7864
7980
|
}
|
|
7865
7981
|
function releaseControlMutationFailure(operation, response) {
|
|
7866
|
-
const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
|
|
7982
|
+
const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
|
|
7867
7983
|
return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
7868
7984
|
}
|
|
7869
7985
|
function releaseControlResponse(payload) {
|
|
@@ -7882,13 +7998,14 @@ var MAX_MIME_TYPE_LENGTH = 255;
|
|
|
7882
7998
|
var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7883
7999
|
var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
|
|
7884
8000
|
var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
|
|
8001
|
+
var BUCKET_REVISION_PATTERN = /^[0-9]{1,20}$/;
|
|
7885
8002
|
var ACTION_ARGUMENTS = {
|
|
7886
8003
|
status: new Set(["action"]),
|
|
7887
8004
|
list_buckets: new Set(["action", "ref"]),
|
|
7888
8005
|
get_bucket: new Set(["action", "ref", "bucket"]),
|
|
7889
8006
|
create_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
|
|
7890
|
-
update_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
|
|
7891
|
-
delete_bucket: new Set(["action", "ref", "bucket"]),
|
|
8007
|
+
update_bucket: new Set(["action", "ref", "bucket", "expected_revision", "public", "file_size_limit", "allowed_mime_types"]),
|
|
8008
|
+
delete_bucket: new Set(["action", "ref", "bucket", "expected_revision", "require_empty"]),
|
|
7892
8009
|
list_files: new Set(["action", "ref", "bucket"]),
|
|
7893
8010
|
upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
|
|
7894
8011
|
delete_file: new Set(["action", "ref", "bucket", "filename"])
|
|
@@ -7944,6 +8061,16 @@ function requiredBucketId(args) {
|
|
|
7944
8061
|
throw new Error("'bucket' is invalid for Storage buckets");
|
|
7945
8062
|
return bucket;
|
|
7946
8063
|
}
|
|
8064
|
+
function requiredBucketRevision(args) {
|
|
8065
|
+
const revision = requiredText(args, "expected_revision");
|
|
8066
|
+
if (!BUCKET_REVISION_PATTERN.test(revision))
|
|
8067
|
+
throw new Error("'expected_revision' is invalid for Storage buckets");
|
|
8068
|
+
return revision;
|
|
8069
|
+
}
|
|
8070
|
+
function assertEmptyBucketDeletion(args) {
|
|
8071
|
+
if (args.require_empty !== true)
|
|
8072
|
+
throw new Error("'require_empty=true' required for 'delete_bucket'");
|
|
8073
|
+
}
|
|
7947
8074
|
function assertActionArguments(action, args) {
|
|
7948
8075
|
const allowedArguments = ACTION_ARGUMENTS[action];
|
|
7949
8076
|
if (!allowedArguments)
|
|
@@ -7978,9 +8105,12 @@ function assertBucketSettings(args) {
|
|
|
7978
8105
|
function isAllowedMimeTypes(candidate) {
|
|
7979
8106
|
return candidate === null || Array.isArray(candidate) && candidate.length <= MAX_MIME_TYPE_COUNT && candidate.every((mimeType) => typeof mimeType === "string" && mimeType.length <= MAX_MIME_TYPE_LENGTH && MIME_TYPE_PATTERN.test(mimeType));
|
|
7980
8107
|
}
|
|
8108
|
+
function isBucketRevision(candidate) {
|
|
8109
|
+
return candidate === null || typeof candidate === "string" && BUCKET_REVISION_PATTERN.test(candidate);
|
|
8110
|
+
}
|
|
7981
8111
|
function safeBucket(candidate, expectedBucket) {
|
|
7982
8112
|
const bucket = bucketRecord(candidate);
|
|
7983
|
-
if (bucket === null || typeof bucket.id !== "string" || !validBucketId(bucket.id) || typeof bucket.name !== "string" || !validBucketId(bucket.name) || typeof bucket.public !== "boolean" || !isFileSizeLimit(bucket.file_size_limit) || !isAllowedMimeTypes(bucket.allowed_mime_types) || expectedBucket !== undefined && bucket.id !== expectedBucket && bucket.name !== expectedBucket) {
|
|
8113
|
+
if (bucket === null || typeof bucket.id !== "string" || !validBucketId(bucket.id) || typeof bucket.name !== "string" || !validBucketId(bucket.name) || typeof bucket.public !== "boolean" || !isFileSizeLimit(bucket.file_size_limit) || !isAllowedMimeTypes(bucket.allowed_mime_types) || !isBucketRevision(bucket.revision) || expectedBucket !== undefined && bucket.id !== expectedBucket && bucket.name !== expectedBucket) {
|
|
7984
8114
|
return null;
|
|
7985
8115
|
}
|
|
7986
8116
|
return {
|
|
@@ -7988,7 +8118,8 @@ function safeBucket(candidate, expectedBucket) {
|
|
|
7988
8118
|
name: bucket.name,
|
|
7989
8119
|
public: bucket.public,
|
|
7990
8120
|
file_size_limit: bucket.file_size_limit,
|
|
7991
|
-
allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types]
|
|
8121
|
+
allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types],
|
|
8122
|
+
revision: bucket.revision
|
|
7992
8123
|
};
|
|
7993
8124
|
}
|
|
7994
8125
|
function safeExactBucket(candidate, expectedBucket) {
|
|
@@ -8012,18 +8143,30 @@ function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
|
|
|
8012
8143
|
return null;
|
|
8013
8144
|
return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
|
|
8014
8145
|
}
|
|
8015
|
-
function safeDeletedBucket(candidate, expectedBucket) {
|
|
8146
|
+
function safeDeletedBucket(candidate, expectedBucket, expectedRevision) {
|
|
8016
8147
|
const receipt = bucketRecord(candidate);
|
|
8017
|
-
return receipt?.id === expectedBucket && receipt.deleted === true
|
|
8148
|
+
return receipt?.id === expectedBucket && receipt.deleted === true && receipt.require_empty === true && receipt.previous_revision === expectedRevision && receipt.new_revision === null ? {
|
|
8149
|
+
bucket_id: expectedBucket,
|
|
8150
|
+
deleted: true,
|
|
8151
|
+
require_empty: true,
|
|
8152
|
+
previous_revision: expectedRevision,
|
|
8153
|
+
new_revision: null
|
|
8154
|
+
} : null;
|
|
8018
8155
|
}
|
|
8019
8156
|
function mutationReadbackResponse(expectation) {
|
|
8020
|
-
const { operation, ref, response, expectedBucket, request } = expectation;
|
|
8157
|
+
const { operation, ref, response, expectedBucket, request, previousRevision, expectedNewRevision } = expectation;
|
|
8021
8158
|
const readback = safeExactBucket(response.data, expectedBucket);
|
|
8022
|
-
const validReadback = readback?.name === expectedBucket && bucketMatchesRequest(readback, request);
|
|
8159
|
+
const validReadback = readback?.name === expectedBucket && readback.revision !== null && (expectedNewRevision === undefined || readback.revision === expectedNewRevision) && bucketMatchesRequest(readback, request);
|
|
8023
8160
|
if (!response.ok || !validReadback || !readback) {
|
|
8024
8161
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
|
|
8025
8162
|
}
|
|
8026
|
-
return releaseControlSuccess(operation, {
|
|
8163
|
+
return releaseControlSuccess(operation, {
|
|
8164
|
+
project_ref: ref,
|
|
8165
|
+
bucket_id: expectedBucket,
|
|
8166
|
+
previous_revision: previousRevision,
|
|
8167
|
+
new_revision: readback.revision,
|
|
8168
|
+
bucket: readback
|
|
8169
|
+
});
|
|
8027
8170
|
}
|
|
8028
8171
|
function bucketResponse(expectation) {
|
|
8029
8172
|
const { operation, ref, response, operationKind, safePayload } = expectation;
|
|
@@ -8049,10 +8192,10 @@ function createBucketRequest(args) {
|
|
|
8049
8192
|
}
|
|
8050
8193
|
function updateBucketRequest(args) {
|
|
8051
8194
|
assertBucketSettings(args);
|
|
8052
|
-
const
|
|
8053
|
-
if (Object.keys(
|
|
8195
|
+
const settings = Object.fromEntries(["public", "file_size_limit", "allowed_mime_types"].filter((field) => args[field] !== undefined).map((field) => [field, args[field]]));
|
|
8196
|
+
if (Object.keys(settings).length === 0)
|
|
8054
8197
|
throw new Error("Bucket update requires at least one field");
|
|
8055
|
-
return
|
|
8198
|
+
return { expected_revision: requiredBucketRevision(args), ...settings };
|
|
8056
8199
|
}
|
|
8057
8200
|
function equalAllowedMimeTypes(candidate, expected) {
|
|
8058
8201
|
if (!Array.isArray(expected))
|
|
@@ -8080,18 +8223,10 @@ async function createBucketMutationReceipt(http, ref, bucket, request) {
|
|
|
8080
8223
|
safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
|
|
8081
8224
|
});
|
|
8082
8225
|
}
|
|
8083
|
-
|
|
8084
|
-
const
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
ref,
|
|
8088
|
-
response: await http.put(bucketPath, request),
|
|
8089
|
-
operationKind: "mutation",
|
|
8090
|
-
safePayload: (apiPayload) => {
|
|
8091
|
-
const updated = safeExactBucket(apiPayload, bucket);
|
|
8092
|
-
return updated && bucketMatchesRequest(updated, request) ? { bucket: updated } : null;
|
|
8093
|
-
}
|
|
8094
|
-
});
|
|
8226
|
+
function safeUpdatedBucket(candidate, expectedBucket, expectedRevision, request) {
|
|
8227
|
+
const receipt = bucketRecord(candidate);
|
|
8228
|
+
const bucket = safeExactBucket(candidate, expectedBucket);
|
|
8229
|
+
return bucket && bucket.name === expectedBucket && bucket.revision !== null && bucket.revision !== expectedRevision && receipt?.previous_revision === expectedRevision && receipt.new_revision === bucket.revision && bucketMatchesRequest(bucket, request) ? bucket : null;
|
|
8095
8230
|
}
|
|
8096
8231
|
async function listBuckets(http, args) {
|
|
8097
8232
|
const ref = requiredProjectRef(args);
|
|
@@ -8134,35 +8269,45 @@ async function createBucket(http, args) {
|
|
|
8134
8269
|
ref,
|
|
8135
8270
|
response: await http.get(bucketPath),
|
|
8136
8271
|
expectedBucket: bucket,
|
|
8137
|
-
request: expectedReadback
|
|
8272
|
+
request: expectedReadback,
|
|
8273
|
+
previousRevision: null
|
|
8138
8274
|
});
|
|
8139
8275
|
}
|
|
8140
8276
|
async function updateBucket(http, args) {
|
|
8141
8277
|
const ref = requiredProjectRef(args);
|
|
8142
8278
|
const bucket = requiredBucketId(args);
|
|
8143
8279
|
const request = updateBucketRequest(args);
|
|
8280
|
+
const expectedRevision = request.expected_revision;
|
|
8144
8281
|
const bucketPath = storageBucketPath(ref, bucket);
|
|
8145
|
-
const
|
|
8146
|
-
if (
|
|
8147
|
-
return
|
|
8282
|
+
const mutation = await http.put(bucketPath, request);
|
|
8283
|
+
if (!mutation.ok)
|
|
8284
|
+
return releaseControlMutationFailure("storage.update_bucket", mutation);
|
|
8285
|
+
const updated = safeUpdatedBucket(mutation.data, bucket, expectedRevision, request);
|
|
8286
|
+
if (!updated)
|
|
8287
|
+
return releaseControlFailure("storage.update_bucket", "OUTCOME_UNKNOWN", mutation.status);
|
|
8148
8288
|
return mutationReadbackResponse({
|
|
8149
8289
|
operation: "storage.update_bucket",
|
|
8150
8290
|
ref,
|
|
8151
8291
|
response: await http.get(bucketPath),
|
|
8152
8292
|
expectedBucket: bucket,
|
|
8153
|
-
request
|
|
8293
|
+
request,
|
|
8294
|
+
previousRevision: expectedRevision,
|
|
8295
|
+
expectedNewRevision: updated.revision ?? undefined
|
|
8154
8296
|
});
|
|
8155
8297
|
}
|
|
8156
8298
|
async function deleteBucket(http, args) {
|
|
8157
8299
|
const ref = requiredProjectRef(args);
|
|
8158
8300
|
const bucket = requiredBucketId(args);
|
|
8301
|
+
const expectedRevision = requiredBucketRevision(args);
|
|
8302
|
+
assertEmptyBucketDeletion(args);
|
|
8159
8303
|
const bucketPath = storageBucketPath(ref, bucket);
|
|
8304
|
+
const deletePath = `${bucketPath}?expected_revision=${encodeURIComponent(expectedRevision)}&require_empty=true`;
|
|
8160
8305
|
const receipt = bucketResponse({
|
|
8161
8306
|
operation: "storage.delete_bucket",
|
|
8162
8307
|
ref,
|
|
8163
|
-
response: await http.delete(
|
|
8308
|
+
response: await http.delete(deletePath),
|
|
8164
8309
|
operationKind: "mutation",
|
|
8165
|
-
safePayload: (candidate) => safeDeletedBucket(candidate, bucket)
|
|
8310
|
+
safePayload: (candidate) => safeDeletedBucket(candidate, bucket, expectedRevision)
|
|
8166
8311
|
});
|
|
8167
8312
|
if (receipt.isError)
|
|
8168
8313
|
return receipt;
|
|
@@ -8203,6 +8348,8 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
|
|
|
8203
8348
|
public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
|
|
8204
8349
|
file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
|
|
8205
8350
|
allowed_mime_types: withDescription(allowedMimeTypesSchema, "[create_bucket/update_bucket] MIME types as a comma-separated or JSON array"),
|
|
8351
|
+
expected_revision: optional(Type.String({ pattern: BUCKET_REVISION_PATTERN.source }), "[update_bucket/delete_bucket] Exact revision from list_buckets/get_bucket"),
|
|
8352
|
+
require_empty: optional(Type.Boolean(), "[delete_bucket] Must be true; deletion never empties a bucket"),
|
|
8206
8353
|
filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
|
|
8207
8354
|
base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
|
|
8208
8355
|
mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
|
|
@@ -8268,12 +8415,79 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
|
|
|
8268
8415
|
}
|
|
8269
8416
|
|
|
8270
8417
|
// src/shared/tools/advanced-tools.ts
|
|
8271
|
-
import {
|
|
8418
|
+
import { createHash as createHash2, timingSafeEqual } from "node:crypto";
|
|
8419
|
+
import {
|
|
8420
|
+
closeSync,
|
|
8421
|
+
constants as fsConstants,
|
|
8422
|
+
existsSync as existsSync3,
|
|
8423
|
+
fstatSync,
|
|
8424
|
+
mkdtempSync,
|
|
8425
|
+
openSync,
|
|
8426
|
+
readFileSync as readFileSync3,
|
|
8427
|
+
rmSync,
|
|
8428
|
+
statSync as statSync2,
|
|
8429
|
+
writeFileSync
|
|
8430
|
+
} from "node:fs";
|
|
8272
8431
|
import { tmpdir } from "node:os";
|
|
8273
8432
|
import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
|
|
8274
8433
|
import { promisify } from "node:util";
|
|
8275
8434
|
import { execFile } from "node:child_process";
|
|
8276
8435
|
var execFileAsync = promisify(execFile);
|
|
8436
|
+
var SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
8437
|
+
function openFileIdentity(descriptor) {
|
|
8438
|
+
const state = fstatSync(descriptor, { bigint: true });
|
|
8439
|
+
if (!state.isFile())
|
|
8440
|
+
throw new Error("Prebundled path must be a regular file");
|
|
8441
|
+
return {
|
|
8442
|
+
dev: state.dev,
|
|
8443
|
+
ino: state.ino,
|
|
8444
|
+
size: state.size,
|
|
8445
|
+
mtimeNs: state.mtimeNs,
|
|
8446
|
+
ctimeNs: state.ctimeNs
|
|
8447
|
+
};
|
|
8448
|
+
}
|
|
8449
|
+
function sameOpenFile(left, right) {
|
|
8450
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
8451
|
+
}
|
|
8452
|
+
function verifiedUtf8Code(bytes) {
|
|
8453
|
+
let code;
|
|
8454
|
+
try {
|
|
8455
|
+
code = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
8456
|
+
} catch (error) {
|
|
8457
|
+
if (error instanceof TypeError)
|
|
8458
|
+
throw new Error("Prebundled file is not valid UTF-8");
|
|
8459
|
+
throw error;
|
|
8460
|
+
}
|
|
8461
|
+
if (!Buffer.from(code, "utf8").equals(bytes)) {
|
|
8462
|
+
throw new Error("Prebundled file does not round-trip as UTF-8");
|
|
8463
|
+
}
|
|
8464
|
+
return code;
|
|
8465
|
+
}
|
|
8466
|
+
function assertExpectedSha256(bytes, expectedSha256) {
|
|
8467
|
+
if (!SHA256_HEX_PATTERN.test(expectedSha256)) {
|
|
8468
|
+
throw new Error("'--expected-sha256' must be a lowercase 64-character SHA-256");
|
|
8469
|
+
}
|
|
8470
|
+
const actualDigest = createHash2("sha256").update(bytes).digest();
|
|
8471
|
+
const expectedDigest = Buffer.from(expectedSha256, "hex");
|
|
8472
|
+
if (!timingSafeEqual(actualDigest, expectedDigest)) {
|
|
8473
|
+
throw new Error("Prebundled file SHA-256 does not match --expected-sha256");
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
function readVerifiedPrebundledCode(pathArg, expectedSha256) {
|
|
8477
|
+
const descriptor = openSync(resolve2(pathArg), fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
|
|
8478
|
+
try {
|
|
8479
|
+
const identityBeforeRead = openFileIdentity(descriptor);
|
|
8480
|
+
const bytes = readFileSync3(descriptor);
|
|
8481
|
+
const identityAfterRead = openFileIdentity(descriptor);
|
|
8482
|
+
if (BigInt(bytes.byteLength) !== identityBeforeRead.size || !sameOpenFile(identityBeforeRead, identityAfterRead)) {
|
|
8483
|
+
throw new Error("Prebundled file changed while it was being read");
|
|
8484
|
+
}
|
|
8485
|
+
assertExpectedSha256(bytes, expectedSha256);
|
|
8486
|
+
return verifiedUtf8Code(bytes);
|
|
8487
|
+
} finally {
|
|
8488
|
+
closeSync(descriptor);
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8277
8491
|
async function runBunBuild(args) {
|
|
8278
8492
|
try {
|
|
8279
8493
|
return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -8298,6 +8512,54 @@ async function bundleEdgeFunctionPath(pathArg) {
|
|
|
8298
8512
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
8299
8513
|
}
|
|
8300
8514
|
}
|
|
8515
|
+
function prebundledDeployCode(pathArg, expectedSha256, minify) {
|
|
8516
|
+
if (typeof expectedSha256 !== "string") {
|
|
8517
|
+
throw new Error("'--expected-sha256' required with '--prebundled-path'");
|
|
8518
|
+
}
|
|
8519
|
+
if (minify !== undefined) {
|
|
8520
|
+
throw new Error("'--minify' cannot be combined with '--prebundled-path'");
|
|
8521
|
+
}
|
|
8522
|
+
return {
|
|
8523
|
+
code: readVerifiedPrebundledCode(pathArg, expectedSha256),
|
|
8524
|
+
prebundled: true,
|
|
8525
|
+
expectedSha256
|
|
8526
|
+
};
|
|
8527
|
+
}
|
|
8528
|
+
async function preparedDeployCode(args) {
|
|
8529
|
+
const codeArg = args.code;
|
|
8530
|
+
const pathArg = args.path;
|
|
8531
|
+
const prebundledPath = args["prebundled-path"];
|
|
8532
|
+
const sources = [codeArg, pathArg, prebundledPath].filter((source) => source !== undefined);
|
|
8533
|
+
if (sources.length !== 1) {
|
|
8534
|
+
throw new Error("Exactly one of '--code', '--path', or '--prebundled-path' is required for 'deploy'");
|
|
8535
|
+
}
|
|
8536
|
+
if (typeof prebundledPath === "string") {
|
|
8537
|
+
return prebundledDeployCode(prebundledPath, args["expected-sha256"], args.minify);
|
|
8538
|
+
}
|
|
8539
|
+
if (args["expected-sha256"] !== undefined) {
|
|
8540
|
+
throw new Error("'--expected-sha256' requires '--prebundled-path'");
|
|
8541
|
+
}
|
|
8542
|
+
if (typeof codeArg === "string")
|
|
8543
|
+
return { code: codeArg, prebundled: false };
|
|
8544
|
+
if (typeof pathArg !== "string")
|
|
8545
|
+
throw new Error("Function deploy source is invalid");
|
|
8546
|
+
return bundledDeployCode(pathArg);
|
|
8547
|
+
}
|
|
8548
|
+
async function bundledDeployCode(pathArg) {
|
|
8549
|
+
try {
|
|
8550
|
+
return { code: await bundleEdgeFunctionPath(pathArg), prebundled: false };
|
|
8551
|
+
} catch (error) {
|
|
8552
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8553
|
+
throw new Error(`Failed to bundle/read path ${pathArg}: ${message}`);
|
|
8554
|
+
}
|
|
8555
|
+
}
|
|
8556
|
+
function rejectPrebundledFlagsOutsideDeploy(action, args) {
|
|
8557
|
+
for (const flag of ["prebundled-path", "expected-sha256"]) {
|
|
8558
|
+
if (action !== "deploy" && args[flag] !== undefined) {
|
|
8559
|
+
throw new Error(`'--${flag}' is not supported for '${action}'`);
|
|
8560
|
+
}
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8301
8563
|
function resolveEntrypoint(pathArg) {
|
|
8302
8564
|
const resolved = resolve2(pathArg);
|
|
8303
8565
|
const stat = statSync2(resolved);
|
|
@@ -8344,21 +8606,53 @@ function parseFunctionFiles(input) {
|
|
|
8344
8606
|
}
|
|
8345
8607
|
}
|
|
8346
8608
|
var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
|
|
8347
|
-
function
|
|
8609
|
+
function positiveFunctionVersion(input, label) {
|
|
8610
|
+
if (typeof input !== "string" && typeof input !== "number") {
|
|
8611
|
+
throw new Error(`${label} must be a canonical positive safe integer`);
|
|
8612
|
+
}
|
|
8613
|
+
const version = String(input);
|
|
8614
|
+
if (!POSITIVE_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
8615
|
+
throw new Error(`${label} must be a canonical positive safe integer`);
|
|
8616
|
+
}
|
|
8617
|
+
return version;
|
|
8618
|
+
}
|
|
8619
|
+
function activeFunctionVersionToken(input) {
|
|
8620
|
+
if (typeof input !== "string" && typeof input !== "number") {
|
|
8621
|
+
throw new Error("Expected active version must be a canonical non-negative safe integer");
|
|
8622
|
+
}
|
|
8348
8623
|
const version = String(input);
|
|
8349
8624
|
if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
8350
|
-
throw new Error("
|
|
8625
|
+
throw new Error("Expected active version must be a canonical non-negative safe integer");
|
|
8351
8626
|
}
|
|
8352
8627
|
return version;
|
|
8353
8628
|
}
|
|
8354
8629
|
var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
|
|
8355
|
-
var
|
|
8630
|
+
var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
|
|
8356
8631
|
var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
8357
|
-
var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
|
|
8632
|
+
var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
|
|
8633
|
+
"action",
|
|
8634
|
+
"ref",
|
|
8635
|
+
"slug",
|
|
8636
|
+
"version",
|
|
8637
|
+
"expected-active-version"
|
|
8638
|
+
]);
|
|
8358
8639
|
var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
8640
|
+
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
|
8641
|
+
Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8642
|
+
]), Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), (input) => positiveFunctionVersion(input, "Function version")));
|
|
8643
|
+
function parseExpectedActiveVersion(input) {
|
|
8644
|
+
if (input === "absent")
|
|
8645
|
+
return input;
|
|
8646
|
+
return activeFunctionVersionToken(input);
|
|
8647
|
+
}
|
|
8648
|
+
var expectedActiveVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
8649
|
+
Type.Literal("absent"),
|
|
8359
8650
|
Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
|
8360
8651
|
Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8361
|
-
]), Type.
|
|
8652
|
+
]), Type.Union([
|
|
8653
|
+
Type.Literal("absent"),
|
|
8654
|
+
Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8655
|
+
]), parseExpectedActiveVersion));
|
|
8362
8656
|
var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
|
|
8363
8657
|
var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
8364
8658
|
var MAX_SECRET_COUNT = 1024;
|
|
@@ -8476,10 +8770,35 @@ function secretsForUpsert(inlineSecrets, environmentNames, environment) {
|
|
|
8476
8770
|
throw new Error("'secrets' array required");
|
|
8477
8771
|
return inlineSecrets;
|
|
8478
8772
|
}
|
|
8773
|
+
var INVALID_FUNCTION_LIST_RESPONSE = "❌ Edge Function list response is invalid";
|
|
8774
|
+
var INVALID_FUNCTION_SOURCE_RESPONSE = "❌ Edge Function source response is invalid";
|
|
8775
|
+
function invalidFunctionReadResponse(message) {
|
|
8776
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
8777
|
+
}
|
|
8778
|
+
function safeFunctionList(payload) {
|
|
8779
|
+
if (!Array.isArray(payload))
|
|
8780
|
+
return null;
|
|
8781
|
+
const functionSlugs = new Set;
|
|
8782
|
+
for (const candidate of payload) {
|
|
8783
|
+
const edgeFunction = objectRecord(candidate);
|
|
8784
|
+
const slug = edgeFunction?.slug;
|
|
8785
|
+
const version = edgeFunction?.version;
|
|
8786
|
+
if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug) || typeof version !== "number" || !Number.isSafeInteger(version) || version < 0 || functionSlugs.has(slug))
|
|
8787
|
+
return null;
|
|
8788
|
+
functionSlugs.add(slug);
|
|
8789
|
+
}
|
|
8790
|
+
return payload;
|
|
8791
|
+
}
|
|
8792
|
+
function functionListResponse(response) {
|
|
8793
|
+
if (!response.ok)
|
|
8794
|
+
return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
|
|
8795
|
+
const functions = safeFunctionList(response.data);
|
|
8796
|
+
return functions ? { content: [{ type: "text", text: JSON.stringify(functions, null, 2) }] } : invalidFunctionReadResponse(INVALID_FUNCTION_LIST_RESPONSE);
|
|
8797
|
+
}
|
|
8479
8798
|
function confirmedFunctionConfig(payload, expected) {
|
|
8480
|
-
|
|
8799
|
+
const response = objectRecord(payload);
|
|
8800
|
+
if (!response)
|
|
8481
8801
|
return false;
|
|
8482
|
-
const response = payload;
|
|
8483
8802
|
if (expected.verify_jwt !== undefined && response.verify_jwt !== expected.verify_jwt)
|
|
8484
8803
|
return false;
|
|
8485
8804
|
if (expected.background_routes !== undefined) {
|
|
@@ -8490,28 +8809,87 @@ function confirmedFunctionConfig(payload, expected) {
|
|
|
8490
8809
|
}
|
|
8491
8810
|
return true;
|
|
8492
8811
|
}
|
|
8493
|
-
function functionSourceCode(payload) {
|
|
8812
|
+
function functionSourceCode(payload, field = "code") {
|
|
8494
8813
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
8495
8814
|
return null;
|
|
8496
|
-
const code = payload
|
|
8815
|
+
const code = payload[field];
|
|
8497
8816
|
return typeof code === "string" ? code : null;
|
|
8498
8817
|
}
|
|
8818
|
+
function requestedSourceVersion(candidate) {
|
|
8819
|
+
return candidate === undefined ? undefined : positiveFunctionVersion(candidate, "Function source version");
|
|
8820
|
+
}
|
|
8821
|
+
function functionSourceOutput(slug, sourceCode, output) {
|
|
8822
|
+
if (!output) {
|
|
8823
|
+
return { content: [{ type: "text", text: JSON.stringify({ code: sourceCode }, null, 2) }] };
|
|
8824
|
+
}
|
|
8825
|
+
const outputPath = resolve2(output);
|
|
8826
|
+
writeFileSync(outputPath, sourceCode, { flag: "wx" });
|
|
8827
|
+
return {
|
|
8828
|
+
content: [{
|
|
8829
|
+
type: "text",
|
|
8830
|
+
text: `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`
|
|
8831
|
+
}]
|
|
8832
|
+
};
|
|
8833
|
+
}
|
|
8499
8834
|
function objectRecord(candidate) {
|
|
8500
8835
|
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
8501
8836
|
}
|
|
8502
|
-
function
|
|
8503
|
-
const
|
|
8837
|
+
function edgeFunctionResourcePath(ref, slug) {
|
|
8838
|
+
const root = `/v1/projects/${projectRefPathSegment(ref, "Edge Functions")}/functions`;
|
|
8839
|
+
if (slug === undefined)
|
|
8840
|
+
return root;
|
|
8841
|
+
if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug)) {
|
|
8842
|
+
throw new Error("'slug' is invalid for Edge Functions");
|
|
8843
|
+
}
|
|
8844
|
+
return `${root}/${encodeURIComponent(slug)}`;
|
|
8845
|
+
}
|
|
8846
|
+
async function readFunctionSource(http, request) {
|
|
8847
|
+
const sourceVersion = requestedSourceVersion(request.version);
|
|
8848
|
+
const resourcePath = edgeFunctionResourcePath(request.projectRef, request.slug);
|
|
8849
|
+
const sourcePath = sourceVersion === undefined ? `${resourcePath}/source` : `${resourcePath}/versions/${encodeURIComponent(sourceVersion)}`;
|
|
8850
|
+
const response = await http.get(sourcePath);
|
|
8504
8851
|
if (!response.ok)
|
|
8505
|
-
return
|
|
8506
|
-
const
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8852
|
+
return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
|
|
8853
|
+
const sourceCode = functionSourceCode(response.data, sourceVersion === undefined ? "code" : "source_code");
|
|
8854
|
+
return sourceCode === null ? invalidFunctionReadResponse(INVALID_FUNCTION_SOURCE_RESPONSE) : functionSourceOutput(request.slug, sourceCode, request.output);
|
|
8855
|
+
}
|
|
8856
|
+
function mutationIdentityMatches(receipt, expectation) {
|
|
8857
|
+
return receipt.success === true && receipt.project_ref === expectation.projectRef && receipt.slug === expectation.slug && receipt.previous_active_version === expectation.expectedActiveVersion;
|
|
8858
|
+
}
|
|
8859
|
+
function validReceiptVersion(activeVersion) {
|
|
8860
|
+
return typeof activeVersion === "string" && POSITIVE_FUNCTION_VERSION_PATTERN.test(activeVersion) && Number.isSafeInteger(Number(activeVersion));
|
|
8861
|
+
}
|
|
8862
|
+
function confirmedMutationVersion(receipt, config, expectation) {
|
|
8863
|
+
const activeVersion = receipt.active_version;
|
|
8864
|
+
if (!validReceiptVersion(activeVersion) || receipt.version !== activeVersion || config.version !== activeVersion || expectation.targetVersion !== undefined && activeVersion !== expectation.targetVersion) {
|
|
8865
|
+
return null;
|
|
8510
8866
|
}
|
|
8511
|
-
return
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8867
|
+
return activeVersion;
|
|
8868
|
+
}
|
|
8869
|
+
function confirmedFunctionMutation(expectation, payload) {
|
|
8870
|
+
const receipt = objectRecord(payload);
|
|
8871
|
+
const config = objectRecord(receipt?.config);
|
|
8872
|
+
if (!receipt || !config || !mutationIdentityMatches(receipt, expectation))
|
|
8873
|
+
return null;
|
|
8874
|
+
const activeVersion = confirmedMutationVersion(receipt, config, expectation);
|
|
8875
|
+
if (activeVersion === null || typeof config.verify_jwt !== "boolean" || !confirmedFunctionConfig(config, expectation.config ?? {}))
|
|
8876
|
+
return null;
|
|
8877
|
+
return { activeVersion, verifyJwt: config.verify_jwt };
|
|
8878
|
+
}
|
|
8879
|
+
function functionMutationResponse(expectation, response) {
|
|
8880
|
+
if (!response.ok)
|
|
8881
|
+
return releaseControlMutationFailure(expectation.operation, response);
|
|
8882
|
+
const confirmed = confirmedFunctionMutation(expectation, response.data);
|
|
8883
|
+
if (!confirmed) {
|
|
8884
|
+
return releaseControlFailure(expectation.operation, "OUTCOME_UNKNOWN", response.status);
|
|
8885
|
+
}
|
|
8886
|
+
return releaseControlSuccess(expectation.operation, {
|
|
8887
|
+
project_ref: expectation.projectRef,
|
|
8888
|
+
slug: expectation.slug,
|
|
8889
|
+
previous_active_version: expectation.expectedActiveVersion,
|
|
8890
|
+
active_version: confirmed.activeVersion,
|
|
8891
|
+
version: confirmed.activeVersion,
|
|
8892
|
+
verify_jwt: confirmed.verifyJwt
|
|
8515
8893
|
});
|
|
8516
8894
|
}
|
|
8517
8895
|
function readOnlyActivationResult() {
|
|
@@ -8523,15 +8901,22 @@ function readOnlyActivationResult() {
|
|
|
8523
8901
|
function functionActivationTarget(args) {
|
|
8524
8902
|
const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
|
|
8525
8903
|
const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
8526
|
-
const version = args.version;
|
|
8527
|
-
|
|
8528
|
-
throw new Error("'ref' is invalid for 'activate'");
|
|
8904
|
+
const version = positiveFunctionVersion(args.version, "Function activation version");
|
|
8905
|
+
projectRefPathSegment(projectRef, "Edge Function activation");
|
|
8529
8906
|
if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
|
|
8530
8907
|
throw new Error("'slug' is invalid for 'activate'");
|
|
8531
|
-
|
|
8532
|
-
|
|
8908
|
+
const expectedActiveVersion = requiredExpectedActiveVersion(args, "activate");
|
|
8909
|
+
return { projectRef, functionSlug, version, expectedActiveVersion };
|
|
8910
|
+
}
|
|
8911
|
+
function requiredExpectedActiveVersion(args, action) {
|
|
8912
|
+
const expected = args["expected-active-version"];
|
|
8913
|
+
if (expected === undefined) {
|
|
8914
|
+
throw new Error(`'--expected-active-version' required for '${action}'`);
|
|
8533
8915
|
}
|
|
8534
|
-
|
|
8916
|
+
const parsed = parseExpectedActiveVersion(expected);
|
|
8917
|
+
if (typeof parsed !== "string")
|
|
8918
|
+
throw new Error("Expected active version is invalid");
|
|
8919
|
+
return parsed;
|
|
8535
8920
|
}
|
|
8536
8921
|
async function activateFunctionVersion(http, args, readOnly = false) {
|
|
8537
8922
|
if (readOnly)
|
|
@@ -8539,29 +8924,40 @@ async function activateFunctionVersion(http, args, readOnly = false) {
|
|
|
8539
8924
|
const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
|
|
8540
8925
|
if (unsupported.length > 0)
|
|
8541
8926
|
throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
|
|
8542
|
-
const { projectRef, functionSlug, version } = functionActivationTarget(args);
|
|
8543
|
-
const endpoint =
|
|
8544
|
-
return
|
|
8927
|
+
const { projectRef, functionSlug, version, expectedActiveVersion } = functionActivationTarget(args);
|
|
8928
|
+
const endpoint = edgeFunctionResourcePath(projectRef, functionSlug) + `/versions/${encodeURIComponent(version)}/activate`;
|
|
8929
|
+
return functionMutationResponse({
|
|
8930
|
+
operation: "edge_functions.activate",
|
|
8931
|
+
projectRef,
|
|
8932
|
+
slug: functionSlug,
|
|
8933
|
+
expectedActiveVersion,
|
|
8934
|
+
targetVersion: version
|
|
8935
|
+
}, await http.postReleaseMutation(endpoint, { expected_active_version: expectedActiveVersion }));
|
|
8545
8936
|
}
|
|
8546
8937
|
function registerAdvancedTools(server, http, environment = process.env, options = {}) {
|
|
8547
|
-
server.tool("edge_functions", `Edge Function management (Deno/Bun serverless).
|
|
8938
|
+
server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Source deploys are bundled; verified prebuilt artifacts stay byte-exact.
|
|
8548
8939
|
Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
|
|
8549
8940
|
action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
|
|
8550
8941
|
ref: withDescription(Type.String(), "Project ref"),
|
|
8551
8942
|
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
|
|
8552
|
-
version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
|
|
8943
|
+
version: withDescription(functionVersionSchema, "[source/activate] Existing immutable Function version; source requires a positive version"),
|
|
8553
8944
|
code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
|
|
8554
8945
|
path: optional(Type.String(), "[deploy/check] Local file path to read code from (alternative to code)"),
|
|
8946
|
+
"prebundled-path": optional(Type.String(), "[deploy] Prebuilt runtime bundle to upload without rebuilding; requires expected-sha256"),
|
|
8947
|
+
"expected-sha256": optional(Type.String({ pattern: SHA256_HEX_PATTERN.source, minLength: 64, maxLength: 64 }), "[deploy] Required lowercase SHA-256 of the exact prebundled-path bytes"),
|
|
8555
8948
|
output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
|
|
8556
8949
|
files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
|
|
8557
8950
|
entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
|
|
8558
8951
|
minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
|
|
8559
8952
|
verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
|
|
8560
|
-
background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI")
|
|
8953
|
+
background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI"),
|
|
8954
|
+
"expected-active-version": withDescription(expectedActiveVersionSchema, "[deploy/deploy_bundle/activate] Required current active version, or 'absent' when none exists")
|
|
8561
8955
|
}, async (args) => {
|
|
8562
8956
|
if (args.action === "activate")
|
|
8563
8957
|
return activateFunctionVersion(http, args, options.readOnly);
|
|
8564
8958
|
const { action, ref, slug, path: pathArg, output, files, entrypoint, minify, verify_jwt, background_routes } = args;
|
|
8959
|
+
rejectPrebundledFlagsOutsideDeploy(action, args);
|
|
8960
|
+
const expectedActiveVersion = action === "deploy" || action === "deploy_bundle" ? requiredExpectedActiveVersion(args, action) : undefined;
|
|
8565
8961
|
let code = args.code;
|
|
8566
8962
|
const need = (f, v) => {
|
|
8567
8963
|
if (!v)
|
|
@@ -8578,16 +8974,10 @@ Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`,
|
|
|
8578
8974
|
if (!hasFunctionConfig()) {
|
|
8579
8975
|
throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
|
|
8580
8976
|
}
|
|
8581
|
-
const cr = await http.patch(
|
|
8977
|
+
const cr = await http.patch(`${edgeFunctionResourcePath(ref, slug)}/config`, functionConfig());
|
|
8582
8978
|
return cr.ok ? `✅ Function ${slug} config updated
|
|
8583
8979
|
${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
|
|
8584
8980
|
};
|
|
8585
|
-
const deploymentPolicyReceiptText = (successText, responsePayload) => {
|
|
8586
|
-
if (!hasFunctionConfig() || confirmedFunctionConfig(responsePayload, functionConfig())) {
|
|
8587
|
-
return successText;
|
|
8588
|
-
}
|
|
8589
|
-
return "❌ Unsafe deployment receipt: POST succeeded but did not confirm the requested function policy. No follow-up PATCH was attempted because code and policy must be activated atomically.";
|
|
8590
|
-
};
|
|
8591
8981
|
const checkSyntax = async (sourceCode) => {
|
|
8592
8982
|
const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
|
|
8593
8983
|
const tmpFile = join2(tmpDir, "index.ts");
|
|
@@ -8602,7 +8992,7 @@ ${e.stderr || e.message}` };
|
|
|
8602
8992
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
8603
8993
|
}
|
|
8604
8994
|
};
|
|
8605
|
-
if (pathArg && !code) {
|
|
8995
|
+
if (action === "check" && pathArg && !code) {
|
|
8606
8996
|
try {
|
|
8607
8997
|
code = await bundleEdgeFunctionPath(pathArg);
|
|
8608
8998
|
} catch (error) {
|
|
@@ -8612,8 +9002,7 @@ ${e.stderr || e.message}` };
|
|
|
8612
9002
|
}
|
|
8613
9003
|
switch (action) {
|
|
8614
9004
|
case "list":
|
|
8615
|
-
|
|
8616
|
-
break;
|
|
9005
|
+
return functionListResponse(await http.get(edgeFunctionResourcePath(ref)));
|
|
8617
9006
|
case "check":
|
|
8618
9007
|
need("code (or path)", code);
|
|
8619
9008
|
const checkRes = await checkSyntax(code);
|
|
@@ -8626,65 +9015,59 @@ ${checkRes.err}`;
|
|
|
8626
9015
|
break;
|
|
8627
9016
|
case "deploy":
|
|
8628
9017
|
need("slug", slug);
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
9018
|
+
const deployCode = await preparedDeployCode(args);
|
|
9019
|
+
if (!deployCode.prebundled) {
|
|
9020
|
+
const deployCheck = await checkSyntax(deployCode.code);
|
|
9021
|
+
if (!deployCheck.ok) {
|
|
9022
|
+
text = `❌ Deployment aborted. Syntax check failed:
|
|
8633
9023
|
${deployCheck.err}`;
|
|
8634
|
-
|
|
9024
|
+
break;
|
|
9025
|
+
}
|
|
8635
9026
|
}
|
|
8636
|
-
const
|
|
8637
|
-
code,
|
|
8638
|
-
minify,
|
|
9027
|
+
const deploymentResponse = await http.postReleaseMutation(edgeFunctionResourcePath(ref, slug), {
|
|
9028
|
+
code: deployCode.code,
|
|
9029
|
+
...deployCode.prebundled ? { prebundled: true, expected_sha256: deployCode.expectedSha256 } : { minify },
|
|
9030
|
+
expected_active_version: expectedActiveVersion,
|
|
8639
9031
|
...functionConfig()
|
|
8640
9032
|
});
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
9033
|
+
return functionMutationResponse({
|
|
9034
|
+
operation: "edge_functions.deploy",
|
|
9035
|
+
projectRef: ref,
|
|
9036
|
+
slug,
|
|
9037
|
+
expectedActiveVersion,
|
|
9038
|
+
config: functionConfig()
|
|
9039
|
+
}, deploymentResponse);
|
|
8647
9040
|
case "deploy_bundle":
|
|
8648
9041
|
need("slug", slug);
|
|
8649
9042
|
need("files", files);
|
|
8650
|
-
const
|
|
9043
|
+
const bundleResponse = await http.postReleaseMutation(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
|
|
8651
9044
|
files,
|
|
8652
9045
|
entrypoint,
|
|
8653
9046
|
minify,
|
|
9047
|
+
expected_active_version: expectedActiveVersion,
|
|
8654
9048
|
...functionConfig()
|
|
8655
9049
|
});
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
9050
|
+
return functionMutationResponse({
|
|
9051
|
+
operation: "edge_functions.deploy_bundle",
|
|
9052
|
+
projectRef: ref,
|
|
9053
|
+
slug,
|
|
9054
|
+
expectedActiveVersion,
|
|
9055
|
+
config: functionConfig()
|
|
9056
|
+
}, bundleResponse);
|
|
8662
9057
|
case "config":
|
|
8663
9058
|
text = await updateFunctionConfig();
|
|
8664
9059
|
break;
|
|
8665
9060
|
case "source":
|
|
8666
9061
|
need("slug", slug);
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
text = JSON.stringify(sr.data, null, 2);
|
|
8674
|
-
break;
|
|
8675
|
-
}
|
|
8676
|
-
const sourceCode = functionSourceCode(sr.data);
|
|
8677
|
-
if (sourceCode === null) {
|
|
8678
|
-
text = "❌ Source response did not contain a string code field";
|
|
8679
|
-
break;
|
|
8680
|
-
}
|
|
8681
|
-
const outputPath = resolve2(output);
|
|
8682
|
-
writeFileSync(outputPath, sourceCode, { flag: "wx" });
|
|
8683
|
-
text = `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`;
|
|
8684
|
-
break;
|
|
9062
|
+
return readFunctionSource(http, {
|
|
9063
|
+
projectRef: ref,
|
|
9064
|
+
slug,
|
|
9065
|
+
version: args.version,
|
|
9066
|
+
output
|
|
9067
|
+
});
|
|
8685
9068
|
case "delete":
|
|
8686
9069
|
need("slug", slug);
|
|
8687
|
-
text = (await http.delete(
|
|
9070
|
+
text = (await http.delete(edgeFunctionResourcePath(ref, slug))).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
|
|
8688
9071
|
break;
|
|
8689
9072
|
default:
|
|
8690
9073
|
text = `❌ Unknown action`;
|
|
@@ -10605,12 +10988,14 @@ var MAX_BODY_FILE_BYTES = 1048576;
|
|
|
10605
10988
|
var MAX_HEADER_COUNT = 64;
|
|
10606
10989
|
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
10607
10990
|
var MAX_SCHEDULE_NAME_LENGTH = 120;
|
|
10991
|
+
var CANONICAL_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10608
10992
|
var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
|
|
10609
10993
|
var ACTION_ARGUMENTS2 = {
|
|
10610
10994
|
list: new Set(["action", "ref"]),
|
|
10995
|
+
get: new Set(["action", "ref", "schedule_id"]),
|
|
10611
10996
|
create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
|
|
10612
|
-
update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
|
|
10613
|
-
delete: new Set(["action", "ref", "schedule_id"])
|
|
10997
|
+
update: new Set(["action", "ref", "schedule_id", "expected_updated_at", "name", "cron", "method", "enabled", "body_file", "header_env"]),
|
|
10998
|
+
delete: new Set(["action", "ref", "schedule_id", "expected_updated_at"])
|
|
10614
10999
|
};
|
|
10615
11000
|
function parseHeaderEnvironment(input) {
|
|
10616
11001
|
if (typeof input !== "string")
|
|
@@ -10730,7 +11115,13 @@ function validScheduleDefinition(schedule) {
|
|
|
10730
11115
|
return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
|
|
10731
11116
|
}
|
|
10732
11117
|
function validScheduleMetadata(schedule) {
|
|
10733
|
-
return typeof schedule.created_at === "string" &&
|
|
11118
|
+
return typeof schedule.created_at === "string" && isCanonicalTimestamp(schedule.updated_at);
|
|
11119
|
+
}
|
|
11120
|
+
function isCanonicalTimestamp(candidate) {
|
|
11121
|
+
if (typeof candidate !== "string" || !CANONICAL_TIMESTAMP_PATTERN.test(candidate))
|
|
11122
|
+
return false;
|
|
11123
|
+
const milliseconds = Date.parse(candidate);
|
|
11124
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
10734
11125
|
}
|
|
10735
11126
|
function safeSchedulePayload(schedule) {
|
|
10736
11127
|
const headerNames = safeHeaderNames(schedule.header_names);
|
|
@@ -10803,6 +11194,17 @@ function listResponse(ref, response) {
|
|
|
10803
11194
|
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10804
11195
|
return releaseControlSuccess(operation, { project_ref: ref, schedules });
|
|
10805
11196
|
}
|
|
11197
|
+
function getResponse(ref, scheduleId, response) {
|
|
11198
|
+
const operation = "scheduled_functions.get";
|
|
11199
|
+
if (!response.ok)
|
|
11200
|
+
return scheduleFailure(operation, response);
|
|
11201
|
+
const payload = objectRecord2(response.data);
|
|
11202
|
+
const schedule = safeSchedule(payload?.schedule);
|
|
11203
|
+
if (payload?.project_ref !== ref || !schedule || schedule.id !== scheduleId) {
|
|
11204
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
11205
|
+
}
|
|
11206
|
+
return releaseControlSuccess(operation, { project_ref: ref, schedule });
|
|
11207
|
+
}
|
|
10806
11208
|
function mutationResponse(expectation, response) {
|
|
10807
11209
|
const { action, ref, requestId, expectedFields } = expectation;
|
|
10808
11210
|
const scheduleId = action === "update" ? expectation.scheduleId : undefined;
|
|
@@ -10812,22 +11214,29 @@ function mutationResponse(expectation, response) {
|
|
|
10812
11214
|
const payload = objectRecord2(response.data);
|
|
10813
11215
|
const schedule = safeSchedule(payload?.schedule);
|
|
10814
11216
|
const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
|
|
10815
|
-
|
|
11217
|
+
const confirmsRevision = action === "create" || payload?.previous_updated_at === expectation.expectedUpdatedAt && schedule !== null && schedule.updated_at > expectation.expectedUpdatedAt;
|
|
11218
|
+
if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || !confirmsRevision || scheduleId !== undefined && schedule.id !== scheduleId) {
|
|
10816
11219
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10817
11220
|
}
|
|
10818
|
-
return releaseControlSuccess(operation, {
|
|
11221
|
+
return releaseControlSuccess(operation, {
|
|
11222
|
+
project_ref: ref,
|
|
11223
|
+
request_id: requestId,
|
|
11224
|
+
...action === "update" ? { previous_updated_at: expectation.expectedUpdatedAt } : {},
|
|
11225
|
+
schedule
|
|
11226
|
+
});
|
|
10819
11227
|
}
|
|
10820
|
-
function deleteResponse(ref, scheduleId, response) {
|
|
11228
|
+
function deleteResponse(ref, scheduleId, expectedUpdatedAt, response) {
|
|
10821
11229
|
const operation = "scheduled_functions.delete";
|
|
10822
11230
|
if (!response.ok)
|
|
10823
11231
|
return releaseControlMutationFailure(operation, response);
|
|
10824
11232
|
const payload = objectRecord2(response.data);
|
|
10825
|
-
if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId) {
|
|
11233
|
+
if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId || payload.deleted_updated_at !== expectedUpdatedAt) {
|
|
10826
11234
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10827
11235
|
}
|
|
10828
11236
|
return releaseControlSuccess(operation, {
|
|
10829
11237
|
project_ref: ref,
|
|
10830
11238
|
schedule_id: scheduleId,
|
|
11239
|
+
deleted_updated_at: expectedUpdatedAt,
|
|
10831
11240
|
deleted: true
|
|
10832
11241
|
});
|
|
10833
11242
|
}
|
|
@@ -10876,7 +11285,15 @@ function requiredCron(args, action) {
|
|
|
10876
11285
|
throw new Error(`'cron' is invalid for '${action}'`);
|
|
10877
11286
|
return cron;
|
|
10878
11287
|
}
|
|
11288
|
+
function requiredExpectedUpdatedAt(args, action) {
|
|
11289
|
+
const candidate = args.expected_updated_at;
|
|
11290
|
+
if (!isCanonicalTimestamp(candidate)) {
|
|
11291
|
+
throw new Error(`'expected_updated_at' must be a canonical UTC timestamp for '${action}'`);
|
|
11292
|
+
}
|
|
11293
|
+
return candidate;
|
|
11294
|
+
}
|
|
10879
11295
|
function updateRequest(args, environment) {
|
|
11296
|
+
const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "update");
|
|
10880
11297
|
const body = scheduleBody(args.body_file);
|
|
10881
11298
|
const headers = scheduleHeaders(args.header_env, environment);
|
|
10882
11299
|
const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
|
|
@@ -10892,11 +11309,18 @@ function updateRequest(args, environment) {
|
|
|
10892
11309
|
if (Object.keys(mutationFields).length === 0) {
|
|
10893
11310
|
throw new Error("Scheduled Function update requires at least one field");
|
|
10894
11311
|
}
|
|
10895
|
-
return {
|
|
11312
|
+
return {
|
|
11313
|
+
request_id: randomUUID(),
|
|
11314
|
+
expected_updated_at: expectedUpdatedAt,
|
|
11315
|
+
...mutationFields
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
function deletePath(schedulePathname, expectedUpdatedAt) {
|
|
11319
|
+
return `${schedulePathname}?expected_updated_at=${encodeURIComponent(expectedUpdatedAt)}`;
|
|
10896
11320
|
}
|
|
10897
11321
|
async function executeScheduleAction(http, environment, args, readOnly = false) {
|
|
10898
11322
|
const action = args.action;
|
|
10899
|
-
if (readOnly && action !== "list")
|
|
11323
|
+
if (readOnly && action !== "list" && action !== "get")
|
|
10900
11324
|
return readOnlyResult2();
|
|
10901
11325
|
assertActionArguments2(action, args);
|
|
10902
11326
|
const ref = requiredText2(args, "ref", action);
|
|
@@ -10908,27 +11332,34 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
|
|
|
10908
11332
|
return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
|
|
10909
11333
|
}
|
|
10910
11334
|
const scheduleId = requiredText2(args, "schedule_id", action);
|
|
11335
|
+
const targetPath = schedulePath(ref, scheduleId);
|
|
11336
|
+
if (action === "get")
|
|
11337
|
+
return getResponse(ref, scheduleId, await http.get(targetPath));
|
|
10911
11338
|
if (action === "update") {
|
|
10912
11339
|
const request = updateRequest(args, environment);
|
|
10913
11340
|
const requestId = request.request_id;
|
|
11341
|
+
const expectedUpdatedAt2 = request.expected_updated_at;
|
|
10914
11342
|
return mutationResponse({
|
|
10915
11343
|
action,
|
|
10916
11344
|
ref,
|
|
10917
11345
|
scheduleId,
|
|
10918
11346
|
requestId,
|
|
11347
|
+
expectedUpdatedAt: expectedUpdatedAt2,
|
|
10919
11348
|
expectedFields: safeMutationFields(request)
|
|
10920
|
-
}, await http.patch(
|
|
11349
|
+
}, await http.patch(targetPath, request));
|
|
10921
11350
|
}
|
|
10922
|
-
|
|
11351
|
+
const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "delete");
|
|
11352
|
+
return deleteResponse(ref, scheduleId, expectedUpdatedAt, await http.delete(deletePath(targetPath, expectedUpdatedAt)));
|
|
10923
11353
|
}
|
|
10924
11354
|
function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
|
|
10925
11355
|
server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
|
|
10926
11356
|
}
|
|
10927
|
-
var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, create, update, delete";
|
|
11357
|
+
var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, get, create, update, delete";
|
|
10928
11358
|
var SCHEDULE_TOOL_SCHEMA = {
|
|
10929
|
-
action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
|
|
11359
|
+
action: withDescription(stringEnum(["list", "get", "create", "update", "delete"]), "Action"),
|
|
10930
11360
|
ref: withDescription(Type.String(), "Project ref"),
|
|
10931
|
-
schedule_id: optional(Type.String(), "[update/delete] Schedule ID"),
|
|
11361
|
+
schedule_id: optional(Type.String(), "[get/update/delete] Schedule ID"),
|
|
11362
|
+
expected_updated_at: optional(Type.String(), "[update/delete] Canonical updated_at from list"),
|
|
10932
11363
|
name: optional(Type.String(), "[create/update] Display name"),
|
|
10933
11364
|
slug: optional(Type.String(), "[create] Edge Function slug"),
|
|
10934
11365
|
cron: optional(Type.String(), "[create/update] Five-field cron expression"),
|
|
@@ -10940,7 +11371,7 @@ var SCHEDULE_TOOL_SCHEMA = {
|
|
|
10940
11371
|
// package.json
|
|
10941
11372
|
var package_default = {
|
|
10942
11373
|
name: "@supacloud/cli",
|
|
10943
|
-
version: "0.
|
|
11374
|
+
version: "0.18.0",
|
|
10944
11375
|
description: "Project-scoped CLI for SupaCloud users",
|
|
10945
11376
|
type: "module",
|
|
10946
11377
|
main: "./dist/index.js",
|
|
@@ -11156,8 +11587,9 @@ EXAMPLES
|
|
|
11156
11587
|
${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
|
|
11157
11588
|
${preferredCommand} ai show_skill
|
|
11158
11589
|
${preferredCommand} ai install_skill --dry_run
|
|
11159
|
-
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
|
|
11160
|
-
${preferredCommand} edge_functions
|
|
11590
|
+
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent
|
|
11591
|
+
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
|
|
11592
|
+
${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
|
|
11161
11593
|
${preferredCommand} scheduled_functions list --ref abc123
|
|
11162
11594
|
${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
|
|
11163
11595
|
${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ until a project-scoped context is resolved.
|
|
|
31
31
|
- `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
|
|
32
32
|
- `auth`: provider and authentication configuration.
|
|
33
33
|
- `storage`: buckets and object-management workflows.
|
|
34
|
-
- `edge_functions`: deploy and configure Edge Functions.
|
|
34
|
+
- `edge_functions`: list, read immutable source, deploy, activate, and configure Edge Functions. Pass the non-negative version read from `list` as `--expected-active-version`; use `absent` only for a new slug. Version `0` is a legacy CAS token and cannot be used as a source or activation target.
|
|
35
35
|
- `frontend`: list, build/deploy, domain, and deployment workflows.
|
|
36
36
|
- `secrets`: project secret management; never print values after write.
|
|
37
37
|
- `queue`, `task_events`, `diagnostics`: asynchronous workload operations and bounded diagnostics.
|