@supacloud/cli 0.30.0 → 0.31.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.
Files changed (3) hide show
  1. package/README.md +43 -0
  2. package/dist/index.js +961 -52
  3. 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 { version: migration.version, name: migration.name, statements: migration.statements };
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 migrationIdentities(data) {
7161
- const identities = migrationRows(data).map(migrationIdentity);
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 exactIdentity = remote.version === local.version && remote.name === local.name;
7205
- if (exactIdentity && exactIdentityIsApplied(local, remote))
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
- const remoteMigrations = migrationIdentities(data);
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 actions = [
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 allActions = readOnly ? actions : [...actions, ...writeActions];
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
- schema: optional(Type.String(), "[*] Schema name (default: public)"),
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) => http.post(`/v1/projects/${ref}/database/sql`, mode ? { sql, mode } : { sql });
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 http.get(migrationInventoryPath(ref), { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
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 http.get(`/v1/projects/${ref}`);
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 http.postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name: args.name, sql: args.sql });
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 http.get(`/v1/projects/${ref}/database/migrations`);
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 migrationPlan = migrationPushPlan(migrationsResult.data, migrationFiles);
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 = migrationPlan.pending;
7554
- const alreadyApplied = migrationPlan.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
- break;
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
- const pending = new Set(migrationPlan.pending.map(({ file }) => file));
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 http.postReleaseMutation(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
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 http.get(`/v1/projects/${ref}/database/migrations`);
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 migrationPlan = migrationPushPlan(r.data, migrationFiles);
7623
- const missing = migrationPlan.pending;
7624
- const alreadyApplied = migrationPlan.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 http.postReleaseMutation(`/v1/projects/${ref}/database/migrations/baseline`, { migrations: missing.map(({ name, version }) => ({ name, version })) });
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 http.get(`/v1/projects/${ref}/database/migrations`, { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
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 http.postReleaseMutation(`/v1/projects/${ref}/database/sql`, { sql, mode: "migration" });
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 rejectPrebundledFlagsOutsideDeploy(action, args) {
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, files, entrypoint, minify, verify_jwt, background_routes } = args;
9807
- rejectPrebundledFlagsOutsideDeploy(action, args);
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
- need("files", files);
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,
@@ -11988,33 +12892,33 @@ function registerSupabaseCliTools(server, options = {}) {
11988
12892
  import {
11989
12893
  cpSync,
11990
12894
  existsSync as existsSync6,
11991
- lstatSync,
12895
+ lstatSync as lstatSync2,
11992
12896
  mkdirSync as mkdirSync2,
11993
12897
  mkdtempSync as mkdtempSync2,
11994
- readdirSync as readdirSync2,
12898
+ readdirSync as readdirSync3,
11995
12899
  readFileSync as readFileSync5,
11996
12900
  renameSync,
11997
12901
  rmSync as rmSync2
11998
12902
  } from "node:fs";
11999
12903
  import { homedir as homedir2 } from "node:os";
12000
- import { dirname as dirname2, join as join4, relative, resolve as resolve4, sep } from "node:path";
12904
+ import { dirname as dirname2, join as join4, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
12001
12905
  import { fileURLToPath } from "node:url";
12002
12906
  var SKILL_NAME = "supacloud-cli";
12003
12907
  function regularFiles(rootDirectory, currentDirectory = rootDirectory) {
12004
12908
  const files = [];
12005
- for (const directoryEntry of readdirSync2(currentDirectory, { withFileTypes: true })) {
12909
+ for (const directoryEntry of readdirSync3(currentDirectory, { withFileTypes: true })) {
12006
12910
  const entryPath = join4(currentDirectory, directoryEntry.name);
12007
12911
  if (directoryEntry.isSymbolicLink())
12008
12912
  throw new Error(`Skill directories cannot contain symlinks: ${entryPath}`);
12009
12913
  if (directoryEntry.isDirectory())
12010
12914
  files.push(...regularFiles(rootDirectory, entryPath));
12011
12915
  if (directoryEntry.isFile())
12012
- files.push(relative(rootDirectory, entryPath).split(sep).join("/"));
12916
+ files.push(relative2(rootDirectory, entryPath).split(sep2).join("/"));
12013
12917
  }
12014
12918
  return files.sort();
12015
12919
  }
12016
12920
  function directoriesMatch(sourceDirectory, destinationDirectory) {
12017
- if (!existsSync6(destinationDirectory) || !lstatSync(destinationDirectory).isDirectory())
12921
+ if (!existsSync6(destinationDirectory) || !lstatSync2(destinationDirectory).isDirectory())
12018
12922
  return false;
12019
12923
  const sourceFiles = regularFiles(sourceDirectory);
12020
12924
  const destinationFiles = regularFiles(destinationDirectory);
@@ -13211,7 +14115,7 @@ function registerReleaseTools(server, http, options = {}) {
13211
14115
  // package.json
13212
14116
  var package_default = {
13213
14117
  name: "@supacloud/cli",
13214
- version: "0.30.0",
14118
+ version: "0.31.0",
13215
14119
  description: "Project-scoped CLI for SupaCloud users",
13216
14120
  type: "module",
13217
14121
  main: "./dist/index.js",
@@ -13470,6 +14374,7 @@ EXAMPLES
13470
14374
  ${preferredCommand} database query --sql "select now()"
13471
14375
  ${preferredCommand} database query --ref abc123 --file ./queries/vector-search.sql
13472
14376
  ${preferredCommand} database migration_inventory --ref abc123
14377
+ ${preferredCommand} database lint_migrations --dir supabase/migrations
13473
14378
  ${preferredCommand} database push_migrations --ref abc123 --dir supabase/migrations --dry_run
13474
14379
  ${preferredCommand} supabase migration_new --name add_accounts
13475
14380
  ${preferredCommand} supabase db_diff --schema public --name add_accounts
@@ -13483,6 +14388,7 @@ EXAMPLES
13483
14388
  ${preferredCommand} edge_functions get_config --ref abc123 --slug hello
13484
14389
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent --expected-activation-id legacy
13485
14390
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4 --expected-activation-id <uuid>
14391
+ ${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
14392
  ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4 --expected-activation-id <uuid>
13487
14393
  ${preferredCommand} scheduled_functions list --ref abc123
13488
14394
  ${preferredCommand} mutations status --ref abc123 --mutation_id 00000000-0000-4000-8000-000000000001
@@ -13585,6 +14491,9 @@ function createCliTools(context, confirmProduction) {
13585
14491
  };
13586
14492
  if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
13587
14493
  registerContextAwareHelp();
14494
+ Object.assign(tools, captureTools((server) => registerDatabaseTools(server, undefined, {
14495
+ localOnly: true
14496
+ })));
13588
14497
  tools.setup_help = {
13589
14498
  schema: {},
13590
14499
  callback: async () => ({