@amerged/ohmyhost-cli 0.1.29 → 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @amerged/ohmyhost-cli
2
2
 
3
- ohmyho.st cli client, version 0.1.29.
3
+ ohmyho.st cli client, version 0.1.30.
4
4
 
5
5
  ```sh
6
6
  npm install --global @amerged/ohmyhost-cli
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __ohmyhostCreateRequire } from "node:module"; const require = __ohmyhostCreateRequire(import.meta.url);
3
3
  import {
4
4
  runProductionCli
5
- } from "./chunk-Y4VB4EGQ.js";
5
+ } from "./chunk-PRLVV4ZG.js";
6
6
  import "./chunk-7D2RFLWM.js";
7
7
 
8
8
  // apps/product-cli/dist/bin.js
@@ -35600,7 +35600,7 @@ function isAdmittedNextVersion(version2) {
35600
35600
  const patch = Number(match[3]);
35601
35601
  if (![major, minor, patch].every(Number.isSafeInteger))
35602
35602
  return false;
35603
- return major === 15 && minor === 5 && patch >= 26 || major === 16 && minor === 3 && patch >= 6 && patch <= 7;
35603
+ return major === 15 && minor === 5 && patch >= 26 || major === 16 && minor === 3 && patch === 8;
35604
35604
  }
35605
35605
  var TYPESCRIPT_BUILD_STAGE = /^tsc(?:[ \t]+(?:--noEmit|-b|--build))?$/u;
35606
35606
  function isAdmittedFrameworkBuildScript(framework, script) {
@@ -35710,7 +35710,7 @@ function resolveFrameworkConfig(basename2, files) {
35710
35710
  var VERIFIED_VERSIONS = Object.freeze({
35711
35711
  vite: /* @__PURE__ */ new Set(["5.4.21", "8.0.16", "8.2.2"]),
35712
35712
  "tanstack-start": /* @__PURE__ */ new Set(["1.168.26", "1.168.49"]),
35713
- nextjs: /* @__PURE__ */ new Set(["15.5.26", "16.3.7"])
35713
+ nextjs: /* @__PURE__ */ new Set(["15.5.26", "16.3.8"])
35714
35714
  });
35715
35715
  function classifyFrameworkVersion(framework, version2) {
35716
35716
  const parsed = parseVersionSpec(version2);
@@ -40236,7 +40236,7 @@ function exact3(v, keys) {
40236
40236
  // apps/product-cli/package.json
40237
40237
  var package_default = {
40238
40238
  name: "@ohmyhost/product-cli",
40239
- version: "0.1.29",
40239
+ version: "0.1.30",
40240
40240
  private: true,
40241
40241
  ohmyhost: {
40242
40242
  deployment: "production",
@@ -46753,11 +46753,7 @@ function classifyFrameworkFamily(dependencies) {
46753
46753
  return vite ? "vite" : "unknown";
46754
46754
  }
46755
46755
 
46756
- // packages/contracts/dist/database-migration-admission.js
46757
- var MAX_MIGRATION_COUNT = 128;
46758
- var MAX_MIGRATION_BYTES = 256 * 1024;
46759
- var MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
46760
- var MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
46756
+ // packages/contracts/dist/database-migration-errors.js
46761
46757
  var DatabaseMigrationAdmissionError = class extends Error {
46762
46758
  reason;
46763
46759
  code = "database_migration_not_admitted";
@@ -46767,143 +46763,337 @@ var DatabaseMigrationAdmissionError = class extends Error {
46767
46763
  this.name = "DatabaseMigrationAdmissionError";
46768
46764
  }
46769
46765
  };
46770
- function admitExpandOnlyMigrations(migrations) {
46771
- if (!Array.isArray(migrations) || migrations.length === 0 || migrations.length > MAX_MIGRATION_COUNT) {
46772
- throw new DatabaseMigrationAdmissionError("invalid");
46773
- }
46774
- let previousPath = "";
46775
- let totalBytes = 0;
46776
- let statementCount = 0;
46777
- const files = migrations.map((migration) => {
46778
- if (typeof migration !== "object" || migration === null || !MIGRATION_PATH.test(migration.path) || migration.path <= previousPath || !(migration.body instanceof Uint8Array) || migration.body.byteLength === 0 || migration.body.byteLength > MAX_MIGRATION_BYTES) {
46779
- throw new DatabaseMigrationAdmissionError("invalid");
46780
- }
46781
- previousPath = migration.path;
46782
- totalBytes += migration.body.byteLength;
46783
- if (totalBytes > MAX_MIGRATION_TOTAL_BYTES || migration.body.includes(0)) {
46784
- throw new DatabaseMigrationAdmissionError("invalid");
46785
- }
46786
- let sql;
46787
- try {
46788
- sql = new TextDecoder("utf-8", { fatal: true }).decode(migration.body);
46789
- } catch {
46790
- throw new DatabaseMigrationAdmissionError("invalid");
46791
- }
46792
- if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 65279) {
46793
- throw new DatabaseMigrationAdmissionError("invalid");
46794
- }
46795
- if (/\bohmyhost\b/iu.test(sql))
46796
- throw new DatabaseMigrationAdmissionError("unsupported");
46797
- rejectProviderSpecificSql(sql);
46798
- const statements = splitSqlStatements(sql);
46799
- if (statements.length === 0)
46800
- throw new DatabaseMigrationAdmissionError("invalid");
46801
- for (const statement of statements)
46802
- admitStatement(statement);
46803
- statementCount += statements.length;
46804
- return Object.freeze({ path: migration.path, statementCount: statements.length });
46805
- });
46806
- return Object.freeze({ files: Object.freeze(files), statementCount });
46807
- }
46808
- function rejectProviderSpecificSql(sql) {
46809
- const normalized = sql.toLowerCase();
46810
- const withoutPortableAuth = normalized.replace(/\bauth\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu, "");
46811
- if (/\bauth\s*\./u.test(withoutPortableAuth) || /\bstorage\s*\./u.test(normalized) || /\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) || /\bcreate\s+policy\b/u.test(normalized) || /\brow\s+level\s+security\b/u.test(normalized) || /\bsupabase\b/u.test(normalized)) {
46812
- throw new DatabaseMigrationAdmissionError("provider_specific");
46813
- }
46814
- }
46815
- function splitSqlStatements(sql) {
46766
+
46767
+ // packages/contracts/dist/database-migration-lexical.js
46768
+ function readMigrationSql(sql) {
46816
46769
  const statements = [];
46770
+ let tokens = [];
46817
46771
  let start = 0;
46818
46772
  let index = 0;
46819
46773
  while (index < sql.length) {
46820
- const character = sql[index];
46774
+ const character = sql[index] ?? "";
46821
46775
  const next = sql[index + 1];
46776
+ if (/\s/u.test(character)) {
46777
+ index += 1;
46778
+ continue;
46779
+ }
46822
46780
  if (character === "-" && next === "-") {
46823
- index = skipLineComment(sql, index + 2);
46781
+ const newline = sql.indexOf("\n", index + 2);
46782
+ index = newline < 0 ? sql.length : newline + 1;
46824
46783
  continue;
46825
46784
  }
46826
46785
  if (character === "/" && next === "*") {
46827
46786
  index = skipBlockComment(sql, index + 2);
46828
46787
  continue;
46829
46788
  }
46830
- if (character === "'") {
46831
- index = skipQuoted(sql, index + 1, "'");
46832
- continue;
46833
- }
46834
- if (character === '"') {
46835
- index = skipQuoted(sql, index + 1, '"');
46789
+ if (character === ";") {
46790
+ if (tokens.length > 0) {
46791
+ statements.push(Object.freeze({ sql: sql.slice(start, index + 1).trim(), tokens }));
46792
+ }
46793
+ tokens = [];
46794
+ start = ++index;
46836
46795
  continue;
46837
46796
  }
46838
- if (character === "$") {
46839
- const delimiter = dollarDelimiter(sql, index);
46840
- if (delimiter !== null) {
46841
- const end = sql.indexOf(delimiter, index + delimiter.length);
46842
- if (end < 0)
46843
- throw new DatabaseMigrationAdmissionError("invalid");
46844
- index = end + delimiter.length;
46845
- continue;
46846
- }
46797
+ const tokenStart = index;
46798
+ let kind = "symbol";
46799
+ let value = character;
46800
+ if (/[uU]/u.test(character) && next === "&" && /['"]/u.test(sql[index + 2] ?? "")) {
46801
+ throw new DatabaseMigrationAdmissionError("unsupported");
46847
46802
  }
46848
- if (character === ";") {
46849
- const statement = sql.slice(start, index).trim();
46850
- if (statement.length > 0)
46851
- statements.push(statement);
46852
- start = index + 1;
46803
+ if (character === "'" || character === '"' || /[eE]/u.test(character) && next === "'") {
46804
+ const escaped = character !== "'" && character !== '"';
46805
+ const quote = character === '"' ? '"' : "'";
46806
+ const quoteStart = index + (escaped ? 1 : 0);
46807
+ index = skipQuoted(sql, quoteStart + 1, quote, escaped);
46808
+ kind = quote === '"' ? "identifier" : "string";
46809
+ value = sql.slice(quoteStart + 1, index - 1).replace(/""/gu, '"');
46810
+ } else if (character === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.test(sql.slice(index))) {
46811
+ const delimiter = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index))?.[0] ?? "";
46812
+ const end = sql.indexOf(delimiter, index + delimiter.length);
46813
+ if (end < 0)
46814
+ throw new DatabaseMigrationAdmissionError("invalid");
46815
+ index = end + delimiter.length;
46816
+ kind = "string";
46817
+ value = sql.slice(tokenStart, index);
46818
+ } else if (/[\p{L}_]/u.test(character)) {
46819
+ index += 1;
46820
+ while (index < sql.length && /[\p{L}\p{N}_$]/u.test(sql[index] ?? ""))
46821
+ index += 1;
46822
+ kind = "word";
46823
+ value = sql.slice(tokenStart, index).toLowerCase();
46824
+ } else {
46825
+ index += 1;
46853
46826
  }
46854
- index += 1;
46827
+ tokens.push(Object.freeze({ kind, value, text: sql.slice(tokenStart, index) }));
46855
46828
  }
46856
- const tail = sql.slice(start).trim();
46857
- if (tail.length > 0)
46858
- statements.push(tail);
46829
+ if (tokens.length > 0)
46830
+ statements.push(Object.freeze({ sql: sql.slice(start).trim(), tokens }));
46859
46831
  return Object.freeze(statements);
46860
46832
  }
46861
- function skipLineComment(sql, index) {
46862
- const newline = sql.indexOf("\n", index);
46863
- return newline < 0 ? sql.length : newline + 1;
46864
- }
46865
46833
  function skipBlockComment(sql, index) {
46866
46834
  let depth = 1;
46867
46835
  while (index < sql.length) {
46868
46836
  if (sql[index] === "/" && sql[index + 1] === "*") {
46869
46837
  depth += 1;
46870
46838
  index += 2;
46871
- continue;
46872
- }
46873
- if (sql[index] === "*" && sql[index + 1] === "/") {
46839
+ } else if (sql[index] === "*" && sql[index + 1] === "/") {
46874
46840
  depth -= 1;
46875
46841
  index += 2;
46876
46842
  if (depth === 0)
46877
46843
  return index;
46878
- continue;
46844
+ } else {
46845
+ index += 1;
46879
46846
  }
46880
- index += 1;
46881
46847
  }
46882
46848
  throw new DatabaseMigrationAdmissionError("invalid");
46883
46849
  }
46884
- function skipQuoted(sql, index, quote) {
46850
+ function skipQuoted(sql, index, quote, escaped) {
46885
46851
  while (index < sql.length) {
46886
- if (sql[index] !== quote) {
46852
+ if (escaped && sql[index] === "\\") {
46853
+ index += 2;
46854
+ } else if (sql[index] === quote) {
46855
+ if (sql[index + 1] === quote) {
46856
+ index += 2;
46857
+ } else {
46858
+ const continuation = quote === "'" ? continuedStringStart(sql, index + 1) : void 0;
46859
+ if (continuation === void 0)
46860
+ return index + 1;
46861
+ index = continuation;
46862
+ }
46863
+ } else {
46887
46864
  index += 1;
46888
- continue;
46889
46865
  }
46890
- if (sql[index + 1] === quote) {
46866
+ }
46867
+ throw new DatabaseMigrationAdmissionError("invalid");
46868
+ }
46869
+ function continuedStringStart(sql, index) {
46870
+ let newline = false;
46871
+ while (index < sql.length) {
46872
+ const character = sql[index] ?? "";
46873
+ if (/[ \t\f\v\n\r]/u.test(character)) {
46874
+ newline ||= character === "\n" || character === "\r";
46875
+ index += 1;
46876
+ } else if (character === "-" && sql[index + 1] === "-") {
46891
46877
  index += 2;
46878
+ while (index < sql.length && sql[index] !== "\n" && sql[index] !== "\r")
46879
+ index += 1;
46880
+ } else {
46881
+ return character === "'" && newline ? index + 1 : void 0;
46882
+ }
46883
+ }
46884
+ return void 0;
46885
+ }
46886
+ function isSqlKeyword(token, value) {
46887
+ return token?.kind === "word" && token.value === value;
46888
+ }
46889
+ function rejectMigrationSqlReferences(tokens, allowIdentityHelpers = false) {
46890
+ for (let index = 0; index < tokens.length; index += 1) {
46891
+ const token = tokens[index];
46892
+ if (token?.kind !== "word" && token?.kind !== "identifier")
46892
46893
  continue;
46894
+ const value = token.value.toLowerCase();
46895
+ if (value === "ohmyhost") {
46896
+ if (allowIdentityHelpers && token.value === "ohmyhost" && tokens[index + 1]?.kind === "symbol" && tokens[index + 1]?.value === "." && ["user_id", "claims"].includes(tokens[index + 2]?.value ?? "") && ["word", "identifier"].includes(tokens[index + 2]?.kind ?? "") && tokens[index + 3]?.kind === "symbol" && tokens[index + 3]?.value === "(" && tokens[index + 4]?.kind === "symbol" && tokens[index + 4]?.value === ")")
46897
+ continue;
46898
+ throw new DatabaseMigrationAdmissionError("unsupported");
46893
46899
  }
46894
- return index + 1;
46900
+ const portableAuthTable = tokens[index + 2];
46901
+ if (value === "auth" && tokens[index + 1]?.value === "." && !(portableAuthTable?.kind === "identifier" && ["account", "rateLimit", "ratelimit", "session", "user", "verification"].includes(portableAuthTable.value)) || value === "storage" && tokens[index + 1]?.value === "." || value === "supabase" || (value === "to" || value === "from") && ["anon", "authenticated", "service_role"].includes(tokens[index + 1]?.value ?? ""))
46902
+ throw new DatabaseMigrationAdmissionError("provider_specific");
46895
46903
  }
46896
- throw new DatabaseMigrationAdmissionError("invalid");
46897
46904
  }
46898
- function dollarDelimiter(sql, index) {
46899
- const match = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(sql.slice(index));
46900
- return match?.[0] ?? null;
46905
+
46906
+ // packages/contracts/dist/database-migration-policies.js
46907
+ var PolicyReader = class {
46908
+ tokens;
46909
+ position = 0;
46910
+ constructor(tokens) {
46911
+ this.tokens = tokens;
46912
+ }
46913
+ take(keyword) {
46914
+ if (!isSqlKeyword(this.tokens[this.position], keyword))
46915
+ return false;
46916
+ this.position += 1;
46917
+ return true;
46918
+ }
46919
+ expect(keyword) {
46920
+ if (!this.take(keyword))
46921
+ throw new DatabaseMigrationAdmissionError("unsupported");
46922
+ }
46923
+ identifier() {
46924
+ const token = this.tokens[this.position++];
46925
+ if (token?.kind !== "word" && token?.kind !== "identifier" || token.value.length === 0 || new TextEncoder().encode(token.value).byteLength > 63) {
46926
+ throw new DatabaseMigrationAdmissionError("unsupported");
46927
+ }
46928
+ return token.value;
46929
+ }
46930
+ target() {
46931
+ const schema = this.identifier();
46932
+ const separator = this.tokens[this.position++];
46933
+ if (schema !== "public" && schema !== "private" || separator?.kind !== "symbol" || separator.value !== ".") {
46934
+ throw new DatabaseMigrationAdmissionError("unsupported");
46935
+ }
46936
+ return { schema, table: this.identifier() };
46937
+ }
46938
+ expression() {
46939
+ const opening = this.tokens[this.position++];
46940
+ if (opening?.kind !== "symbol" || opening.value !== "(")
46941
+ throw new DatabaseMigrationAdmissionError("invalid");
46942
+ const start = this.position;
46943
+ let depth = 1;
46944
+ while (this.position < this.tokens.length) {
46945
+ const token = this.tokens[this.position++];
46946
+ if (token?.kind !== "symbol")
46947
+ continue;
46948
+ if (token.value === "(")
46949
+ depth += 1;
46950
+ if (token.value === ")")
46951
+ depth -= 1;
46952
+ if (depth === 0) {
46953
+ if (this.position - 1 === start)
46954
+ throw new DatabaseMigrationAdmissionError("invalid");
46955
+ rejectMigrationSqlReferences(this.tokens.slice(start, this.position - 1), true);
46956
+ return;
46957
+ }
46958
+ }
46959
+ throw new DatabaseMigrationAdmissionError("invalid");
46960
+ }
46961
+ end() {
46962
+ if (this.position !== this.tokens.length)
46963
+ throw new DatabaseMigrationAdmissionError("unsupported");
46964
+ }
46965
+ };
46966
+ function readMigrationRlsTarget(tokens) {
46967
+ const first = tokens[0]?.value;
46968
+ const policy = tokens[0]?.kind === "word" && isSqlKeyword(tokens[1], "policy") && ["create", "alter", "drop"].includes(first ?? "");
46969
+ const rls = isSqlKeyword(tokens[0], "alter") && isSqlKeyword(tokens[1], "table") && tokens.some((token, index) => isSqlKeyword(token, "row") && isSqlKeyword(tokens[index + 1], "level") && isSqlKeyword(tokens[index + 2], "security"));
46970
+ if (!policy && !rls)
46971
+ return void 0;
46972
+ const reader = new PolicyReader(tokens);
46973
+ reader.position = 2;
46974
+ if (rls) {
46975
+ rejectMigrationSqlReferences(tokens);
46976
+ const target2 = reader.target();
46977
+ const action = reader.take("enable") ? "enable" : reader.take("force") ? "force" : void 0;
46978
+ if (action === void 0)
46979
+ throw new DatabaseMigrationAdmissionError("destructive");
46980
+ reader.expect("row");
46981
+ reader.expect("level");
46982
+ reader.expect("security");
46983
+ reader.end();
46984
+ return Object.freeze({ ...target2, action });
46985
+ }
46986
+ if (first === "drop" && reader.take("if"))
46987
+ reader.expect("exists");
46988
+ const policyName = reader.identifier();
46989
+ reader.expect("on");
46990
+ const target = reader.target();
46991
+ rejectMigrationSqlReferences(tokens.slice(0, reader.position));
46992
+ if (first === "drop") {
46993
+ reader.take("restrict");
46994
+ reader.end();
46995
+ return Object.freeze({ ...target, policy: policyName, action: "drop" });
46996
+ }
46997
+ if (first === "create" && reader.take("as")) {
46998
+ if (!reader.take("permissive"))
46999
+ reader.expect("restrictive");
47000
+ }
47001
+ if (first === "create" && reader.take("for")) {
47002
+ if (!["all", "select", "insert", "update", "delete"].some((command) => reader.take(command))) {
47003
+ throw new DatabaseMigrationAdmissionError("unsupported");
47004
+ }
47005
+ }
47006
+ if (reader.take("to"))
47007
+ reader.expect("public");
47008
+ let expression = false;
47009
+ if (reader.take("using")) {
47010
+ reader.expression();
47011
+ expression = true;
47012
+ }
47013
+ if (reader.take("with")) {
47014
+ reader.expect("check");
47015
+ reader.expression();
47016
+ expression = true;
47017
+ }
47018
+ reader.end();
47019
+ if (first === "alter" && !expression)
47020
+ throw new DatabaseMigrationAdmissionError("unsupported");
47021
+ return Object.freeze({
47022
+ ...target,
47023
+ policy: policyName,
47024
+ action: first === "create" ? "create" : "alter"
47025
+ });
46901
47026
  }
46902
- function admitStatement(statement) {
46903
- const normalized = normalizeStatement(statement);
47027
+
47028
+ // packages/contracts/dist/database-migration-admission.js
47029
+ var MAX_MIGRATION_COUNT = 128;
47030
+ var MAX_MIGRATION_BYTES = 256 * 1024;
47031
+ var MAX_MIGRATION_TOTAL_BYTES = 2 * 1024 * 1024;
47032
+ var MIGRATION_PATH = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
47033
+ function admitExpandOnlyMigrations(migrations) {
47034
+ if (!Array.isArray(migrations) || migrations.length === 0 || migrations.length > MAX_MIGRATION_COUNT) {
47035
+ throw new DatabaseMigrationAdmissionError("invalid");
47036
+ }
47037
+ let previousPath = "";
47038
+ let totalBytes = 0;
47039
+ let statementCount = 0;
47040
+ const files = migrations.map((migration) => {
47041
+ if (typeof migration !== "object" || migration === null || !MIGRATION_PATH.test(migration.path) || migration.path <= previousPath || !(migration.body instanceof Uint8Array) || migration.body.byteLength === 0 || migration.body.byteLength > MAX_MIGRATION_BYTES) {
47042
+ throw new DatabaseMigrationAdmissionError("invalid");
47043
+ }
47044
+ previousPath = migration.path;
47045
+ totalBytes += migration.body.byteLength;
47046
+ if (totalBytes > MAX_MIGRATION_TOTAL_BYTES || migration.body.includes(0)) {
47047
+ throw new DatabaseMigrationAdmissionError("invalid");
47048
+ }
47049
+ let sql;
47050
+ try {
47051
+ sql = new TextDecoder("utf-8", { fatal: true }).decode(migration.body);
47052
+ } catch {
47053
+ throw new DatabaseMigrationAdmissionError("invalid");
47054
+ }
47055
+ if (sql.length === 0 || sql.includes("\r") || sql.charCodeAt(0) === 65279) {
47056
+ throw new DatabaseMigrationAdmissionError("invalid");
47057
+ }
47058
+ const statements = parseDatabaseMigrationStatements(sql);
47059
+ statementCount += statements.length;
47060
+ return Object.freeze({ path: migration.path, statementCount: statements.length });
47061
+ });
47062
+ return Object.freeze({ files: Object.freeze(files), statementCount });
47063
+ }
47064
+ function parseDatabaseMigrationStatements(sql) {
47065
+ if (sql.length === 0 || sql.includes("\0") || sql.includes("\r") || sql.charCodeAt(0) === 65279) {
47066
+ throw new DatabaseMigrationAdmissionError("invalid");
47067
+ }
47068
+ const statements = readMigrationSql(sql);
47069
+ if (statements.length === 0)
47070
+ throw new DatabaseMigrationAdmissionError("invalid");
47071
+ return Object.freeze(statements.map((statement) => {
47072
+ rejectMigrationSqlReferences(statement.tokens, true);
47073
+ const rlsTarget = readMigrationRlsTarget(statement.tokens);
47074
+ if (rlsTarget !== void 0)
47075
+ return Object.freeze({ sql: statement.sql, rlsTarget });
47076
+ rejectMigrationSqlReferences(statement.tokens);
47077
+ admitStatement(statement.tokens, statement.sql);
47078
+ return Object.freeze({ sql: statement.sql });
47079
+ }));
47080
+ }
47081
+ function rejectFunctionBodyReferences(sql) {
47082
+ if (/\bohmyhost\b|\brow\s+level\s+security\b|\b(?:create|alter|drop)\s+policy\b|\bu&['"]/iu.test(sql)) {
47083
+ throw new DatabaseMigrationAdmissionError("unsupported");
47084
+ }
47085
+ const normalized = sql.toLowerCase();
47086
+ const withoutPortableAuth = normalized.replace(/(?:\bauth|"auth")\s*\.\s*"(?:account|ratelimit|session|user|verification)"/gu, "");
47087
+ if (/(?:\bauth|"auth")\s*\./u.test(withoutPortableAuth) || /(?:\bstorage|"storage")\s*\./u.test(normalized) || /\b(?:to|from)\s+(?:anon|authenticated|service_role)\b/u.test(normalized) || /\bsupabase\b/u.test(normalized)) {
47088
+ throw new DatabaseMigrationAdmissionError("provider_specific");
47089
+ }
47090
+ }
47091
+ function admitStatement(tokens, sql) {
47092
+ const normalized = tokens.map((token) => token.kind === "string" ? "'literal'" : token.text.toLowerCase()).join(" ").replace(/\s*\.\s*/gu, ".");
46904
47093
  if (normalized === "set check_function_bodies = false")
46905
47094
  return;
46906
47095
  if (/^create\s+function\b/u.test(normalized)) {
47096
+ rejectFunctionBodyReferences(sql);
46907
47097
  if (!/\blanguage\s+(?:sql|plpgsql)\b/u.test(normalized) || /\bsecurity\s+definer\b/u.test(normalized) && !/\bset\s+search_path\s*(?:=\s*|to\s+)/u.test(normalized)) {
46908
47098
  throw new DatabaseMigrationAdmissionError("unsupported");
46909
47099
  }
@@ -46923,9 +47113,6 @@ function admitStatement(statement) {
46923
47113
  }
46924
47114
  throw new DatabaseMigrationAdmissionError("unsupported");
46925
47115
  }
46926
- function normalizeStatement(statement) {
46927
- return statement.replace(/--[^\n]*(?:\n|$)/gu, " ").replace(/\/\*[\s\S]*?\*\//gu, " ").trim().replace(/\s+/gu, " ").toLowerCase();
46928
- }
46929
47116
 
46930
47117
  // apps/product-cli/dist/repository-init.js
46931
47118
  var MAXIMUM_PACKAGE_BYTES = 1024 * 1024;
@@ -47648,7 +47835,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
47648
47835
  const unsupported = compatibility.frameworks.filter((entry) => entry.classification === "unsupported").map((entry) => `${FRAMEWORK_PACKAGE[entry.framework]}@${entry.version || "missing"}`).join(", ");
47649
47836
  blockers.push({
47650
47837
  code: "framework_version_unsupported",
47651
- message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.6\u201316.3.7, vite 5.4.0\u20138.2.2, @tanstack/react-start 1.168.26\u20131.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`
47838
+ message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.8, vite 5.4.0\u20138.2.2, @tanstack/react-start 1.168.26\u20131.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`
47652
47839
  });
47653
47840
  }
47654
47841
  const scripts = packageJson["scripts"];
@@ -47686,7 +47873,7 @@ function repositoryBlockers(packageJson, framework, nextStandaloneOutput, tanSta
47686
47873
  if (!nextConfigured) {
47687
47874
  blockers.push({
47688
47875
  code: "next_adapter_unconfigured",
47689
- message: 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.6\u201316.3.7, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.'
47876
+ message: 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.8, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.'
47690
47877
  });
47691
47878
  }
47692
47879
  }
package/dist/index.js CHANGED
@@ -99,7 +99,7 @@ import {
99
99
  selectWorkspace,
100
100
  verifiedUserId,
101
101
  workspaceOf
102
- } from "./chunk-Y4VB4EGQ.js";
102
+ } from "./chunk-PRLVV4ZG.js";
103
103
  import {
104
104
  CredentialStoreIntegrityError,
105
105
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",
@@ -1242,7 +1242,7 @@ function repositoryBlockers(
1242
1242
  .join(", ");
1243
1243
  blockers.push({
1244
1244
  code: "framework_version_unsupported",
1245
- message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.6–16.3.7, vite 5.4.0–8.2.2, @tanstack/react-start 1.168.26–1.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`,
1245
+ message: `${unsupported} is outside the versions ohmyho.st builds: next 15.5.26+ (15.5.x) or 16.3.8, vite 5.4.0–8.2.2, @tanstack/react-start 1.168.26–1.168.49, as an exact version or a ^ or ~ range whose base version is inside. Pin a version in that range in package.json, update the lockfile, commit and push.`,
1246
1246
  });
1247
1247
  }
1248
1248
  const scripts = packageJson["scripts"];
@@ -1297,7 +1297,7 @@ function repositoryBlockers(
1297
1297
  blockers.push({
1298
1298
  code: "next_adapter_unconfigured",
1299
1299
  message:
1300
- 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.6–16.3.7, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.',
1300
+ 'Next.js needs scripts.build exactly "next build" or "next build --webpack", react and react-dom at exact versions such as "19.1.0" (no ^ or ~), next within 15.5.26+ (15.5.x) or 16.3.8, and exactly one next.config.js, .mjs or .ts, with at most one open-next.config file. ohmyho.st adds the OpenNext build itself; do not make it the build script.',
1301
1301
  });
1302
1302
  }
1303
1303
  }
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/sdk-ts",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {