@amerged/ohmyhost-mcp 0.1.28 → 0.1.30

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 (39) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +1419 -0
  3. package/dist/{chunk-GIP4M4CK.js → chunk-MVT76LJ5.js} +92 -28
  4. package/dist/index.js +1 -1
  5. package/dist/stdio-main.js +24153 -2354
  6. package/package.json +1 -1
  7. package/src/apps/mcp/package.json +1 -1
  8. package/src/apps/mcp/src/local-client.ts +12 -2
  9. package/src/apps/mcp/src/local-server.ts +333 -4
  10. package/src/apps/product-cli/package.json +4 -2
  11. package/src/apps/product-cli/src/cli.ts +312 -2
  12. package/src/apps/product-cli/src/command.ts +261 -0
  13. package/src/apps/product-cli/src/composition.ts +74 -4
  14. package/src/apps/product-cli/src/index.ts +6 -0
  15. package/src/apps/product-cli/src/product-api.ts +5 -0
  16. package/src/apps/product-cli/src/project-link-store.ts +92 -1
  17. package/src/apps/product-cli/src/repository-init.ts +2 -2
  18. package/src/apps/product-cli/src/runtime-contract.ts +34 -1
  19. package/src/apps/product-cli/src/source-managed-client.ts +189 -0
  20. package/src/apps/product-cli/src/source-publication-store.ts +563 -0
  21. package/src/apps/product-cli/src/source-publisher.ts +195 -0
  22. package/src/apps/product-cli/src/source-workspace.ts +552 -0
  23. package/src/packages/agent-skills/src/generated-skill-resources.ts +43 -15
  24. package/src/packages/contracts/package.json +10 -0
  25. package/src/packages/contracts/src/database-migration-admission.ts +65 -125
  26. package/src/packages/contracts/src/database-migration-errors.ts +14 -0
  27. package/src/packages/contracts/src/database-migration-lexical.ts +180 -0
  28. package/src/packages/contracts/src/database-migration-policies.ts +150 -0
  29. package/src/packages/contracts/src/framework-admission.ts +1 -1
  30. package/src/packages/contracts/src/framework-build-script.mjs +1 -2
  31. package/src/packages/contracts/src/managed-sources.ts +582 -0
  32. package/src/packages/contracts/src/mcp-source-codec.ts +150 -0
  33. package/src/packages/contracts/src/operation-failure.ts +22 -6
  34. package/src/packages/contracts/src/problem-policy.ts +61 -14
  35. package/src/packages/sdk-ts/package.json +1 -1
  36. package/src/packages/sdk-ts/src/generated/index.ts +92 -0
  37. package/src/packages/sdk-ts/src/generated/sdk.gen.ts +541 -0
  38. package/src/packages/sdk-ts/src/generated/types.gen.ts +911 -4
  39. package/src/packages/workos-auth-contracts/src/tokens.ts +61 -4
@@ -5,6 +5,11 @@
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "exports": {
8
+ "./mcp-source-codec": {
9
+ "source": "./src/mcp-source-codec.ts",
10
+ "types": "./dist/mcp-source-codec.d.ts",
11
+ "import": "./dist/mcp-source-codec.js"
12
+ },
8
13
  "./operation-failure": {
9
14
  "source": "./src/operation-failure.ts",
10
15
  "types": "./dist/operation-failure.d.ts",
@@ -121,6 +126,11 @@
121
126
  "source": "./src/project-data-changes.ts",
122
127
  "types": "./dist/project-data-changes.d.ts",
123
128
  "import": "./dist/project-data-changes.js"
129
+ },
130
+ "./managed-sources": {
131
+ "source": "./src/managed-sources.ts",
132
+ "types": "./dist/managed-sources.d.ts",
133
+ "import": "./dist/managed-sources.js"
124
134
  }
125
135
  },
