@supacloud/cli 0.16.0 → 0.17.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 +65 -9
- package/dist/index.js +464 -136
- 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 positive integer
|
|
216
|
+
version from `edge_functions list`; use `absent` only when creating a slug that
|
|
217
|
+
does not yet exist. A stale value returns HTTP 409 without building, preheating,
|
|
218
|
+
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
|
|
@@ -196,27 +229,50 @@ 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
231
|
retry.
|
|
232
|
+
Version `0` is reserved for service-internal legacy recovery and cannot be used
|
|
233
|
+
as a public CLI/API activation target or expected active version.
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"schema": "supacloud.cli.release-control.v1",
|
|
238
|
+
"ok": true,
|
|
239
|
+
"operation": "edge_functions.deploy_bundle",
|
|
240
|
+
"project_ref": "abc123",
|
|
241
|
+
"slug": "hello",
|
|
242
|
+
"previous_active_version": "7",
|
|
243
|
+
"active_version": "8",
|
|
244
|
+
"version": "8",
|
|
245
|
+
"verify_jwt": true
|
|
246
|
+
}
|
|
247
|
+
```
|
|
199
248
|
|
|
200
249
|
Scheduled Function lifecycle operations are also project-scoped:
|
|
201
250
|
|
|
202
251
|
```bash
|
|
203
252
|
supacloud-cli scheduled_functions create --ref abc123 --name nightly \
|
|
204
253
|
--slug cleanup --cron "0 2 * * *" --method POST
|
|
254
|
+
supacloud-cli scheduled_functions get --ref abc123 --schedule_id <id>
|
|
205
255
|
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>
|
|
256
|
+
--expected_updated_at <updated_at-from-list> --cron "0 3 * * *"
|
|
257
|
+
supacloud-cli scheduled_functions delete --ref abc123 --schedule_id <id> \
|
|
258
|
+
--expected_updated_at <updated_at-from-list>
|
|
208
259
|
```
|
|
209
260
|
|
|
210
261
|
Schedule IDs are canonical UUIDv4 values returned by create/list. Cron values
|
|
211
262
|
use bounded numeric five-field syntax with wildcards, lists, ranges, and steps;
|
|
212
263
|
out-of-range endpoints and steps are rejected before HTTP dispatch.
|
|
264
|
+
Update and delete require the exact canonical UTC `updated_at` returned by list.
|
|
265
|
+
A stale revision fails with HTTP 409 and performs no mutation; read the list
|
|
266
|
+
again before deciding whether to issue a new write.
|
|
213
267
|
|
|
214
268
|
Use `--body_file ./payload.json` for a JSON-object request body. Header values
|
|
215
269
|
must come from environment variables: pass a JSON name mapping such as
|
|
216
270
|
`--header_env '{"x-schedule-token":"SCHEDULE_TOKEN"}'`. Platform-owned
|
|
217
271
|
`authorization`, `apikey`, and `x-project-ref` headers cannot be overridden. Receipts never
|
|
218
272
|
include header values or body content; list and mutation receipts report only
|
|
219
|
-
whether the body is empty and the configured header names.
|
|
273
|
+
whether the body is empty and the configured header names. Update receipts bind
|
|
274
|
+
`previous_updated_at` to the requested revision and return a newer `updated_at`;
|
|
275
|
+
delete receipts return the matched revision as `deleted_updated_at`.
|
|
220
276
|
|
|
221
277
|
For secret writes, `--from-env` accepts a comma-separated list of environment
|
|
222
278
|
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"],
|
|
@@ -6964,6 +6964,28 @@ function migrationRows(data) {
|
|
|
6964
6964
|
function migrationIdentityKey(version, name) {
|
|
6965
6965
|
return `${version}\x00${name}`;
|
|
6966
6966
|
}
|
|
6967
|
+
function migrationIdentities(data) {
|
|
6968
|
+
return migrationRows(data).flatMap((row) => row.version == null || row.name == null ? [] : [{ version: String(row.version), name: String(row.name) }]);
|
|
6969
|
+
}
|
|
6970
|
+
function identityConflicts(local, remote) {
|
|
6971
|
+
const reusesVersionOrName = local.version === remote.version || local.name === remote.name;
|
|
6972
|
+
const exactlyMatches = local.version === remote.version && local.name === remote.name;
|
|
6973
|
+
return reusesVersionOrName && !exactlyMatches;
|
|
6974
|
+
}
|
|
6975
|
+
function migrationIdentityConflicts(data, migrationFiles) {
|
|
6976
|
+
const remoteMigrations = migrationIdentities(data);
|
|
6977
|
+
return migrationFiles.flatMap((localMigration) => remoteMigrations.filter((remoteMigration) => identityConflicts(localMigration, remoteMigration)).map((remoteMigration) => ({ file: localMigration.file, local: localMigration, remote: remoteMigration })));
|
|
6978
|
+
}
|
|
6979
|
+
function assertNoMigrationIdentityConflicts(data, migrationFiles) {
|
|
6980
|
+
const conflicts = migrationIdentityConflicts(data, migrationFiles);
|
|
6981
|
+
if (!conflicts.length)
|
|
6982
|
+
return;
|
|
6983
|
+
throw new Error([
|
|
6984
|
+
"Migration identity conflicts:",
|
|
6985
|
+
...conflicts.map(({ file, local, remote }) => `- ${file} (${local.version}) conflicts with remote ${remote.name} (${remote.version})`)
|
|
6986
|
+
].join(`
|
|
6987
|
+
`));
|
|
6988
|
+
}
|
|
6967
6989
|
function nameBoundMigrationMarkerKeys(data) {
|
|
6968
6990
|
const keys = new Set;
|
|
6969
6991
|
for (const row of migrationRows(data)) {
|
|
@@ -7227,6 +7249,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7227
7249
|
text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
|
|
7228
7250
|
break;
|
|
7229
7251
|
}
|
|
7252
|
+
assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
|
|
7230
7253
|
if (args.dry_run) {
|
|
7231
7254
|
const appliedKeys = appliedMigrationKeys(migrationsResult.data);
|
|
7232
7255
|
const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
|
|
@@ -7882,13 +7905,14 @@ var MAX_MIME_TYPE_LENGTH = 255;
|
|
|
7882
7905
|
var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7883
7906
|
var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
|
|
7884
7907
|
var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
|
|
7908
|
+
var BUCKET_REVISION_PATTERN = /^[0-9]{1,20}$/;
|
|
7885
7909
|
var ACTION_ARGUMENTS = {
|
|
7886
7910
|
status: new Set(["action"]),
|
|
7887
7911
|
list_buckets: new Set(["action", "ref"]),
|
|
7888
7912
|
get_bucket: new Set(["action", "ref", "bucket"]),
|
|
7889
7913
|
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"]),
|
|
7914
|
+
update_bucket: new Set(["action", "ref", "bucket", "expected_revision", "public", "file_size_limit", "allowed_mime_types"]),
|
|
7915
|
+
delete_bucket: new Set(["action", "ref", "bucket", "expected_revision", "require_empty"]),
|
|
7892
7916
|
list_files: new Set(["action", "ref", "bucket"]),
|
|
7893
7917
|
upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
|
|
7894
7918
|
delete_file: new Set(["action", "ref", "bucket", "filename"])
|
|
@@ -7944,6 +7968,16 @@ function requiredBucketId(args) {
|
|
|
7944
7968
|
throw new Error("'bucket' is invalid for Storage buckets");
|
|
7945
7969
|
return bucket;
|
|
7946
7970
|
}
|
|
7971
|
+
function requiredBucketRevision(args) {
|
|
7972
|
+
const revision = requiredText(args, "expected_revision");
|
|
7973
|
+
if (!BUCKET_REVISION_PATTERN.test(revision))
|
|
7974
|
+
throw new Error("'expected_revision' is invalid for Storage buckets");
|
|
7975
|
+
return revision;
|
|
7976
|
+
}
|
|
7977
|
+
function assertEmptyBucketDeletion(args) {
|
|
7978
|
+
if (args.require_empty !== true)
|
|
7979
|
+
throw new Error("'require_empty=true' required for 'delete_bucket'");
|
|
7980
|
+
}
|
|
7947
7981
|
function assertActionArguments(action, args) {
|
|
7948
7982
|
const allowedArguments = ACTION_ARGUMENTS[action];
|
|
7949
7983
|
if (!allowedArguments)
|
|
@@ -7978,9 +8012,12 @@ function assertBucketSettings(args) {
|
|
|
7978
8012
|
function isAllowedMimeTypes(candidate) {
|
|
7979
8013
|
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
8014
|
}
|
|
8015
|
+
function isBucketRevision(candidate) {
|
|
8016
|
+
return candidate === null || typeof candidate === "string" && BUCKET_REVISION_PATTERN.test(candidate);
|
|
8017
|
+
}
|
|
7981
8018
|
function safeBucket(candidate, expectedBucket) {
|
|
7982
8019
|
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) {
|
|
8020
|
+
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
8021
|
return null;
|
|
7985
8022
|
}
|
|
7986
8023
|
return {
|
|
@@ -7988,7 +8025,8 @@ function safeBucket(candidate, expectedBucket) {
|
|
|
7988
8025
|
name: bucket.name,
|
|
7989
8026
|
public: bucket.public,
|
|
7990
8027
|
file_size_limit: bucket.file_size_limit,
|
|
7991
|
-
allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types]
|
|
8028
|
+
allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types],
|
|
8029
|
+
revision: bucket.revision
|
|
7992
8030
|
};
|
|
7993
8031
|
}
|
|
7994
8032
|
function safeExactBucket(candidate, expectedBucket) {
|
|
@@ -8012,18 +8050,30 @@ function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
|
|
|
8012
8050
|
return null;
|
|
8013
8051
|
return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
|
|
8014
8052
|
}
|
|
8015
|
-
function safeDeletedBucket(candidate, expectedBucket) {
|
|
8053
|
+
function safeDeletedBucket(candidate, expectedBucket, expectedRevision) {
|
|
8016
8054
|
const receipt = bucketRecord(candidate);
|
|
8017
|
-
return receipt?.id === expectedBucket && receipt.deleted === true
|
|
8055
|
+
return receipt?.id === expectedBucket && receipt.deleted === true && receipt.require_empty === true && receipt.previous_revision === expectedRevision && receipt.new_revision === null ? {
|
|
8056
|
+
bucket_id: expectedBucket,
|
|
8057
|
+
deleted: true,
|
|
8058
|
+
require_empty: true,
|
|
8059
|
+
previous_revision: expectedRevision,
|
|
8060
|
+
new_revision: null
|
|
8061
|
+
} : null;
|
|
8018
8062
|
}
|
|
8019
8063
|
function mutationReadbackResponse(expectation) {
|
|
8020
|
-
const { operation, ref, response, expectedBucket, request } = expectation;
|
|
8064
|
+
const { operation, ref, response, expectedBucket, request, previousRevision, expectedNewRevision } = expectation;
|
|
8021
8065
|
const readback = safeExactBucket(response.data, expectedBucket);
|
|
8022
|
-
const validReadback = readback?.name === expectedBucket && bucketMatchesRequest(readback, request);
|
|
8066
|
+
const validReadback = readback?.name === expectedBucket && readback.revision !== null && (expectedNewRevision === undefined || readback.revision === expectedNewRevision) && bucketMatchesRequest(readback, request);
|
|
8023
8067
|
if (!response.ok || !validReadback || !readback) {
|
|
8024
8068
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
|
|
8025
8069
|
}
|
|
8026
|
-
return releaseControlSuccess(operation, {
|
|
8070
|
+
return releaseControlSuccess(operation, {
|
|
8071
|
+
project_ref: ref,
|
|
8072
|
+
bucket_id: expectedBucket,
|
|
8073
|
+
previous_revision: previousRevision,
|
|
8074
|
+
new_revision: readback.revision,
|
|
8075
|
+
bucket: readback
|
|
8076
|
+
});
|
|
8027
8077
|
}
|
|
8028
8078
|
function bucketResponse(expectation) {
|
|
8029
8079
|
const { operation, ref, response, operationKind, safePayload } = expectation;
|
|
@@ -8049,10 +8099,10 @@ function createBucketRequest(args) {
|
|
|
8049
8099
|
}
|
|
8050
8100
|
function updateBucketRequest(args) {
|
|
8051
8101
|
assertBucketSettings(args);
|
|
8052
|
-
const
|
|
8053
|
-
if (Object.keys(
|
|
8102
|
+
const settings = Object.fromEntries(["public", "file_size_limit", "allowed_mime_types"].filter((field) => args[field] !== undefined).map((field) => [field, args[field]]));
|
|
8103
|
+
if (Object.keys(settings).length === 0)
|
|
8054
8104
|
throw new Error("Bucket update requires at least one field");
|
|
8055
|
-
return
|
|
8105
|
+
return { expected_revision: requiredBucketRevision(args), ...settings };
|
|
8056
8106
|
}
|
|
8057
8107
|
function equalAllowedMimeTypes(candidate, expected) {
|
|
8058
8108
|
if (!Array.isArray(expected))
|
|
@@ -8080,18 +8130,10 @@ async function createBucketMutationReceipt(http, ref, bucket, request) {
|
|
|
8080
8130
|
safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
|
|
8081
8131
|
});
|
|
8082
8132
|
}
|
|
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
|
-
});
|
|
8133
|
+
function safeUpdatedBucket(candidate, expectedBucket, expectedRevision, request) {
|
|
8134
|
+
const receipt = bucketRecord(candidate);
|
|
8135
|
+
const bucket = safeExactBucket(candidate, expectedBucket);
|
|
8136
|
+
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
8137
|
}
|
|
8096
8138
|
async function listBuckets(http, args) {
|
|
8097
8139
|
const ref = requiredProjectRef(args);
|
|
@@ -8134,35 +8176,45 @@ async function createBucket(http, args) {
|
|
|
8134
8176
|
ref,
|
|
8135
8177
|
response: await http.get(bucketPath),
|
|
8136
8178
|
expectedBucket: bucket,
|
|
8137
|
-
request: expectedReadback
|
|
8179
|
+
request: expectedReadback,
|
|
8180
|
+
previousRevision: null
|
|
8138
8181
|
});
|
|
8139
8182
|
}
|
|
8140
8183
|
async function updateBucket(http, args) {
|
|
8141
8184
|
const ref = requiredProjectRef(args);
|
|
8142
8185
|
const bucket = requiredBucketId(args);
|
|
8143
8186
|
const request = updateBucketRequest(args);
|
|
8187
|
+
const expectedRevision = request.expected_revision;
|
|
8144
8188
|
const bucketPath = storageBucketPath(ref, bucket);
|
|
8145
|
-
const
|
|
8146
|
-
if (
|
|
8147
|
-
return
|
|
8189
|
+
const mutation = await http.put(bucketPath, request);
|
|
8190
|
+
if (!mutation.ok)
|
|
8191
|
+
return releaseControlMutationFailure("storage.update_bucket", mutation);
|
|
8192
|
+
const updated = safeUpdatedBucket(mutation.data, bucket, expectedRevision, request);
|
|
8193
|
+
if (!updated)
|
|
8194
|
+
return releaseControlFailure("storage.update_bucket", "OUTCOME_UNKNOWN", mutation.status);
|
|
8148
8195
|
return mutationReadbackResponse({
|
|
8149
8196
|
operation: "storage.update_bucket",
|
|
8150
8197
|
ref,
|
|
8151
8198
|
response: await http.get(bucketPath),
|
|
8152
8199
|
expectedBucket: bucket,
|
|
8153
|
-
request
|
|
8200
|
+
request,
|
|
8201
|
+
previousRevision: expectedRevision,
|
|
8202
|
+
expectedNewRevision: updated.revision ?? undefined
|
|
8154
8203
|
});
|
|
8155
8204
|
}
|
|
8156
8205
|
async function deleteBucket(http, args) {
|
|
8157
8206
|
const ref = requiredProjectRef(args);
|
|
8158
8207
|
const bucket = requiredBucketId(args);
|
|
8208
|
+
const expectedRevision = requiredBucketRevision(args);
|
|
8209
|
+
assertEmptyBucketDeletion(args);
|
|
8159
8210
|
const bucketPath = storageBucketPath(ref, bucket);
|
|
8211
|
+
const deletePath = `${bucketPath}?expected_revision=${encodeURIComponent(expectedRevision)}&require_empty=true`;
|
|
8160
8212
|
const receipt = bucketResponse({
|
|
8161
8213
|
operation: "storage.delete_bucket",
|
|
8162
8214
|
ref,
|
|
8163
|
-
response: await http.delete(
|
|
8215
|
+
response: await http.delete(deletePath),
|
|
8164
8216
|
operationKind: "mutation",
|
|
8165
|
-
safePayload: (candidate) => safeDeletedBucket(candidate, bucket)
|
|
8217
|
+
safePayload: (candidate) => safeDeletedBucket(candidate, bucket, expectedRevision)
|
|
8166
8218
|
});
|
|
8167
8219
|
if (receipt.isError)
|
|
8168
8220
|
return receipt;
|
|
@@ -8203,6 +8255,8 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
|
|
|
8203
8255
|
public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
|
|
8204
8256
|
file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
|
|
8205
8257
|
allowed_mime_types: withDescription(allowedMimeTypesSchema, "[create_bucket/update_bucket] MIME types as a comma-separated or JSON array"),
|
|
8258
|
+
expected_revision: optional(Type.String({ pattern: BUCKET_REVISION_PATTERN.source }), "[update_bucket/delete_bucket] Exact revision from list_buckets/get_bucket"),
|
|
8259
|
+
require_empty: optional(Type.Boolean(), "[delete_bucket] Must be true; deletion never empties a bucket"),
|
|
8206
8260
|
filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
|
|
8207
8261
|
base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
|
|
8208
8262
|
mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
|
|
@@ -8268,12 +8322,79 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
|
|
|
8268
8322
|
}
|
|
8269
8323
|
|
|
8270
8324
|
// src/shared/tools/advanced-tools.ts
|
|
8271
|
-
import {
|
|
8325
|
+
import { createHash as createHash2, timingSafeEqual } from "node:crypto";
|
|
8326
|
+
import {
|
|
8327
|
+
closeSync,
|
|
8328
|
+
constants as fsConstants,
|
|
8329
|
+
existsSync as existsSync3,
|
|
8330
|
+
fstatSync,
|
|
8331
|
+
mkdtempSync,
|
|
8332
|
+
openSync,
|
|
8333
|
+
readFileSync as readFileSync3,
|
|
8334
|
+
rmSync,
|
|
8335
|
+
statSync as statSync2,
|
|
8336
|
+
writeFileSync
|
|
8337
|
+
} from "node:fs";
|
|
8272
8338
|
import { tmpdir } from "node:os";
|
|
8273
8339
|
import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
|
|
8274
8340
|
import { promisify } from "node:util";
|
|
8275
8341
|
import { execFile } from "node:child_process";
|
|
8276
8342
|
var execFileAsync = promisify(execFile);
|
|
8343
|
+
var SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
8344
|
+
function openFileIdentity(descriptor) {
|
|
8345
|
+
const state = fstatSync(descriptor, { bigint: true });
|
|
8346
|
+
if (!state.isFile())
|
|
8347
|
+
throw new Error("Prebundled path must be a regular file");
|
|
8348
|
+
return {
|
|
8349
|
+
dev: state.dev,
|
|
8350
|
+
ino: state.ino,
|
|
8351
|
+
size: state.size,
|
|
8352
|
+
mtimeNs: state.mtimeNs,
|
|
8353
|
+
ctimeNs: state.ctimeNs
|
|
8354
|
+
};
|
|
8355
|
+
}
|
|
8356
|
+
function sameOpenFile(left, right) {
|
|
8357
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
8358
|
+
}
|
|
8359
|
+
function verifiedUtf8Code(bytes) {
|
|
8360
|
+
let code;
|
|
8361
|
+
try {
|
|
8362
|
+
code = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
8363
|
+
} catch (error) {
|
|
8364
|
+
if (error instanceof TypeError)
|
|
8365
|
+
throw new Error("Prebundled file is not valid UTF-8");
|
|
8366
|
+
throw error;
|
|
8367
|
+
}
|
|
8368
|
+
if (!Buffer.from(code, "utf8").equals(bytes)) {
|
|
8369
|
+
throw new Error("Prebundled file does not round-trip as UTF-8");
|
|
8370
|
+
}
|
|
8371
|
+
return code;
|
|
8372
|
+
}
|
|
8373
|
+
function assertExpectedSha256(bytes, expectedSha256) {
|
|
8374
|
+
if (!SHA256_HEX_PATTERN.test(expectedSha256)) {
|
|
8375
|
+
throw new Error("'--expected-sha256' must be a lowercase 64-character SHA-256");
|
|
8376
|
+
}
|
|
8377
|
+
const actualDigest = createHash2("sha256").update(bytes).digest();
|
|
8378
|
+
const expectedDigest = Buffer.from(expectedSha256, "hex");
|
|
8379
|
+
if (!timingSafeEqual(actualDigest, expectedDigest)) {
|
|
8380
|
+
throw new Error("Prebundled file SHA-256 does not match --expected-sha256");
|
|
8381
|
+
}
|
|
8382
|
+
}
|
|
8383
|
+
function readVerifiedPrebundledCode(pathArg, expectedSha256) {
|
|
8384
|
+
const descriptor = openSync(resolve2(pathArg), fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
|
|
8385
|
+
try {
|
|
8386
|
+
const identityBeforeRead = openFileIdentity(descriptor);
|
|
8387
|
+
const bytes = readFileSync3(descriptor);
|
|
8388
|
+
const identityAfterRead = openFileIdentity(descriptor);
|
|
8389
|
+
if (BigInt(bytes.byteLength) !== identityBeforeRead.size || !sameOpenFile(identityBeforeRead, identityAfterRead)) {
|
|
8390
|
+
throw new Error("Prebundled file changed while it was being read");
|
|
8391
|
+
}
|
|
8392
|
+
assertExpectedSha256(bytes, expectedSha256);
|
|
8393
|
+
return verifiedUtf8Code(bytes);
|
|
8394
|
+
} finally {
|
|
8395
|
+
closeSync(descriptor);
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8277
8398
|
async function runBunBuild(args) {
|
|
8278
8399
|
try {
|
|
8279
8400
|
return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -8298,6 +8419,54 @@ async function bundleEdgeFunctionPath(pathArg) {
|
|
|
8298
8419
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
8299
8420
|
}
|
|
8300
8421
|
}
|
|
8422
|
+
function prebundledDeployCode(pathArg, expectedSha256, minify) {
|
|
8423
|
+
if (typeof expectedSha256 !== "string") {
|
|
8424
|
+
throw new Error("'--expected-sha256' required with '--prebundled-path'");
|
|
8425
|
+
}
|
|
8426
|
+
if (minify !== undefined) {
|
|
8427
|
+
throw new Error("'--minify' cannot be combined with '--prebundled-path'");
|
|
8428
|
+
}
|
|
8429
|
+
return {
|
|
8430
|
+
code: readVerifiedPrebundledCode(pathArg, expectedSha256),
|
|
8431
|
+
prebundled: true,
|
|
8432
|
+
expectedSha256
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
async function preparedDeployCode(args) {
|
|
8436
|
+
const codeArg = args.code;
|
|
8437
|
+
const pathArg = args.path;
|
|
8438
|
+
const prebundledPath = args["prebundled-path"];
|
|
8439
|
+
const sources = [codeArg, pathArg, prebundledPath].filter((source) => source !== undefined);
|
|
8440
|
+
if (sources.length !== 1) {
|
|
8441
|
+
throw new Error("Exactly one of '--code', '--path', or '--prebundled-path' is required for 'deploy'");
|
|
8442
|
+
}
|
|
8443
|
+
if (typeof prebundledPath === "string") {
|
|
8444
|
+
return prebundledDeployCode(prebundledPath, args["expected-sha256"], args.minify);
|
|
8445
|
+
}
|
|
8446
|
+
if (args["expected-sha256"] !== undefined) {
|
|
8447
|
+
throw new Error("'--expected-sha256' requires '--prebundled-path'");
|
|
8448
|
+
}
|
|
8449
|
+
if (typeof codeArg === "string")
|
|
8450
|
+
return { code: codeArg, prebundled: false };
|
|
8451
|
+
if (typeof pathArg !== "string")
|
|
8452
|
+
throw new Error("Function deploy source is invalid");
|
|
8453
|
+
return bundledDeployCode(pathArg);
|
|
8454
|
+
}
|
|
8455
|
+
async function bundledDeployCode(pathArg) {
|
|
8456
|
+
try {
|
|
8457
|
+
return { code: await bundleEdgeFunctionPath(pathArg), prebundled: false };
|
|
8458
|
+
} catch (error) {
|
|
8459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8460
|
+
throw new Error(`Failed to bundle/read path ${pathArg}: ${message}`);
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
function rejectPrebundledFlagsOutsideDeploy(action, args) {
|
|
8464
|
+
for (const flag of ["prebundled-path", "expected-sha256"]) {
|
|
8465
|
+
if (action !== "deploy" && args[flag] !== undefined) {
|
|
8466
|
+
throw new Error(`'--${flag}' is not supported for '${action}'`);
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8301
8470
|
function resolveEntrypoint(pathArg) {
|
|
8302
8471
|
const resolved = resolve2(pathArg);
|
|
8303
8472
|
const stat = statSync2(resolved);
|
|
@@ -8344,21 +8513,42 @@ function parseFunctionFiles(input) {
|
|
|
8344
8513
|
}
|
|
8345
8514
|
}
|
|
8346
8515
|
var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
|
|
8347
|
-
function
|
|
8516
|
+
function positiveFunctionVersion(input, label) {
|
|
8517
|
+
if (typeof input !== "string" && typeof input !== "number") {
|
|
8518
|
+
throw new Error(`${label} must be a canonical positive safe integer`);
|
|
8519
|
+
}
|
|
8348
8520
|
const version = String(input);
|
|
8349
|
-
if (!
|
|
8350
|
-
throw new Error(
|
|
8521
|
+
if (!POSITIVE_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
8522
|
+
throw new Error(`${label} must be a canonical positive safe integer`);
|
|
8351
8523
|
}
|
|
8352
8524
|
return version;
|
|
8353
8525
|
}
|
|
8354
|
-
var
|
|
8355
|
-
var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
8526
|
+
var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
|
|
8356
8527
|
var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
8357
|
-
var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
|
|
8528
|
+
var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
|
|
8529
|
+
"action",
|
|
8530
|
+
"ref",
|
|
8531
|
+
"slug",
|
|
8532
|
+
"version",
|
|
8533
|
+
"expected-active-version"
|
|
8534
|
+
]);
|
|
8358
8535
|
var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
8359
|
-
Type.Integer({ minimum:
|
|
8360
|
-
Type.String({ pattern:
|
|
8361
|
-
]), Type.String({ pattern:
|
|
8536
|
+
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
|
8537
|
+
Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8538
|
+
]), Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), (input) => positiveFunctionVersion(input, "Function version")));
|
|
8539
|
+
function parseExpectedActiveVersion(input) {
|
|
8540
|
+
if (input === "absent")
|
|
8541
|
+
return input;
|
|
8542
|
+
return positiveFunctionVersion(input, "Expected active version");
|
|
8543
|
+
}
|
|
8544
|
+
var expectedActiveVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
8545
|
+
Type.Literal("absent"),
|
|
8546
|
+
Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
|
|
8547
|
+
Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8548
|
+
]), Type.Union([
|
|
8549
|
+
Type.Literal("absent"),
|
|
8550
|
+
Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8551
|
+
]), parseExpectedActiveVersion));
|
|
8362
8552
|
var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
|
|
8363
8553
|
var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
8364
8554
|
var MAX_SECRET_COUNT = 1024;
|
|
@@ -8476,10 +8666,35 @@ function secretsForUpsert(inlineSecrets, environmentNames, environment) {
|
|
|
8476
8666
|
throw new Error("'secrets' array required");
|
|
8477
8667
|
return inlineSecrets;
|
|
8478
8668
|
}
|
|
8669
|
+
var INVALID_FUNCTION_LIST_RESPONSE = "❌ Edge Function list response is invalid";
|
|
8670
|
+
var INVALID_FUNCTION_SOURCE_RESPONSE = "❌ Edge Function source response is invalid";
|
|
8671
|
+
function invalidFunctionReadResponse(message) {
|
|
8672
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
8673
|
+
}
|
|
8674
|
+
function safeFunctionList(payload) {
|
|
8675
|
+
if (!Array.isArray(payload))
|
|
8676
|
+
return null;
|
|
8677
|
+
const functionSlugs = new Set;
|
|
8678
|
+
for (const candidate of payload) {
|
|
8679
|
+
const edgeFunction = objectRecord(candidate);
|
|
8680
|
+
const slug = edgeFunction?.slug;
|
|
8681
|
+
const version = edgeFunction?.version;
|
|
8682
|
+
if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug) || typeof version !== "number" || !Number.isSafeInteger(version) || version < 1 || functionSlugs.has(slug))
|
|
8683
|
+
return null;
|
|
8684
|
+
functionSlugs.add(slug);
|
|
8685
|
+
}
|
|
8686
|
+
return payload;
|
|
8687
|
+
}
|
|
8688
|
+
function functionListResponse(response) {
|
|
8689
|
+
if (!response.ok)
|
|
8690
|
+
return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
|
|
8691
|
+
const functions = safeFunctionList(response.data);
|
|
8692
|
+
return functions ? { content: [{ type: "text", text: JSON.stringify(functions, null, 2) }] } : invalidFunctionReadResponse(INVALID_FUNCTION_LIST_RESPONSE);
|
|
8693
|
+
}
|
|
8479
8694
|
function confirmedFunctionConfig(payload, expected) {
|
|
8480
|
-
|
|
8695
|
+
const response = objectRecord(payload);
|
|
8696
|
+
if (!response)
|
|
8481
8697
|
return false;
|
|
8482
|
-
const response = payload;
|
|
8483
8698
|
if (expected.verify_jwt !== undefined && response.verify_jwt !== expected.verify_jwt)
|
|
8484
8699
|
return false;
|
|
8485
8700
|
if (expected.background_routes !== undefined) {
|
|
@@ -8490,28 +8705,87 @@ function confirmedFunctionConfig(payload, expected) {
|
|
|
8490
8705
|
}
|
|
8491
8706
|
return true;
|
|
8492
8707
|
}
|
|
8493
|
-
function functionSourceCode(payload) {
|
|
8708
|
+
function functionSourceCode(payload, field = "code") {
|
|
8494
8709
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
8495
8710
|
return null;
|
|
8496
|
-
const code = payload
|
|
8711
|
+
const code = payload[field];
|
|
8497
8712
|
return typeof code === "string" ? code : null;
|
|
8498
8713
|
}
|
|
8714
|
+
function requestedSourceVersion(candidate) {
|
|
8715
|
+
return candidate === undefined ? undefined : positiveFunctionVersion(candidate, "Function source version");
|
|
8716
|
+
}
|
|
8717
|
+
function functionSourceOutput(slug, sourceCode, output) {
|
|
8718
|
+
if (!output) {
|
|
8719
|
+
return { content: [{ type: "text", text: JSON.stringify({ code: sourceCode }, null, 2) }] };
|
|
8720
|
+
}
|
|
8721
|
+
const outputPath = resolve2(output);
|
|
8722
|
+
writeFileSync(outputPath, sourceCode, { flag: "wx" });
|
|
8723
|
+
return {
|
|
8724
|
+
content: [{
|
|
8725
|
+
type: "text",
|
|
8726
|
+
text: `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`
|
|
8727
|
+
}]
|
|
8728
|
+
};
|
|
8729
|
+
}
|
|
8499
8730
|
function objectRecord(candidate) {
|
|
8500
8731
|
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
8501
8732
|
}
|
|
8502
|
-
function
|
|
8503
|
-
const
|
|
8733
|
+
function edgeFunctionResourcePath(ref, slug) {
|
|
8734
|
+
const root = `/v1/projects/${projectRefPathSegment(ref, "Edge Functions")}/functions`;
|
|
8735
|
+
if (slug === undefined)
|
|
8736
|
+
return root;
|
|
8737
|
+
if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug)) {
|
|
8738
|
+
throw new Error("'slug' is invalid for Edge Functions");
|
|
8739
|
+
}
|
|
8740
|
+
return `${root}/${encodeURIComponent(slug)}`;
|
|
8741
|
+
}
|
|
8742
|
+
async function readFunctionSource(http, request) {
|
|
8743
|
+
const sourceVersion = requestedSourceVersion(request.version);
|
|
8744
|
+
const resourcePath = edgeFunctionResourcePath(request.projectRef, request.slug);
|
|
8745
|
+
const sourcePath = sourceVersion === undefined ? `${resourcePath}/source` : `${resourcePath}/versions/${encodeURIComponent(sourceVersion)}`;
|
|
8746
|
+
const response = await http.get(sourcePath);
|
|
8504
8747
|
if (!response.ok)
|
|
8505
|
-
return
|
|
8506
|
-
const
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8748
|
+
return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
|
|
8749
|
+
const sourceCode = functionSourceCode(response.data, sourceVersion === undefined ? "code" : "source_code");
|
|
8750
|
+
return sourceCode === null ? invalidFunctionReadResponse(INVALID_FUNCTION_SOURCE_RESPONSE) : functionSourceOutput(request.slug, sourceCode, request.output);
|
|
8751
|
+
}
|
|
8752
|
+
function mutationIdentityMatches(receipt, expectation) {
|
|
8753
|
+
return receipt.success === true && receipt.project_ref === expectation.projectRef && receipt.slug === expectation.slug && receipt.previous_active_version === expectation.expectedActiveVersion;
|
|
8754
|
+
}
|
|
8755
|
+
function validReceiptVersion(activeVersion) {
|
|
8756
|
+
return typeof activeVersion === "string" && POSITIVE_FUNCTION_VERSION_PATTERN.test(activeVersion) && Number.isSafeInteger(Number(activeVersion));
|
|
8757
|
+
}
|
|
8758
|
+
function confirmedMutationVersion(receipt, config, expectation) {
|
|
8759
|
+
const activeVersion = receipt.active_version;
|
|
8760
|
+
if (!validReceiptVersion(activeVersion) || receipt.version !== activeVersion || config.version !== activeVersion || expectation.targetVersion !== undefined && activeVersion !== expectation.targetVersion) {
|
|
8761
|
+
return null;
|
|
8510
8762
|
}
|
|
8511
|
-
return
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8763
|
+
return activeVersion;
|
|
8764
|
+
}
|
|
8765
|
+
function confirmedFunctionMutation(expectation, payload) {
|
|
8766
|
+
const receipt = objectRecord(payload);
|
|
8767
|
+
const config = objectRecord(receipt?.config);
|
|
8768
|
+
if (!receipt || !config || !mutationIdentityMatches(receipt, expectation))
|
|
8769
|
+
return null;
|
|
8770
|
+
const activeVersion = confirmedMutationVersion(receipt, config, expectation);
|
|
8771
|
+
if (activeVersion === null || typeof config.verify_jwt !== "boolean" || !confirmedFunctionConfig(config, expectation.config ?? {}))
|
|
8772
|
+
return null;
|
|
8773
|
+
return { activeVersion, verifyJwt: config.verify_jwt };
|
|
8774
|
+
}
|
|
8775
|
+
function functionMutationResponse(expectation, response) {
|
|
8776
|
+
if (!response.ok)
|
|
8777
|
+
return releaseControlMutationFailure(expectation.operation, response);
|
|
8778
|
+
const confirmed = confirmedFunctionMutation(expectation, response.data);
|
|
8779
|
+
if (!confirmed) {
|
|
8780
|
+
return releaseControlFailure(expectation.operation, "OUTCOME_UNKNOWN", response.status);
|
|
8781
|
+
}
|
|
8782
|
+
return releaseControlSuccess(expectation.operation, {
|
|
8783
|
+
project_ref: expectation.projectRef,
|
|
8784
|
+
slug: expectation.slug,
|
|
8785
|
+
previous_active_version: expectation.expectedActiveVersion,
|
|
8786
|
+
active_version: confirmed.activeVersion,
|
|
8787
|
+
version: confirmed.activeVersion,
|
|
8788
|
+
verify_jwt: confirmed.verifyJwt
|
|
8515
8789
|
});
|
|
8516
8790
|
}
|
|
8517
8791
|
function readOnlyActivationResult() {
|
|
@@ -8523,15 +8797,22 @@ function readOnlyActivationResult() {
|
|
|
8523
8797
|
function functionActivationTarget(args) {
|
|
8524
8798
|
const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
|
|
8525
8799
|
const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
8526
|
-
const version = args.version;
|
|
8527
|
-
|
|
8528
|
-
throw new Error("'ref' is invalid for 'activate'");
|
|
8800
|
+
const version = positiveFunctionVersion(args.version, "Function activation version");
|
|
8801
|
+
projectRefPathSegment(projectRef, "Edge Function activation");
|
|
8529
8802
|
if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
|
|
8530
8803
|
throw new Error("'slug' is invalid for 'activate'");
|
|
8531
|
-
|
|
8532
|
-
|
|
8804
|
+
const expectedActiveVersion = requiredExpectedActiveVersion(args, "activate");
|
|
8805
|
+
return { projectRef, functionSlug, version, expectedActiveVersion };
|
|
8806
|
+
}
|
|
8807
|
+
function requiredExpectedActiveVersion(args, action) {
|
|
8808
|
+
const expected = args["expected-active-version"];
|
|
8809
|
+
if (expected === undefined) {
|
|
8810
|
+
throw new Error(`'--expected-active-version' required for '${action}'`);
|
|
8533
8811
|
}
|
|
8534
|
-
|
|
8812
|
+
const parsed = parseExpectedActiveVersion(expected);
|
|
8813
|
+
if (typeof parsed !== "string")
|
|
8814
|
+
throw new Error("Expected active version is invalid");
|
|
8815
|
+
return parsed;
|
|
8535
8816
|
}
|
|
8536
8817
|
async function activateFunctionVersion(http, args, readOnly = false) {
|
|
8537
8818
|
if (readOnly)
|
|
@@ -8539,29 +8820,40 @@ async function activateFunctionVersion(http, args, readOnly = false) {
|
|
|
8539
8820
|
const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
|
|
8540
8821
|
if (unsupported.length > 0)
|
|
8541
8822
|
throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
|
|
8542
|
-
const { projectRef, functionSlug, version } = functionActivationTarget(args);
|
|
8543
|
-
const endpoint =
|
|
8544
|
-
return
|
|
8823
|
+
const { projectRef, functionSlug, version, expectedActiveVersion } = functionActivationTarget(args);
|
|
8824
|
+
const endpoint = edgeFunctionResourcePath(projectRef, functionSlug) + `/versions/${encodeURIComponent(version)}/activate`;
|
|
8825
|
+
return functionMutationResponse({
|
|
8826
|
+
operation: "edge_functions.activate",
|
|
8827
|
+
projectRef,
|
|
8828
|
+
slug: functionSlug,
|
|
8829
|
+
expectedActiveVersion,
|
|
8830
|
+
targetVersion: version
|
|
8831
|
+
}, await http.post(endpoint, { expected_active_version: expectedActiveVersion }));
|
|
8545
8832
|
}
|
|
8546
8833
|
function registerAdvancedTools(server, http, environment = process.env, options = {}) {
|
|
8547
|
-
server.tool("edge_functions", `Edge Function management (Deno/Bun serverless).
|
|
8834
|
+
server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Source deploys are bundled; verified prebuilt artifacts stay byte-exact.
|
|
8548
8835
|
Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
|
|
8549
8836
|
action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
|
|
8550
8837
|
ref: withDescription(Type.String(), "Project ref"),
|
|
8551
8838
|
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
|
|
8552
|
-
version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
|
|
8839
|
+
version: withDescription(functionVersionSchema, "[source/activate] Existing immutable Function version; source requires a positive version"),
|
|
8553
8840
|
code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
|
|
8554
8841
|
path: optional(Type.String(), "[deploy/check] Local file path to read code from (alternative to code)"),
|
|
8842
|
+
"prebundled-path": optional(Type.String(), "[deploy] Prebuilt runtime bundle to upload without rebuilding; requires expected-sha256"),
|
|
8843
|
+
"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
8844
|
output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
|
|
8556
8845
|
files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
|
|
8557
8846
|
entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
|
|
8558
8847
|
minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
|
|
8559
8848
|
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")
|
|
8849
|
+
background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI"),
|
|
8850
|
+
"expected-active-version": withDescription(expectedActiveVersionSchema, "[deploy/deploy_bundle/activate] Required current active version, or 'absent' when none exists")
|
|
8561
8851
|
}, async (args) => {
|
|
8562
8852
|
if (args.action === "activate")
|
|
8563
8853
|
return activateFunctionVersion(http, args, options.readOnly);
|
|
8564
8854
|
const { action, ref, slug, path: pathArg, output, files, entrypoint, minify, verify_jwt, background_routes } = args;
|
|
8855
|
+
rejectPrebundledFlagsOutsideDeploy(action, args);
|
|
8856
|
+
const expectedActiveVersion = action === "deploy" || action === "deploy_bundle" ? requiredExpectedActiveVersion(args, action) : undefined;
|
|
8565
8857
|
let code = args.code;
|
|
8566
8858
|
const need = (f, v) => {
|
|
8567
8859
|
if (!v)
|
|
@@ -8578,16 +8870,10 @@ Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`,
|
|
|
8578
8870
|
if (!hasFunctionConfig()) {
|
|
8579
8871
|
throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
|
|
8580
8872
|
}
|
|
8581
|
-
const cr = await http.patch(
|
|
8873
|
+
const cr = await http.patch(`${edgeFunctionResourcePath(ref, slug)}/config`, functionConfig());
|
|
8582
8874
|
return cr.ok ? `✅ Function ${slug} config updated
|
|
8583
8875
|
${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
|
|
8584
8876
|
};
|
|
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
8877
|
const checkSyntax = async (sourceCode) => {
|
|
8592
8878
|
const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
|
|
8593
8879
|
const tmpFile = join2(tmpDir, "index.ts");
|
|
@@ -8602,7 +8888,7 @@ ${e.stderr || e.message}` };
|
|
|
8602
8888
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
8603
8889
|
}
|
|
8604
8890
|
};
|
|
8605
|
-
if (pathArg && !code) {
|
|
8891
|
+
if (action === "check" && pathArg && !code) {
|
|
8606
8892
|
try {
|
|
8607
8893
|
code = await bundleEdgeFunctionPath(pathArg);
|
|
8608
8894
|
} catch (error) {
|
|
@@ -8612,8 +8898,7 @@ ${e.stderr || e.message}` };
|
|
|
8612
8898
|
}
|
|
8613
8899
|
switch (action) {
|
|
8614
8900
|
case "list":
|
|
8615
|
-
|
|
8616
|
-
break;
|
|
8901
|
+
return functionListResponse(await http.get(edgeFunctionResourcePath(ref)));
|
|
8617
8902
|
case "check":
|
|
8618
8903
|
need("code (or path)", code);
|
|
8619
8904
|
const checkRes = await checkSyntax(code);
|
|
@@ -8626,65 +8911,59 @@ ${checkRes.err}`;
|
|
|
8626
8911
|
break;
|
|
8627
8912
|
case "deploy":
|
|
8628
8913
|
need("slug", slug);
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8914
|
+
const deployCode = await preparedDeployCode(args);
|
|
8915
|
+
if (!deployCode.prebundled) {
|
|
8916
|
+
const deployCheck = await checkSyntax(deployCode.code);
|
|
8917
|
+
if (!deployCheck.ok) {
|
|
8918
|
+
text = `❌ Deployment aborted. Syntax check failed:
|
|
8633
8919
|
${deployCheck.err}`;
|
|
8634
|
-
|
|
8920
|
+
break;
|
|
8921
|
+
}
|
|
8635
8922
|
}
|
|
8636
|
-
const
|
|
8637
|
-
code,
|
|
8638
|
-
minify,
|
|
8923
|
+
const deploymentResponse = await http.post(edgeFunctionResourcePath(ref, slug), {
|
|
8924
|
+
code: deployCode.code,
|
|
8925
|
+
...deployCode.prebundled ? { prebundled: true, expected_sha256: deployCode.expectedSha256 } : { minify },
|
|
8926
|
+
expected_active_version: expectedActiveVersion,
|
|
8639
8927
|
...functionConfig()
|
|
8640
8928
|
});
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8929
|
+
return functionMutationResponse({
|
|
8930
|
+
operation: "edge_functions.deploy",
|
|
8931
|
+
projectRef: ref,
|
|
8932
|
+
slug,
|
|
8933
|
+
expectedActiveVersion,
|
|
8934
|
+
config: functionConfig()
|
|
8935
|
+
}, deploymentResponse);
|
|
8647
8936
|
case "deploy_bundle":
|
|
8648
8937
|
need("slug", slug);
|
|
8649
8938
|
need("files", files);
|
|
8650
|
-
const
|
|
8939
|
+
const bundleResponse = await http.post(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
|
|
8651
8940
|
files,
|
|
8652
8941
|
entrypoint,
|
|
8653
8942
|
minify,
|
|
8943
|
+
expected_active_version: expectedActiveVersion,
|
|
8654
8944
|
...functionConfig()
|
|
8655
8945
|
});
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8946
|
+
return functionMutationResponse({
|
|
8947
|
+
operation: "edge_functions.deploy_bundle",
|
|
8948
|
+
projectRef: ref,
|
|
8949
|
+
slug,
|
|
8950
|
+
expectedActiveVersion,
|
|
8951
|
+
config: functionConfig()
|
|
8952
|
+
}, bundleResponse);
|
|
8662
8953
|
case "config":
|
|
8663
8954
|
text = await updateFunctionConfig();
|
|
8664
8955
|
break;
|
|
8665
8956
|
case "source":
|
|
8666
8957
|
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;
|
|
8958
|
+
return readFunctionSource(http, {
|
|
8959
|
+
projectRef: ref,
|
|
8960
|
+
slug,
|
|
8961
|
+
version: args.version,
|
|
8962
|
+
output
|
|
8963
|
+
});
|
|
8685
8964
|
case "delete":
|
|
8686
8965
|
need("slug", slug);
|
|
8687
|
-
text = (await http.delete(
|
|
8966
|
+
text = (await http.delete(edgeFunctionResourcePath(ref, slug))).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
|
|
8688
8967
|
break;
|
|
8689
8968
|
default:
|
|
8690
8969
|
text = `❌ Unknown action`;
|
|
@@ -10605,12 +10884,14 @@ var MAX_BODY_FILE_BYTES = 1048576;
|
|
|
10605
10884
|
var MAX_HEADER_COUNT = 64;
|
|
10606
10885
|
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
10607
10886
|
var MAX_SCHEDULE_NAME_LENGTH = 120;
|
|
10887
|
+
var CANONICAL_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10608
10888
|
var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
|
|
10609
10889
|
var ACTION_ARGUMENTS2 = {
|
|
10610
10890
|
list: new Set(["action", "ref"]),
|
|
10891
|
+
get: new Set(["action", "ref", "schedule_id"]),
|
|
10611
10892
|
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"])
|
|
10893
|
+
update: new Set(["action", "ref", "schedule_id", "expected_updated_at", "name", "cron", "method", "enabled", "body_file", "header_env"]),
|
|
10894
|
+
delete: new Set(["action", "ref", "schedule_id", "expected_updated_at"])
|
|
10614
10895
|
};
|
|
10615
10896
|
function parseHeaderEnvironment(input) {
|
|
10616
10897
|
if (typeof input !== "string")
|
|
@@ -10730,7 +11011,13 @@ function validScheduleDefinition(schedule) {
|
|
|
10730
11011
|
return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
|
|
10731
11012
|
}
|
|
10732
11013
|
function validScheduleMetadata(schedule) {
|
|
10733
|
-
return typeof schedule.created_at === "string" &&
|
|
11014
|
+
return typeof schedule.created_at === "string" && isCanonicalTimestamp(schedule.updated_at);
|
|
11015
|
+
}
|
|
11016
|
+
function isCanonicalTimestamp(candidate) {
|
|
11017
|
+
if (typeof candidate !== "string" || !CANONICAL_TIMESTAMP_PATTERN.test(candidate))
|
|
11018
|
+
return false;
|
|
11019
|
+
const milliseconds = Date.parse(candidate);
|
|
11020
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
|
|
10734
11021
|
}
|
|
10735
11022
|
function safeSchedulePayload(schedule) {
|
|
10736
11023
|
const headerNames = safeHeaderNames(schedule.header_names);
|
|
@@ -10803,6 +11090,17 @@ function listResponse(ref, response) {
|
|
|
10803
11090
|
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10804
11091
|
return releaseControlSuccess(operation, { project_ref: ref, schedules });
|
|
10805
11092
|
}
|
|
11093
|
+
function getResponse(ref, scheduleId, response) {
|
|
11094
|
+
const operation = "scheduled_functions.get";
|
|
11095
|
+
if (!response.ok)
|
|
11096
|
+
return scheduleFailure(operation, response);
|
|
11097
|
+
const payload = objectRecord2(response.data);
|
|
11098
|
+
const schedule = safeSchedule(payload?.schedule);
|
|
11099
|
+
if (payload?.project_ref !== ref || !schedule || schedule.id !== scheduleId) {
|
|
11100
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
11101
|
+
}
|
|
11102
|
+
return releaseControlSuccess(operation, { project_ref: ref, schedule });
|
|
11103
|
+
}
|
|
10806
11104
|
function mutationResponse(expectation, response) {
|
|
10807
11105
|
const { action, ref, requestId, expectedFields } = expectation;
|
|
10808
11106
|
const scheduleId = action === "update" ? expectation.scheduleId : undefined;
|
|
@@ -10812,22 +11110,29 @@ function mutationResponse(expectation, response) {
|
|
|
10812
11110
|
const payload = objectRecord2(response.data);
|
|
10813
11111
|
const schedule = safeSchedule(payload?.schedule);
|
|
10814
11112
|
const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
|
|
10815
|
-
|
|
11113
|
+
const confirmsRevision = action === "create" || payload?.previous_updated_at === expectation.expectedUpdatedAt && schedule !== null && schedule.updated_at > expectation.expectedUpdatedAt;
|
|
11114
|
+
if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || !confirmsRevision || scheduleId !== undefined && schedule.id !== scheduleId) {
|
|
10816
11115
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10817
11116
|
}
|
|
10818
|
-
return releaseControlSuccess(operation, {
|
|
11117
|
+
return releaseControlSuccess(operation, {
|
|
11118
|
+
project_ref: ref,
|
|
11119
|
+
request_id: requestId,
|
|
11120
|
+
...action === "update" ? { previous_updated_at: expectation.expectedUpdatedAt } : {},
|
|
11121
|
+
schedule
|
|
11122
|
+
});
|
|
10819
11123
|
}
|
|
10820
|
-
function deleteResponse(ref, scheduleId, response) {
|
|
11124
|
+
function deleteResponse(ref, scheduleId, expectedUpdatedAt, response) {
|
|
10821
11125
|
const operation = "scheduled_functions.delete";
|
|
10822
11126
|
if (!response.ok)
|
|
10823
11127
|
return releaseControlMutationFailure(operation, response);
|
|
10824
11128
|
const payload = objectRecord2(response.data);
|
|
10825
|
-
if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId) {
|
|
11129
|
+
if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId || payload.deleted_updated_at !== expectedUpdatedAt) {
|
|
10826
11130
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10827
11131
|
}
|
|
10828
11132
|
return releaseControlSuccess(operation, {
|
|
10829
11133
|
project_ref: ref,
|
|
10830
11134
|
schedule_id: scheduleId,
|
|
11135
|
+
deleted_updated_at: expectedUpdatedAt,
|
|
10831
11136
|
deleted: true
|
|
10832
11137
|
});
|
|
10833
11138
|
}
|
|
@@ -10876,7 +11181,15 @@ function requiredCron(args, action) {
|
|
|
10876
11181
|
throw new Error(`'cron' is invalid for '${action}'`);
|
|
10877
11182
|
return cron;
|
|
10878
11183
|
}
|
|
11184
|
+
function requiredExpectedUpdatedAt(args, action) {
|
|
11185
|
+
const candidate = args.expected_updated_at;
|
|
11186
|
+
if (!isCanonicalTimestamp(candidate)) {
|
|
11187
|
+
throw new Error(`'expected_updated_at' must be a canonical UTC timestamp for '${action}'`);
|
|
11188
|
+
}
|
|
11189
|
+
return candidate;
|
|
11190
|
+
}
|
|
10879
11191
|
function updateRequest(args, environment) {
|
|
11192
|
+
const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "update");
|
|
10880
11193
|
const body = scheduleBody(args.body_file);
|
|
10881
11194
|
const headers = scheduleHeaders(args.header_env, environment);
|
|
10882
11195
|
const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
|
|
@@ -10892,11 +11205,18 @@ function updateRequest(args, environment) {
|
|
|
10892
11205
|
if (Object.keys(mutationFields).length === 0) {
|
|
10893
11206
|
throw new Error("Scheduled Function update requires at least one field");
|
|
10894
11207
|
}
|
|
10895
|
-
return {
|
|
11208
|
+
return {
|
|
11209
|
+
request_id: randomUUID(),
|
|
11210
|
+
expected_updated_at: expectedUpdatedAt,
|
|
11211
|
+
...mutationFields
|
|
11212
|
+
};
|
|
11213
|
+
}
|
|
11214
|
+
function deletePath(schedulePathname, expectedUpdatedAt) {
|
|
11215
|
+
return `${schedulePathname}?expected_updated_at=${encodeURIComponent(expectedUpdatedAt)}`;
|
|
10896
11216
|
}
|
|
10897
11217
|
async function executeScheduleAction(http, environment, args, readOnly = false) {
|
|
10898
11218
|
const action = args.action;
|
|
10899
|
-
if (readOnly && action !== "list")
|
|
11219
|
+
if (readOnly && action !== "list" && action !== "get")
|
|
10900
11220
|
return readOnlyResult2();
|
|
10901
11221
|
assertActionArguments2(action, args);
|
|
10902
11222
|
const ref = requiredText2(args, "ref", action);
|
|
@@ -10908,27 +11228,34 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
|
|
|
10908
11228
|
return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
|
|
10909
11229
|
}
|
|
10910
11230
|
const scheduleId = requiredText2(args, "schedule_id", action);
|
|
11231
|
+
const targetPath = schedulePath(ref, scheduleId);
|
|
11232
|
+
if (action === "get")
|
|
11233
|
+
return getResponse(ref, scheduleId, await http.get(targetPath));
|
|
10911
11234
|
if (action === "update") {
|
|
10912
11235
|
const request = updateRequest(args, environment);
|
|
10913
11236
|
const requestId = request.request_id;
|
|
11237
|
+
const expectedUpdatedAt2 = request.expected_updated_at;
|
|
10914
11238
|
return mutationResponse({
|
|
10915
11239
|
action,
|
|
10916
11240
|
ref,
|
|
10917
11241
|
scheduleId,
|
|
10918
11242
|
requestId,
|
|
11243
|
+
expectedUpdatedAt: expectedUpdatedAt2,
|
|
10919
11244
|
expectedFields: safeMutationFields(request)
|
|
10920
|
-
}, await http.patch(
|
|
11245
|
+
}, await http.patch(targetPath, request));
|
|
10921
11246
|
}
|
|
10922
|
-
|
|
11247
|
+
const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "delete");
|
|
11248
|
+
return deleteResponse(ref, scheduleId, expectedUpdatedAt, await http.delete(deletePath(targetPath, expectedUpdatedAt)));
|
|
10923
11249
|
}
|
|
10924
11250
|
function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
|
|
10925
11251
|
server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
|
|
10926
11252
|
}
|
|
10927
|
-
var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, create, update, delete";
|
|
11253
|
+
var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, get, create, update, delete";
|
|
10928
11254
|
var SCHEDULE_TOOL_SCHEMA = {
|
|
10929
|
-
action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
|
|
11255
|
+
action: withDescription(stringEnum(["list", "get", "create", "update", "delete"]), "Action"),
|
|
10930
11256
|
ref: withDescription(Type.String(), "Project ref"),
|
|
10931
|
-
schedule_id: optional(Type.String(), "[update/delete] Schedule ID"),
|
|
11257
|
+
schedule_id: optional(Type.String(), "[get/update/delete] Schedule ID"),
|
|
11258
|
+
expected_updated_at: optional(Type.String(), "[update/delete] Canonical updated_at from list"),
|
|
10932
11259
|
name: optional(Type.String(), "[create/update] Display name"),
|
|
10933
11260
|
slug: optional(Type.String(), "[create] Edge Function slug"),
|
|
10934
11261
|
cron: optional(Type.String(), "[create/update] Five-field cron expression"),
|
|
@@ -10940,7 +11267,7 @@ var SCHEDULE_TOOL_SCHEMA = {
|
|
|
10940
11267
|
// package.json
|
|
10941
11268
|
var package_default = {
|
|
10942
11269
|
name: "@supacloud/cli",
|
|
10943
|
-
version: "0.
|
|
11270
|
+
version: "0.17.0",
|
|
10944
11271
|
description: "Project-scoped CLI for SupaCloud users",
|
|
10945
11272
|
type: "module",
|
|
10946
11273
|
main: "./dist/index.js",
|
|
@@ -11156,8 +11483,9 @@ EXAMPLES
|
|
|
11156
11483
|
${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
|
|
11157
11484
|
${preferredCommand} ai show_skill
|
|
11158
11485
|
${preferredCommand} ai install_skill --dry_run
|
|
11159
|
-
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
|
|
11160
|
-
${preferredCommand} edge_functions
|
|
11486
|
+
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent
|
|
11487
|
+
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
|
|
11488
|
+
${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
|
|
11161
11489
|
${preferredCommand} scheduled_functions list --ref abc123
|
|
11162
11490
|
${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
|
|
11163
11491
|
${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 positive version read from `list` to `source --version` and as `--expected-active-version`; use `absent` only for a new slug. Version `0` is internal-only.
|
|
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.
|