@supacloud/cli 0.30.0 → 0.32.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 +43 -0
- package/dist/index.js +967 -52
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6464,6 +6464,7 @@ var ACTION_POLICY = {
|
|
|
6464
6464
|
},
|
|
6465
6465
|
database: {
|
|
6466
6466
|
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"],
|
|
6467
|
+
local: ["lint_migrations", "lint"],
|
|
6467
6468
|
write: ["query", "apply_migration", "push_migrations", "baseline_migrations", "create_table_rls"]
|
|
6468
6469
|
},
|
|
6469
6470
|
supabase: {
|
|
@@ -6471,8 +6472,8 @@ var ACTION_POLICY = {
|
|
|
6471
6472
|
write: ["push"]
|
|
6472
6473
|
},
|
|
6473
6474
|
auth: {
|
|
6474
|
-
read: ["list_users", "get_user", "list_providers", "get_provider", "supported_providers", "get_settings", "get_config"],
|
|
6475
|
-
write: ["generate_link", "configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
|
|
6475
|
+
read: ["list_users", "get_user", "list_providers", "get_provider", "supported_providers", "get_settings", "get_config", "get_oauth_server"],
|
|
6476
|
+
write: ["generate_link", "configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config", "migrate_oauth_server"]
|
|
6476
6477
|
},
|
|
6477
6478
|
oauth_clients: {
|
|
6478
6479
|
read: ["list", "get"],
|
|
@@ -6986,12 +6987,643 @@ function releaseControlErrorResponse(payload) {
|
|
|
6986
6987
|
return { ...releaseControlResponse(payload), isError: true };
|
|
6987
6988
|
}
|
|
6988
6989
|
|
|
6990
|
+
// src/shared/tools/migration-risk.ts
|
|
6991
|
+
function startsEscapeString(sql, quoteIndex) {
|
|
6992
|
+
const prefixIndex = quoteIndex - 1;
|
|
6993
|
+
const prefix = sql[prefixIndex] || "";
|
|
6994
|
+
const preceding = sql[prefixIndex - 1] || "";
|
|
6995
|
+
return /[eE]/.test(prefix) && !/[A-Za-z0-9_$]/.test(preceding);
|
|
6996
|
+
}
|
|
6997
|
+
function dollarQuoteTagAt(sql, index) {
|
|
6998
|
+
const previous = sql[index - 1] || "";
|
|
6999
|
+
if (/[A-Za-z0-9_$]/.test(previous))
|
|
7000
|
+
return "";
|
|
7001
|
+
return sql.slice(index).match(/^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/)?.[0] || "";
|
|
7002
|
+
}
|
|
7003
|
+
function maskSingleQuotedString(sql, start) {
|
|
7004
|
+
const usesBackslashEscapes = startsEscapeString(sql, start);
|
|
7005
|
+
let end = start + 1;
|
|
7006
|
+
while (end < sql.length) {
|
|
7007
|
+
if (usesBackslashEscapes && sql[end] === "\\" && sql[end + 1]) {
|
|
7008
|
+
end += 2;
|
|
7009
|
+
} else if (sql[end] === "'" && sql[end + 1] === "'") {
|
|
7010
|
+
end += 2;
|
|
7011
|
+
} else if (sql[end] === "'") {
|
|
7012
|
+
end += 1;
|
|
7013
|
+
return { masked: " ".repeat(end - start), end };
|
|
7014
|
+
} else {
|
|
7015
|
+
end += 1;
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
throw new Error("Unterminated SQL single-quoted string");
|
|
7019
|
+
}
|
|
7020
|
+
function maskDoubleQuotedIdentifier(sql, start) {
|
|
7021
|
+
let end = start + 1;
|
|
7022
|
+
while (end < sql.length) {
|
|
7023
|
+
if (sql[end] === '"' && sql[end + 1] === '"') {
|
|
7024
|
+
end += 2;
|
|
7025
|
+
} else if (sql[end] === '"') {
|
|
7026
|
+
end += 1;
|
|
7027
|
+
return { masked: `"${"_".repeat(Math.max(0, end - start - 2))}"`, end };
|
|
7028
|
+
} else {
|
|
7029
|
+
end += 1;
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
throw new Error("Unterminated SQL double-quoted identifier");
|
|
7033
|
+
}
|
|
7034
|
+
function preserveDoubleQuotedIdentifier(sql, start) {
|
|
7035
|
+
const identifier = maskDoubleQuotedIdentifier(sql, start);
|
|
7036
|
+
return { masked: sql.slice(start, identifier.end), end: identifier.end };
|
|
7037
|
+
}
|
|
7038
|
+
function lineCommentEnd(sql, start) {
|
|
7039
|
+
const end = sql.indexOf(`
|
|
7040
|
+
`, start + 2);
|
|
7041
|
+
return end === -1 ? sql.length : end;
|
|
7042
|
+
}
|
|
7043
|
+
function blockCommentEnd(sql, start) {
|
|
7044
|
+
let depth = 1;
|
|
7045
|
+
let cursor = start + 2;
|
|
7046
|
+
while (cursor < sql.length && depth > 0) {
|
|
7047
|
+
if (sql.startsWith("/*", cursor)) {
|
|
7048
|
+
depth++;
|
|
7049
|
+
cursor += 2;
|
|
7050
|
+
} else if (sql.startsWith("*/", cursor)) {
|
|
7051
|
+
depth--;
|
|
7052
|
+
cursor += 2;
|
|
7053
|
+
} else {
|
|
7054
|
+
cursor++;
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
if (depth > 0)
|
|
7058
|
+
throw new Error("Unterminated SQL block comment");
|
|
7059
|
+
return cursor;
|
|
7060
|
+
}
|
|
7061
|
+
function maskDollarQuotedString(sql, start) {
|
|
7062
|
+
const tag = dollarQuoteTagAt(sql, start);
|
|
7063
|
+
if (!tag)
|
|
7064
|
+
return null;
|
|
7065
|
+
const closingTag = sql.indexOf(tag, start + tag.length);
|
|
7066
|
+
if (closingTag === -1)
|
|
7067
|
+
throw new Error("Unterminated SQL dollar-quoted body");
|
|
7068
|
+
const end = closingTag + tag.length;
|
|
7069
|
+
return { masked: " ".repeat(end - start), end };
|
|
7070
|
+
}
|
|
7071
|
+
function maskSqlSpan(sql, start, quotedIdentifier) {
|
|
7072
|
+
if (sql.startsWith("--", start)) {
|
|
7073
|
+
const end = lineCommentEnd(sql, start);
|
|
7074
|
+
return { masked: " ".repeat(end - start), end };
|
|
7075
|
+
}
|
|
7076
|
+
if (sql.startsWith("/*", start)) {
|
|
7077
|
+
const end = blockCommentEnd(sql, start);
|
|
7078
|
+
return { masked: " ".repeat(end - start), end };
|
|
7079
|
+
}
|
|
7080
|
+
if (sql[start] === "'")
|
|
7081
|
+
return maskSingleQuotedString(sql, start);
|
|
7082
|
+
if (sql[start] === '"')
|
|
7083
|
+
return quotedIdentifier(sql, start);
|
|
7084
|
+
if (sql[start] === "$")
|
|
7085
|
+
return maskDollarQuotedString(sql, start);
|
|
7086
|
+
return null;
|
|
7087
|
+
}
|
|
7088
|
+
function maskSql(sql, quotedIdentifier) {
|
|
7089
|
+
let masked = "";
|
|
7090
|
+
let cursor = 0;
|
|
7091
|
+
while (cursor < sql.length) {
|
|
7092
|
+
const protectedSpan = maskSqlSpan(sql, cursor, quotedIdentifier);
|
|
7093
|
+
if (protectedSpan) {
|
|
7094
|
+
masked += protectedSpan.masked;
|
|
7095
|
+
cursor = protectedSpan.end;
|
|
7096
|
+
} else {
|
|
7097
|
+
masked += sql[cursor];
|
|
7098
|
+
cursor++;
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
return masked;
|
|
7102
|
+
}
|
|
7103
|
+
function maskSqlNoise(sql) {
|
|
7104
|
+
return maskSql(sql, maskDoubleQuotedIdentifier);
|
|
7105
|
+
}
|
|
7106
|
+
function maskSqlPolicyNoise(sql) {
|
|
7107
|
+
return maskSql(sql, preserveDoubleQuotedIdentifier);
|
|
7108
|
+
}
|
|
7109
|
+
function splitSqlStatements(sql) {
|
|
7110
|
+
const masked = maskSqlNoise(sql);
|
|
7111
|
+
const statements = [];
|
|
7112
|
+
let lastIndex = 0;
|
|
7113
|
+
let cursor = 0;
|
|
7114
|
+
while (cursor < masked.length) {
|
|
7115
|
+
if (masked[cursor] === ";") {
|
|
7116
|
+
const rawStatement = sql.slice(lastIndex, cursor).trim();
|
|
7117
|
+
if (rawStatement.length > 0) {
|
|
7118
|
+
statements.push(rawStatement);
|
|
7119
|
+
}
|
|
7120
|
+
lastIndex = cursor + 1;
|
|
7121
|
+
}
|
|
7122
|
+
cursor++;
|
|
7123
|
+
}
|
|
7124
|
+
const trailing = sql.slice(lastIndex).trim();
|
|
7125
|
+
if (trailing.length > 0) {
|
|
7126
|
+
statements.push(trailing);
|
|
7127
|
+
}
|
|
7128
|
+
return statements;
|
|
7129
|
+
}
|
|
7130
|
+
function normalizedTransactionStatement(statement) {
|
|
7131
|
+
return maskSqlNoise(statement).replace(/\s+/g, " ").trim();
|
|
7132
|
+
}
|
|
7133
|
+
function migrationExecutionStatements(statements) {
|
|
7134
|
+
if (statements.length < 2)
|
|
7135
|
+
return [...statements];
|
|
7136
|
+
const first = normalizedTransactionStatement(statements[0]);
|
|
7137
|
+
const last = normalizedTransactionStatement(statements[statements.length - 1]);
|
|
7138
|
+
const hasOuterTransaction = /^(?:BEGIN(?:\s+(?:WORK|TRANSACTION))?|START\s+TRANSACTION)$/i.test(first) && /^(?:COMMIT|END)(?:\s+(?:WORK|TRANSACTION))?$/i.test(last);
|
|
7139
|
+
return hasOuterTransaction ? statements.slice(1, -1) : [...statements];
|
|
7140
|
+
}
|
|
7141
|
+
function splitTopLevelClauses(sql) {
|
|
7142
|
+
const masked = maskSqlNoise(sql);
|
|
7143
|
+
const clauses = [];
|
|
7144
|
+
let parenthesisDepth = 0;
|
|
7145
|
+
let bracketDepth = 0;
|
|
7146
|
+
let clauseStart = 0;
|
|
7147
|
+
for (let cursor = 0;cursor < masked.length; cursor += 1) {
|
|
7148
|
+
const character = masked[cursor];
|
|
7149
|
+
if (character === "(")
|
|
7150
|
+
parenthesisDepth += 1;
|
|
7151
|
+
else if (character === ")")
|
|
7152
|
+
parenthesisDepth -= 1;
|
|
7153
|
+
else if (character === "[")
|
|
7154
|
+
bracketDepth += 1;
|
|
7155
|
+
else if (character === "]")
|
|
7156
|
+
bracketDepth -= 1;
|
|
7157
|
+
if (parenthesisDepth < 0 || bracketDepth < 0)
|
|
7158
|
+
throw new Error("Unbalanced SQL delimiters");
|
|
7159
|
+
if (character === "," && parenthesisDepth === 0 && bracketDepth === 0) {
|
|
7160
|
+
clauses.push(sql.slice(clauseStart, cursor).trim());
|
|
7161
|
+
clauseStart = cursor + 1;
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
if (parenthesisDepth !== 0 || bracketDepth !== 0)
|
|
7165
|
+
throw new Error("Unbalanced SQL delimiters");
|
|
7166
|
+
clauses.push(sql.slice(clauseStart).trim());
|
|
7167
|
+
return clauses.filter(Boolean);
|
|
7168
|
+
}
|
|
7169
|
+
var RISK_RULES = [
|
|
7170
|
+
{
|
|
7171
|
+
type: "destructive_drop_database",
|
|
7172
|
+
level: "HIGH",
|
|
7173
|
+
pattern: /\bDROP\s+DATABASE\b/i,
|
|
7174
|
+
description: "Drops entire database.",
|
|
7175
|
+
recommendation: "Never drop databases in automated migrations.",
|
|
7176
|
+
blocksTransactionalPush: true
|
|
7177
|
+
},
|
|
7178
|
+
{
|
|
7179
|
+
type: "destructive_drop_schema",
|
|
7180
|
+
level: "HIGH",
|
|
7181
|
+
pattern: /\bDROP\s+SCHEMA\b/i,
|
|
7182
|
+
description: "Drops entire schema and all contained objects.",
|
|
7183
|
+
recommendation: "Ensure schema objects are migrated or deleted in contract phase before dropping schema."
|
|
7184
|
+
},
|
|
7185
|
+
{
|
|
7186
|
+
type: "destructive_drop_table",
|
|
7187
|
+
level: "HIGH",
|
|
7188
|
+
pattern: /\bDROP\s+TABLE\b/i,
|
|
7189
|
+
description: "Drops table and permanently removes data. Old code referencing this table will break.",
|
|
7190
|
+
recommendation: "Follow Expand-Contract: Deprecate usage in application code before dropping table."
|
|
7191
|
+
},
|
|
7192
|
+
{
|
|
7193
|
+
type: "destructive_drop_column",
|
|
7194
|
+
level: "HIGH",
|
|
7195
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bDROP\s+(?:COLUMN\s+)?[a-z0-9_"]+/i,
|
|
7196
|
+
excludePattern: /\bDROP\s+CONSTRAINT\b|\bALTER\s+(?:COLUMN\s+)?[a-z0-9_"]+\s+DROP\s+(?:DEFAULT|NOT\s+NULL|IDENTITY|EXPRESSION)\b/i,
|
|
7197
|
+
description: "Drops column. Running application code selecting or writing this column will fail immediately.",
|
|
7198
|
+
recommendation: "Follow Expand-Contract: Ensure all running application versions have stopped accessing this column before dropping."
|
|
7199
|
+
},
|
|
7200
|
+
{
|
|
7201
|
+
type: "destructive_drop_constraint",
|
|
7202
|
+
level: "HIGH",
|
|
7203
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bDROP\s+CONSTRAINT\b/i,
|
|
7204
|
+
description: "Drops constraint. Dropping constraints may break uniqueness guarantees or foreign key integrity.",
|
|
7205
|
+
recommendation: "Ensure application code and data no longer depend on this constraint before dropping."
|
|
7206
|
+
},
|
|
7207
|
+
{
|
|
7208
|
+
type: "destructive_drop_view",
|
|
7209
|
+
level: "HIGH",
|
|
7210
|
+
pattern: /\bDROP\s+(?:MATERIALIZED\s+)?VIEW\b/i,
|
|
7211
|
+
description: "Drops view or materialized view. Queries or API endpoints selecting from this view will break.",
|
|
7212
|
+
recommendation: "Follow Expand-Contract: Deprecate client/API dependencies on this view before dropping."
|
|
7213
|
+
},
|
|
7214
|
+
{
|
|
7215
|
+
type: "destructive_truncate",
|
|
7216
|
+
level: "HIGH",
|
|
7217
|
+
pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i,
|
|
7218
|
+
description: "Truncates table data permanently.",
|
|
7219
|
+
recommendation: "Avoid TRUNCATE in schema migrations; use soft deletes or targeted archived data cleanup."
|
|
7220
|
+
},
|
|
7221
|
+
{
|
|
7222
|
+
type: "destructive_rename_column",
|
|
7223
|
+
level: "HIGH",
|
|
7224
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bRENAME\s+(?:COLUMN\s+)?[a-z0-9_"]+\s+TO\b/i,
|
|
7225
|
+
description: "Renames column. Causes immediate downtime for old application code expecting the previous column name.",
|
|
7226
|
+
recommendation: "Follow Expand-Contract: Add new column, dual-write, backfill, migrate reads, then drop old column."
|
|
7227
|
+
},
|
|
7228
|
+
{
|
|
7229
|
+
type: "destructive_rename_table",
|
|
7230
|
+
level: "HIGH",
|
|
7231
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bRENAME\s+TO\b/i,
|
|
7232
|
+
description: "Renames table. Causes immediate downtime for application code.",
|
|
7233
|
+
recommendation: "Follow Expand-Contract: Create a view or alias table during transition."
|
|
7234
|
+
},
|
|
7235
|
+
{
|
|
7236
|
+
type: "manual_review_do_block",
|
|
7237
|
+
level: "HIGH",
|
|
7238
|
+
pattern: /^\s*DO\b/i,
|
|
7239
|
+
description: "DO blocks can execute dynamic or procedural DDL that static rules cannot inspect safely.",
|
|
7240
|
+
recommendation: "Move schema changes into explicit SQL statements; push_migrations rejects opaque procedural SQL.",
|
|
7241
|
+
blocksTransactionalPush: true
|
|
7242
|
+
},
|
|
7243
|
+
{
|
|
7244
|
+
type: "manual_review_procedural_definition",
|
|
7245
|
+
level: "HIGH",
|
|
7246
|
+
pattern: /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\b/i,
|
|
7247
|
+
description: "Defines procedural SQL whose body can hide data, privilege, or schema side effects from static rules.",
|
|
7248
|
+
recommendation: "Review the complete function or procedure body and require strict migration approval."
|
|
7249
|
+
},
|
|
7250
|
+
{
|
|
7251
|
+
type: "manual_review_procedure_call",
|
|
7252
|
+
level: "HIGH",
|
|
7253
|
+
pattern: /^\s*CALL\b/i,
|
|
7254
|
+
description: "Calls a stored procedure whose side effects cannot be determined statically.",
|
|
7255
|
+
recommendation: "Replace the call with explicit migration SQL or require strict manual approval."
|
|
7256
|
+
},
|
|
7257
|
+
{
|
|
7258
|
+
type: "locking_vacuum_full",
|
|
7259
|
+
level: "HIGH",
|
|
7260
|
+
pattern: /^\s*VACUUM\b/i,
|
|
7261
|
+
description: "VACUUM cannot run inside the transactional migration executor; VACUUM FULL also locks and rewrites the table.",
|
|
7262
|
+
recommendation: "Run VACUUM through an approved non-transactional maintenance path; do not place it in push_migrations files.",
|
|
7263
|
+
blocksTransactionalPush: true
|
|
7264
|
+
},
|
|
7265
|
+
{
|
|
7266
|
+
type: "locking_cluster",
|
|
7267
|
+
level: "HIGH",
|
|
7268
|
+
pattern: /^\s*CLUSTER\s+[a-z0-9_"]+/i,
|
|
7269
|
+
description: "CLUSTER acquires an ACCESS EXCLUSIVE lock on the table, blocking all concurrent access.",
|
|
7270
|
+
recommendation: "Avoid CLUSTER on active production tables; consider pg_repack for online table reordering."
|
|
7271
|
+
},
|
|
7272
|
+
{
|
|
7273
|
+
type: "unsupported_concurrent_index_migration",
|
|
7274
|
+
level: "HIGH",
|
|
7275
|
+
pattern: /\b(?:CREATE\s+(?:UNIQUE\s+)?INDEX|DROP\s+INDEX|REINDEX\b[\s\S]*?)\s+CONCURRENTLY\b/i,
|
|
7276
|
+
description: "CONCURRENTLY operations cannot run inside the transactional migration executor.",
|
|
7277
|
+
recommendation: "Run the concurrent index operation through an approved non-transactional maintenance path, outside push_migrations.",
|
|
7278
|
+
blocksTransactionalPush: true
|
|
7279
|
+
},
|
|
7280
|
+
{
|
|
7281
|
+
type: "unsupported_non_transactional_migration",
|
|
7282
|
+
level: "HIGH",
|
|
7283
|
+
pattern: /\b(?:CREATE\s+DATABASE|CREATE\s+TABLESPACE|DROP\s+TABLESPACE|CREATE\s+SUBSCRIPTION|DROP\s+SUBSCRIPTION|ALTER\s+SYSTEM)\b/i,
|
|
7284
|
+
description: "This operation cannot run inside the transactional, project-scoped migration executor.",
|
|
7285
|
+
recommendation: "Use an approved platform maintenance path instead of push_migrations.",
|
|
7286
|
+
blocksTransactionalPush: true
|
|
7287
|
+
},
|
|
7288
|
+
{
|
|
7289
|
+
type: "locking_non_concurrent_index",
|
|
7290
|
+
level: "MEDIUM",
|
|
7291
|
+
pattern: /\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+(?!CONCURRENTLY\b)/i,
|
|
7292
|
+
description: "Creates index non-concurrently. Acquires SHARE lock and blocks concurrent writes (INSERT/UPDATE/DELETE) on the table until index build completes.",
|
|
7293
|
+
recommendation: "For a live large table, use the approved non-transactional maintenance path for CREATE INDEX CONCURRENTLY; push_migrations cannot execute it."
|
|
7294
|
+
},
|
|
7295
|
+
{
|
|
7296
|
+
type: "locking_drop_index_non_concurrent",
|
|
7297
|
+
level: "MEDIUM",
|
|
7298
|
+
pattern: /\bDROP\s+INDEX\s+(?!CONCURRENTLY\b)/i,
|
|
7299
|
+
description: "Drops index non-concurrently. Acquires ACCESS EXCLUSIVE lock on the table, blocking concurrent reads and writes.",
|
|
7300
|
+
recommendation: "For a live table, use the approved non-transactional maintenance path for DROP INDEX CONCURRENTLY; push_migrations cannot execute it."
|
|
7301
|
+
},
|
|
7302
|
+
{
|
|
7303
|
+
type: "locking_alter_column_set_not_null",
|
|
7304
|
+
level: "MEDIUM",
|
|
7305
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bALTER\s+(?:COLUMN\s+)?[a-z0-9_"]+\s+SET\s+NOT\s+NULL\b/i,
|
|
7306
|
+
description: "Setting NOT NULL on an existing column scans the entire table under an ACCESS EXCLUSIVE lock unless a validated CHECK constraint already exists.",
|
|
7307
|
+
recommendation: "Add a CHECK (column IS NOT NULL) NOT VALID constraint first, validate it with VALIDATE CONSTRAINT, then SET NOT NULL."
|
|
7308
|
+
},
|
|
7309
|
+
{
|
|
7310
|
+
type: "locking_alter_type",
|
|
7311
|
+
level: "MEDIUM",
|
|
7312
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bALTER\s+(?:COLUMN\s+)?[a-z0-9_"]+\s+(?:SET\s+DATA\s+)?TYPE\b/i,
|
|
7313
|
+
description: "Altering column type acquires ACCESS EXCLUSIVE lock and may trigger a full table rewrite.",
|
|
7314
|
+
recommendation: "Add a new column with the target type, dual-write, copy data, switch reads, then drop old column."
|
|
7315
|
+
},
|
|
7316
|
+
{
|
|
7317
|
+
type: "locking_unique_constraint_without_index",
|
|
7318
|
+
level: "MEDIUM",
|
|
7319
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bADD\s+CONSTRAINT\b[\s\S]*?\bUNIQUE\b/i,
|
|
7320
|
+
excludePattern: /\bUNIQUE\s+USING\s+INDEX\b/i,
|
|
7321
|
+
description: "Adding UNIQUE constraint directly acquires an ACCESS EXCLUSIVE lock while creating the index.",
|
|
7322
|
+
recommendation: "Build the unique index through the approved non-transactional maintenance path, then attach it with ADD CONSTRAINT ... UNIQUE USING INDEX in a migration."
|
|
7323
|
+
},
|
|
7324
|
+
{
|
|
7325
|
+
type: "locking_foreign_key_without_not_valid",
|
|
7326
|
+
level: "MEDIUM",
|
|
7327
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bADD\s+CONSTRAINT\b[\s\S]*?\bFOREIGN\s+KEY\b(?!\s*[^;]*?\bNOT\s+VALID\b)/i,
|
|
7328
|
+
description: "Adding a FOREIGN KEY constraint without NOT VALID locks the table for a full scan to validate existing rows.",
|
|
7329
|
+
recommendation: "Add foreign key constraint with 'NOT VALID' first, then run 'ALTER TABLE ... VALIDATE CONSTRAINT' separately."
|
|
7330
|
+
},
|
|
7331
|
+
{
|
|
7332
|
+
type: "locking_check_constraint_without_not_valid",
|
|
7333
|
+
level: "MEDIUM",
|
|
7334
|
+
pattern: /\bALTER\s+TABLE\b[\s\S]*?\bADD\s+CONSTRAINT\b[\s\S]*?\bCHECK\b(?!\s*[^;]*?\bNOT\s+VALID\b)/i,
|
|
7335
|
+
description: "Adding a CHECK constraint without NOT VALID scans the entire table under an ACCESS EXCLUSIVE lock.",
|
|
7336
|
+
recommendation: "Add constraint with 'NOT VALID' first, then validate with 'ALTER TABLE ... VALIDATE CONSTRAINT'."
|
|
7337
|
+
}
|
|
7338
|
+
];
|
|
7339
|
+
var MIGRATION_LEDGER_RELATION = String.raw`(?:(?:"?(?:supabase_migrations|public)"?)\s*\.\s*)?"?schema_migrations"?`;
|
|
7340
|
+
var MIGRATION_LEDGER_RELATION_END = String.raw`(?=$|[\s,;(*])`;
|
|
7341
|
+
var MIGRATION_LEDGER_DDL_OR_MAINTENANCE_PREFIX = String.raw`(?:CREATE\s+(?:UNIQUE\s+)?INDEX\b[^;]*\bON\s+(?:ONLY\s+)?|CREATE\s+(?:CONSTRAINT\s+)?TRIGGER\b[^;]*\bON\s+|DROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|CREATE\s+RULE\b[^;]*\bTO\s+|DROP\s+RULE\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|CREATE\s+POLICY\b[^;]*\bON\s+|ALTER\s+POLICY\b[^;]*\bON\s+|DROP\s+POLICY\s+(?:IF\s+EXISTS\s+)?[^;]*\bON\s+|COMMENT\s+ON\s+TABLE\s+|SECURITY\s+LABEL(?:\s+FOR\s+[^;\s]+)?\s+ON\s+TABLE\s+|REINDEX(?:\s*\([^;)]*\))?\s+TABLE\s+(?:CONCURRENTLY\s+)?|CLUSTER(?:\s+VERBOSE)?\s+|VACUUM(?:\s*\([^;)]*\))?(?:\s+(?:FULL|FREEZE|VERBOSE|ANALYZE))*\s+|ANALYZE(?:\s*\([^;)]*\))?(?:\s+VERBOSE)?\s+)`;
|
|
7342
|
+
var MIGRATION_LEDGER_MODIFICATION_PATTERN = new RegExp(String.raw`\b(?:(?:CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?|INSERT\s+INTO\s+(?:ONLY\s+)?|UPDATE\s+(?:ONLY\s+)?|DELETE\s+FROM\s+(?:ONLY\s+)?|MERGE\s+INTO\s+(?:ONLY\s+)?|ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?|COPY\s+)` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`|(?:DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?|TRUNCATE(?:\s+TABLE)?\s+|LOCK\s+(?:TABLE\s+)?)[^;]*` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`|` + MIGRATION_LEDGER_DDL_OR_MAINTENANCE_PREFIX + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`)`, "i");
|
|
7343
|
+
var NON_TABLE_PRIVILEGE_TARGET = String.raw`(?:ALL\s+(?:FUNCTIONS|PROCEDURES|ROUTINES|SEQUENCES)\s+IN\s+SCHEMA|DATABASE|DOMAIN|FOREIGN\s+DATA\s+WRAPPER|FOREIGN\s+SERVER|FUNCTION|LANGUAGE|LARGE\s+OBJECT|PARAMETER|PROCEDURE|ROUTINE|SCHEMA|SEQUENCE|TABLESPACE|TYPE)\b`;
|
|
7344
|
+
var MIGRATION_LEDGER_PRIVILEGE_PATTERN = new RegExp(String.raw`\b(?:GRANT|REVOKE)\b[^;]*\bON\s+(?:(?:TABLE\s+)?(?!` + NON_TABLE_PRIVILEGE_TARGET + String.raw`)(?:(?!\b(?:TO|FROM)\b)[^;])*?` + MIGRATION_LEDGER_RELATION + MIGRATION_LEDGER_RELATION_END + String.raw`(?=[^;]*\b(?:TO|FROM)\b)|ALL\s+TABLES\s+IN\s+SCHEMA\s+"?(?:supabase_migrations|public)"?(?=$|[\s,;]))`, "i");
|
|
7345
|
+
var PUSH_BLOCKER_RULES = [
|
|
7346
|
+
{
|
|
7347
|
+
type: "unsupported_project_scope_management",
|
|
7348
|
+
level: "HIGH",
|
|
7349
|
+
pattern: /\b(?:ALTER\s+DATABASE|DROP\s+SCHEMA\s+(?:IF\s+EXISTS\s+)?(?:[^;]*,\s*)?public(?=\s*(?:,|CASCADE\b|RESTRICT\b|$))|(?:DROP|REASSIGN)\s+OWNED\b|(?:CREATE|ALTER|DROP)\s+(?:ROLE|USER)\b|ALTER\s+SUBSCRIPTION\b)\b/i,
|
|
7350
|
+
description: "Attempts cluster-wide or platform-owned database management outside the project migration boundary.",
|
|
7351
|
+
recommendation: "Use an approved platform administration path instead of push_migrations.",
|
|
7352
|
+
blocksTransactionalPush: true
|
|
7353
|
+
},
|
|
7354
|
+
{
|
|
7355
|
+
type: "unsupported_platform_schema_management",
|
|
7356
|
+
level: "HIGH",
|
|
7357
|
+
pattern: /\b(?:ALTER\s+SCHEMA\s+"?public"?|(?:CREATE\s+SCHEMA\s+(?:IF\s+NOT\s+EXISTS\s+)?|ALTER\s+SCHEMA\s+)"?supabase_migrations"?|DROP\s+SCHEMA\s+(?:IF\s+EXISTS\s+)?(?:[^;]*,\s*)?"?supabase_migrations"?(?=\s*(?:,|CASCADE\b|RESTRICT\b|$)))\b/i,
|
|
7358
|
+
description: "Attempts to rename or re-own the public API schema, or to manage the platform migration schema.",
|
|
7359
|
+
recommendation: "Keep public and supabase_migrations under platform ownership; change project-owned schemas instead.",
|
|
7360
|
+
blocksTransactionalPush: true
|
|
7361
|
+
},
|
|
7362
|
+
{
|
|
7363
|
+
type: "unsupported_unicode_escaped_identifier",
|
|
7364
|
+
level: "HIGH",
|
|
7365
|
+
pattern: /\bU&"/i,
|
|
7366
|
+
description: "Uses a Unicode-escaped identifier that cannot be safely canonicalized by the migration policy.",
|
|
7367
|
+
recommendation: "Use a plain or directly quoted PostgreSQL identifier in controlled migrations.",
|
|
7368
|
+
blocksTransactionalPush: true
|
|
7369
|
+
},
|
|
7370
|
+
{
|
|
7371
|
+
type: "unsupported_server_access",
|
|
7372
|
+
level: "HIGH",
|
|
7373
|
+
pattern: /^\s*COPY\b|\b(?:(?:lo_import|lo_export|pg_read_file|pg_write_file|pg_ls_dir|pg_stat_file|pg_execute_server_program)\s*\(|pg_(?:terminate|cancel)_backend\s*\(|LOAD\b)/i,
|
|
7374
|
+
description: "Attempts server file, process, backend, or dynamic-library access from a project migration.",
|
|
7375
|
+
recommendation: "Run host-level maintenance through an approved platform administration path.",
|
|
7376
|
+
blocksTransactionalPush: true
|
|
7377
|
+
},
|
|
7378
|
+
{
|
|
7379
|
+
type: "unsupported_external_database_access",
|
|
7380
|
+
level: "HIGH",
|
|
7381
|
+
pattern: /\bdblink(?:_[a-z_]+)?\s*\(|\b(?:CREATE|ALTER|DROP)\s+(?:SERVER|USER\s+MAPPING|FOREIGN\s+DATA\s+WRAPPER)\b|\bIMPORT\s+FOREIGN\s+SCHEMA\b|\bCREATE\s+FOREIGN\s+TABLE\b/i,
|
|
7382
|
+
description: "Attempts external database or foreign-data-wrapper access outside the project migration boundary.",
|
|
7383
|
+
recommendation: "Provision external connectivity through an approved platform administration path.",
|
|
7384
|
+
blocksTransactionalPush: true
|
|
7385
|
+
},
|
|
7386
|
+
{
|
|
7387
|
+
type: "unsupported_transaction_control",
|
|
7388
|
+
level: "HIGH",
|
|
7389
|
+
pattern: /^\s*(?:BEGIN|START\s+TRANSACTION|COMMIT|END|ROLLBACK|ABORT|SAVEPOINT|RELEASE\s+SAVEPOINT|PREPARE\s+TRANSACTION)\b/i,
|
|
7390
|
+
description: "Contains transaction control inside a migration whose atomic transaction is owned by the platform.",
|
|
7391
|
+
recommendation: "Remove internal transaction control; one matching outer BEGIN/COMMIT wrapper is stripped automatically.",
|
|
7392
|
+
blocksTransactionalPush: true
|
|
7393
|
+
},
|
|
7394
|
+
{
|
|
7395
|
+
type: "unsupported_session_control",
|
|
7396
|
+
level: "HIGH",
|
|
7397
|
+
pattern: /\b(?:SET\s+(?:(?:LOCAL|SESSION)\s+)?(?:ROLE|SESSION\s+AUTHORIZATION)|RESET\s+(?:ROLE|SESSION\s+AUTHORIZATION)|DISCARD\s+ALL)\b/i,
|
|
7398
|
+
description: "Attempts to change the database session identity or discard platform-managed session state.",
|
|
7399
|
+
recommendation: "Keep migrations within the delegated project role and remove session identity control.",
|
|
7400
|
+
blocksTransactionalPush: true
|
|
7401
|
+
},
|
|
7402
|
+
{
|
|
7403
|
+
type: "unsupported_advisory_lock_control",
|
|
7404
|
+
level: "HIGH",
|
|
7405
|
+
pattern: /\bpg_(?:try_)?advisory_(?:xact_)?(?:lock|unlock)(?:_shared|_all)?\s*\(/i,
|
|
7406
|
+
description: "Attempts advisory lock control that can conflict with the platform migration lock.",
|
|
7407
|
+
recommendation: "Remove custom advisory lock operations; push_migrations already serializes project migrations.",
|
|
7408
|
+
blocksTransactionalPush: true
|
|
7409
|
+
}
|
|
7410
|
+
];
|
|
7411
|
+
var QUOTED_FUNCTION_BLOCKER_RULES = [
|
|
7412
|
+
{
|
|
7413
|
+
type: "unsupported_server_access",
|
|
7414
|
+
level: "HIGH",
|
|
7415
|
+
pattern: /"(?:lo_import|lo_export|pg_read_file|pg_write_file|pg_ls_dir|pg_stat_file|pg_execute_server_program|pg_terminate_backend|pg_cancel_backend)"\s*\(/,
|
|
7416
|
+
description: "Attempts server file, process, backend, or dynamic-library access from a project migration.",
|
|
7417
|
+
recommendation: "Run host-level maintenance through an approved platform administration path.",
|
|
7418
|
+
blocksTransactionalPush: true
|
|
7419
|
+
},
|
|
7420
|
+
{
|
|
7421
|
+
type: "unsupported_external_database_access",
|
|
7422
|
+
level: "HIGH",
|
|
7423
|
+
pattern: /"dblink(?:_[a-z_]+)?"\s*\(/,
|
|
7424
|
+
description: "Attempts external database or foreign-data-wrapper access outside the project migration boundary.",
|
|
7425
|
+
recommendation: "Provision external connectivity through an approved platform administration path.",
|
|
7426
|
+
blocksTransactionalPush: true
|
|
7427
|
+
},
|
|
7428
|
+
{
|
|
7429
|
+
type: "unsupported_advisory_lock_control",
|
|
7430
|
+
level: "HIGH",
|
|
7431
|
+
pattern: /"pg_(?:try_)?advisory_(?:xact_)?(?:lock|unlock)(?:_shared|_all)?"\s*\(/,
|
|
7432
|
+
description: "Attempts advisory lock control that can conflict with the platform migration lock.",
|
|
7433
|
+
recommendation: "Remove custom advisory lock operations; push_migrations already serializes project migrations.",
|
|
7434
|
+
blocksTransactionalPush: true
|
|
7435
|
+
}
|
|
7436
|
+
];
|
|
7437
|
+
var MIGRATION_LEDGER_BLOCKER_RULES = [
|
|
7438
|
+
{
|
|
7439
|
+
type: "unsupported_public_schema_removal",
|
|
7440
|
+
level: "HIGH",
|
|
7441
|
+
pattern: /\bDROP\s+SCHEMA\s+(?:IF\s+EXISTS\s+)?(?:[^;]*,\s*)?"?public"?(?=\s*(?:,|CASCADE\b|RESTRICT\b|$))/i,
|
|
7442
|
+
description: "Attempts to remove the platform-required public schema.",
|
|
7443
|
+
recommendation: "Drop project objects individually through an approved contract migration; do not remove public.",
|
|
7444
|
+
blocksTransactionalPush: true
|
|
7445
|
+
},
|
|
7446
|
+
{
|
|
7447
|
+
type: "unsupported_migration_ledger_access",
|
|
7448
|
+
level: "HIGH",
|
|
7449
|
+
pattern: MIGRATION_LEDGER_MODIFICATION_PATTERN,
|
|
7450
|
+
description: "Attempts to modify or bypass the platform-owned migration ledger.",
|
|
7451
|
+
recommendation: "Let push_migrations record the ledger entry after the migration transaction commits.",
|
|
7452
|
+
blocksTransactionalPush: true
|
|
7453
|
+
},
|
|
7454
|
+
{
|
|
7455
|
+
type: "unsupported_migration_ledger_privileges",
|
|
7456
|
+
level: "HIGH",
|
|
7457
|
+
pattern: MIGRATION_LEDGER_PRIVILEGE_PATTERN,
|
|
7458
|
+
description: "Attempts to change privileges on the platform-owned migration ledger.",
|
|
7459
|
+
recommendation: "Keep migration ledger privileges under platform control.",
|
|
7460
|
+
blocksTransactionalPush: true
|
|
7461
|
+
},
|
|
7462
|
+
{
|
|
7463
|
+
type: "unsupported_migration_ledger_recorder_access",
|
|
7464
|
+
level: "HIGH",
|
|
7465
|
+
pattern: /"?supabase_migrations"?\s*\.\s*"?record_schema_migration"?\s*\(/i,
|
|
7466
|
+
description: "Attempts to call or redefine the platform migration ledger recorder.",
|
|
7467
|
+
recommendation: "Let push_migrations invoke the recorder with a platform-issued lease.",
|
|
7468
|
+
blocksTransactionalPush: true
|
|
7469
|
+
}
|
|
7470
|
+
];
|
|
7471
|
+
function matchingRisks(rawStatement, maskedStatement, rules) {
|
|
7472
|
+
return rules.filter((rule) => rule.pattern.test(maskedStatement) && !rule.excludePattern?.test(maskedStatement)).map((rule) => ({
|
|
7473
|
+
level: rule.level,
|
|
7474
|
+
type: rule.type,
|
|
7475
|
+
description: rule.description,
|
|
7476
|
+
recommendation: rule.recommendation,
|
|
7477
|
+
statementSnippet: rawStatement.replace(/\s+/g, " ").slice(0, 100),
|
|
7478
|
+
...rule.blocksTransactionalPush ? { blocksTransactionalPush: true } : {}
|
|
7479
|
+
}));
|
|
7480
|
+
}
|
|
7481
|
+
function defaultExpression(maskedClause) {
|
|
7482
|
+
const defaultIndex = maskedClause.search(/\bDEFAULT\b/i);
|
|
7483
|
+
if (defaultIndex === -1)
|
|
7484
|
+
return "";
|
|
7485
|
+
const expression = maskedClause.slice(defaultIndex + "DEFAULT".length);
|
|
7486
|
+
const constraintIndex = expression.search(/\b(?:COLLATE|CONSTRAINT|GENERATED|NOT\s+NULL|NULL|PRIMARY\s+KEY|REFERENCES|UNIQUE|CHECK)\b/i);
|
|
7487
|
+
return (constraintIndex === -1 ? expression : expression.slice(0, constraintIndex)).trim();
|
|
7488
|
+
}
|
|
7489
|
+
function defaultMayRequireTableRewrite(maskedClause) {
|
|
7490
|
+
const expression = defaultExpression(maskedClause);
|
|
7491
|
+
if (!expression)
|
|
7492
|
+
return false;
|
|
7493
|
+
const withoutKnownStableCalls = expression.replace(/\b(?:now|transaction_timestamp|statement_timestamp)\s*\(\s*\)/gi, "");
|
|
7494
|
+
return /\b[A-Za-z_][A-Za-z0-9_$]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_$]*)?\s*\(/.test(withoutKnownStableCalls);
|
|
7495
|
+
}
|
|
7496
|
+
function addedNotNullColumnRisks(rawStatement) {
|
|
7497
|
+
const maskedStatement = maskSqlNoise(rawStatement);
|
|
7498
|
+
if (!/^\s*ALTER\s+TABLE\b/i.test(maskedStatement))
|
|
7499
|
+
return [];
|
|
7500
|
+
const risks = [];
|
|
7501
|
+
for (const clause of splitTopLevelClauses(rawStatement)) {
|
|
7502
|
+
const maskedClause = maskSqlNoise(clause);
|
|
7503
|
+
if (!/\bADD\s+(?!CONSTRAINT\b)(?:COLUMN\s+)?[a-z0-9_"]+\s+[\s\S]*\bNOT\s+NULL\b/i.test(maskedClause))
|
|
7504
|
+
continue;
|
|
7505
|
+
const snippet = clause.replace(/\s+/g, " ").slice(0, 100);
|
|
7506
|
+
if (!/\bDEFAULT\b/i.test(maskedClause)) {
|
|
7507
|
+
risks.push({
|
|
7508
|
+
level: "MEDIUM",
|
|
7509
|
+
type: "locking_not_null_no_default",
|
|
7510
|
+
description: "Adding a NOT NULL column without a DEFAULT value requires full table verification and fails when existing rows cannot satisfy it.",
|
|
7511
|
+
recommendation: "Follow Expand-Contract: add the column as nullable, backfill it, then add NOT NULL separately.",
|
|
7512
|
+
statementSnippet: snippet
|
|
7513
|
+
});
|
|
7514
|
+
} else if (defaultMayRequireTableRewrite(maskedClause)) {
|
|
7515
|
+
risks.push({
|
|
7516
|
+
level: "MEDIUM",
|
|
7517
|
+
type: "locking_not_null_expression_default",
|
|
7518
|
+
description: "Adding a NOT NULL column with a function-based DEFAULT may rewrite the table while holding a strong lock.",
|
|
7519
|
+
recommendation: "Use a literal or known stable default, or add the column nullable and backfill in a separate step.",
|
|
7520
|
+
statementSnippet: snippet
|
|
7521
|
+
});
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
return risks;
|
|
7525
|
+
}
|
|
7526
|
+
function analyzeMigrationSql(sql) {
|
|
7527
|
+
const statements = migrationExecutionStatements(splitSqlStatements(sql));
|
|
7528
|
+
if (statements.length === 0) {
|
|
7529
|
+
return [{
|
|
7530
|
+
level: "HIGH",
|
|
7531
|
+
type: "unsupported_empty_migration",
|
|
7532
|
+
description: "Contains no executable SQL after removing the platform-managed outer transaction wrapper.",
|
|
7533
|
+
recommendation: "Remove the empty migration file or add the intended project-scoped SQL statement.",
|
|
7534
|
+
statementSnippet: sql.replace(/\s+/g, " ").slice(0, 100),
|
|
7535
|
+
blocksTransactionalPush: true
|
|
7536
|
+
}];
|
|
7537
|
+
}
|
|
7538
|
+
const risks = [];
|
|
7539
|
+
for (const rawStatement of statements) {
|
|
7540
|
+
const masked = maskSqlNoise(rawStatement);
|
|
7541
|
+
const policyMasked = maskSqlPolicyNoise(rawStatement);
|
|
7542
|
+
risks.push(...matchingRisks(rawStatement, masked, RISK_RULES));
|
|
7543
|
+
risks.push(...addedNotNullColumnRisks(rawStatement));
|
|
7544
|
+
risks.push(...matchingRisks(rawStatement, masked, PUSH_BLOCKER_RULES));
|
|
7545
|
+
risks.push(...matchingRisks(rawStatement, policyMasked, QUOTED_FUNCTION_BLOCKER_RULES));
|
|
7546
|
+
risks.push(...matchingRisks(rawStatement, policyMasked, MIGRATION_LEDGER_BLOCKER_RULES));
|
|
7547
|
+
}
|
|
7548
|
+
return risks;
|
|
7549
|
+
}
|
|
7550
|
+
function analyzeMigrationFiles(files) {
|
|
7551
|
+
const fileRisks = [];
|
|
7552
|
+
let highCount = 0;
|
|
7553
|
+
let mediumCount = 0;
|
|
7554
|
+
let lowCount = 0;
|
|
7555
|
+
let transactionalPushBlockerCount = 0;
|
|
7556
|
+
for (const { file, sql } of files) {
|
|
7557
|
+
const risks = analyzeMigrationSql(sql);
|
|
7558
|
+
transactionalPushBlockerCount += risks.filter((risk) => risk.blocksTransactionalPush).length;
|
|
7559
|
+
let fileLevel = "LOW";
|
|
7560
|
+
for (const risk of risks) {
|
|
7561
|
+
if (risk.level === "HIGH") {
|
|
7562
|
+
fileLevel = "HIGH";
|
|
7563
|
+
highCount++;
|
|
7564
|
+
} else if (risk.level === "MEDIUM") {
|
|
7565
|
+
if (fileLevel !== "HIGH")
|
|
7566
|
+
fileLevel = "MEDIUM";
|
|
7567
|
+
mediumCount++;
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
if (fileLevel === "LOW") {
|
|
7571
|
+
lowCount++;
|
|
7572
|
+
}
|
|
7573
|
+
fileRisks.push({
|
|
7574
|
+
file,
|
|
7575
|
+
overallRisk: fileLevel,
|
|
7576
|
+
risks
|
|
7577
|
+
});
|
|
7578
|
+
}
|
|
7579
|
+
let overallRisk = "LOW";
|
|
7580
|
+
if (highCount > 0)
|
|
7581
|
+
overallRisk = "HIGH";
|
|
7582
|
+
else if (mediumCount > 0)
|
|
7583
|
+
overallRisk = "MEDIUM";
|
|
7584
|
+
return {
|
|
7585
|
+
overallRisk,
|
|
7586
|
+
highRiskCount: highCount,
|
|
7587
|
+
mediumRiskCount: mediumCount,
|
|
7588
|
+
lowRiskCount: lowCount,
|
|
7589
|
+
transactionalPushBlockerCount,
|
|
7590
|
+
files: fileRisks
|
|
7591
|
+
};
|
|
7592
|
+
}
|
|
7593
|
+
function formatMigrationRiskReport(analysis) {
|
|
7594
|
+
const lines = [];
|
|
7595
|
+
const riskIcon = analysis.overallRisk === "HIGH" ? "\uD83D\uDD34" : analysis.overallRisk === "MEDIUM" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
|
|
7596
|
+
lines.push(`${riskIcon} Migration Risk Level: ${analysis.overallRisk}`);
|
|
7597
|
+
lines.push(`Total issues detected: ${analysis.highRiskCount} High (Destructive), ${analysis.mediumRiskCount} Medium (Locking/Constraint)`);
|
|
7598
|
+
const filesWithRisks = analysis.files.filter((fileRisk) => fileRisk.risks.length > 0);
|
|
7599
|
+
if (!filesWithRisks.length) {
|
|
7600
|
+
lines.push("", "✅ All migrations follow safe, non-blocking Expand-Contract patterns.");
|
|
7601
|
+
return lines.join(`
|
|
7602
|
+
`);
|
|
7603
|
+
}
|
|
7604
|
+
lines.push("", "Detailed Findings:");
|
|
7605
|
+
for (const { file, overallRisk, risks } of filesWithRisks) {
|
|
7606
|
+
const fileIcon = overallRisk === "HIGH" ? "\uD83D\uDD34" : "\uD83D\uDFE1";
|
|
7607
|
+
lines.push(` ${fileIcon} ${file} [${overallRisk}]`);
|
|
7608
|
+
for (const risk of risks) {
|
|
7609
|
+
lines.push(` - [${risk.level}] ${risk.description}`);
|
|
7610
|
+
if (risk.statementSnippet) {
|
|
7611
|
+
lines.push(` Snippet: ${risk.statementSnippet}`);
|
|
7612
|
+
}
|
|
7613
|
+
lines.push(` \uD83D\uDCA1 Suggestion: ${risk.recommendation}`);
|
|
7614
|
+
}
|
|
7615
|
+
}
|
|
7616
|
+
return lines.join(`
|
|
7617
|
+
`);
|
|
7618
|
+
}
|
|
7619
|
+
|
|
6989
7620
|
// src/shared/tools/database-tools.ts
|
|
6990
7621
|
var MAX_MIGRATION_VERSION = 9223372036854775807n;
|
|
6991
7622
|
var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
|
|
6992
7623
|
var FALLBACK_MIGRATION_VERSION_RANGE = 1000000000000000000n;
|
|
6993
7624
|
var FALLBACK_MIGRATION_VERSION_LIMIT = FALLBACK_MIGRATION_VERSION_BASE + FALLBACK_MIGRATION_VERSION_RANGE;
|
|
6994
7625
|
var MAX_MIGRATION_INVENTORY_BYTES = 64 * 1024 * 1024;
|
|
7626
|
+
var GENERIC_MIGRATION_NAMES = new Set(["cli_push"]);
|
|
6995
7627
|
function isMigrationInventoryVersion(version) {
|
|
6996
7628
|
if (typeof version !== "string" || !/^\d{1,19}$/.test(version))
|
|
6997
7629
|
return false;
|
|
@@ -7155,10 +7787,37 @@ function migrationIdentity(row) {
|
|
|
7155
7787
|
if (!isMigrationInventoryVersion(migration.version) || !isMigrationInventoryName(migration.name)) {
|
|
7156
7788
|
throw new Error("Invalid remote migration identity");
|
|
7157
7789
|
}
|
|
7158
|
-
return {
|
|
7790
|
+
return {
|
|
7791
|
+
version: migration.version,
|
|
7792
|
+
name: migration.name !== null && GENERIC_MIGRATION_NAMES.has(migration.name) ? null : migration.name,
|
|
7793
|
+
statements: migration.statements
|
|
7794
|
+
};
|
|
7795
|
+
}
|
|
7796
|
+
function historicalMigrationVersion(version) {
|
|
7797
|
+
if (isMigrationInventoryVersion(version))
|
|
7798
|
+
return version;
|
|
7799
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version))
|
|
7800
|
+
return null;
|
|
7801
|
+
const normalized = String(version);
|
|
7802
|
+
return isMigrationInventoryVersion(normalized) ? normalized : null;
|
|
7159
7803
|
}
|
|
7160
|
-
function
|
|
7161
|
-
|
|
7804
|
+
function baselineMigrationIdentity(row) {
|
|
7805
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
7806
|
+
throw new Error("Invalid remote migration identity");
|
|
7807
|
+
}
|
|
7808
|
+
const migration = row;
|
|
7809
|
+
const version = historicalMigrationVersion(migration.version);
|
|
7810
|
+
if (!version || !isMigrationInventoryName(migration.name)) {
|
|
7811
|
+
throw new Error("Invalid remote migration identity");
|
|
7812
|
+
}
|
|
7813
|
+
return {
|
|
7814
|
+
version,
|
|
7815
|
+
name: migration.name !== null && GENERIC_MIGRATION_NAMES.has(migration.name) ? null : migration.name,
|
|
7816
|
+
statements: migration.statements
|
|
7817
|
+
};
|
|
7818
|
+
}
|
|
7819
|
+
function migrationIdentities(data, parseIdentity) {
|
|
7820
|
+
const identities = migrationRows(data).map(parseIdentity);
|
|
7162
7821
|
const versions = new Set;
|
|
7163
7822
|
const names = new Set;
|
|
7164
7823
|
for (const identity of identities) {
|
|
@@ -7201,15 +7860,21 @@ function migrationDisposition(local, remoteMigrations) {
|
|
|
7201
7860
|
if (sameVersion[0] && sameName[0] && sameVersion[0] !== sameName[0])
|
|
7202
7861
|
throw migrationIdentityConflict(local);
|
|
7203
7862
|
const remote = sameVersion[0] ?? sameName[0];
|
|
7204
|
-
const
|
|
7205
|
-
if (
|
|
7863
|
+
const versionIdentity = remote.version === local.version && (remote.name === local.name || remote.name === null);
|
|
7864
|
+
if (versionIdentity && exactIdentityIsApplied(local, remote))
|
|
7206
7865
|
return "applied";
|
|
7207
7866
|
if (!sameVersion.length && legacyIdentityIsApplied(local, remote))
|
|
7208
7867
|
return "applied";
|
|
7209
7868
|
throw migrationIdentityConflict(local);
|
|
7210
7869
|
}
|
|
7211
7870
|
function migrationPushPlan(data, migrationFiles) {
|
|
7212
|
-
|
|
7871
|
+
return migrationPlan(data, migrationFiles, migrationIdentity);
|
|
7872
|
+
}
|
|
7873
|
+
function migrationBaselinePlan(data, migrationFiles) {
|
|
7874
|
+
return migrationPlan(data, migrationFiles, baselineMigrationIdentity);
|
|
7875
|
+
}
|
|
7876
|
+
function migrationPlan(data, migrationFiles, parseIdentity) {
|
|
7877
|
+
const remoteMigrations = migrationIdentities(data, parseIdentity);
|
|
7213
7878
|
const plan = { alreadyApplied: [], pending: [] };
|
|
7214
7879
|
for (const migration of migrationFiles) {
|
|
7215
7880
|
const disposition = migrationDisposition(migration, remoteMigrations);
|
|
@@ -7303,6 +7968,13 @@ function confirmedSqlBatchReceipt(payload, expectedCommands) {
|
|
|
7303
7968
|
return true;
|
|
7304
7969
|
});
|
|
7305
7970
|
}
|
|
7971
|
+
function schemaReloadStatus(payload) {
|
|
7972
|
+
const receipt = recordPayload(payload);
|
|
7973
|
+
const schemaReload = recordPayload(receipt?.schema_reload);
|
|
7974
|
+
if (schemaReload?.ddl_committed !== true)
|
|
7975
|
+
return null;
|
|
7976
|
+
return schemaReload.status === "notified" || schemaReload.status === "notification_failed" ? schemaReload.status : null;
|
|
7977
|
+
}
|
|
7306
7978
|
function rlsStatementCommands(policyMode) {
|
|
7307
7979
|
return [
|
|
7308
7980
|
"CREATE",
|
|
@@ -7347,8 +8019,9 @@ function vectorWarningsForPendingMigrations(migrations, vectorEnabled) {
|
|
|
7347
8019
|
return warnings;
|
|
7348
8020
|
}
|
|
7349
8021
|
function registerDatabaseTools(server, http, config = {}) {
|
|
7350
|
-
const { readOnly = false, projectRef } = config;
|
|
7351
|
-
const
|
|
8022
|
+
const { localOnly = false, readOnly = false, projectRef } = config;
|
|
8023
|
+
const localActions = ["lint_migrations", "lint"];
|
|
8024
|
+
const readActions = [
|
|
7352
8025
|
"query",
|
|
7353
8026
|
"list_tables",
|
|
7354
8027
|
"describe_columns",
|
|
@@ -7368,16 +8041,21 @@ function registerDatabaseTools(server, http, config = {}) {
|
|
|
7368
8041
|
"generate_types"
|
|
7369
8042
|
];
|
|
7370
8043
|
const writeActions = ["apply_migration", "push_migrations", "baseline_migrations", "create_table_rls"];
|
|
7371
|
-
const
|
|
8044
|
+
const remoteActions = [...readActions, ...localActions];
|
|
8045
|
+
const allActions = localOnly ? localActions : readOnly ? remoteActions : [...remoteActions, ...writeActions];
|
|
7372
8046
|
server.tool("database", `Database operations: query, schema, RLS, migrations, stats.
|
|
7373
|
-
Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
8047
|
+
Actions: ${allActions.join(", ")}${localOnly ? " (local-only mode)" : readOnly ? " (read-only mode)" : ""}`, {
|
|
7374
8048
|
action: withDescription(stringEnum(allActions), "Action"),
|
|
7375
8049
|
ref: projectRef ? Type.Optional(Type.String()) : optional(Type.String(), "Project ref"),
|
|
7376
|
-
sql: optional(Type.String(), "[query/apply_migration] SQL statement"),
|
|
7377
|
-
file: optional(Type.String(), "[query/apply_migration] Read SQL from local file path (avoids shell escaping issues with $$ and multi-statement DDL)"),
|
|
7378
|
-
dir: optional(Type.String(), "[push_migrations/baseline_migrations] Directory containing .sql migration files (default: supabase/migrations)"),
|
|
8050
|
+
sql: optional(Type.String(), "[query/apply_migration/lint_migrations/lint] SQL statement"),
|
|
8051
|
+
file: optional(Type.String(), "[query/apply_migration/lint_migrations/lint] Read SQL from local file path (avoids shell escaping issues with $$ and multi-statement DDL)"),
|
|
8052
|
+
dir: optional(Type.String(), "[push_migrations/baseline_migrations/lint_migrations/lint] Directory containing .sql migration files (default: supabase/migrations)"),
|
|
7379
8053
|
dry_run: optional(Type.Boolean(), "[push_migrations/baseline_migrations] Preview changes without applying them"),
|
|
7380
|
-
|
|
8054
|
+
strict: optional(Type.Boolean(), "[push_migrations/lint_migrations/lint] Exit with error if high-risk destructive migrations are detected"),
|
|
8055
|
+
fail_on_high: optional(Type.Boolean(), "[push_migrations/lint_migrations/lint] Alias for --strict"),
|
|
8056
|
+
fail_on_medium: optional(Type.Boolean(), "[push_migrations/lint_migrations/lint] Exit with error if medium-risk or high-risk migrations are detected"),
|
|
8057
|
+
json: optional(Type.Boolean(), "[lint_migrations/lint] Output structured JSON analysis"),
|
|
8058
|
+
schema: optional(Type.String(), "[describe_columns/list_indexes/list_constraints/rls_status/rls_policies/create_table_rls] Schema name (default: public)"),
|
|
7381
8059
|
table: optional(Type.String(), "[describe_columns/indexes/constraints/rls_*] Table name"),
|
|
7382
8060
|
schemas: optional(Type.Array(Type.String()), "[list_tables/generate_types] Schemas array"),
|
|
7383
8061
|
user_id: optional(Type.String(), "[get_auth_user] User UUID"),
|
|
@@ -7388,17 +8066,25 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7388
8066
|
owner_column: optional(Type.String(), "[create_table_rls owner] UUID owner column matched to auth.uid()")
|
|
7389
8067
|
}, async (args) => {
|
|
7390
8068
|
const { action } = args;
|
|
8069
|
+
if (localOnly && action !== "lint_migrations" && action !== "lint") {
|
|
8070
|
+
throw new Error(`Database action '${String(action)}' requires Management API context`);
|
|
8071
|
+
}
|
|
8072
|
+
const managementHttp = () => {
|
|
8073
|
+
if (!http)
|
|
8074
|
+
throw new Error(`Database action '${String(action)}' requires Management API context`);
|
|
8075
|
+
return http;
|
|
8076
|
+
};
|
|
7391
8077
|
const ref = args.ref || projectRef;
|
|
7392
8078
|
const schema = args.schema || "public";
|
|
7393
8079
|
const schemas = args.schemas || ["public"];
|
|
7394
|
-
if (args.file && !args.sql) {
|
|
8080
|
+
if (args.file && !args.sql && action !== "lint_migrations" && action !== "lint") {
|
|
7395
8081
|
try {
|
|
7396
8082
|
args.sql = readFileSync2(args.file, "utf-8");
|
|
7397
8083
|
} catch (e) {
|
|
7398
8084
|
return { content: [{ type: "text", text: `❌ Failed to read file ${args.file}: ${e.message}` }] };
|
|
7399
8085
|
}
|
|
7400
8086
|
}
|
|
7401
|
-
const execSql = async (sql, mode) =>
|
|
8087
|
+
const execSql = async (sql, mode) => managementHttp().post(`/v1/projects/${ref}/database/sql`, mode ? { sql, mode } : { sql });
|
|
7402
8088
|
let text;
|
|
7403
8089
|
switch (action) {
|
|
7404
8090
|
case "query": {
|
|
@@ -7504,11 +8190,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7504
8190
|
break;
|
|
7505
8191
|
}
|
|
7506
8192
|
case "migration_inventory": {
|
|
7507
|
-
const response = await
|
|
8193
|
+
const response = await managementHttp().get(migrationInventoryPath(ref), { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
|
|
7508
8194
|
return migrationInventoryResponse(response);
|
|
7509
8195
|
}
|
|
7510
8196
|
case "project_url": {
|
|
7511
|
-
const r = await
|
|
8197
|
+
const r = await managementHttp().get(`/v1/projects/${ref}`);
|
|
7512
8198
|
text = r.ok ? JSON.stringify({ url: r.data.api?.url || `https://${ref}.supabase.co` }, null, 2) : `❌ Failed (${r.status})`;
|
|
7513
8199
|
break;
|
|
7514
8200
|
}
|
|
@@ -7518,10 +8204,49 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7518
8204
|
text = r.ok ? generateTypeScriptTypes(r.data, schemas) : `❌ Failed (${r.status})`;
|
|
7519
8205
|
break;
|
|
7520
8206
|
}
|
|
8207
|
+
case "lint_migrations":
|
|
8208
|
+
case "lint": {
|
|
8209
|
+
let migrationFiles = [];
|
|
8210
|
+
const explicitInputs = [args.sql, args.file, args.dir].filter((input) => typeof input === "string" && input.length > 0);
|
|
8211
|
+
if (explicitInputs.length > 1) {
|
|
8212
|
+
throw new Error("Use only one of --sql, --file, or --dir for migration lint");
|
|
8213
|
+
}
|
|
8214
|
+
if (args.sql) {
|
|
8215
|
+
migrationFiles = [{ file: "inline.sql", sql: args.sql }];
|
|
8216
|
+
} else if (args.file) {
|
|
8217
|
+
if (!existsSync2(args.file) || !statSync(args.file).isFile()) {
|
|
8218
|
+
throw new Error(`Migration file not found: ${args.file}`);
|
|
8219
|
+
}
|
|
8220
|
+
migrationFiles = [{ file: args.file, sql: readFileSync2(args.file, "utf8") }];
|
|
8221
|
+
} else {
|
|
8222
|
+
const dir = args.dir || "supabase/migrations";
|
|
8223
|
+
if (!existsSync2(dir) || !statSync(dir).isDirectory()) {
|
|
8224
|
+
throw new Error(`Migration directory not found: ${dir}`);
|
|
8225
|
+
}
|
|
8226
|
+
const files = readdirSync(dir).filter((file) => file.endsWith(".sql")).sort();
|
|
8227
|
+
if (!files.length) {
|
|
8228
|
+
text = `No .sql migration files found in ${dir}`;
|
|
8229
|
+
break;
|
|
8230
|
+
}
|
|
8231
|
+
migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
8232
|
+
}
|
|
8233
|
+
const analysis = analyzeMigrationFiles(migrationFiles);
|
|
8234
|
+
if (args.json) {
|
|
8235
|
+
text = JSON.stringify(analysis, null, 2);
|
|
8236
|
+
} else {
|
|
8237
|
+
text = formatMigrationRiskReport(analysis);
|
|
8238
|
+
}
|
|
8239
|
+
const strict = args.strict === true || args.fail_on_high === true;
|
|
8240
|
+
const failOnMedium = args.fail_on_medium === true;
|
|
8241
|
+
if (strict && analysis.highRiskCount > 0 || failOnMedium && (analysis.highRiskCount > 0 || analysis.mediumRiskCount > 0)) {
|
|
8242
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
8243
|
+
}
|
|
8244
|
+
break;
|
|
8245
|
+
}
|
|
7521
8246
|
case "apply_migration": {
|
|
7522
8247
|
if (!args.name || !args.sql)
|
|
7523
8248
|
throw new Error("'name' and 'sql' required");
|
|
7524
|
-
const r = await
|
|
8249
|
+
const r = await managementHttp().postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name: args.name, sql: args.sql });
|
|
7525
8250
|
if (!r.ok)
|
|
7526
8251
|
return releaseControlMutationFailure("database.apply_migration", r);
|
|
7527
8252
|
if (r.status !== 200) {
|
|
@@ -7544,14 +8269,18 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7544
8269
|
break;
|
|
7545
8270
|
}
|
|
7546
8271
|
const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
7547
|
-
const migrationsResult = await
|
|
8272
|
+
const migrationsResult = await managementHttp().get(`/v1/projects/${ref}/database/migrations`);
|
|
7548
8273
|
if (!migrationsResult.ok) {
|
|
7549
8274
|
return releaseControlFailure("database.push_migrations", "HTTP_ERROR", migrationsResult.transportError ? null : migrationsResult.status);
|
|
7550
8275
|
}
|
|
7551
|
-
const
|
|
8276
|
+
const migrationPlan2 = migrationPushPlan(migrationsResult.data, migrationFiles);
|
|
8277
|
+
const riskAnalysis = analyzeMigrationFiles(migrationPlan2.pending);
|
|
8278
|
+
const strict = args.strict === true || args.fail_on_high === true;
|
|
8279
|
+
const failOnMedium = args.fail_on_medium === true;
|
|
8280
|
+
const riskPolicyFailed = strict && riskAnalysis.highRiskCount > 0 || failOnMedium && (riskAnalysis.highRiskCount > 0 || riskAnalysis.mediumRiskCount > 0);
|
|
7552
8281
|
if (args.dry_run) {
|
|
7553
|
-
const pending2 =
|
|
7554
|
-
const alreadyApplied =
|
|
8282
|
+
const pending2 = migrationPlan2.pending;
|
|
8283
|
+
const alreadyApplied = migrationPlan2.alreadyApplied;
|
|
7555
8284
|
const pendingWithSql = pending2;
|
|
7556
8285
|
let vectorEnabled = null;
|
|
7557
8286
|
if (pendingWithSql.some(({ sql }) => sqlReferencesVector(sql) || sqlCreatesVectorExtension(sql))) {
|
|
@@ -7559,6 +8288,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7559
8288
|
vectorEnabled = extResult.ok ? extensionRows(extResult.data).some((row) => row.name === "vector" || row.extname === "vector") : null;
|
|
7560
8289
|
}
|
|
7561
8290
|
const warnings = vectorWarningsForPendingMigrations(pendingWithSql, vectorEnabled);
|
|
8291
|
+
const riskReport = formatMigrationRiskReport(riskAnalysis);
|
|
7562
8292
|
text = [
|
|
7563
8293
|
`Migration dry run for ${dir}`,
|
|
7564
8294
|
`Total: ${migrationFiles.length}`,
|
|
@@ -7568,12 +8298,43 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7568
8298
|
"",
|
|
7569
8299
|
"Already applied:",
|
|
7570
8300
|
...alreadyApplied.length ? alreadyApplied.map(({ file, version }) => ` - ${file} (${version})`) : [" - none"],
|
|
7571
|
-
...warnings.length ? ["", "Warnings:", ...warnings.map((warning) => ` - ${warning}`)] : []
|
|
8301
|
+
...warnings.length ? ["", "Warnings:", ...warnings.map((warning) => ` - ${warning}`)] : [],
|
|
8302
|
+
"",
|
|
8303
|
+
"Risk Assessment:",
|
|
8304
|
+
...riskReport.split(`
|
|
8305
|
+
`).map((line) => ` ${line}`)
|
|
7572
8306
|
].join(`
|
|
7573
8307
|
`);
|
|
7574
|
-
|
|
8308
|
+
return {
|
|
8309
|
+
content: [{ type: "text", text }],
|
|
8310
|
+
...riskPolicyFailed || riskAnalysis.transactionalPushBlockerCount > 0 ? { isError: true } : {}
|
|
8311
|
+
};
|
|
8312
|
+
}
|
|
8313
|
+
if (riskAnalysis.transactionalPushBlockerCount > 0) {
|
|
8314
|
+
const riskReport = formatMigrationRiskReport(riskAnalysis);
|
|
8315
|
+
return {
|
|
8316
|
+
content: [{
|
|
8317
|
+
type: "text",
|
|
8318
|
+
text: `❌ Migration push aborted because the transactional executor cannot run one or more statements:
|
|
8319
|
+
|
|
8320
|
+
${riskReport}`
|
|
8321
|
+
}],
|
|
8322
|
+
isError: true
|
|
8323
|
+
};
|
|
7575
8324
|
}
|
|
7576
|
-
|
|
8325
|
+
if (riskPolicyFailed) {
|
|
8326
|
+
const riskReport = formatMigrationRiskReport(riskAnalysis);
|
|
8327
|
+
return {
|
|
8328
|
+
content: [{
|
|
8329
|
+
type: "text",
|
|
8330
|
+
text: `❌ Migration push aborted due to strict risk policy:
|
|
8331
|
+
|
|
8332
|
+
${riskReport}`
|
|
8333
|
+
}],
|
|
8334
|
+
isError: true
|
|
8335
|
+
};
|
|
8336
|
+
}
|
|
8337
|
+
const pending = new Set(migrationPlan2.pending.map(({ file }) => file));
|
|
7577
8338
|
const applied = [];
|
|
7578
8339
|
const skipped = [];
|
|
7579
8340
|
for (const { file, name, version, sql } of migrationFiles) {
|
|
@@ -7581,7 +8342,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7581
8342
|
skipped.push(file);
|
|
7582
8343
|
continue;
|
|
7583
8344
|
}
|
|
7584
|
-
const r = await
|
|
8345
|
+
const r = await managementHttp().postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
|
|
7585
8346
|
if (r.ok && r.status === 200 && confirmedMigrationReceipt(r.data, { name, version, sql })) {
|
|
7586
8347
|
applied.push(file);
|
|
7587
8348
|
} else if (isAlreadyAppliedMigrationResponse(r, { name, version, sql })) {
|
|
@@ -7615,13 +8376,13 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7615
8376
|
break;
|
|
7616
8377
|
}
|
|
7617
8378
|
const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
7618
|
-
const r = await
|
|
8379
|
+
const r = await managementHttp().get(`/v1/projects/${ref}/database/migrations`);
|
|
7619
8380
|
if (!r.ok) {
|
|
7620
8381
|
return releaseControlFailure("database.baseline_migrations", "HTTP_ERROR", r.transportError ? null : r.status);
|
|
7621
8382
|
}
|
|
7622
|
-
const
|
|
7623
|
-
const missing =
|
|
7624
|
-
const alreadyApplied =
|
|
8383
|
+
const migrationPlan2 = migrationBaselinePlan(r.data, migrationFiles);
|
|
8384
|
+
const missing = migrationPlan2.pending;
|
|
8385
|
+
const alreadyApplied = migrationPlan2.alreadyApplied;
|
|
7625
8386
|
if (args.dry_run) {
|
|
7626
8387
|
text = [
|
|
7627
8388
|
`Migration baseline dry run for ${dir}`,
|
|
@@ -7644,7 +8405,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7644
8405
|
`);
|
|
7645
8406
|
break;
|
|
7646
8407
|
}
|
|
7647
|
-
const baselineResult = await
|
|
8408
|
+
const baselineResult = await managementHttp().postReleaseMutation(`/v1/projects/${ref}/database/migrations/baseline`, { migrations: missing.map(({ name, version }) => ({ name, version })) });
|
|
7648
8409
|
if (!baselineResult.ok) {
|
|
7649
8410
|
return releaseControlMutationFailure("database.baseline_migrations", baselineResult);
|
|
7650
8411
|
}
|
|
@@ -7655,7 +8416,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7655
8416
|
if (!receipt) {
|
|
7656
8417
|
return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", baselineResult.status);
|
|
7657
8418
|
}
|
|
7658
|
-
const inventoryResult = await
|
|
8419
|
+
const inventoryResult = await managementHttp().get(`/v1/projects/${ref}/database/migrations`, { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
|
|
7659
8420
|
if (!inventoryResult.ok || !baselineInventoryIsApplied(inventoryResult.data, missing)) {
|
|
7660
8421
|
return releaseControlFailure("database.baseline_migrations", "OUTCOME_UNKNOWN", inventoryResult.transportError ? null : inventoryResult.status);
|
|
7661
8422
|
}
|
|
@@ -7678,7 +8439,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7678
8439
|
throw new Error("Invalid RLS policy mode");
|
|
7679
8440
|
const policySql = buildRlsPolicySql(qualifiedTable, policyMode, args.owner_column);
|
|
7680
8441
|
const sql = `BEGIN; CREATE TABLE IF NOT EXISTS ${qualifiedTable} (${columns}); ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY; ${policySql} COMMIT;`;
|
|
7681
|
-
const r = await
|
|
8442
|
+
const r = await managementHttp().postReleaseMutation(`/v1/projects/${ref}/database/sql`, { sql, mode: "migration" });
|
|
7682
8443
|
if (!r.ok)
|
|
7683
8444
|
return releaseControlMutationFailure("database.create_table_rls", r);
|
|
7684
8445
|
if (r.status !== 200) {
|
|
@@ -7687,6 +8448,13 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
7687
8448
|
if (!confirmedSqlBatchReceipt(r.data, rlsStatementCommands(policyMode))) {
|
|
7688
8449
|
return releaseControlFailure("database.create_table_rls", "OUTCOME_UNKNOWN", r.status);
|
|
7689
8450
|
}
|
|
8451
|
+
const reloadStatus = schemaReloadStatus(r.data);
|
|
8452
|
+
if (reloadStatus === "notification_failed") {
|
|
8453
|
+
return releaseControlFailure("database.create_table_rls", "PARTIAL_SUCCESS", r.status, { ddl_committed: true, schema_reload: { status: reloadStatus } });
|
|
8454
|
+
}
|
|
8455
|
+
if (reloadStatus !== "notified") {
|
|
8456
|
+
return releaseControlFailure("database.create_table_rls", "OUTCOME_UNKNOWN", r.status);
|
|
8457
|
+
}
|
|
7690
8458
|
text = `✅ Table '${schema}.${args.table}' created with RLS (${policyMode === "owner" ? "auth.uid() owner policy" : "deny-all by default"})`;
|
|
7691
8459
|
break;
|
|
7692
8460
|
}
|
|
@@ -7975,6 +8743,7 @@ var MAX_AUTH_READ_BYTES = 64 * 1024;
|
|
|
7975
8743
|
var AUTH_READ_TIMEOUT_MS = 5000;
|
|
7976
8744
|
var USER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7977
8745
|
var AUTH_LINK_TYPES = ["magiclink", "recovery", "invite"];
|
|
8746
|
+
var OAUTH_AUTHORIZATION_PATH_MAX_LENGTH = 2048;
|
|
7978
8747
|
var SAFE_USER_FIELDS = [
|
|
7979
8748
|
"email",
|
|
7980
8749
|
"phone",
|
|
@@ -8044,6 +8813,81 @@ function requiredRef(candidate) {
|
|
|
8044
8813
|
throw new Error("'ref' is required");
|
|
8045
8814
|
return projectRefPathSegment(candidate.trim(), "Auth");
|
|
8046
8815
|
}
|
|
8816
|
+
function optionalAuthorizationPath(candidate) {
|
|
8817
|
+
if (candidate === undefined)
|
|
8818
|
+
return;
|
|
8819
|
+
return boundedText(candidate, "authorization_path", OAUTH_AUTHORIZATION_PATH_MAX_LENGTH);
|
|
8820
|
+
}
|
|
8821
|
+
function oauthServerStatus(value, expectedRef) {
|
|
8822
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
8823
|
+
return null;
|
|
8824
|
+
const status = value;
|
|
8825
|
+
if (status.project_ref !== expectedRef || typeof status.enabled !== "boolean" || typeof status.allow_dynamic_registration !== "boolean" || typeof status.issuer !== "string" || typeof status.signing_alg !== "string" || typeof status.oidc_id_token_ready !== "boolean" || typeof status.migration_status !== "string")
|
|
8826
|
+
return null;
|
|
8827
|
+
return {
|
|
8828
|
+
project_ref: status.project_ref,
|
|
8829
|
+
enabled: status.enabled,
|
|
8830
|
+
allow_dynamic_registration: status.allow_dynamic_registration,
|
|
8831
|
+
issuer: status.issuer,
|
|
8832
|
+
authorization_path: typeof status.authorization_path === "string" ? status.authorization_path : null,
|
|
8833
|
+
signing_alg: status.signing_alg,
|
|
8834
|
+
key_id: typeof status.key_id === "string" ? status.key_id : null,
|
|
8835
|
+
oidc_id_token_ready: status.oidc_id_token_ready,
|
|
8836
|
+
migration_status: status.migration_status
|
|
8837
|
+
};
|
|
8838
|
+
}
|
|
8839
|
+
function oauthServerFailure(operation, response) {
|
|
8840
|
+
return {
|
|
8841
|
+
isError: true,
|
|
8842
|
+
content: [{
|
|
8843
|
+
type: "text",
|
|
8844
|
+
text: JSON.stringify({
|
|
8845
|
+
ok: false,
|
|
8846
|
+
operation,
|
|
8847
|
+
http_status: response.transportError || response.responseReadError ? null : response.status,
|
|
8848
|
+
error: response.responseReadError ? "INVALID_RESPONSE" : response.transportError ? "NETWORK_ERROR" : "HTTP_ERROR"
|
|
8849
|
+
}, null, 2)
|
|
8850
|
+
}]
|
|
8851
|
+
};
|
|
8852
|
+
}
|
|
8853
|
+
async function getOAuthServer(http, ref) {
|
|
8854
|
+
const response = await http.get(`/v1/projects/${ref}/auth/oauth-server`, {
|
|
8855
|
+
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
8856
|
+
responseTimeoutMs: AUTH_READ_TIMEOUT_MS
|
|
8857
|
+
});
|
|
8858
|
+
if (!response.ok)
|
|
8859
|
+
return oauthServerFailure("auth.get_oauth_server", response);
|
|
8860
|
+
const status = oauthServerStatus(response.data, ref);
|
|
8861
|
+
if (!status)
|
|
8862
|
+
return oauthServerFailure("auth.get_oauth_server", { ...response, responseReadError: true });
|
|
8863
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.get_oauth_server", ...status }, null, 2) }] };
|
|
8864
|
+
}
|
|
8865
|
+
async function migrateOAuthServer(http, args) {
|
|
8866
|
+
const ref = requiredRef(args.ref);
|
|
8867
|
+
const body = {
|
|
8868
|
+
allow_dynamic_registration: args.allow_dynamic_registration === true
|
|
8869
|
+
};
|
|
8870
|
+
const authorizationPath = optionalAuthorizationPath(args.authorization_path);
|
|
8871
|
+
if (authorizationPath !== undefined)
|
|
8872
|
+
body.authorization_path = authorizationPath;
|
|
8873
|
+
const mutation = await http.postReleaseMutation(`/v1/projects/${ref}/auth/oauth-server/migrate`, body);
|
|
8874
|
+
if (!mutation.ok)
|
|
8875
|
+
return oauthServerFailure("auth.migrate_oauth_server", mutation);
|
|
8876
|
+
const mutationStatus = oauthServerStatus(mutation.data, ref);
|
|
8877
|
+
if (!mutationStatus)
|
|
8878
|
+
return oauthServerFailure("auth.migrate_oauth_server", { ...mutation, responseReadError: true });
|
|
8879
|
+
const read = await http.get(`/v1/projects/${ref}/auth/oauth-server`, {
|
|
8880
|
+
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
8881
|
+
responseTimeoutMs: AUTH_READ_TIMEOUT_MS
|
|
8882
|
+
});
|
|
8883
|
+
if (!read.ok)
|
|
8884
|
+
return oauthServerFailure("auth.migrate_oauth_server", read);
|
|
8885
|
+
const status = oauthServerStatus(read.data, ref);
|
|
8886
|
+
if (!status || status.signing_alg !== "ES256" || status.oidc_id_token_ready !== true) {
|
|
8887
|
+
return oauthServerFailure("auth.migrate_oauth_server", { ...read, responseReadError: true });
|
|
8888
|
+
}
|
|
8889
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.migrate_oauth_server", ...status }, null, 2) }] };
|
|
8890
|
+
}
|
|
8047
8891
|
function requiredUserId(candidate) {
|
|
8048
8892
|
if (typeof candidate !== "string" || !USER_ID_PATTERN.test(candidate.trim())) {
|
|
8049
8893
|
throw new Error("'user_id' must be a UUID");
|
|
@@ -8300,7 +9144,7 @@ function formatProviders(data) {
|
|
|
8300
9144
|
}
|
|
8301
9145
|
function registerAuthTools(server, http) {
|
|
8302
9146
|
server.tool("auth", `Auth & OAuth provider management, controlled user lookup, and login-link generation.
|
|
8303
|
-
Actions: list_users, get_user, generate_link, list_providers, get_provider, configure_provider, update_provider, disable_provider, supported_providers, wechat_mini, wechat_open, get_settings, update_settings, get_config, update_config`, {
|
|
9147
|
+
Actions: list_users, get_user, generate_link, list_providers, get_provider, configure_provider, update_provider, disable_provider, supported_providers, wechat_mini, wechat_open, get_settings, update_settings, get_config, update_config, get_oauth_server, migrate_oauth_server`, {
|
|
8304
9148
|
action: withDescription(stringEnum([
|
|
8305
9149
|
"list_users",
|
|
8306
9150
|
"get_user",
|
|
@@ -8316,7 +9160,9 @@ Actions: list_users, get_user, generate_link, list_providers, get_provider, conf
|
|
|
8316
9160
|
"get_settings",
|
|
8317
9161
|
"update_settings",
|
|
8318
9162
|
"get_config",
|
|
8319
|
-
"update_config"
|
|
9163
|
+
"update_config",
|
|
9164
|
+
"get_oauth_server",
|
|
9165
|
+
"migrate_oauth_server"
|
|
8320
9166
|
]), "Action to perform"),
|
|
8321
9167
|
ref: optional(Type.String(), "Project ref (required for most actions)"),
|
|
8322
9168
|
user_id: optional(Type.String(), "[get_user] Exact auth user UUID"),
|
|
@@ -8334,7 +9180,9 @@ Actions: list_users, get_user, generate_link, list_providers, get_provider, conf
|
|
|
8334
9180
|
url: optional(Type.String(), "[configure] Custom OAuth URL"),
|
|
8335
9181
|
app_id: optional(Type.String(), "[wechat_*] WeChat App ID"),
|
|
8336
9182
|
app_secret: optional(Type.String(), "[wechat_*] WeChat App Secret"),
|
|
8337
|
-
config: optional(authConfigSchema, "[update_settings/update_config] Config fields as a JSON object")
|
|
9183
|
+
config: optional(authConfigSchema, "[update_settings/update_config] Config fields as a JSON object"),
|
|
9184
|
+
allow_dynamic_registration: optional(Type.Boolean(), "[migrate_oauth_server] Enable dynamic client registration"),
|
|
9185
|
+
authorization_path: optional(Type.String(), "[migrate_oauth_server] Hosted OAuth authorization path")
|
|
8338
9186
|
}, async (args) => {
|
|
8339
9187
|
const { action, ref, provider, client_id, client_secret, redirect_uri, url, app_id, app_secret, config } = args;
|
|
8340
9188
|
const need = (f) => {
|
|
@@ -8350,6 +9198,10 @@ Actions: list_users, get_user, generate_link, list_providers, get_provider, conf
|
|
|
8350
9198
|
return getUser(http, args);
|
|
8351
9199
|
case "generate_link":
|
|
8352
9200
|
return generateLink(http, args);
|
|
9201
|
+
case "get_oauth_server":
|
|
9202
|
+
return getOAuthServer(http, requiredRef(ref));
|
|
9203
|
+
case "migrate_oauth_server":
|
|
9204
|
+
return migrateOAuthServer(http, args);
|
|
8353
9205
|
case "list_providers":
|
|
8354
9206
|
need("ref");
|
|
8355
9207
|
const lp = await http.get(`/v1/projects/${ref}/auth/providers`);
|
|
@@ -9078,15 +9930,17 @@ import {
|
|
|
9078
9930
|
constants as fsConstants,
|
|
9079
9931
|
existsSync as existsSync3,
|
|
9080
9932
|
fstatSync,
|
|
9933
|
+
lstatSync,
|
|
9081
9934
|
mkdtempSync,
|
|
9082
9935
|
openSync,
|
|
9083
9936
|
readFileSync as readFileSync3,
|
|
9937
|
+
readdirSync as readdirSync2,
|
|
9084
9938
|
rmSync,
|
|
9085
9939
|
statSync as statSync2,
|
|
9086
9940
|
writeFileSync
|
|
9087
9941
|
} from "node:fs";
|
|
9088
9942
|
import { tmpdir } from "node:os";
|
|
9089
|
-
import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
|
|
9943
|
+
import { basename as basename2, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
9090
9944
|
import { promisify } from "node:util";
|
|
9091
9945
|
import { execFile } from "node:child_process";
|
|
9092
9946
|
|
|
@@ -9240,6 +10094,7 @@ function confirmedFunctionDeletion(payload, expectation) {
|
|
|
9240
10094
|
// src/shared/tools/advanced-tools.ts
|
|
9241
10095
|
var execFileAsync = promisify(execFile);
|
|
9242
10096
|
var SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
|
|
10097
|
+
var FORBIDDEN_BUNDLE_SEGMENTS = new Set(["node_modules", ".git"]);
|
|
9243
10098
|
function openFileIdentity(descriptor) {
|
|
9244
10099
|
const state = fstatSync(descriptor, { bigint: true });
|
|
9245
10100
|
if (!state.isFile())
|
|
@@ -9294,6 +10149,51 @@ function readVerifiedPrebundledCode(pathArg, expectedSha256) {
|
|
|
9294
10149
|
closeSync(descriptor);
|
|
9295
10150
|
}
|
|
9296
10151
|
}
|
|
10152
|
+
function canonicalBundlePath(root, filePath) {
|
|
10153
|
+
const bundlePath = relative(root, filePath).split(sep).join("/");
|
|
10154
|
+
const segments = bundlePath.split("/");
|
|
10155
|
+
if (!bundlePath || bundlePath.startsWith("/") || segments.some((segment) => !segment || segment === "." || segment === ".." || segment.startsWith("._") || FORBIDDEN_BUNDLE_SEGMENTS.has(segment))) {
|
|
10156
|
+
throw new Error(`Bundle directory contains a forbidden path: ${bundlePath || filePath}`);
|
|
10157
|
+
}
|
|
10158
|
+
return bundlePath;
|
|
10159
|
+
}
|
|
10160
|
+
function readBundleDirectory(root, directory = root) {
|
|
10161
|
+
const files = {};
|
|
10162
|
+
const entries = readdirSync2(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
10163
|
+
for (const entry of entries) {
|
|
10164
|
+
const entryPath = join2(directory, entry.name);
|
|
10165
|
+
const bundlePath = canonicalBundlePath(root, entryPath);
|
|
10166
|
+
const entryState = lstatSync(entryPath);
|
|
10167
|
+
if (entryState.isSymbolicLink())
|
|
10168
|
+
throw new Error(`Bundle directory must not contain symlinks: ${bundlePath}`);
|
|
10169
|
+
if (entryState.isDirectory()) {
|
|
10170
|
+
Object.assign(files, readBundleDirectory(root, entryPath));
|
|
10171
|
+
continue;
|
|
10172
|
+
}
|
|
10173
|
+
if (!entryState.isFile())
|
|
10174
|
+
throw new Error(`Bundle directory must contain only regular files: ${bundlePath}`);
|
|
10175
|
+
files[bundlePath] = verifiedUtf8Code(readFileSync3(entryPath));
|
|
10176
|
+
}
|
|
10177
|
+
return files;
|
|
10178
|
+
}
|
|
10179
|
+
function preparedBundleFiles(args) {
|
|
10180
|
+
const files = args.files;
|
|
10181
|
+
const bundleDirectory = args["bundle-dir"];
|
|
10182
|
+
if (files === undefined === (bundleDirectory === undefined)) {
|
|
10183
|
+
throw new Error("Exactly one of '--files' or '--bundle-dir' is required for 'deploy_bundle'");
|
|
10184
|
+
}
|
|
10185
|
+
if (typeof bundleDirectory !== "string")
|
|
10186
|
+
return files;
|
|
10187
|
+
const resolvedDirectory = resolve2(bundleDirectory);
|
|
10188
|
+
const rootState = lstatSync(resolvedDirectory);
|
|
10189
|
+
if (rootState.isSymbolicLink() || !rootState.isDirectory()) {
|
|
10190
|
+
throw new Error("'--bundle-dir' must be a regular directory and must not be a symlink");
|
|
10191
|
+
}
|
|
10192
|
+
const bundleFiles = readBundleDirectory(resolvedDirectory);
|
|
10193
|
+
if (Object.keys(bundleFiles).length === 0)
|
|
10194
|
+
throw new Error("'--bundle-dir' must not be empty");
|
|
10195
|
+
return bundleFiles;
|
|
10196
|
+
}
|
|
9297
10197
|
async function runBunBuild(args) {
|
|
9298
10198
|
try {
|
|
9299
10199
|
return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -9359,12 +10259,15 @@ async function bundledDeployCode(pathArg) {
|
|
|
9359
10259
|
throw new Error(`Failed to bundle/read path ${pathArg}: ${message}`);
|
|
9360
10260
|
}
|
|
9361
10261
|
}
|
|
9362
|
-
function
|
|
10262
|
+
function rejectActionSpecificFlags(action, args) {
|
|
9363
10263
|
for (const flag of ["prebundled-path", "expected-sha256"]) {
|
|
9364
10264
|
if (action !== "deploy" && args[flag] !== undefined) {
|
|
9365
10265
|
throw new Error(`'--${flag}' is not supported for '${action}'`);
|
|
9366
10266
|
}
|
|
9367
10267
|
}
|
|
10268
|
+
if (action !== "deploy_bundle" && args["bundle-dir"] !== undefined) {
|
|
10269
|
+
throw new Error(`'--bundle-dir' is not supported for '${action}'`);
|
|
10270
|
+
}
|
|
9368
10271
|
}
|
|
9369
10272
|
function resolveEntrypoint(pathArg) {
|
|
9370
10273
|
const resolved = resolve2(pathArg);
|
|
@@ -9794,6 +10697,7 @@ Actions: list, get_config, deploy, deploy_bundle, config, source, activate, dele
|
|
|
9794
10697
|
"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"),
|
|
9795
10698
|
output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
|
|
9796
10699
|
files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
|
|
10700
|
+
"bundle-dir": optional(Type.String(), "[deploy_bundle] Local self-contained UTF-8 bundle directory; excludes node_modules, .git, AppleDouble, symlinks, and special files"),
|
|
9797
10701
|
entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
|
|
9798
10702
|
minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
|
|
9799
10703
|
verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
|
|
@@ -9803,8 +10707,8 @@ Actions: list, get_config, deploy, deploy_bundle, config, source, activate, dele
|
|
|
9803
10707
|
}, async (args) => {
|
|
9804
10708
|
if (args.action === "activate")
|
|
9805
10709
|
return activateFunctionVersion(http, args, options.readOnly);
|
|
9806
|
-
const { action, ref, slug, path: pathArg, output,
|
|
9807
|
-
|
|
10710
|
+
const { action, ref, slug, path: pathArg, output, entrypoint, minify, verify_jwt, background_routes } = args;
|
|
10711
|
+
rejectActionSpecificFlags(action, args);
|
|
9808
10712
|
const expectedActiveVersion = action === "deploy" || action === "deploy_bundle" ? requiredExpectedActiveVersion(args, action) : undefined;
|
|
9809
10713
|
const expectedActivationId = FUNCTION_IDENTITY_MUTATIONS.has(action) ? requiredExpectedActivationId(args, action) : undefined;
|
|
9810
10714
|
let code = args.code;
|
|
@@ -9884,9 +10788,9 @@ ${deployCheck.err}`;
|
|
|
9884
10788
|
}, deploymentResponse);
|
|
9885
10789
|
case "deploy_bundle":
|
|
9886
10790
|
need("slug", slug);
|
|
9887
|
-
|
|
10791
|
+
const deployBundleFiles = preparedBundleFiles(args);
|
|
9888
10792
|
const bundleResponse = await http.postReleaseMutation(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
|
|
9889
|
-
files,
|
|
10793
|
+
files: deployBundleFiles,
|
|
9890
10794
|
entrypoint,
|
|
9891
10795
|
minify,
|
|
9892
10796
|
expected_active_version: expectedActiveVersion,
|
|
@@ -11196,6 +12100,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
11196
12100
|
managed_upstream: optional(stringEnum(["edge-functions"]), "[upsert_route/update_route] 托管上游(同步 Edge Function)"),
|
|
11197
12101
|
upstream_tls_insecure_skip_verify: optional(Type.Boolean(), "[upsert_route/update_route] 上游 TLS 跳过校验"),
|
|
11198
12102
|
static_root: optional(Type.String(), "[upsert_route/update_route] 静态站点根目录"),
|
|
12103
|
+
spa: optional(Type.Boolean(), "[upsert_route/update_route] 是否启用 SPA 单页回退"),
|
|
11199
12104
|
protocol: optional(stringEnum(["http", "https"]), "[upsert_route/update_route] 可选请求协议匹配"),
|
|
11200
12105
|
redirect_to: optional(Type.String(), "[upsert_route/update_route] 带固定 host 的绝对 http(s) 目标,可在末尾使用 {http.request.uri}"),
|
|
11201
12106
|
redirect_status: withDescription(redirectStatus, "[upsert_route/update_route] 重定向状态码,默认 308"),
|
|
@@ -11242,6 +12147,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
11242
12147
|
managed_upstream,
|
|
11243
12148
|
upstream_tls_insecure_skip_verify,
|
|
11244
12149
|
static_root,
|
|
12150
|
+
spa,
|
|
11245
12151
|
protocol,
|
|
11246
12152
|
redirect_to,
|
|
11247
12153
|
redirect_status,
|
|
@@ -11291,6 +12197,8 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
11291
12197
|
body.upstream_tls_insecure_skip_verify = upstream_tls_insecure_skip_verify;
|
|
11292
12198
|
if (static_root !== undefined)
|
|
11293
12199
|
body.static_root = static_root;
|
|
12200
|
+
if (spa !== undefined)
|
|
12201
|
+
body.spa = spa;
|
|
11294
12202
|
if (protocol !== undefined)
|
|
11295
12203
|
body.protocol = protocol;
|
|
11296
12204
|
if (redirect_to !== undefined)
|
|
@@ -11328,6 +12236,8 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
11328
12236
|
body.upstream_tls_insecure_skip_verify = upstream_tls_insecure_skip_verify;
|
|
11329
12237
|
if (static_root !== undefined)
|
|
11330
12238
|
body.static_root = static_root;
|
|
12239
|
+
if (spa !== undefined)
|
|
12240
|
+
body.spa = spa;
|
|
11331
12241
|
if (protocol !== undefined)
|
|
11332
12242
|
body.protocol = protocol;
|
|
11333
12243
|
if (redirect_to !== undefined)
|
|
@@ -11988,33 +12898,33 @@ function registerSupabaseCliTools(server, options = {}) {
|
|
|
11988
12898
|
import {
|
|
11989
12899
|
cpSync,
|
|
11990
12900
|
existsSync as existsSync6,
|
|
11991
|
-
lstatSync,
|
|
12901
|
+
lstatSync as lstatSync2,
|
|
11992
12902
|
mkdirSync as mkdirSync2,
|
|
11993
12903
|
mkdtempSync as mkdtempSync2,
|
|
11994
|
-
readdirSync as
|
|
12904
|
+
readdirSync as readdirSync3,
|
|
11995
12905
|
readFileSync as readFileSync5,
|
|
11996
12906
|
renameSync,
|
|
11997
12907
|
rmSync as rmSync2
|
|
11998
12908
|
} from "node:fs";
|
|
11999
12909
|
import { homedir as homedir2 } from "node:os";
|
|
12000
|
-
import { dirname as dirname2, join as join4, relative, resolve as resolve4, sep } from "node:path";
|
|
12910
|
+
import { dirname as dirname2, join as join4, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
|
|
12001
12911
|
import { fileURLToPath } from "node:url";
|
|
12002
12912
|
var SKILL_NAME = "supacloud-cli";
|
|
12003
12913
|
function regularFiles(rootDirectory, currentDirectory = rootDirectory) {
|
|
12004
12914
|
const files = [];
|
|
12005
|
-
for (const directoryEntry of
|
|
12915
|
+
for (const directoryEntry of readdirSync3(currentDirectory, { withFileTypes: true })) {
|
|
12006
12916
|
const entryPath = join4(currentDirectory, directoryEntry.name);
|
|
12007
12917
|
if (directoryEntry.isSymbolicLink())
|
|
12008
12918
|
throw new Error(`Skill directories cannot contain symlinks: ${entryPath}`);
|
|
12009
12919
|
if (directoryEntry.isDirectory())
|
|
12010
12920
|
files.push(...regularFiles(rootDirectory, entryPath));
|
|
12011
12921
|
if (directoryEntry.isFile())
|
|
12012
|
-
files.push(
|
|
12922
|
+
files.push(relative2(rootDirectory, entryPath).split(sep2).join("/"));
|
|
12013
12923
|
}
|
|
12014
12924
|
return files.sort();
|
|
12015
12925
|
}
|
|
12016
12926
|
function directoriesMatch(sourceDirectory, destinationDirectory) {
|
|
12017
|
-
if (!existsSync6(destinationDirectory) || !
|
|
12927
|
+
if (!existsSync6(destinationDirectory) || !lstatSync2(destinationDirectory).isDirectory())
|
|
12018
12928
|
return false;
|
|
12019
12929
|
const sourceFiles = regularFiles(sourceDirectory);
|
|
12020
12930
|
const destinationFiles = regularFiles(destinationDirectory);
|
|
@@ -13211,7 +14121,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
13211
14121
|
// package.json
|
|
13212
14122
|
var package_default = {
|
|
13213
14123
|
name: "@supacloud/cli",
|
|
13214
|
-
version: "0.
|
|
14124
|
+
version: "0.32.0",
|
|
13215
14125
|
description: "Project-scoped CLI for SupaCloud users",
|
|
13216
14126
|
type: "module",
|
|
13217
14127
|
main: "./dist/index.js",
|
|
@@ -13470,6 +14380,7 @@ EXAMPLES
|
|
|
13470
14380
|
${preferredCommand} database query --sql "select now()"
|
|
13471
14381
|
${preferredCommand} database query --ref abc123 --file ./queries/vector-search.sql
|
|
13472
14382
|
${preferredCommand} database migration_inventory --ref abc123
|
|
14383
|
+
${preferredCommand} database lint_migrations --dir supabase/migrations
|
|
13473
14384
|
${preferredCommand} database push_migrations --ref abc123 --dir supabase/migrations --dry_run
|
|
13474
14385
|
${preferredCommand} supabase migration_new --name add_accounts
|
|
13475
14386
|
${preferredCommand} supabase db_diff --schema public --name add_accounts
|
|
@@ -13483,6 +14394,7 @@ EXAMPLES
|
|
|
13483
14394
|
${preferredCommand} edge_functions get_config --ref abc123 --slug hello
|
|
13484
14395
|
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent --expected-activation-id legacy
|
|
13485
14396
|
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4 --expected-activation-id <uuid>
|
|
14397
|
+
${preferredCommand} edge_functions deploy_bundle --ref abc123 --slug supauth --bundle-dir ./artifacts/supacloud-app/function-bundle --entrypoint index.ts --expected-active-version 4 --expected-activation-id <uuid>
|
|
13486
14398
|
${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4 --expected-activation-id <uuid>
|
|
13487
14399
|
${preferredCommand} scheduled_functions list --ref abc123
|
|
13488
14400
|
${preferredCommand} mutations status --ref abc123 --mutation_id 00000000-0000-4000-8000-000000000001
|
|
@@ -13585,6 +14497,9 @@ function createCliTools(context, confirmProduction) {
|
|
|
13585
14497
|
};
|
|
13586
14498
|
if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
|
|
13587
14499
|
registerContextAwareHelp();
|
|
14500
|
+
Object.assign(tools, captureTools((server) => registerDatabaseTools(server, undefined, {
|
|
14501
|
+
localOnly: true
|
|
14502
|
+
})));
|
|
13588
14503
|
tools.setup_help = {
|
|
13589
14504
|
schema: {},
|
|
13590
14505
|
callback: async () => ({
|