126
136
  "scripts": {
@@ -1,23 +1,23 @@
1
+ import { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
2
+ import {
3
+ readMigrationSql,
4
+ rejectMigrationSqlReferences,
5
+ type MigrationSqlToken,
6
+ } from "./database-migration-lexical.js";
7
+ import {
8
+ readMigrationRlsTarget,
9
+ type DatabaseMigrationRlsTarget,
10
+ } from "./database-migration-policies.js";
11
+
12
+ export { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
13
+ export type { DatabaseMigrationAdmissionReason } from "./database-migration-errors.js";
14
+ export type { DatabaseMigrationRlsTarget } from "./database-migration-policies.js";
15
+
1
16
  const MAX_MIGRATION_COUNT = 128;
2
17
  const MAX_MIGRATION_BYTES = 256 * 1024;
3
18
  const MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
4
19
  const MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
5
20
 
6
- export type DatabaseMigrationAdmissionReason =
7
- | "destructive"
8
- | "invalid"
9
- | "provider_specific"
10
- | "unsupported";
11
-
12
- export class DatabaseMigrationAdmissionError extends Error {
13
- public readonly code = "database_migration_not_admitted";
14
-
15
- public constructor(public readonly reason: DatabaseMigrationAdmissionReason) {
16
- super("Database migration is not admitted");
17
- this.name = "DatabaseMigrationAdmissionError";
18
- }
19
- }
20
-
21
21
  export interface DatabaseMigrationInput {
22
22
  readonly path: string;
23
23
  readonly body: Uint8Array;
@@ -67,127 +67,76 @@ export function admitExpandOnlyMigrations(
67
67
  if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 0xfeff) {
68
68
  throw new DatabaseMigrationAdmissionError("invalid");
69
69
  }
70
- if (/\bohmyhost\b/iu.test(sql)) throw new DatabaseMigrationAdmissionError("unsupported");
71
- rejectProviderSpecificSql(sql);
72
- const statements = splitSqlStatements(sql);
73
- if (statements.length === 0) throw new DatabaseMigrationAdmissionError("invalid");
74
- for (const statement of statements) admitStatement(statement);
70
+ const statements = parseDatabaseMigrationStatements(sql);
75
71
  statementCount += statements.length;
76
72
  return Object.freeze({ path: migration.path, statementCount: statements.length });
77
73
  });
78
74
  return Object.freeze({ files: Object.freeze(files), statementCount });
79
75
  }
80
76
 
81
- function rejectProviderSpecificSql(sql: string): void {
77
+ export interface DatabaseMigrationStatement {
78
+ readonly sql: string;
79
+ readonly rlsTarget?: DatabaseMigrationRlsTarget;
80
+ }
81
+
82
+ export function parseDatabaseMigrationStatements(
83
+ sql: string,
84
+ ): readonly DatabaseMigrationStatement[] {
85
+ if (
86
+ sql.length === 0 ||
87
+ sql.includes("\0") ||
88
+ sql.includes("\r") ||
89
+ sql.charCodeAt(0) === 0xfeff
90
+ ) {
91
+ throw new DatabaseMigrationAdmissionError("invalid");
92
+ }
93
+ const statements = readMigrationSql(sql);
94
+ if (statements.length === 0) throw new DatabaseMigrationAdmissionError("invalid");
95
+ return Object.freeze(
96
+ statements.map((statement) => {
97
+ // Provider references are forbidden throughout executable policy SQL, while
98
+ // the native helper exception is narrowed by the policy reader below.
99
+ rejectMigrationSqlReferences(statement.tokens, true);
100
+ const rlsTarget = readMigrationRlsTarget(statement.tokens);
101
+ if (rlsTarget !== undefined) return Object.freeze({ sql: statement.sql, rlsTarget });
102
+ rejectMigrationSqlReferences(statement.tokens);
103
+ admitStatement(statement.tokens, statement.sql);
104
+ return Object.freeze({ sql: statement.sql });
105
+ }),
106
+ );
107
+ }
108
+
109
+ function rejectFunctionBodyReferences(sql: string): void {
110
+ if (
111
+ /\bohmyhost\b|\brow\s+level\s+security\b|\b(?:create|alter|drop)\s+policy\b|\bu&['"]/iu.test(
112
+ sql,
113
+ )
114
+ ) {
115
+ throw new DatabaseMigrationAdmissionError("unsupported");
116
+ }
82
117
  const normalized = sql.toLowerCase();
83
118
  const withoutPortableAuth = normalized.replace(
84
- /\bauth\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu,
119
+ /(?:\bauth|"auth")\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu,
85
120
  "",
86
121
  );
87
122
  if (
88
- /\bauth\s*\./u.test(withoutPortableAuth) ||
89
- /\bstorage\s*\./u.test(normalized) ||
123
+ /(?:\bauth|"auth")\s*\./u.test(withoutPortableAuth) ||
124
+ /(?:\bstorage|"storage")\s*\./u.test(normalized) ||
90
125
  /\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) ||
91
- /\bcreate\s+policy\b/u.test(normalized) ||
92
- /\brow\s+level\s+security\b/u.test(normalized) ||
93
126
  /\bsupabase\b/u.test(normalized)
94
127
  ) {
95
128
  throw new DatabaseMigrationAdmissionError("provider_specific");
96
129
  }
97
130
  }
98
131
 
99
- function splitSqlStatements(sql: string): readonly string[] {
100
- const statements: string[] = [];
101
- let start = 0;
102
- let index = 0;
103
- while (index < sql.length) {
104
- const character = sql[index];
105
- const next = sql[index + 1];
106
- if (character === "-" && next === "-") {
107
- index = skipLineComment(sql, index + 2);
108
- continue;
109
- }
110
- if (character === "/" && next === "*") {
111
- index = skipBlockComment(sql, index + 2);
112
- continue;
113
- }
114
- if (character === "'") {
115
- index = skipQuoted(sql, index + 1, "'");
116
- continue;
117
- }
118
- if (character === '"') {
119
- index = skipQuoted(sql, index + 1, '"');
120
- continue;
121
- }
122
- if (character === "$") {
123
- const delimiter = dollarDelimiter(sql, index);
124
- if (delimiter !== null) {
125
- const end = sql.indexOf(delimiter, index + delimiter.length);
126
- if (end < 0) throw new DatabaseMigrationAdmissionError("invalid");
127
- index = end + delimiter.length;
128
- continue;
129
- }
130
- }
131
- if (character === ";") {
132
- const statement = sql.slice(start, index).trim();
133
- if (statement.length > 0) statements.push(statement);
134
- start = index + 1;
135
- }
136
- index += 1;
137
- }
138
- const tail = sql.slice(start).trim();
139
- if (tail.length > 0) statements.push(tail);
140
- return Object.freeze(statements);
141
- }
142
-
143
- function skipLineComment(sql: string, index: number): number {
144
- const newline = sql.indexOf("\n", index);
145
- return newline < 0 ? sql.length : newline + 1;
146
- }
147
-
148
- function skipBlockComment(sql: string, index: number): number {
149
- let depth = 1;
150
- while (index < sql.length) {
151
- if (sql[index] === "/" && sql[index + 1] === "*") {
152
- depth += 1;
153
- index += 2;
154
- continue;
155
- }
156
- if (sql[index] === "*" && sql[index + 1] === "/") {
157
- depth -= 1;
158
- index += 2;
159
- if (depth === 0) return index;
160
- continue;
161
- }
162
- index += 1;
163
- }
164
- throw new DatabaseMigrationAdmissionError("invalid");
165
- }
166
-
167
- function skipQuoted(sql: string, index: number, quote: "'" | '"'): number {
168
- while (index < sql.length) {
169
- if (sql[index] !== quote) {
170
- index += 1;
171
- continue;
172
- }
173
- if (sql[index + 1] === quote) {
174
- index += 2;
175
- continue;
176
- }
177
- return index + 1;
178
- }
179
- throw new DatabaseMigrationAdmissionError("invalid");
180
- }
181
-
182
- function dollarDelimiter(sql: string, index: number): string | null {
183
- const match = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index));
184
- return match?.[0] ?? null;
185
- }
186
-
187
- function admitStatement(statement: string): void {
188
- const normalized = normalizeStatement(statement);
132
+ function admitStatement(tokens: readonly MigrationSqlToken[], sql: string): void {
133
+ const normalized = tokens
134
+ .map((token) => (token.kind === "string" ? "'literal'" : token.text.toLowerCase()))
135
+ .join(" ")
136
+ .replace(/\s*\.\s*/gu, ".");
189
137
  if (normalized === "set check_function_bodies = false") return;
190
138
  if (/^create\s+function\b/u.test(normalized)) {
139
+ rejectFunctionBodyReferences(sql);
191
140
  if (
192
141
  !/\blanguage\s+(?:sql|plpgsql)\b/u.test(normalized) ||
193
142
  (/\bsecurity\s+definer\b/u.test(normalized) &&
@@ -224,12 +173,3 @@ function admitStatement(statement: string): void {
224
173
  }
225
174
  throw new DatabaseMigrationAdmissionError("unsupported");
226
175
  }
227
-
228
- function normalizeStatement(statement: string): string {
229
- return statement
230
- .replace(/--[^\n]*(?:\n|$)/gu, " ")
231
- .replace(/\/\*[\s\S]*?\*\//gu, " ")
232
- .trim()
233
- .replace(/\s+/gu, " ")
234
- .toLowerCase();
235
- }
@@ -0,0 +1,14 @@
1
+ export type DatabaseMigrationAdmissionReason =
2
+ | "destructive"
3
+ | "invalid"
4
+ | "provider_specific"
5
+ | "unsupported";
6
+
7
+ export class DatabaseMigrationAdmissionError extends Error {
8
+ public readonly code = "database_migration_not_admitted";
9
+
10
+ public constructor(public readonly reason: DatabaseMigrationAdmissionReason) {
11
+ super("Database migration is not admitted");
12
+ this.name = "DatabaseMigrationAdmissionError";
13
+ }
14
+ }
@@ -0,0 +1,180 @@
1
+ import { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
2
+
3
+ export interface MigrationSqlToken {
4
+ readonly kind: "word" | "identifier" | "string" | "symbol";
5
+ readonly value: string;
6
+ readonly text: string;
7
+ }
8
+
9
+ export interface MigrationSqlStatement {
10
+ readonly sql: string;
11
+ readonly tokens: readonly MigrationSqlToken[];
12
+ }
13
+
14
+ /** SQL-aware boundaries only; PostgreSQL remains the expression parser. */
15
+ export function readMigrationSql(sql: string): readonly MigrationSqlStatement[] {
16
+ const statements: MigrationSqlStatement[] = [];
17
+ let tokens: MigrationSqlToken[] = [];
18
+ let start = 0;
19
+ let index = 0;
20
+ while (index < sql.length) {
21
+ const character = sql[index] ?? "";
22
+ const next = sql[index + 1];
23
+ if (/\s/u.test(character)) {
24
+ index += 1;
25
+ continue;
26
+ }
27
+ if (character === "-" && next === "-") {
28
+ const newline = sql.indexOf("\n", index + 2);
29
+ index = newline < 0 ? sql.length : newline + 1;
30
+ continue;
31
+ }
32
+ if (character === "/" && next === "*") {
33
+ index = skipBlockComment(sql, index + 2);
34
+ continue;
35
+ }
36
+ if (character === ";") {
37
+ if (tokens.length > 0) {
38
+ statements.push(Object.freeze({ sql: sql.slice(start, index + 1).trim(), tokens }));
39
+ }
40
+ tokens = [];
41
+ start = ++index;
42
+ continue;
43
+ }
44
+ const tokenStart = index;
45
+ let kind: MigrationSqlToken["kind"] = "symbol";
46
+ let value = character;
47
+ if (/[uU]/u.test(character) && next === "&" && /['"]/u.test(sql[index + 2] ?? "")) {
48
+ // Unicode escapes can conceal reserved schema identifiers.
49
+ throw new DatabaseMigrationAdmissionError("unsupported");
50
+ }
51
+ if (character === "'" || character === '"' || (/[eE]/u.test(character) && next === "'")) {
52
+ const escaped = character !== "'" && character !== '"';
53
+ const quote = character === '"' ? '"' : "'";
54
+ const quoteStart = index + (escaped ? 1 : 0);
55
+ index = skipQuoted(sql, quoteStart + 1, quote, escaped);
56
+ kind = quote === '"' ? "identifier" : "string";
57
+ value = sql.slice(quoteStart + 1, index - 1).replace(/""/gu, '"');
58
+ } else if (character === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.test(sql.slice(index))) {
59
+ const delimiter = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index))?.[0] ?? "";
60
+ const end = sql.indexOf(delimiter, index + delimiter.length);
61
+ if (end < 0) throw new DatabaseMigrationAdmissionError("invalid");
62
+ index = end + delimiter.length;
63
+ kind = "string";
64
+ value = sql.slice(tokenStart, index);
65
+ } else if (/[\p{L}_]/u.test(character)) {
66
+ index += 1;
67
+ while (index < sql.length && /[\p{L}\p{N}_$]/u.test(sql[index] ?? "")) index += 1;
68
+ kind = "word";
69
+ value = sql.slice(tokenStart, index).toLowerCase();
70
+ } else {
71
+ index += 1;
72
+ }
73
+ tokens.push(Object.freeze({ kind, value, text: sql.slice(tokenStart, index) }));
74
+ }
75
+ if (tokens.length > 0) statements.push(Object.freeze({ sql: sql.slice(start).trim(), tokens }));
76
+ return Object.freeze(statements);
77
+ }
78
+
79
+ function skipBlockComment(sql: string, index: number): number {
80
+ let depth = 1;
81
+ while (index < sql.length) {
82
+ if (sql[index] === "/" && sql[index + 1] === "*") {
83
+ depth += 1;
84
+ index += 2;
85
+ } else if (sql[index] === "*" && sql[index + 1] === "/") {
86
+ depth -= 1;
87
+ index += 2;
88
+ if (depth === 0) return index;
89
+ } else {
90
+ index += 1;
91
+ }
92
+ }
93
+ throw new DatabaseMigrationAdmissionError("invalid");
94
+ }
95
+
96
+ function skipQuoted(sql: string, index: number, quote: string, escaped: boolean): number {
97
+ while (index < sql.length) {
98
+ if (escaped && sql[index] === "\\") {
99
+ index += 2;
100
+ } else if (sql[index] === quote) {
101
+ if (sql[index + 1] === quote) {
102
+ index += 2;
103
+ } else {
104
+ const continuation = quote === "'" ? continuedStringStart(sql, index + 1) : undefined;
105
+ if (continuation === undefined) return index + 1;
106
+ // Keep the initial E-string's escape semantics in every continuation.
107
+ index = continuation;
108
+ }
109
+ } else {
110
+ index += 1;
111
+ }
112
+ }
113
+ throw new DatabaseMigrationAdmissionError("invalid");
114
+ }
115
+
116
+ function continuedStringStart(sql: string, index: number): number | undefined {
117
+ // Match PostgreSQL scan.l quotecontinue with a linear scan: at least one
118
+ // newline, with line comments as whitespace. Block comments end continuation.
119
+ let newline = false;
120
+ while (index < sql.length) {
121
+ const character = sql[index] ?? "";
122
+ if (/[ \t\f\v\n\r]/u.test(character)) {
123
+ newline ||= character === "\n" || character === "\r";
124
+ index += 1;
125
+ } else if (character === "-" && sql[index + 1] === "-") {
126
+ index += 2;
127
+ while (index < sql.length && sql[index] !== "\n" && sql[index] !== "\r") index += 1;
128
+ } else {
129
+ return character === "'" && newline ? index + 1 : undefined;
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+
135
+ export function isSqlKeyword(token: MigrationSqlToken | undefined, value: string): boolean {
136
+ return token?.kind === "word" && token.value === value;
137
+ }
138
+
139
+ export function rejectMigrationSqlReferences(
140
+ tokens: readonly MigrationSqlToken[],
141
+ allowIdentityHelpers = false,
142
+ ): void {
143
+ for (let index = 0; index < tokens.length; index += 1) {
144
+ const token = tokens[index];
145
+ if (token?.kind !== "word" && token?.kind !== "identifier") continue;
146
+ const value = token.value.toLowerCase();
147
+ if (value === "ohmyhost") {
148
+ if (
149
+ allowIdentityHelpers &&
150
+ token.value === "ohmyhost" &&
151
+ tokens[index + 1]?.kind === "symbol" &&
152
+ tokens[index + 1]?.value === "." &&
153
+ ["user_id", "claims"].includes(tokens[index + 2]?.value ?? "") &&
154
+ ["word", "identifier"].includes(tokens[index + 2]?.kind ?? "") &&
155
+ tokens[index + 3]?.kind === "symbol" &&
156
+ tokens[index + 3]?.value === "(" &&
157
+ tokens[index + 4]?.kind === "symbol" &&
158
+ tokens[index + 4]?.value === ")"
159
+ )
160
+ continue;
161
+ throw new DatabaseMigrationAdmissionError("unsupported");
162
+ }
163
+ const portableAuthTable = tokens[index + 2];
164
+ if (
165
+ (value === "auth" &&
166
+ tokens[index + 1]?.value === "." &&
167
+ !(
168
+ portableAuthTable?.kind === "identifier" &&
169
+ ["account", "rateLimit", "ratelimit", "session", "user", "verification"].includes(
170
+ portableAuthTable.value,
171
+ )
172
+ )) ||
173
+ (value === "storage" && tokens[index + 1]?.value === ".") ||
174
+ value === "supabase" ||
175
+ ((value === "to" || value === "from") &&
176
+ ["anon", "authenticated", "service_role"].includes(tokens[index + 1]?.value ?? ""))
177
+ )
178
+ throw new DatabaseMigrationAdmissionError("provider_specific");
179
+ }
180
+ }
@@ -0,0 +1,150 @@
1
+ import { DatabaseMigrationAdmissionError } from "./database-migration-errors.js";
2
+ import {
3
+ isSqlKeyword,
4
+ rejectMigrationSqlReferences,
5
+ type MigrationSqlToken,
6
+ } from "./database-migration-lexical.js";
7
+
8
+ export interface DatabaseMigrationRlsTarget {
9
+ readonly schema: "public" | "private";
10
+ readonly table: string;
11
+ readonly policy?: string;
12
+ readonly action: "create" | "alter" | "drop" | "enable" | "force";
13
+ }
14
+
15
+ class PolicyReader {
16
+ public position = 0;
17
+
18
+ public constructor(private readonly tokens: readonly MigrationSqlToken[]) {}
19
+
20
+ public take(keyword: string): boolean {
21
+ if (!isSqlKeyword(this.tokens[this.position], keyword)) return false;
22
+ this.position += 1;
23
+ return true;
24
+ }
25
+
26
+ public expect(keyword: string): void {
27
+ if (!this.take(keyword)) throw new DatabaseMigrationAdmissionError("unsupported");
28
+ }
29
+
30
+ public identifier(): string {
31
+ const token = this.tokens[this.position++];
32
+ if (
33
+ (token?.kind !== "word" && token?.kind !== "identifier") ||
34
+ token.value.length === 0 ||
35
+ new TextEncoder().encode(token.value).byteLength > 63
36
+ ) {
37
+ throw new DatabaseMigrationAdmissionError("unsupported");
38
+ }
39
+ return token.value;
40
+ }
41
+
42
+ public target(): Readonly<{ schema: "public" | "private"; table: string }> {
43
+ const schema = this.identifier();
44
+ const separator = this.tokens[this.position++];
45
+ if (
46
+ (schema !== "public" && schema !== "private") ||
47
+ separator?.kind !== "symbol" ||
48
+ separator.value !== "."
49
+ ) {
50
+ throw new DatabaseMigrationAdmissionError("unsupported");
51
+ }
52
+ return { schema, table: this.identifier() };
53
+ }
54
+
55
+ public expression(): void {
56
+ const opening = this.tokens[this.position++];
57
+ if (opening?.kind !== "symbol" || opening.value !== "(")
58
+ throw new DatabaseMigrationAdmissionError("invalid");
59
+ const start = this.position;
60
+ let depth = 1;
61
+ while (this.position < this.tokens.length) {
62
+ const token = this.tokens[this.position++];
63
+ if (token?.kind !== "symbol") continue;
64
+ if (token.value === "(") depth += 1;
65
+ if (token.value === ")") depth -= 1;
66
+ if (depth === 0) {
67
+ if (this.position - 1 === start) throw new DatabaseMigrationAdmissionError("invalid");
68
+ rejectMigrationSqlReferences(this.tokens.slice(start, this.position - 1), true);
69
+ return;
70
+ }
71
+ }
72
+ throw new DatabaseMigrationAdmissionError("invalid");
73
+ }
74
+
75
+ public end(): void {
76
+ if (this.position !== this.tokens.length)
77
+ throw new DatabaseMigrationAdmissionError("unsupported");
78
+ }
79
+ }
80
+
81
+ export function readMigrationRlsTarget(
82
+ tokens: readonly MigrationSqlToken[],
83
+ ): DatabaseMigrationRlsTarget | undefined {
84
+ const first = tokens[0]?.value;
85
+ const policy =
86
+ tokens[0]?.kind === "word" &&
87
+ isSqlKeyword(tokens[1], "policy") &&
88
+ ["create", "alter", "drop"].includes(first ?? "");
89
+ const rls =
90
+ isSqlKeyword(tokens[0], "alter") &&
91
+ isSqlKeyword(tokens[1], "table") &&
92
+ tokens.some(
93
+ (token, index) =>
94
+ isSqlKeyword(token, "row") &&
95
+ isSqlKeyword(tokens[index + 1], "level") &&
96
+ isSqlKeyword(tokens[index + 2], "security"),
97
+ );
98
+ if (!policy && !rls) return undefined;
99
+ // Header and table identifiers must never use the reserved namespace. Helpers
100
+ // are admitted separately, only inside balanced policy expressions.
101
+ const reader = new PolicyReader(tokens);
102
+ reader.position = 2;
103
+ if (rls) {
104
+ rejectMigrationSqlReferences(tokens);
105
+ const target = reader.target();
106
+ const action = reader.take("enable") ? "enable" : reader.take("force") ? "force" : undefined;
107
+ if (action === undefined) throw new DatabaseMigrationAdmissionError("destructive");
108
+ reader.expect("row");
109
+ reader.expect("level");
110
+ reader.expect("security");
111
+ reader.end();
112
+ return Object.freeze({ ...target, action });
113
+ }
114
+ if (first === "drop" && reader.take("if")) reader.expect("exists");
115
+ const policyName = reader.identifier();
116
+ reader.expect("on");
117
+ const target = reader.target();
118
+ rejectMigrationSqlReferences(tokens.slice(0, reader.position));
119
+ if (first === "drop") {
120
+ reader.take("restrict");
121
+ reader.end();
122
+ return Object.freeze({ ...target, policy: policyName, action: "drop" });
123
+ }
124
+ if (first === "create" && reader.take("as")) {
125
+ if (!reader.take("permissive")) reader.expect("restrictive");
126
+ }
127
+ if (first === "create" && reader.take("for")) {
128
+ if (!["all", "select", "insert", "update", "delete"].some((command) => reader.take(command))) {
129
+ throw new DatabaseMigrationAdmissionError("unsupported");
130
+ }
131
+ }
132
+ if (reader.take("to")) reader.expect("public");
133
+ let expression = false;
134
+ if (reader.take("using")) {
135
+ reader.expression();
136
+ expression = true;
137
+ }
138
+ if (reader.take("with")) {
139
+ reader.expect("check");
140
+ reader.expression();
141
+ expression = true;
142
+ }
143
+ reader.end();
144
+ if (first === "alter" && !expression) throw new DatabaseMigrationAdmissionError("unsupported");
145
+ return Object.freeze({
146
+ ...target,
147
+ policy: policyName,
148
+ action: first === "create" ? "create" : "alter",
149
+ });
150
+ }
@@ -129,7 +129,7 @@ export interface FrameworkVersionAdmission {
129
129
  const VERIFIED_VERSIONS: Readonly<Record<AdmittedFramework, ReadonlySet<string>>> = Object.freeze({
130
130
  vite: new Set(["5.4.21", "8.0.16", "8.2.2"]),
131
131
  "tanstack-start": new Set(["1.168.26", "1.168.49"]),
132
- nextjs: new Set(["15.5.26", "16.3.7"]),
132
+ nextjs: new Set(["15.5.26", "16.3.8"]),
133
133
  });
134
134
 
135
135
  export function classifyFrameworkVersion(
@@ -14,8 +14,7 @@ export function isAdmittedNextVersion(version) {
14
14
  const patch = Number(match[3]);
15
15
  if (![major, minor, patch].every(Number.isSafeInteger)) return false;
16
16
  return (
17
- (major === 15 && minor === 5 && patch >= 26) ||
18
- (major === 16 && minor === 3 && patch >= 6 && patch <= 7)
17
+ (major === 15 && minor === 5 && patch >= 26) || (major === 16 && minor === 3 && patch === 8)
19
18
  );
20
19
  }
21
20