@supacloud/cli 0.15.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 +80 -8
- package/dist/index.js +953 -205
- package/package.json +1 -1
- package/skills/supacloud-cli/references/command-map.md +1 -1
package/dist/index.js
CHANGED
|
@@ -6471,7 +6471,7 @@ var ACTION_POLICY = {
|
|
|
6471
6471
|
write: ["task_cancel", "task_retry", "update_background_settings"]
|
|
6472
6472
|
},
|
|
6473
6473
|
database: {
|
|
6474
|
-
read: ["list_tables", "describe_columns", "list_indexes", "list_constraints", "list_extensions", "rls_status", "rls_policies", "list_auth_users", "get_auth_user", "connections", "stats", "slow_queries", "list_migrations", "project_url", "generate_types"],
|
|
6474
|
+
read: ["list_tables", "describe_columns", "list_indexes", "list_constraints", "list_extensions", "rls_status", "rls_policies", "list_auth_users", "get_auth_user", "connections", "stats", "slow_queries", "list_migrations", "migration_inventory", "project_url", "generate_types"],
|
|
6475
6475
|
write: ["query", "apply_migration", "push_migrations", "baseline_migrations", "create_table_rls"]
|
|
6476
6476
|
},
|
|
6477
6477
|
supabase: {
|
|
@@ -6483,15 +6483,15 @@ var ACTION_POLICY = {
|
|
|
6483
6483
|
write: ["configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
|
|
6484
6484
|
},
|
|
6485
6485
|
storage: {
|
|
6486
|
-
read: ["status", "list_buckets", "list_files"],
|
|
6487
|
-
write: ["upload_base64", "delete_file"]
|
|
6486
|
+
read: ["status", "list_buckets", "get_bucket", "list_files"],
|
|
6487
|
+
write: ["create_bucket", "update_bucket", "delete_bucket", "upload_base64", "delete_file"]
|
|
6488
6488
|
},
|
|
6489
6489
|
edge_functions: {
|
|
6490
6490
|
read: ["list", "source"],
|
|
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"],
|
|
@@ -6784,10 +6784,120 @@ class HttpTransport {
|
|
|
6784
6784
|
import { createHash } from "node:crypto";
|
|
6785
6785
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
6786
6786
|
import { basename, join } from "node:path";
|
|
6787
|
+
|
|
6788
|
+
// src/shared/project-ref.ts
|
|
6789
|
+
var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
6790
|
+
function projectRefPathSegment(ref, operation) {
|
|
6791
|
+
if (typeof ref !== "string" || !PROJECT_REF_PATTERN.test(ref)) {
|
|
6792
|
+
throw new Error(`'ref' is invalid for ${operation}`);
|
|
6793
|
+
}
|
|
6794
|
+
return encodeURIComponent(ref);
|
|
6795
|
+
}
|
|
6796
|
+
|
|
6797
|
+
// src/shared/tools/database-tools.ts
|
|
6787
6798
|
var MAX_MIGRATION_VERSION = 9223372036854775807n;
|
|
6788
6799
|
var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
|
|
6789
6800
|
var FALLBACK_MIGRATION_VERSION_RANGE = 1000000000000000000n;
|
|
6790
6801
|
var FALLBACK_MIGRATION_VERSION_LIMIT = FALLBACK_MIGRATION_VERSION_BASE + FALLBACK_MIGRATION_VERSION_RANGE;
|
|
6802
|
+
var MAX_MIGRATION_INVENTORY_BYTES = 64 * 1024 * 1024;
|
|
6803
|
+
function isMigrationInventoryVersion(version) {
|
|
6804
|
+
if (typeof version !== "string" || !/^\d{1,19}$/.test(version))
|
|
6805
|
+
return false;
|
|
6806
|
+
const numericVersion = BigInt(version);
|
|
6807
|
+
return numericVersion >= 1n && numericVersion <= MAX_MIGRATION_VERSION && numericVersion.toString() === version;
|
|
6808
|
+
}
|
|
6809
|
+
function isMigrationInventoryName(name) {
|
|
6810
|
+
return name === null || typeof name === "string" && name.length <= 255 && name.length > 0 && name.trim() === name;
|
|
6811
|
+
}
|
|
6812
|
+
function isMigrationInventoryStatements(statements) {
|
|
6813
|
+
return Array.isArray(statements) && statements.every((statement) => typeof statement === "string" && statement.length > 0 && statement.trim() === statement && !statement.includes("\r"));
|
|
6814
|
+
}
|
|
6815
|
+
function isMigrationInventoryAppliedAt(appliedAt) {
|
|
6816
|
+
return appliedAt === null || typeof appliedAt === "string" && appliedAt.trim() === appliedAt && appliedAt.length > 0 && !Number.isNaN(Date.parse(appliedAt));
|
|
6817
|
+
}
|
|
6818
|
+
function migrationInventoryChecksum(entry) {
|
|
6819
|
+
return createHash("sha256").update(JSON.stringify({
|
|
6820
|
+
version: entry.version,
|
|
6821
|
+
name: entry.name,
|
|
6822
|
+
statements: entry.statements
|
|
6823
|
+
})).digest("hex");
|
|
6824
|
+
}
|
|
6825
|
+
function migrationInventoryEntry(rawEntry) {
|
|
6826
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry))
|
|
6827
|
+
return null;
|
|
6828
|
+
const entry = rawEntry;
|
|
6829
|
+
if (!isMigrationInventoryVersion(entry.version))
|
|
6830
|
+
return null;
|
|
6831
|
+
if (!isMigrationInventoryName(entry.name))
|
|
6832
|
+
return null;
|
|
6833
|
+
if (!isMigrationInventoryStatements(entry.statements))
|
|
6834
|
+
return null;
|
|
6835
|
+
if (typeof entry.statement_count !== "number" || !Number.isInteger(entry.statement_count))
|
|
6836
|
+
return null;
|
|
6837
|
+
if (entry.statement_count !== entry.statements.length)
|
|
6838
|
+
return null;
|
|
6839
|
+
if (typeof entry.checksum !== "string" || !/^[0-9a-f]{64}$/.test(entry.checksum))
|
|
6840
|
+
return null;
|
|
6841
|
+
if (!isMigrationInventoryAppliedAt(entry.applied_at))
|
|
6842
|
+
return null;
|
|
6843
|
+
const migration = {
|
|
6844
|
+
version: entry.version,
|
|
6845
|
+
name: entry.name,
|
|
6846
|
+
statements: entry.statements,
|
|
6847
|
+
statement_count: entry.statement_count,
|
|
6848
|
+
checksum: entry.checksum,
|
|
6849
|
+
applied_at: entry.applied_at
|
|
6850
|
+
};
|
|
6851
|
+
return migration.checksum === migrationInventoryChecksum(migration) ? migration : null;
|
|
6852
|
+
}
|
|
6853
|
+
function compareMigrationInventoryEntries(left, right) {
|
|
6854
|
+
const leftVersion = BigInt(left.version);
|
|
6855
|
+
const rightVersion = BigInt(right.version);
|
|
6856
|
+
if (leftVersion === rightVersion)
|
|
6857
|
+
return 0;
|
|
6858
|
+
return leftVersion < rightVersion ? -1 : 1;
|
|
6859
|
+
}
|
|
6860
|
+
function migrationInventory(payload) {
|
|
6861
|
+
if (!Array.isArray(payload))
|
|
6862
|
+
return null;
|
|
6863
|
+
const inventory = [];
|
|
6864
|
+
const versions = new Set;
|
|
6865
|
+
for (const rawEntry of payload) {
|
|
6866
|
+
const entry = migrationInventoryEntry(rawEntry);
|
|
6867
|
+
if (!entry)
|
|
6868
|
+
return null;
|
|
6869
|
+
if (versions.has(entry.version))
|
|
6870
|
+
return null;
|
|
6871
|
+
versions.add(entry.version);
|
|
6872
|
+
inventory.push(entry);
|
|
6873
|
+
}
|
|
6874
|
+
return inventory.sort(compareMigrationInventoryEntries);
|
|
6875
|
+
}
|
|
6876
|
+
function migrationInventoryPath(ref) {
|
|
6877
|
+
return `/v1/projects/${projectRefPathSegment(ref, "migration_inventory")}/database/migrations`;
|
|
6878
|
+
}
|
|
6879
|
+
function migrationInventoryFailure(code, httpStatus) {
|
|
6880
|
+
return {
|
|
6881
|
+
isError: true,
|
|
6882
|
+
content: [{
|
|
6883
|
+
type: "text",
|
|
6884
|
+
text: JSON.stringify({
|
|
6885
|
+
ok: false,
|
|
6886
|
+
operation: "database.migration_inventory",
|
|
6887
|
+
error: { code, http_status: httpStatus }
|
|
6888
|
+
}, null, 2)
|
|
6889
|
+
}]
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
function migrationInventoryResponse(response) {
|
|
6893
|
+
if (!response.ok) {
|
|
6894
|
+
return migrationInventoryFailure("HTTP_ERROR", response.transportError ? null : response.status);
|
|
6895
|
+
}
|
|
6896
|
+
const inventory = migrationInventory(response.data);
|
|
6897
|
+
if (!inventory)
|
|
6898
|
+
return migrationInventoryFailure("INVALID_RESPONSE", response.status);
|
|
6899
|
+
return { content: [{ type: "text", text: JSON.stringify(inventory, null, 2) }] };
|
|
6900
|
+
}
|
|
6791
6901
|
function readMigrationFile(dir, file) {
|
|
6792
6902
|
const rawBytes = readFileSync2(join(dir, file));
|
|
6793
6903
|
return {
|
|
@@ -6854,6 +6964,28 @@ function migrationRows(data) {
|
|
|
6854
6964
|
function migrationIdentityKey(version, name) {
|
|
6855
6965
|
return `${version}\x00${name}`;
|
|
6856
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
|
+
}
|
|
6857
6989
|
function nameBoundMigrationMarkerKeys(data) {
|
|
6858
6990
|
const keys = new Set;
|
|
6859
6991
|
for (const row of migrationRows(data)) {
|
|
@@ -6939,6 +7071,7 @@ function registerDatabaseTools(server, http, config = {}) {
|
|
|
6939
7071
|
"stats",
|
|
6940
7072
|
"slow_queries",
|
|
6941
7073
|
"list_migrations",
|
|
7074
|
+
"migration_inventory",
|
|
6942
7075
|
"project_url",
|
|
6943
7076
|
"generate_types"
|
|
6944
7077
|
];
|
|
@@ -7078,6 +7211,10 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7078
7211
|
text = r.ok ? formatMigrations(r.data) : `❌ Failed (${r.status})`;
|
|
7079
7212
|
break;
|
|
7080
7213
|
}
|
|
7214
|
+
case "migration_inventory": {
|
|
7215
|
+
const response = await http.get(migrationInventoryPath(ref), { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
|
|
7216
|
+
return migrationInventoryResponse(response);
|
|
7217
|
+
}
|
|
7081
7218
|
case "project_url": {
|
|
7082
7219
|
const r = await http.get(`/v1/projects/${ref}`);
|
|
7083
7220
|
text = r.ok ? JSON.stringify({ url: r.data.api?.url || `https://${ref}.supabase.co` }, null, 2) : `❌ Failed (${r.status})`;
|
|
@@ -7112,6 +7249,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7112
7249
|
text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
|
|
7113
7250
|
break;
|
|
7114
7251
|
}
|
|
7252
|
+
assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
|
|
7115
7253
|
if (args.dry_run) {
|
|
7116
7254
|
const appliedKeys = appliedMigrationKeys(migrationsResult.data);
|
|
7117
7255
|
const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
|
|
@@ -7729,135 +7867,534 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7729
7867
|
});
|
|
7730
7868
|
}
|
|
7731
7869
|
|
|
7870
|
+
// src/shared/tools/release-control-response.ts
|
|
7871
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
7872
|
+
function releaseControlSuccess(operation, payload) {
|
|
7873
|
+
return releaseControlResponse({
|
|
7874
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7875
|
+
ok: true,
|
|
7876
|
+
operation,
|
|
7877
|
+
...payload
|
|
7878
|
+
});
|
|
7879
|
+
}
|
|
7880
|
+
function releaseControlFailure(operation, code, httpStatus) {
|
|
7881
|
+
return releaseControlErrorResponse({
|
|
7882
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7883
|
+
ok: false,
|
|
7884
|
+
operation,
|
|
7885
|
+
error: { code, http_status: httpStatus }
|
|
7886
|
+
});
|
|
7887
|
+
}
|
|
7888
|
+
function releaseControlMutationFailure(operation, response) {
|
|
7889
|
+
const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
|
|
7890
|
+
return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
7891
|
+
}
|
|
7892
|
+
function releaseControlResponse(payload) {
|
|
7893
|
+
return {
|
|
7894
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
7895
|
+
};
|
|
7896
|
+
}
|
|
7897
|
+
function releaseControlErrorResponse(payload) {
|
|
7898
|
+
return { ...releaseControlResponse(payload), isError: true };
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7732
7901
|
// src/shared/tools/storage-tools.ts
|
|
7902
|
+
var MAX_BUCKET_ID_LENGTH = 100;
|
|
7903
|
+
var MAX_MIME_TYPE_COUNT = 100;
|
|
7904
|
+
var MAX_MIME_TYPE_LENGTH = 255;
|
|
7905
|
+
var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7906
|
+
var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
|
|
7907
|
+
var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
|
|
7908
|
+
var BUCKET_REVISION_PATTERN = /^[0-9]{1,20}$/;
|
|
7909
|
+
var ACTION_ARGUMENTS = {
|
|
7910
|
+
status: new Set(["action"]),
|
|
7911
|
+
list_buckets: new Set(["action", "ref"]),
|
|
7912
|
+
get_bucket: new Set(["action", "ref", "bucket"]),
|
|
7913
|
+
create_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
|
|
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"]),
|
|
7916
|
+
list_files: new Set(["action", "ref", "bucket"]),
|
|
7917
|
+
upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
|
|
7918
|
+
delete_file: new Set(["action", "ref", "bucket", "filename"])
|
|
7919
|
+
};
|
|
7920
|
+
function normalizedMimeTypes(candidate) {
|
|
7921
|
+
return Array.isArray(candidate) ? candidate.map((mimeType) => typeof mimeType === "string" ? mimeType.trim() : mimeType) : candidate;
|
|
7922
|
+
}
|
|
7923
|
+
function parseAllowedMimeTypes(input) {
|
|
7924
|
+
if (Array.isArray(input))
|
|
7925
|
+
return normalizedMimeTypes(input);
|
|
7926
|
+
const trimmed = input.trim();
|
|
7927
|
+
if (!trimmed)
|
|
7928
|
+
return [];
|
|
7929
|
+
if (!trimmed.startsWith("[")) {
|
|
7930
|
+
return normalizedMimeTypes(trimmed.split(","));
|
|
7931
|
+
}
|
|
7932
|
+
try {
|
|
7933
|
+
return normalizedMimeTypes(JSON.parse(trimmed));
|
|
7934
|
+
} catch (error) {
|
|
7935
|
+
if (!(error instanceof SyntaxError))
|
|
7936
|
+
throw error;
|
|
7937
|
+
throw new Error("Invalid allowed_mime_types JSON array");
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7940
|
+
var allowedMimeTypesSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), Type.Array(Type.String())]), Type.Array(Type.String({
|
|
7941
|
+
minLength: 1,
|
|
7942
|
+
maxLength: MAX_MIME_TYPE_LENGTH,
|
|
7943
|
+
pattern: MIME_TYPE_PATTERN.source
|
|
7944
|
+
}), { maxItems: MAX_MIME_TYPE_COUNT }), parseAllowedMimeTypes));
|
|
7945
|
+
var fileSizeLimitSchema = Type.Optional(Type.Integer({
|
|
7946
|
+
minimum: 1,
|
|
7947
|
+
maximum: Number.MAX_SAFE_INTEGER
|
|
7948
|
+
}));
|
|
7949
|
+
function requiredText(args, field) {
|
|
7950
|
+
const text = args[field];
|
|
7951
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
7952
|
+
throw new Error(`'${field}' required for '${String(args.action)}'`);
|
|
7953
|
+
}
|
|
7954
|
+
return text.trim();
|
|
7955
|
+
}
|
|
7956
|
+
function validBucketId(bucket) {
|
|
7957
|
+
return BUCKET_ID_PATTERN.test(bucket);
|
|
7958
|
+
}
|
|
7959
|
+
function requiredProjectRef(args) {
|
|
7960
|
+
const ref = requiredText(args, "ref");
|
|
7961
|
+
if (!PROJECT_REF_PATTERN2.test(ref))
|
|
7962
|
+
throw new Error("'ref' is invalid for Storage buckets");
|
|
7963
|
+
return ref;
|
|
7964
|
+
}
|
|
7965
|
+
function requiredBucketId(args) {
|
|
7966
|
+
const bucket = requiredText(args, "bucket");
|
|
7967
|
+
if (!validBucketId(bucket))
|
|
7968
|
+
throw new Error("'bucket' is invalid for Storage buckets");
|
|
7969
|
+
return bucket;
|
|
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
|
+
}
|
|
7981
|
+
function assertActionArguments(action, args) {
|
|
7982
|
+
const allowedArguments = ACTION_ARGUMENTS[action];
|
|
7983
|
+
if (!allowedArguments)
|
|
7984
|
+
throw new Error(`Unsupported Storage action '${action}'`);
|
|
7985
|
+
const unsupported = Object.keys(args).filter((field) => !allowedArguments.has(field));
|
|
7986
|
+
if (unsupported.length > 0)
|
|
7987
|
+
throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
|
|
7988
|
+
}
|
|
7989
|
+
function storageBucketPath(ref, bucket) {
|
|
7990
|
+
if (!PROJECT_REF_PATTERN2.test(ref))
|
|
7991
|
+
throw new Error("'ref' is invalid for Storage buckets");
|
|
7992
|
+
if (bucket !== undefined && !validBucketId(bucket)) {
|
|
7993
|
+
throw new Error("'bucket' is invalid for Storage buckets");
|
|
7994
|
+
}
|
|
7995
|
+
const root = `/v1/projects/${encodeURIComponent(ref)}/storage/buckets`;
|
|
7996
|
+
return bucket === undefined ? root : `${root}/${encodeURIComponent(bucket)}`;
|
|
7997
|
+
}
|
|
7998
|
+
function bucketRecord(candidate) {
|
|
7999
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
8000
|
+
}
|
|
8001
|
+
function isFileSizeLimit(candidate) {
|
|
8002
|
+
return candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0;
|
|
8003
|
+
}
|
|
8004
|
+
function assertBucketSettings(args) {
|
|
8005
|
+
if (args.file_size_limit !== undefined && (args.file_size_limit === null || !isFileSizeLimit(args.file_size_limit))) {
|
|
8006
|
+
throw new Error("'file_size_limit' must be a positive safe integer");
|
|
8007
|
+
}
|
|
8008
|
+
if (args.allowed_mime_types !== undefined && (args.allowed_mime_types === null || !isAllowedMimeTypes(args.allowed_mime_types))) {
|
|
8009
|
+
throw new Error("'allowed_mime_types' is invalid");
|
|
8010
|
+
}
|
|
8011
|
+
}
|
|
8012
|
+
function isAllowedMimeTypes(candidate) {
|
|
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));
|
|
8014
|
+
}
|
|
8015
|
+
function isBucketRevision(candidate) {
|
|
8016
|
+
return candidate === null || typeof candidate === "string" && BUCKET_REVISION_PATTERN.test(candidate);
|
|
8017
|
+
}
|
|
8018
|
+
function safeBucket(candidate, expectedBucket) {
|
|
8019
|
+
const bucket = bucketRecord(candidate);
|
|
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) {
|
|
8021
|
+
return null;
|
|
8022
|
+
}
|
|
8023
|
+
return {
|
|
8024
|
+
id: bucket.id,
|
|
8025
|
+
name: bucket.name,
|
|
8026
|
+
public: bucket.public,
|
|
8027
|
+
file_size_limit: bucket.file_size_limit,
|
|
8028
|
+
allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types],
|
|
8029
|
+
revision: bucket.revision
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
function safeExactBucket(candidate, expectedBucket) {
|
|
8033
|
+
const bucket = safeBucket(candidate);
|
|
8034
|
+
return bucket?.id === expectedBucket ? bucket : null;
|
|
8035
|
+
}
|
|
8036
|
+
function safeBucketList(candidate) {
|
|
8037
|
+
if (!Array.isArray(candidate))
|
|
8038
|
+
return null;
|
|
8039
|
+
const buckets = candidate.map((bucket) => safeBucket(bucket));
|
|
8040
|
+
if (buckets.some((bucket) => bucket === null))
|
|
8041
|
+
return null;
|
|
8042
|
+
const safeBuckets = buckets;
|
|
8043
|
+
const ids = safeBuckets.map((bucket) => bucket.id);
|
|
8044
|
+
const names = safeBuckets.map((bucket) => bucket.name);
|
|
8045
|
+
return new Set(ids).size === ids.length && new Set(names).size === names.length ? safeBuckets : null;
|
|
8046
|
+
}
|
|
8047
|
+
function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
|
|
8048
|
+
const bucket = bucketRecord(candidate);
|
|
8049
|
+
if (bucket?.id !== expectedBucket || bucket.name !== expectedBucket || bucket.public !== (request.public === true))
|
|
8050
|
+
return null;
|
|
8051
|
+
return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
|
|
8052
|
+
}
|
|
8053
|
+
function safeDeletedBucket(candidate, expectedBucket, expectedRevision) {
|
|
8054
|
+
const receipt = bucketRecord(candidate);
|
|
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;
|
|
8062
|
+
}
|
|
8063
|
+
function mutationReadbackResponse(expectation) {
|
|
8064
|
+
const { operation, ref, response, expectedBucket, request, previousRevision, expectedNewRevision } = expectation;
|
|
8065
|
+
const readback = safeExactBucket(response.data, expectedBucket);
|
|
8066
|
+
const validReadback = readback?.name === expectedBucket && readback.revision !== null && (expectedNewRevision === undefined || readback.revision === expectedNewRevision) && bucketMatchesRequest(readback, request);
|
|
8067
|
+
if (!response.ok || !validReadback || !readback) {
|
|
8068
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
|
|
8069
|
+
}
|
|
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
|
+
});
|
|
8077
|
+
}
|
|
8078
|
+
function bucketResponse(expectation) {
|
|
8079
|
+
const { operation, ref, response, operationKind, safePayload } = expectation;
|
|
8080
|
+
if (!response.ok) {
|
|
8081
|
+
return operationKind === "mutation" ? releaseControlMutationFailure(operation, response) : releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
|
|
8082
|
+
}
|
|
8083
|
+
const payload = safePayload(response.data);
|
|
8084
|
+
if (!payload) {
|
|
8085
|
+
return operationKind === "mutation" ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status) : releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
8086
|
+
}
|
|
8087
|
+
return releaseControlSuccess(operation, { project_ref: ref, ...payload });
|
|
8088
|
+
}
|
|
8089
|
+
function createBucketRequest(args) {
|
|
8090
|
+
assertBucketSettings(args);
|
|
8091
|
+
const request = { name: requiredText(args, "bucket") };
|
|
8092
|
+
if (args.public !== undefined)
|
|
8093
|
+
request.public = args.public;
|
|
8094
|
+
if (args.file_size_limit !== undefined)
|
|
8095
|
+
request.file_size_limit = args.file_size_limit;
|
|
8096
|
+
if (args.allowed_mime_types !== undefined)
|
|
8097
|
+
request.allowed_mime_types = args.allowed_mime_types;
|
|
8098
|
+
return request;
|
|
8099
|
+
}
|
|
8100
|
+
function updateBucketRequest(args) {
|
|
8101
|
+
assertBucketSettings(args);
|
|
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)
|
|
8104
|
+
throw new Error("Bucket update requires at least one field");
|
|
8105
|
+
return { expected_revision: requiredBucketRevision(args), ...settings };
|
|
8106
|
+
}
|
|
8107
|
+
function equalAllowedMimeTypes(candidate, expected) {
|
|
8108
|
+
if (!Array.isArray(expected))
|
|
8109
|
+
return false;
|
|
8110
|
+
if (expected.length === 0 && candidate === null)
|
|
8111
|
+
return true;
|
|
8112
|
+
return Array.isArray(candidate) && candidate.length === expected.length && candidate.every((mimeType, index) => mimeType === expected[index]);
|
|
8113
|
+
}
|
|
8114
|
+
function bucketMatchesRequest(candidate, request) {
|
|
8115
|
+
const bucket = bucketRecord(candidate);
|
|
8116
|
+
if (!bucket)
|
|
8117
|
+
return false;
|
|
8118
|
+
if (request.public !== undefined && bucket.public !== request.public)
|
|
8119
|
+
return false;
|
|
8120
|
+
if (request.file_size_limit !== undefined && bucket.file_size_limit !== request.file_size_limit)
|
|
8121
|
+
return false;
|
|
8122
|
+
return request.allowed_mime_types === undefined || equalAllowedMimeTypes(bucket.allowed_mime_types, request.allowed_mime_types);
|
|
8123
|
+
}
|
|
8124
|
+
async function createBucketMutationReceipt(http, ref, bucket, request) {
|
|
8125
|
+
return bucketResponse({
|
|
8126
|
+
operation: "storage.create_bucket",
|
|
8127
|
+
ref,
|
|
8128
|
+
response: await http.post(storageBucketPath(ref), request),
|
|
8129
|
+
operationKind: "mutation",
|
|
8130
|
+
safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
|
|
8131
|
+
});
|
|
8132
|
+
}
|
|
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;
|
|
8137
|
+
}
|
|
8138
|
+
async function listBuckets(http, args) {
|
|
8139
|
+
const ref = requiredProjectRef(args);
|
|
8140
|
+
return bucketResponse({
|
|
8141
|
+
operation: "storage.list_buckets",
|
|
8142
|
+
ref,
|
|
8143
|
+
response: await http.get(storageBucketPath(ref)),
|
|
8144
|
+
operationKind: "read",
|
|
8145
|
+
safePayload: (candidate) => {
|
|
8146
|
+
const buckets = safeBucketList(candidate);
|
|
8147
|
+
return buckets ? { buckets } : null;
|
|
8148
|
+
}
|
|
8149
|
+
});
|
|
8150
|
+
}
|
|
8151
|
+
async function getBucket(http, args) {
|
|
8152
|
+
const ref = requiredProjectRef(args);
|
|
8153
|
+
const bucket = requiredBucketId(args);
|
|
8154
|
+
return bucketResponse({
|
|
8155
|
+
operation: "storage.get_bucket",
|
|
8156
|
+
ref,
|
|
8157
|
+
response: await http.get(storageBucketPath(ref, bucket)),
|
|
8158
|
+
operationKind: "read",
|
|
8159
|
+
safePayload: (candidate) => {
|
|
8160
|
+
const safeReadback = safeBucket(candidate, bucket);
|
|
8161
|
+
return safeReadback ? { bucket: safeReadback } : null;
|
|
8162
|
+
}
|
|
8163
|
+
});
|
|
8164
|
+
}
|
|
8165
|
+
async function createBucket(http, args) {
|
|
8166
|
+
const ref = requiredProjectRef(args);
|
|
8167
|
+
const request = createBucketRequest(args);
|
|
8168
|
+
const bucket = request.name;
|
|
8169
|
+
const bucketPath = storageBucketPath(ref, bucket);
|
|
8170
|
+
const receipt = await createBucketMutationReceipt(http, ref, bucket, request);
|
|
8171
|
+
if (receipt.isError)
|
|
8172
|
+
return receipt;
|
|
8173
|
+
const expectedReadback = { ...request, public: request.public === true };
|
|
8174
|
+
return mutationReadbackResponse({
|
|
8175
|
+
operation: "storage.create_bucket",
|
|
8176
|
+
ref,
|
|
8177
|
+
response: await http.get(bucketPath),
|
|
8178
|
+
expectedBucket: bucket,
|
|
8179
|
+
request: expectedReadback,
|
|
8180
|
+
previousRevision: null
|
|
8181
|
+
});
|
|
8182
|
+
}
|
|
8183
|
+
async function updateBucket(http, args) {
|
|
8184
|
+
const ref = requiredProjectRef(args);
|
|
8185
|
+
const bucket = requiredBucketId(args);
|
|
8186
|
+
const request = updateBucketRequest(args);
|
|
8187
|
+
const expectedRevision = request.expected_revision;
|
|
8188
|
+
const bucketPath = storageBucketPath(ref, bucket);
|
|
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);
|
|
8195
|
+
return mutationReadbackResponse({
|
|
8196
|
+
operation: "storage.update_bucket",
|
|
8197
|
+
ref,
|
|
8198
|
+
response: await http.get(bucketPath),
|
|
8199
|
+
expectedBucket: bucket,
|
|
8200
|
+
request,
|
|
8201
|
+
previousRevision: expectedRevision,
|
|
8202
|
+
expectedNewRevision: updated.revision ?? undefined
|
|
8203
|
+
});
|
|
8204
|
+
}
|
|
8205
|
+
async function deleteBucket(http, args) {
|
|
8206
|
+
const ref = requiredProjectRef(args);
|
|
8207
|
+
const bucket = requiredBucketId(args);
|
|
8208
|
+
const expectedRevision = requiredBucketRevision(args);
|
|
8209
|
+
assertEmptyBucketDeletion(args);
|
|
8210
|
+
const bucketPath = storageBucketPath(ref, bucket);
|
|
8211
|
+
const deletePath = `${bucketPath}?expected_revision=${encodeURIComponent(expectedRevision)}&require_empty=true`;
|
|
8212
|
+
const receipt = bucketResponse({
|
|
8213
|
+
operation: "storage.delete_bucket",
|
|
8214
|
+
ref,
|
|
8215
|
+
response: await http.delete(deletePath),
|
|
8216
|
+
operationKind: "mutation",
|
|
8217
|
+
safePayload: (candidate) => safeDeletedBucket(candidate, bucket, expectedRevision)
|
|
8218
|
+
});
|
|
8219
|
+
if (receipt.isError)
|
|
8220
|
+
return receipt;
|
|
8221
|
+
const readback = await http.get(bucketPath);
|
|
8222
|
+
if (readback.ok || readback.transportError || readback.status !== 404) {
|
|
8223
|
+
return releaseControlFailure("storage.delete_bucket", "OUTCOME_UNKNOWN", readback.transportError ? null : readback.status);
|
|
8224
|
+
}
|
|
8225
|
+
return receipt;
|
|
8226
|
+
}
|
|
8227
|
+
var BUCKET_ACTION_HANDLERS = {
|
|
8228
|
+
list_buckets: listBuckets,
|
|
8229
|
+
get_bucket: getBucket,
|
|
8230
|
+
create_bucket: createBucket,
|
|
8231
|
+
update_bucket: updateBucket,
|
|
8232
|
+
delete_bucket: deleteBucket
|
|
8233
|
+
};
|
|
8234
|
+
function executeBucketAction(action, http, args) {
|
|
8235
|
+
if (!Object.hasOwn(BUCKET_ACTION_HANDLERS, action))
|
|
8236
|
+
return null;
|
|
8237
|
+
return BUCKET_ACTION_HANDLERS[action](http, args);
|
|
8238
|
+
}
|
|
7733
8239
|
function registerStorageTools(server, http) {
|
|
7734
8240
|
server.tool("storage", `S3/MinIO storage management.
|
|
7735
|
-
Actions: status, list_buckets, list_files, upload_base64, delete_file`, {
|
|
7736
|
-
action: withDescription(stringEnum([
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
8241
|
+
Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_bucket, list_files, upload_base64, delete_file`, {
|
|
8242
|
+
action: withDescription(stringEnum([
|
|
8243
|
+
"status",
|
|
8244
|
+
"list_buckets",
|
|
8245
|
+
"get_bucket",
|
|
8246
|
+
"create_bucket",
|
|
8247
|
+
"update_bucket",
|
|
8248
|
+
"delete_bucket",
|
|
8249
|
+
"list_files",
|
|
8250
|
+
"upload_base64",
|
|
8251
|
+
"delete_file"
|
|
8252
|
+
]), "Action"),
|
|
8253
|
+
ref: optional(Type.String({ pattern: PROJECT_REF_PATTERN2.source }), "[list_buckets/get_bucket/create_bucket/update_bucket/delete_bucket/list_files/upload_base64/delete_file] Project ref"),
|
|
8254
|
+
bucket: optional(Type.String({ pattern: BUCKET_ID_PATTERN.source }), "[get_bucket/create_bucket/update_bucket/delete_bucket/list_files/upload_base64/delete_file] Bucket name or ID"),
|
|
8255
|
+
public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
|
|
8256
|
+
file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
|
|
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"),
|
|
8260
|
+
filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
|
|
7740
8261
|
base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
|
|
7741
8262
|
mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
|
|
7742
8263
|
}, async (args) => {
|
|
7743
|
-
const
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
return Array.isArray(data) ? fmtFn(data) : JSON.stringify(data, null, 2);
|
|
7750
|
-
};
|
|
8264
|
+
const action = String(args.action);
|
|
8265
|
+
assertActionArguments(action, args);
|
|
8266
|
+
const bucketAction = executeBucketAction(action, http, args);
|
|
8267
|
+
const bucketActionResponse = bucketAction ? await bucketAction : null;
|
|
8268
|
+
if (bucketActionResponse)
|
|
8269
|
+
return bucketActionResponse;
|
|
7751
8270
|
let text;
|
|
7752
8271
|
switch (action) {
|
|
7753
8272
|
case "status":
|
|
7754
8273
|
text = JSON.stringify((await http.get("/v1/storage/status")).data, null, 2);
|
|
7755
8274
|
break;
|
|
7756
|
-
case "list_buckets": {
|
|
7757
|
-
need("ref", ref);
|
|
7758
|
-
const res = await http.get(`/v1/storage/${ref}/buckets`);
|
|
7759
|
-
if (!res.ok) {
|
|
7760
|
-
text = `❌ Failed (${res.status})`;
|
|
7761
|
-
break;
|
|
7762
|
-
}
|
|
7763
|
-
const buckets = res.data;
|
|
7764
|
-
if (!Array.isArray(buckets) || !buckets.length) {
|
|
7765
|
-
text = "No buckets found.";
|
|
7766
|
-
break;
|
|
7767
|
-
}
|
|
7768
|
-
text = `\uD83D\uDCE6 Buckets (${buckets.length}):
|
|
7769
|
-
` + buckets.map((b) => ` - ${b.name} (${b.public ? "\uD83D\uDD13 public" : "\uD83D\uDD12 private"})`).join(`
|
|
7770
|
-
`);
|
|
7771
|
-
break;
|
|
7772
|
-
}
|
|
7773
8275
|
case "list_files": {
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
const
|
|
7777
|
-
if (!
|
|
7778
|
-
text = `❌ Failed (${
|
|
8276
|
+
const ref = requiredProjectRef(args);
|
|
8277
|
+
const bucket = requiredBucketId(args);
|
|
8278
|
+
const response = await http.get(`/v1/storage/${ref}/buckets/${bucket}/files`);
|
|
8279
|
+
if (!response.ok) {
|
|
8280
|
+
text = `❌ Failed (${response.status})`;
|
|
7779
8281
|
break;
|
|
7780
8282
|
}
|
|
7781
|
-
const files =
|
|
8283
|
+
const files = response.data;
|
|
7782
8284
|
if (!Array.isArray(files) || !files.length) {
|
|
7783
8285
|
text = "No files.";
|
|
7784
8286
|
break;
|
|
7785
8287
|
}
|
|
7786
8288
|
text = `\uD83D\uDCC1 Files (${files.length}):
|
|
7787
|
-
` + files.map((
|
|
8289
|
+
` + files.map((file) => ` - ${file.name} (${file.size ? (file.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
|
|
7788
8290
|
`);
|
|
7789
8291
|
break;
|
|
7790
8292
|
}
|
|
7791
8293
|
case "upload_base64": {
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
8294
|
+
const ref = requiredProjectRef(args);
|
|
8295
|
+
const bucket = requiredBucketId(args);
|
|
8296
|
+
const filename = requiredText(args, "filename");
|
|
8297
|
+
const base64Content = requiredText(args, "base64_content");
|
|
7796
8298
|
try {
|
|
7797
|
-
const buffer = Buffer.from(
|
|
7798
|
-
const blob = new Blob([buffer], { type: mime_type
|
|
8299
|
+
const buffer = Buffer.from(base64Content, "base64");
|
|
8300
|
+
const blob = new Blob([buffer], { type: typeof args.mime_type === "string" ? args.mime_type : "application/octet-stream" });
|
|
7799
8301
|
const formData = new FormData;
|
|
7800
8302
|
formData.append("file", blob, filename);
|
|
7801
|
-
const
|
|
7802
|
-
text =
|
|
7803
|
-
} catch (
|
|
7804
|
-
text = `❌ Error: ${
|
|
8303
|
+
const response = await http.postMultipart(`/v1/storage/${ref}/buckets/${bucket}/upload`, formData);
|
|
8304
|
+
text = response.ok ? `✅ File ${filename} uploaded to ${bucket}` : `❌ Upload failed (${response.status})`;
|
|
8305
|
+
} catch (error) {
|
|
8306
|
+
text = `❌ Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
7805
8307
|
}
|
|
7806
8308
|
break;
|
|
7807
8309
|
}
|
|
7808
|
-
case "delete_file":
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` :
|
|
8310
|
+
case "delete_file": {
|
|
8311
|
+
const ref = requiredProjectRef(args);
|
|
8312
|
+
const bucket = requiredBucketId(args);
|
|
8313
|
+
const filename = requiredText(args, "filename");
|
|
8314
|
+
text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` : "❌ Failed";
|
|
7813
8315
|
break;
|
|
8316
|
+
}
|
|
7814
8317
|
default:
|
|
7815
|
-
|
|
8318
|
+
return releaseControlFailure(`storage.${action}`, "INVALID_RESPONSE", null);
|
|
7816
8319
|
}
|
|
7817
8320
|
return { content: [{ type: "text", text }] };
|
|
7818
8321
|
});
|
|
7819
8322
|
}
|
|
7820
8323
|
|
|
7821
8324
|
// src/shared/tools/advanced-tools.ts
|
|
7822
|
-
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";
|
|
7823
8338
|
import { tmpdir } from "node:os";
|
|
7824
8339
|
import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
|
|
7825
8340
|
import { promisify } from "node:util";
|
|
7826
8341
|
import { execFile } from "node:child_process";
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
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
|
+
};
|
|
7837
8355
|
}
|
|
7838
|
-
function
|
|
7839
|
-
return
|
|
7840
|
-
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7841
|
-
ok: false,
|
|
7842
|
-
operation,
|
|
7843
|
-
error: { code, http_status: httpStatus }
|
|
7844
|
-
});
|
|
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;
|
|
7845
8358
|
}
|
|
7846
|
-
function
|
|
7847
|
-
|
|
7848
|
-
|
|
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;
|
|
7849
8372
|
}
|
|
7850
|
-
function
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
}
|
|
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
|
+
}
|
|
7854
8382
|
}
|
|
7855
|
-
function
|
|
7856
|
-
|
|
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
|
+
}
|
|
7857
8397
|
}
|
|
7858
|
-
|
|
7859
|
-
// src/shared/tools/advanced-tools.ts
|
|
7860
|
-
var execFileAsync = promisify(execFile);
|
|
7861
8398
|
async function runBunBuild(args) {
|
|
7862
8399
|
try {
|
|
7863
8400
|
return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -7882,6 +8419,54 @@ async function bundleEdgeFunctionPath(pathArg) {
|
|
|
7882
8419
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
7883
8420
|
}
|
|
7884
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
|
+
}
|
|
7885
8470
|
function resolveEntrypoint(pathArg) {
|
|
7886
8471
|
const resolved = resolve2(pathArg);
|
|
7887
8472
|
const stat = statSync2(resolved);
|
|
@@ -7928,21 +8513,42 @@ function parseFunctionFiles(input) {
|
|
|
7928
8513
|
}
|
|
7929
8514
|
}
|
|
7930
8515
|
var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
|
|
7931
|
-
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
|
+
}
|
|
7932
8520
|
const version = String(input);
|
|
7933
|
-
if (!
|
|
7934
|
-
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`);
|
|
7935
8523
|
}
|
|
7936
8524
|
return version;
|
|
7937
8525
|
}
|
|
7938
|
-
var
|
|
7939
|
-
var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
8526
|
+
var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
|
|
7940
8527
|
var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
7941
|
-
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
|
+
]);
|
|
7942
8535
|
var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
7943
|
-
Type.Integer({ minimum:
|
|
7944
|
-
Type.String({ pattern:
|
|
7945
|
-
]), 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));
|
|
7946
8552
|
var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
|
|
7947
8553
|
var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
7948
8554
|
var MAX_SECRET_COUNT = 1024;
|
|
@@ -8060,10 +8666,35 @@ function secretsForUpsert(inlineSecrets, environmentNames, environment) {
|
|
|
8060
8666
|
throw new Error("'secrets' array required");
|
|
8061
8667
|
return inlineSecrets;
|
|
8062
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
|
+
}
|
|
8063
8694
|
function confirmedFunctionConfig(payload, expected) {
|
|
8064
|
-
|
|
8695
|
+
const response = objectRecord(payload);
|
|
8696
|
+
if (!response)
|
|
8065
8697
|
return false;
|
|
8066
|
-
const response = payload;
|
|
8067
8698
|
if (expected.verify_jwt !== undefined && response.verify_jwt !== expected.verify_jwt)
|
|
8068
8699
|
return false;
|
|
8069
8700
|
if (expected.background_routes !== undefined) {
|
|
@@ -8074,28 +8705,87 @@ function confirmedFunctionConfig(payload, expected) {
|
|
|
8074
8705
|
}
|
|
8075
8706
|
return true;
|
|
8076
8707
|
}
|
|
8077
|
-
function functionSourceCode(payload) {
|
|
8708
|
+
function functionSourceCode(payload, field = "code") {
|
|
8078
8709
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
8079
8710
|
return null;
|
|
8080
|
-
const code = payload
|
|
8711
|
+
const code = payload[field];
|
|
8081
8712
|
return typeof code === "string" ? code : null;
|
|
8082
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
|
+
}
|
|
8083
8730
|
function objectRecord(candidate) {
|
|
8084
8731
|
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
8085
8732
|
}
|
|
8086
|
-
function
|
|
8087
|
-
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);
|
|
8088
8747
|
if (!response.ok)
|
|
8089
|
-
return
|
|
8090
|
-
const
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
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;
|
|
8094
8762
|
}
|
|
8095
|
-
return
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
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
|
|
8099
8789
|
});
|
|
8100
8790
|
}
|
|
8101
8791
|
function readOnlyActivationResult() {
|
|
@@ -8107,15 +8797,22 @@ function readOnlyActivationResult() {
|
|
|
8107
8797
|
function functionActivationTarget(args) {
|
|
8108
8798
|
const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
|
|
8109
8799
|
const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
8110
|
-
const version = args.version;
|
|
8111
|
-
|
|
8112
|
-
throw new Error("'ref' is invalid for 'activate'");
|
|
8800
|
+
const version = positiveFunctionVersion(args.version, "Function activation version");
|
|
8801
|
+
projectRefPathSegment(projectRef, "Edge Function activation");
|
|
8113
8802
|
if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
|
|
8114
8803
|
throw new Error("'slug' is invalid for 'activate'");
|
|
8115
|
-
|
|
8116
|
-
|
|
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}'`);
|
|
8117
8811
|
}
|
|
8118
|
-
|
|
8812
|
+
const parsed = parseExpectedActiveVersion(expected);
|
|
8813
|
+
if (typeof parsed !== "string")
|
|
8814
|
+
throw new Error("Expected active version is invalid");
|
|
8815
|
+
return parsed;
|
|
8119
8816
|
}
|
|
8120
8817
|
async function activateFunctionVersion(http, args, readOnly = false) {
|
|
8121
8818
|
if (readOnly)
|
|
@@ -8123,29 +8820,40 @@ async function activateFunctionVersion(http, args, readOnly = false) {
|
|
|
8123
8820
|
const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
|
|
8124
8821
|
if (unsupported.length > 0)
|
|
8125
8822
|
throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
|
|
8126
|
-
const { projectRef, functionSlug, version } = functionActivationTarget(args);
|
|
8127
|
-
const endpoint =
|
|
8128
|
-
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 }));
|
|
8129
8832
|
}
|
|
8130
8833
|
function registerAdvancedTools(server, http, environment = process.env, options = {}) {
|
|
8131
|
-
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.
|
|
8132
8835
|
Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
|
|
8133
8836
|
action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
|
|
8134
8837
|
ref: withDescription(Type.String(), "Project ref"),
|
|
8135
8838
|
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
|
|
8136
|
-
version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
|
|
8839
|
+
version: withDescription(functionVersionSchema, "[source/activate] Existing immutable Function version; source requires a positive version"),
|
|
8137
8840
|
code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
|
|
8138
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"),
|
|
8139
8844
|
output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
|
|
8140
8845
|
files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
|
|
8141
8846
|
entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
|
|
8142
8847
|
minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
|
|
8143
8848
|
verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
|
|
8144
|
-
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")
|
|
8145
8851
|
}, async (args) => {
|
|
8146
8852
|
if (args.action === "activate")
|
|
8147
8853
|
return activateFunctionVersion(http, args, options.readOnly);
|
|
8148
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;
|
|
8149
8857
|
let code = args.code;
|
|
8150
8858
|
const need = (f, v) => {
|
|
8151
8859
|
if (!v)
|
|
@@ -8162,16 +8870,10 @@ Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`,
|
|
|
8162
8870
|
if (!hasFunctionConfig()) {
|
|
8163
8871
|
throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
|
|
8164
8872
|
}
|
|
8165
|
-
const cr = await http.patch(
|
|
8873
|
+
const cr = await http.patch(`${edgeFunctionResourcePath(ref, slug)}/config`, functionConfig());
|
|
8166
8874
|
return cr.ok ? `✅ Function ${slug} config updated
|
|
8167
8875
|
${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
|
|
8168
8876
|
};
|
|
8169
|
-
const deploymentPolicyReceiptText = (successText, responsePayload) => {
|
|
8170
|
-
if (!hasFunctionConfig() || confirmedFunctionConfig(responsePayload, functionConfig())) {
|
|
8171
|
-
return successText;
|
|
8172
|
-
}
|
|
8173
|
-
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.";
|
|
8174
|
-
};
|
|
8175
8877
|
const checkSyntax = async (sourceCode) => {
|
|
8176
8878
|
const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
|
|
8177
8879
|
const tmpFile = join2(tmpDir, "index.ts");
|
|
@@ -8186,7 +8888,7 @@ ${e.stderr || e.message}` };
|
|
|
8186
8888
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
8187
8889
|
}
|
|
8188
8890
|
};
|
|
8189
|
-
if (pathArg && !code) {
|
|
8891
|
+
if (action === "check" && pathArg && !code) {
|
|
8190
8892
|
try {
|
|
8191
8893
|
code = await bundleEdgeFunctionPath(pathArg);
|
|
8192
8894
|
} catch (error) {
|
|
@@ -8196,8 +8898,7 @@ ${e.stderr || e.message}` };
|
|
|
8196
8898
|
}
|
|
8197
8899
|
switch (action) {
|
|
8198
8900
|
case "list":
|
|
8199
|
-
|
|
8200
|
-
break;
|
|
8901
|
+
return functionListResponse(await http.get(edgeFunctionResourcePath(ref)));
|
|
8201
8902
|
case "check":
|
|
8202
8903
|
need("code (or path)", code);
|
|
8203
8904
|
const checkRes = await checkSyntax(code);
|
|
@@ -8210,65 +8911,59 @@ ${checkRes.err}`;
|
|
|
8210
8911
|
break;
|
|
8211
8912
|
case "deploy":
|
|
8212
8913
|
need("slug", slug);
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
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:
|
|
8217
8919
|
${deployCheck.err}`;
|
|
8218
|
-
|
|
8920
|
+
break;
|
|
8921
|
+
}
|
|
8219
8922
|
}
|
|
8220
|
-
const
|
|
8221
|
-
code,
|
|
8222
|
-
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,
|
|
8223
8927
|
...functionConfig()
|
|
8224
8928
|
});
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8929
|
+
return functionMutationResponse({
|
|
8930
|
+
operation: "edge_functions.deploy",
|
|
8931
|
+
projectRef: ref,
|
|
8932
|
+
slug,
|
|
8933
|
+
expectedActiveVersion,
|
|
8934
|
+
config: functionConfig()
|
|
8935
|
+
}, deploymentResponse);
|
|
8231
8936
|
case "deploy_bundle":
|
|
8232
8937
|
need("slug", slug);
|
|
8233
8938
|
need("files", files);
|
|
8234
|
-
const
|
|
8939
|
+
const bundleResponse = await http.post(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
|
|
8235
8940
|
files,
|
|
8236
8941
|
entrypoint,
|
|
8237
8942
|
minify,
|
|
8943
|
+
expected_active_version: expectedActiveVersion,
|
|
8238
8944
|
...functionConfig()
|
|
8239
8945
|
});
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8946
|
+
return functionMutationResponse({
|
|
8947
|
+
operation: "edge_functions.deploy_bundle",
|
|
8948
|
+
projectRef: ref,
|
|
8949
|
+
slug,
|
|
8950
|
+
expectedActiveVersion,
|
|
8951
|
+
config: functionConfig()
|
|
8952
|
+
}, bundleResponse);
|
|
8246
8953
|
case "config":
|
|
8247
8954
|
text = await updateFunctionConfig();
|
|
8248
8955
|
break;
|
|
8249
8956
|
case "source":
|
|
8250
8957
|
need("slug", slug);
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
text = JSON.stringify(sr.data, null, 2);
|
|
8258
|
-
break;
|
|
8259
|
-
}
|
|
8260
|
-
const sourceCode = functionSourceCode(sr.data);
|
|
8261
|
-
if (sourceCode === null) {
|
|
8262
|
-
text = "❌ Source response did not contain a string code field";
|
|
8263
|
-
break;
|
|
8264
|
-
}
|
|
8265
|
-
const outputPath = resolve2(output);
|
|
8266
|
-
writeFileSync(outputPath, sourceCode, { flag: "wx" });
|
|
8267
|
-
text = `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`;
|
|
8268
|
-
break;
|
|
8958
|
+
return readFunctionSource(http, {
|
|
8959
|
+
projectRef: ref,
|
|
8960
|
+
slug,
|
|
8961
|
+
version: args.version,
|
|
8962
|
+
output
|
|
8963
|
+
});
|
|
8269
8964
|
case "delete":
|
|
8270
8965
|
need("slug", slug);
|
|
8271
|
-
text = (await http.delete(
|
|
8966
|
+
text = (await http.delete(edgeFunctionResourcePath(ref, slug))).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
|
|
8272
8967
|
break;
|
|
8273
8968
|
default:
|
|
8274
8969
|
text = `❌ Unknown action`;
|
|
@@ -10180,7 +10875,6 @@ var FORBIDDEN_HEADER_NAMES = new Set([
|
|
|
10180
10875
|
"via",
|
|
10181
10876
|
"x-project-ref"
|
|
10182
10877
|
]);
|
|
10183
|
-
var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
10184
10878
|
var SCHEDULE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10185
10879
|
var SAFE_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
10186
10880
|
var CRON_PART_PATTERN = /^(\*|([0-9]+)(?:-([0-9]+))?)(?:\/([0-9]+))?$/;
|
|
@@ -10190,12 +10884,14 @@ var MAX_BODY_FILE_BYTES = 1048576;
|
|
|
10190
10884
|
var MAX_HEADER_COUNT = 64;
|
|
10191
10885
|
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
10192
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$/;
|
|
10193
10888
|
var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
|
|
10194
|
-
var
|
|
10889
|
+
var ACTION_ARGUMENTS2 = {
|
|
10195
10890
|
list: new Set(["action", "ref"]),
|
|
10891
|
+
get: new Set(["action", "ref", "schedule_id"]),
|
|
10196
10892
|
create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
|
|
10197
|
-
update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
|
|
10198
|
-
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"])
|
|
10199
10895
|
};
|
|
10200
10896
|
function parseHeaderEnvironment(input) {
|
|
10201
10897
|
if (typeof input !== "string")
|
|
@@ -10315,7 +11011,13 @@ function validScheduleDefinition(schedule) {
|
|
|
10315
11011
|
return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
|
|
10316
11012
|
}
|
|
10317
11013
|
function validScheduleMetadata(schedule) {
|
|
10318
|
-
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;
|
|
10319
11021
|
}
|
|
10320
11022
|
function safeSchedulePayload(schedule) {
|
|
10321
11023
|
const headerNames = safeHeaderNames(schedule.header_names);
|
|
@@ -10349,26 +11051,25 @@ function safeSchedule(candidate) {
|
|
|
10349
11051
|
updated_at: schedule.updated_at
|
|
10350
11052
|
};
|
|
10351
11053
|
}
|
|
10352
|
-
function
|
|
11054
|
+
function requiredText2(args, name, action) {
|
|
10353
11055
|
const candidate = args[name];
|
|
10354
11056
|
if (typeof candidate !== "string" || !candidate.trim()) {
|
|
10355
11057
|
throw new Error(`'${name}' is required for '${action}'`);
|
|
10356
11058
|
}
|
|
10357
11059
|
return candidate.trim();
|
|
10358
11060
|
}
|
|
10359
|
-
function
|
|
10360
|
-
const unsupported = Object.keys(args).filter((name) => !
|
|
11061
|
+
function assertActionArguments2(action, args) {
|
|
11062
|
+
const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS2[action].has(name));
|
|
10361
11063
|
if (unsupported.length > 0) {
|
|
10362
11064
|
throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
|
|
10363
11065
|
}
|
|
10364
11066
|
}
|
|
10365
11067
|
function schedulePath(ref, scheduleId) {
|
|
10366
|
-
|
|
10367
|
-
throw new Error("'ref' is invalid for Scheduled Functions");
|
|
11068
|
+
const projectRefSegment = projectRefPathSegment(ref, "Scheduled Functions");
|
|
10368
11069
|
if (scheduleId !== undefined && !SCHEDULE_ID_PATTERN.test(scheduleId)) {
|
|
10369
11070
|
throw new Error("'schedule_id' is invalid");
|
|
10370
11071
|
}
|
|
10371
|
-
const root = `/v1/projects/${
|
|
11072
|
+
const root = `/v1/projects/${projectRefSegment}/scheduled-functions`;
|
|
10372
11073
|
return scheduleId ? `${root}/${encodeURIComponent(scheduleId)}` : root;
|
|
10373
11074
|
}
|
|
10374
11075
|
function scheduleFailure(operation, response) {
|
|
@@ -10389,6 +11090,17 @@ function listResponse(ref, response) {
|
|
|
10389
11090
|
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10390
11091
|
return releaseControlSuccess(operation, { project_ref: ref, schedules });
|
|
10391
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
|
+
}
|
|
10392
11104
|
function mutationResponse(expectation, response) {
|
|
10393
11105
|
const { action, ref, requestId, expectedFields } = expectation;
|
|
10394
11106
|
const scheduleId = action === "update" ? expectation.scheduleId : undefined;
|
|
@@ -10398,22 +11110,29 @@ function mutationResponse(expectation, response) {
|
|
|
10398
11110
|
const payload = objectRecord2(response.data);
|
|
10399
11111
|
const schedule = safeSchedule(payload?.schedule);
|
|
10400
11112
|
const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
|
|
10401
|
-
|
|
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) {
|
|
10402
11115
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10403
11116
|
}
|
|
10404
|
-
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
|
+
});
|
|
10405
11123
|
}
|
|
10406
|
-
function deleteResponse(ref, scheduleId, response) {
|
|
11124
|
+
function deleteResponse(ref, scheduleId, expectedUpdatedAt, response) {
|
|
10407
11125
|
const operation = "scheduled_functions.delete";
|
|
10408
11126
|
if (!response.ok)
|
|
10409
11127
|
return releaseControlMutationFailure(operation, response);
|
|
10410
11128
|
const payload = objectRecord2(response.data);
|
|
10411
|
-
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) {
|
|
10412
11130
|
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10413
11131
|
}
|
|
10414
11132
|
return releaseControlSuccess(operation, {
|
|
10415
11133
|
project_ref: ref,
|
|
10416
11134
|
schedule_id: scheduleId,
|
|
11135
|
+
deleted_updated_at: expectedUpdatedAt,
|
|
10417
11136
|
deleted: true
|
|
10418
11137
|
});
|
|
10419
11138
|
}
|
|
@@ -10429,7 +11148,7 @@ function createRequest(args, environment) {
|
|
|
10429
11148
|
name: requiredName(args, "create"),
|
|
10430
11149
|
slug: requiredSlug(args, "create"),
|
|
10431
11150
|
cron: requiredCron(args, "create"),
|
|
10432
|
-
method:
|
|
11151
|
+
method: requiredText2(args, "method", "create"),
|
|
10433
11152
|
body: scheduleBody(args.body_file) ?? {},
|
|
10434
11153
|
headers: scheduleHeaders(args.header_env, environment) ?? {}
|
|
10435
11154
|
};
|
|
@@ -10445,24 +11164,32 @@ function safeMutationFields(request) {
|
|
|
10445
11164
|
return safeFields;
|
|
10446
11165
|
}
|
|
10447
11166
|
function requiredName(args, action) {
|
|
10448
|
-
const name =
|
|
11167
|
+
const name = requiredText2(args, "name", action);
|
|
10449
11168
|
if (name.length > MAX_SCHEDULE_NAME_LENGTH)
|
|
10450
11169
|
throw new Error(`'name' is too long for '${action}'`);
|
|
10451
11170
|
return name;
|
|
10452
11171
|
}
|
|
10453
11172
|
function requiredSlug(args, action) {
|
|
10454
|
-
const slug =
|
|
11173
|
+
const slug = requiredText2(args, "slug", action);
|
|
10455
11174
|
if (!SAFE_SLUG_PATTERN.test(slug))
|
|
10456
11175
|
throw new Error(`'slug' is invalid for '${action}'`);
|
|
10457
11176
|
return slug;
|
|
10458
11177
|
}
|
|
10459
11178
|
function requiredCron(args, action) {
|
|
10460
|
-
const cron =
|
|
11179
|
+
const cron = requiredText2(args, "cron", action);
|
|
10461
11180
|
if (!validScheduledFunctionCron(cron))
|
|
10462
11181
|
throw new Error(`'cron' is invalid for '${action}'`);
|
|
10463
11182
|
return cron;
|
|
10464
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
|
+
}
|
|
10465
11191
|
function updateRequest(args, environment) {
|
|
11192
|
+
const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "update");
|
|
10466
11193
|
const body = scheduleBody(args.body_file);
|
|
10467
11194
|
const headers = scheduleHeaders(args.header_env, environment);
|
|
10468
11195
|
const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
|
|
@@ -10478,14 +11205,21 @@ function updateRequest(args, environment) {
|
|
|
10478
11205
|
if (Object.keys(mutationFields).length === 0) {
|
|
10479
11206
|
throw new Error("Scheduled Function update requires at least one field");
|
|
10480
11207
|
}
|
|
10481
|
-
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)}`;
|
|
10482
11216
|
}
|
|
10483
11217
|
async function executeScheduleAction(http, environment, args, readOnly = false) {
|
|
10484
11218
|
const action = args.action;
|
|
10485
|
-
if (readOnly && action !== "list")
|
|
11219
|
+
if (readOnly && action !== "list" && action !== "get")
|
|
10486
11220
|
return readOnlyResult2();
|
|
10487
|
-
|
|
10488
|
-
const ref =
|
|
11221
|
+
assertActionArguments2(action, args);
|
|
11222
|
+
const ref = requiredText2(args, "ref", action);
|
|
10489
11223
|
if (action === "list")
|
|
10490
11224
|
return listResponse(ref, await http.get(schedulePath(ref)));
|
|
10491
11225
|
if (action === "create") {
|
|
@@ -10493,28 +11227,35 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
|
|
|
10493
11227
|
const requestId = request.request_id;
|
|
10494
11228
|
return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
|
|
10495
11229
|
}
|
|
10496
|
-
const scheduleId =
|
|
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));
|
|
10497
11234
|
if (action === "update") {
|
|
10498
11235
|
const request = updateRequest(args, environment);
|
|
10499
11236
|
const requestId = request.request_id;
|
|
11237
|
+
const expectedUpdatedAt2 = request.expected_updated_at;
|
|
10500
11238
|
return mutationResponse({
|
|
10501
11239
|
action,
|
|
10502
11240
|
ref,
|
|
10503
11241
|
scheduleId,
|
|
10504
11242
|
requestId,
|
|
11243
|
+
expectedUpdatedAt: expectedUpdatedAt2,
|
|
10505
11244
|
expectedFields: safeMutationFields(request)
|
|
10506
|
-
}, await http.patch(
|
|
11245
|
+
}, await http.patch(targetPath, request));
|
|
10507
11246
|
}
|
|
10508
|
-
|
|
11247
|
+
const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "delete");
|
|
11248
|
+
return deleteResponse(ref, scheduleId, expectedUpdatedAt, await http.delete(deletePath(targetPath, expectedUpdatedAt)));
|
|
10509
11249
|
}
|
|
10510
11250
|
function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
|
|
10511
11251
|
server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
|
|
10512
11252
|
}
|
|
10513
|
-
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";
|
|
10514
11254
|
var SCHEDULE_TOOL_SCHEMA = {
|
|
10515
|
-
action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
|
|
11255
|
+
action: withDescription(stringEnum(["list", "get", "create", "update", "delete"]), "Action"),
|
|
10516
11256
|
ref: withDescription(Type.String(), "Project ref"),
|
|
10517
|
-
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"),
|
|
10518
11259
|
name: optional(Type.String(), "[create/update] Display name"),
|
|
10519
11260
|
slug: optional(Type.String(), "[create] Edge Function slug"),
|
|
10520
11261
|
cron: optional(Type.String(), "[create/update] Five-field cron expression"),
|
|
@@ -10526,7 +11267,7 @@ var SCHEDULE_TOOL_SCHEMA = {
|
|
|
10526
11267
|
// package.json
|
|
10527
11268
|
var package_default = {
|
|
10528
11269
|
name: "@supacloud/cli",
|
|
10529
|
-
version: "0.
|
|
11270
|
+
version: "0.17.0",
|
|
10530
11271
|
description: "Project-scoped CLI for SupaCloud users",
|
|
10531
11272
|
type: "module",
|
|
10532
11273
|
main: "./dist/index.js",
|
|
@@ -10731,6 +11472,7 @@ EXAMPLES
|
|
|
10731
11472
|
${preferredCommand} frontend list --ref abc123
|
|
10732
11473
|
${preferredCommand} database query --sql "select now()"
|
|
10733
11474
|
${preferredCommand} database query --ref abc123 --file ./queries/vector-search.sql
|
|
11475
|
+
${preferredCommand} database migration_inventory --ref abc123
|
|
10734
11476
|
${preferredCommand} database push_migrations --ref abc123 --dir supabase/migrations --dry_run
|
|
10735
11477
|
${preferredCommand} supabase migration_new --name add_accounts
|
|
10736
11478
|
${preferredCommand} supabase db_diff --schema public --name add_accounts
|
|
@@ -10741,8 +11483,9 @@ EXAMPLES
|
|
|
10741
11483
|
${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
|
|
10742
11484
|
${preferredCommand} ai show_skill
|
|
10743
11485
|
${preferredCommand} ai install_skill --dry_run
|
|
10744
|
-
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
|
|
10745
|
-
${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
|
|
10746
11489
|
${preferredCommand} scheduled_functions list --ref abc123
|
|
10747
11490
|
${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
|
|
10748
11491
|
${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
|
|
@@ -10824,6 +11567,11 @@ function createCliTools(context, confirmProduction) {
|
|
|
10824
11567
|
})
|
|
10825
11568
|
};
|
|
10826
11569
|
}
|
|
11570
|
+
const storageContextCallback = tools.storage.callback;
|
|
11571
|
+
const storageHelpTool = captureTools((server) => registerStorageTools(server, {})).storage;
|
|
11572
|
+
if (storageHelpTool) {
|
|
11573
|
+
tools.storage = { schema: storageHelpTool.schema, callback: storageContextCallback };
|
|
11574
|
+
}
|
|
10827
11575
|
const branchContextCallback = tools.branch.callback;
|
|
10828
11576
|
const branchHelpTool = captureTools((server) => registerBranchTools(server, {}, {
|
|
10829
11577
|
readOnly: true
|