@amerged/ohmyhost-mcp 0.1.37 → 0.1.39

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 (45) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +27 -1
  3. package/dist/{chunk-CHAFMFTK.js → chunk-KQS5IMSO.js} +7606 -2403
  4. package/dist/index.js +1 -1
  5. package/dist/licenses/pgcf-bridge-LICENSE +201 -0
  6. package/dist/licenses/pgcf-bridge-NOTICE +1 -0
  7. package/dist/stdio-main.js +3853 -34
  8. package/package.json +1 -1
  9. package/src/apps/mcp/package.json +4 -4
  10. package/src/apps/mcp/src/local-client.ts +15 -9
  11. package/src/apps/mcp/src/local-server.ts +36 -18
  12. package/src/apps/product-cli/package.json +4 -3
  13. package/src/apps/product-cli/src/cli.ts +7 -5
  14. package/src/apps/product-cli/src/command.ts +18 -5
  15. package/src/apps/product-cli/src/product-api.ts +22 -15
  16. package/src/apps/product-cli/src/psql-session.ts +54 -19
  17. package/src/apps/product-cli/src/repository-init.ts +181 -9
  18. package/src/packages/agent-skills/package.json +1 -1
  19. package/src/packages/agent-skills/src/generated-skill-resources.ts +16 -16
  20. package/src/packages/contracts/package.json +6 -1
  21. package/src/packages/contracts/src/credit-pricing.ts +7 -1
  22. package/src/packages/contracts/src/database-access.ts +53 -12
  23. package/src/packages/contracts/src/database-compute.ts +66 -11
  24. package/src/packages/contracts/src/database-sizes.ts +114 -0
  25. package/src/packages/contracts/src/framework-admission-corpus.ts +15 -18
  26. package/src/packages/contracts/src/framework-build-script.mjs +10 -3
  27. package/src/packages/contracts/src/index.ts +3 -0
  28. package/src/packages/contracts/src/operation-failure.ts +2 -2
  29. package/src/packages/contracts/src/platform-origins.ts +8 -0
  30. package/src/packages/contracts/src/problem-policy.ts +16 -2
  31. package/src/packages/contracts/src/storage-gateway-origin.ts +93 -0
  32. package/src/packages/postgres-bridge/LICENSE +201 -0
  33. package/src/packages/postgres-bridge/NOTICE +1 -0
  34. package/src/packages/postgres-bridge/package.json +26 -0
  35. package/src/packages/postgres-bridge/src/auth.ts +159 -0
  36. package/src/packages/postgres-bridge/src/bridge.ts +168 -0
  37. package/src/packages/postgres-bridge/src/index.ts +2 -0
  38. package/src/packages/postgres-bridge/src/options.ts +49 -0
  39. package/src/packages/sdk-ts/package.json +2 -2
  40. package/src/packages/sdk-ts/src/generated/index.ts +1 -0
  41. package/src/packages/sdk-ts/src/generated/sdk.gen.ts +9 -8
  42. package/src/packages/sdk-ts/src/generated/types.gen.ts +91 -24
  43. package/src/packages/sdk-ts/src/index.ts +1 -0
  44. package/src/packages/sdk-ts/src/managed-mail-attachment.ts +107 -0
  45. package/src/packages/workos-auth-contracts/package.json +1 -1
@@ -5,6 +5,11 @@
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "exports": {
8
+ "./database-sizes": {
9
+ "source": "./src/database-sizes.ts",
10
+ "types": "./dist/database-sizes.d.ts",
11
+ "import": "./dist/database-sizes.js"
12
+ },
8
13
  "./shared-mail": {
9
14
  "source": "./src/shared-mail.ts",
10
15
  "types": "./dist/shared-mail.d.ts",
@@ -150,7 +155,7 @@
150
155
  },
151
156
  "scripts": {
152
157
  "build": "tsc --project tsconfig.build.json",
153
- "typecheck": "tsc --project tsconfig.json --noEmit",
158
+ "typecheck": "tsc --project tsconfig.json --noEmit --incremental --tsBuildInfoFile typecheck.tsbuildinfo",
154
159
  "test": "vitest run --root ../.. packages/contracts/test",
155
160
  "validate": "pnpm run typecheck && pnpm run test"
156
161
  },
@@ -96,7 +96,7 @@ export interface PublishedCreditRateCard {
96
96
  readonly meter: string;
97
97
  readonly unit: string;
98
98
  readonly units_per_charge: string;
99
- readonly provider_cost_micros: string;
99
+ readonly provider_cost_micros: string | null;
100
100
  readonly credit_micros: string;
101
101
  }[];
102
102
  }
@@ -159,6 +159,12 @@ export function assertPublishedCreditRateCards(
159
159
  meters.add(identity);
160
160
  for (const key of ["units_per_charge", "provider_cost_micros", "credit_micros"]) {
161
161
  const amount = rate[key];
162
+ if (
163
+ key === "provider_cost_micros" &&
164
+ amount === null &&
165
+ rate["meter"] === "pgcf.memory.active"
166
+ )
167
+ continue;
162
168
  if (
163
169
  typeof amount !== "string" ||
164
170
  !/^(0|[1-9][0-9]{0,18})$/u.test(amount) ||
@@ -93,13 +93,12 @@ export interface ProjectDatabaseAccessRequest {
93
93
  readonly ttl_seconds: number;
94
94
  readonly label: string | null;
95
95
  }
96
- export interface ProjectDatabaseAccess {
96
+ export interface ProjectDatabaseAccessFields {
97
97
  readonly access_id: string;
98
98
  readonly environment: DatabaseEnvironment;
99
99
  readonly mode: DatabaseAccessMode;
100
100
  readonly role_name: string;
101
101
  readonly host: string;
102
- readonly database: "neondb";
103
102
  readonly label: string | null;
104
103
  readonly state: "active" | "expired" | "revoked";
105
104
  readonly issued_at: string;
@@ -113,10 +112,25 @@ export interface ProjectDatabaseAccess {
113
112
  | "data_change"
114
113
  | null;
115
114
  }
116
- export interface ProjectDatabaseAccessCredential extends ProjectDatabaseAccess {
115
+ export type ProjectDatabaseAccess = ProjectDatabaseAccessFields &
116
+ (
117
+ | {
118
+ readonly database: "neondb";
119
+ readonly provider?: never;
120
+ readonly transport?: never;
121
+ readonly websocket_url?: never;
122
+ }
123
+ | {
124
+ readonly provider: "pgcf";
125
+ readonly transport: "wss";
126
+ readonly websocket_url: string;
127
+ readonly database: string;
128
+ }
129
+ );
130
+ export type ProjectDatabaseAccessCredential = ProjectDatabaseAccess & {
117
131
  readonly connection_uri: string;
118
132
  readonly psql_command: string;
119
- }
133
+ };
120
134
  export interface ProjectDatabaseAccessPage {
121
135
  readonly items: readonly ProjectDatabaseAccess[];
122
136
  }
@@ -163,17 +177,27 @@ export function parseProjectDatabaseAccessRequest(value: unknown): ProjectDataba
163
177
  export function assertProjectDatabaseAccess(
164
178
  value: unknown,
165
179
  ): asserts value is ProjectDatabaseAccess {
166
- if (!record(value) || !exact(value, ACCESS_KEYS)) throw new TypeError("Invalid database access");
180
+ if (!record(value) || !exact(value, accessKeys(value)))
181
+ throw new TypeError("Invalid database access");
167
182
  assertAccessFields(value);
168
183
  }
169
184
  export function assertProjectDatabaseAccessCredential(
170
185
  value: unknown,
171
186
  ): asserts value is ProjectDatabaseAccessCredential {
172
- if (!record(value) || !exact(value, [...ACCESS_KEYS, "connection_uri", "psql_command"]))
187
+ if (!record(value) || !exact(value, [...accessKeys(value), "connection_uri", "psql_command"]))
173
188
  throw new TypeError("Invalid database access credential");
174
189
  assertAccessFields(value);
175
190
  const uri = value["connection_uri"];
176
- if (typeof uri !== "string" || value["psql_command"] !== `psql '${uri}'`)
191
+ if (
192
+ typeof uri !== "string" ||
193
+ (value["provider"] === "pgcf"
194
+ ? typeof value["psql_command"] !== "string" ||
195
+ !new RegExp(
196
+ `^ohmyhost database psql --project [0-7][0-9A-HJKMNP-TV-Z]{25} --environment ${value["environment"]} --mode ${value["mode"]}$`,
197
+ "u",
198
+ ).test(value["psql_command"])
199
+ : value["psql_command"] !== `psql '${uri}'`)
200
+ )
177
201
  throw new TypeError("Invalid database access credential");
178
202
  let parsed: URL;
179
203
  try {
@@ -182,13 +206,20 @@ export function assertProjectDatabaseAccessCredential(
182
206
  throw new TypeError("Invalid database access credential");
183
207
  }
184
208
  if (
185
- parsed.protocol !== "postgresql:" ||
209
+ (value["provider"] === "pgcf"
210
+ ? !["postgres:", "postgresql:"].includes(parsed.protocol)
211
+ : parsed.protocol !== "postgresql:") ||
186
212
  decodeURIComponent(parsed.username) !== value["role_name"] ||
187
213
  parsed.password === "" ||
188
214
  parsed.hostname !== value["host"] ||
189
- parsed.host !== value["host"] ||
190
- decodeURIComponent(parsed.pathname.slice(1)) !== "neondb" ||
191
- parsed.search !== "?sslmode=require"
215
+ (value["provider"] === "pgcf"
216
+ ? parsed.port !== "" && parsed.port !== "5432"
217
+ : parsed.host !== value["host"]) ||
218
+ decodeURIComponent(parsed.pathname.slice(1)) !== value["database"] ||
219
+ parsed.hash !== "" ||
220
+ (value["provider"] === "pgcf"
221
+ ? !["", "?sslmode=require", "?sslmode=verify-full"].includes(parsed.search)
222
+ : parsed.search !== "?sslmode=require")
192
223
  )
193
224
  throw new TypeError("Invalid database access credential");
194
225
  }
@@ -204,6 +235,11 @@ export function assertProjectDatabaseAccessPage(
204
235
  throw new TypeError("Invalid database access page");
205
236
  for (const item of value["items"]) assertProjectDatabaseAccess(item);
206
237
  }
238
+ function accessKeys(value: Record<string, unknown>): readonly string[] {
239
+ return value["provider"] === "pgcf"
240
+ ? [...ACCESS_KEYS, "provider", "transport", "websocket_url"]
241
+ : ACCESS_KEYS;
242
+ }
207
243
  function assertAccessFields(value: Record<string, unknown>): void {
208
244
  const revoked = value["revoked_at"],
209
245
  reason = value["revocation_reason"];
@@ -216,7 +252,12 @@ function assertAccessFields(value: Record<string, unknown>): void {
216
252
  !ACCESS_ROLE_PATTERN.test(value["role_name"]) ||
217
253
  typeof value["host"] !== "string" ||
218
254
  !/^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$/u.test(value["host"]) ||
219
- value["database"] !== "neondb" ||
255
+ (value["provider"] === "pgcf"
256
+ ? typeof value["database"] !== "string" ||
257
+ !/^[a-z][a-z0-9]{19}$/u.test(value["database"]) ||
258
+ value["transport"] !== "wss" ||
259
+ value["websocket_url"] !== `wss://${value["host"]}/`
260
+ : value["database"] !== "neondb") ||
220
261
  (value["label"] !== null && accessLabel(value["label"]) !== value["label"]) ||
221
262
  !["active", "expired", "revoked"].includes(String(value["state"])) ||
222
263
  !timestamp(value["issued_at"]) ||
@@ -1,19 +1,39 @@
1
+ import { databaseSizeForClass } from "./database-sizes.js";
2
+
3
+ export type PgcfComputeState = "provisioning" | "cold" | "warm" | "suspended" | "error";
4
+ export interface PgcfDatabaseCompute {
5
+ readonly provider: "pgcf";
6
+ readonly size_class_id: string;
7
+ readonly memory_mib: number;
8
+ readonly cpu_millicores: number;
9
+ readonly storage_mib: number;
10
+ readonly max_connections: number;
11
+ readonly suspend_timeout_seconds: number;
12
+ readonly state: PgcfComputeState;
13
+ readonly pending_state: PgcfComputeState | null;
14
+ readonly disabled: boolean;
15
+ readonly region: "us" | "eu";
16
+ }
17
+
1
18
  export interface DatabaseCompute {
2
19
  readonly project_id: string;
3
20
  readonly environment: "dev" | "prod";
4
21
  readonly data_mode: "shared" | "isolated";
5
22
  readonly observed_at: string;
6
- readonly database: null | {
7
- readonly min_cu: number;
8
- readonly max_cu: number;
9
- readonly min_memory_gb: number;
10
- readonly max_memory_gb: number;
11
- readonly suspend_timeout_seconds: number;
12
- readonly state: "init" | "active" | "idle";
13
- readonly pending_state: "init" | "active" | "idle" | null;
14
- readonly disabled: boolean;
15
- readonly region: string;
16
- };
23
+ readonly database:
24
+ | null
25
+ | PgcfDatabaseCompute
26
+ | {
27
+ readonly min_cu: number;
28
+ readonly max_cu: number;
29
+ readonly min_memory_gb: number;
30
+ readonly max_memory_gb: number;
31
+ readonly suspend_timeout_seconds: number;
32
+ readonly state: "init" | "active" | "idle";
33
+ readonly pending_state: "init" | "active" | "idle" | null;
34
+ readonly disabled: boolean;
35
+ readonly region: string;
36
+ };
17
37
  }
18
38
  export function assertDatabaseCompute(value: unknown): asserts value is DatabaseCompute {
19
39
  if (
@@ -30,6 +50,10 @@ export function assertDatabaseCompute(value: unknown): asserts value is Database
30
50
  throw new TypeError("Invalid database compute report");
31
51
  const db = value["database"];
32
52
  if (db === null) return;
53
+ if (record(db) && db["provider"] === "pgcf") {
54
+ assertPgcfCompute(db);
55
+ return;
56
+ }
33
57
  if (
34
58
  !record(db) ||
35
59
  !exact(db, [
@@ -64,6 +88,37 @@ export function assertDatabaseCompute(value: unknown): asserts value is Database
64
88
  )
65
89
  throw new TypeError("Invalid database compute configuration");
66
90
  }
91
+ function assertPgcfCompute(db: Record<string, unknown>): void {
92
+ const size = databaseSizeForClass(db["size_class_id"]);
93
+ const states = ["provisioning", "cold", "warm", "suspended", "error"];
94
+ if (
95
+ !exact(db, [
96
+ "cpu_millicores",
97
+ "disabled",
98
+ "max_connections",
99
+ "memory_mib",
100
+ "pending_state",
101
+ "provider",
102
+ "region",
103
+ "size_class_id",
104
+ "state",
105
+ "storage_mib",
106
+ "suspend_timeout_seconds",
107
+ ]) ||
108
+ db["memory_mib"] !== size.memoryMiB ||
109
+ db["cpu_millicores"] !== size.cpuMillicores ||
110
+ db["storage_mib"] !== size.storageMiB ||
111
+ db["max_connections"] !== size.maxConnections ||
112
+ !Number.isSafeInteger(db["suspend_timeout_seconds"]) ||
113
+ Number(db["suspend_timeout_seconds"]) < 60 ||
114
+ Number(db["suspend_timeout_seconds"]) > 900 ||
115
+ !states.includes(String(db["state"])) ||
116
+ (db["pending_state"] !== null && !states.includes(String(db["pending_state"]))) ||
117
+ typeof db["disabled"] !== "boolean" ||
118
+ !["us", "eu"].includes(String(db["region"]))
119
+ )
120
+ throw new TypeError("Invalid fixed database compute configuration");
121
+ }
67
122
  function record(v: unknown): v is Record<string, unknown> {
68
123
  return typeof v === "object" && v !== null && !Array.isArray(v);
69
124
  }
@@ -0,0 +1,114 @@
1
+ export type DatabaseMemoryMiB = 256 | 512 | 1024 | 2048 | 4096;
2
+ export type DatabaseComputeSelection =
3
+ | { readonly memoryMiB: DatabaseMemoryMiB; readonly profile?: never }
4
+ | { readonly profile: "standard" | "performance"; readonly memoryMiB?: never };
5
+
6
+ export function parseDatabaseComputeChangeRequest(value: unknown): {
7
+ readonly environment: "dev" | "prod";
8
+ readonly selection: DatabaseComputeSelection;
9
+ } {
10
+ if (value === null || typeof value !== "object" || Array.isArray(value))
11
+ throw new TypeError("Invalid database compute change");
12
+ const input = value as Record<string, unknown>;
13
+ const keys = Object.keys(input).sort().join(",");
14
+ if (
15
+ (keys !== "confirm,environment,memory_mib" && keys !== "confirm,environment,profile") ||
16
+ input["confirm"] !== true ||
17
+ (input["environment"] !== "dev" && input["environment"] !== "prod")
18
+ )
19
+ throw new TypeError("Invalid database compute change");
20
+ if (keys === "confirm,environment,memory_mib")
21
+ return {
22
+ environment: input["environment"],
23
+ selection: { memoryMiB: databaseSizeForMemory(input["memory_mib"]).memoryMiB },
24
+ };
25
+ if (input["profile"] !== "standard" && input["profile"] !== "performance")
26
+ throw new TypeError("Invalid transitional database profile");
27
+ return { environment: input["environment"], selection: { profile: input["profile"] } };
28
+ }
29
+
30
+ export interface DatabaseSize {
31
+ readonly memoryMiB: DatabaseMemoryMiB;
32
+ readonly storageMiB: number;
33
+ readonly cpuMillicores: number;
34
+ readonly maxConnections: number;
35
+ readonly sizeClassId: string;
36
+ readonly creditsPerAwakeHour: number;
37
+ readonly paidRequired: boolean;
38
+ }
39
+
40
+ /** Operator-approved product sizes; storage is total nominal volume, including WAL. */
41
+ export const DATABASE_SIZES: readonly DatabaseSize[] = Object.freeze([
42
+ Object.freeze({
43
+ memoryMiB: 256,
44
+ storageMiB: 500,
45
+ cpuMillicores: 250,
46
+ maxConnections: 20,
47
+ sizeClassId: "r256-s500m",
48
+ creditsPerAwakeHour: 10,
49
+ paidRequired: false,
50
+ } as const),
51
+ Object.freeze({
52
+ memoryMiB: 512,
53
+ storageMiB: 1024,
54
+ cpuMillicores: 250,
55
+ maxConnections: 40,
56
+ sizeClassId: "r512-s1g",
57
+ creditsPerAwakeHour: 20,
58
+ paidRequired: true,
59
+ } as const),
60
+ Object.freeze({
61
+ memoryMiB: 1024,
62
+ storageMiB: 2048,
63
+ cpuMillicores: 500,
64
+ maxConnections: 80,
65
+ sizeClassId: "r1024-s2g",
66
+ creditsPerAwakeHour: 40,
67
+ paidRequired: true,
68
+ } as const),
69
+ Object.freeze({
70
+ memoryMiB: 2048,
71
+ storageMiB: 4096,
72
+ cpuMillicores: 1000,
73
+ maxConnections: 160,
74
+ sizeClassId: "r2048-s4g",
75
+ creditsPerAwakeHour: 80,
76
+ paidRequired: true,
77
+ } as const),
78
+ Object.freeze({
79
+ memoryMiB: 4096,
80
+ storageMiB: 8192,
81
+ cpuMillicores: 2000,
82
+ maxConnections: 200,
83
+ sizeClassId: "r4096-s8g",
84
+ creditsPerAwakeHour: 160,
85
+ paidRequired: true,
86
+ } as const),
87
+ ]);
88
+
89
+ export function databaseSizeForMemory(value: unknown): DatabaseSize {
90
+ const size = DATABASE_SIZES.find((candidate) => candidate.memoryMiB === value);
91
+ if (size === undefined) throw new TypeError("Unsupported database memory size");
92
+ return size;
93
+ }
94
+
95
+ export function databaseSizeForClass(value: unknown): DatabaseSize {
96
+ const size = DATABASE_SIZES.find((candidate) => candidate.sizeClassId === value);
97
+ if (size === undefined) throw new TypeError("Unsupported database size class");
98
+ return size;
99
+ }
100
+
101
+ /** A downgrade never silently reduces a database's physical allocation. */
102
+ export function assertDatabaseSizeChange(input: {
103
+ readonly memoryMiB: unknown;
104
+ readonly paid: boolean;
105
+ readonly currentStorageMiB: number;
106
+ }): DatabaseSize {
107
+ const size = databaseSizeForMemory(input.memoryMiB);
108
+ if (size.paidRequired && !input.paid) throw new TypeError("Database size requires Paid");
109
+ if (!Number.isSafeInteger(input.currentStorageMiB) || input.currentStorageMiB < 0)
110
+ throw new TypeError("Invalid current database allocation");
111
+ if (size.storageMiB < input.currentStorageMiB)
112
+ throw new TypeError("A smaller database volume requires a separate data migration");
113
+ return size;
114
+ }
@@ -48,7 +48,7 @@ const CORPUS_NEXT_PAGE = `export default function Home() {
48
48
  return <main>Hello from ohmyho.st</main>;
49
49
  }
50
50
  `;
51
- // The config `create-next-app@16.4.0 --yes --no-tailwind` writes: Cache Components with partial
51
+ // The config `create-next-app@16.4.1 --yes --no-tailwind` writes: Cache Components with partial
52
52
  // prefetching.
53
53
  const CREATE_NEXT_APP_CONFIG = `import type { NextConfig } from "next";
54
54
 
@@ -124,10 +124,7 @@ export const FRAMEWORK_ADMISSION_CONFORMANCE_CORPUS: Readonly<{
124
124
  Readonly<{ diagnostic: FrameworkConversionDiagnostic }>)[];
125
125
  }> = Object.freeze({
126
126
  admitted: Object.freeze([
127
- corpusCase(
128
- "Next.js 16.3.8 with npm on the edge runtime (the exact customer source)",
129
- nextSource(),
130
- ),
127
+ corpusCase("Next.js 16.4.1 with npm on the edge runtime", nextSource()),
131
128
  corpusCase("Next.js 17.0.0, a later stable major", nextSource({ declared: "17.0.0" })),
132
129
  corpusCase(
133
130
  "Vite ^8.2.0 resolved to 8.3.4 by pnpm-lock.yaml",
@@ -147,26 +144,26 @@ export const FRAMEWORK_ADMISSION_CONFORMANCE_CORPUS: Readonly<{
147
144
  Object.freeze({
148
145
  ...corpusCase(
149
146
  "Next.js ^16.3.0 with bun.lock, which ohmyho.st does not resolve",
150
- nextSource({ declared: "^16.3.0", resolved: "16.4.0", manager: "bun" }),
147
+ nextSource({ declared: "^16.3.0", resolved: "16.4.1", manager: "bun" }),
151
148
  ),
152
149
  packages: Object.freeze(["next"] as const),
153
150
  }),
154
151
  ]),
155
152
  refused: Object.freeze([
156
153
  refusedCase(
157
- "Next.js 16.3.7 below the 16.x security minimum",
158
- nextSource({ declared: "16.3.7" }),
159
- versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
154
+ "Next.js 16.4.0 below the 16.x security minimum",
155
+ nextSource({ declared: "16.4.0" }),
156
+ versionFinding("package-lock.json", "next", "16.4.0", "16.4.1"),
160
157
  ),
161
158
  refusedCase(
162
- "Next.js 15.5.26 declared with yarn below the 15.x security minimum",
163
- nextSource({ declared: "15.5.26", manager: "yarn" }),
164
- versionFinding("package.json", "next", "15.5.26", "15.5.27"),
159
+ "Next.js 15.5.28 declared with yarn, because no 15.x release is admitted",
160
+ nextSource({ declared: "15.5.28", manager: "yarn" }),
161
+ versionFinding("package.json", "next", "15.5.28", "16.4.1"),
165
162
  ),
166
163
  refusedCase(
167
- "Next.js ^16.3.0 resolved to 16.3.7 by package-lock.json",
168
- nextSource({ declared: "^16.3.0", resolved: "16.3.7" }),
169
- versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
164
+ "Next.js ^16.3.0 resolved to 16.4.0 by package-lock.json",
165
+ nextSource({ declared: "^16.3.0", resolved: "16.4.0" }),
166
+ versionFinding("package-lock.json", "next", "16.4.0", "16.4.1"),
170
167
  ),
171
168
  refusedCase(
172
169
  "TanStack Start 1.168.59 resolved by pnpm-lock.yaml below its security minimum",
@@ -354,7 +351,7 @@ function nextSource(
354
351
  ): Readonly<Record<string, string>> {
355
352
  const manager = options.manager ?? "npm";
356
353
  const [packageManager, lockfile] = CORPUS_MANAGERS[manager];
357
- const next = options.declared ?? "16.3.8";
354
+ const next = options.declared ?? "16.4.1";
358
355
  const react = { react: "19.1.2", "react-dom": "19.1.2" };
359
356
  const runtime = options.field === "devDependencies" ? react : { next, ...react };
360
357
  return Object.freeze({
@@ -380,7 +377,7 @@ function nextSource(
380
377
  }
381
378
 
382
379
  /**
383
- * The next.config.ts and Tailwind CSS files and packages `create-next-app@16.4.0 --yes` writes, in
380
+ * The next.config.ts and Tailwind CSS files and packages `create-next-app@16.4.1 --yes` writes, in
384
381
  * an App Router source with the packageManager, lockfile and ohmyhost.yaml ohmyho.st requires;
385
382
  * `tailwind: false` is its --no-tailwind output. On Turbopack, its default bundler, it adds only the
386
383
  * Turbopack rule for Tailwind CSS; `postcss` adds the postcss.config.mjs and @tailwindcss/postcss it
@@ -390,7 +387,7 @@ function createNextAppSource(
390
387
  options: Readonly<{ tailwind: boolean; postcss?: boolean }>,
391
388
  ): Readonly<Record<string, string>> {
392
389
  const [packageManager, lockfile] = CORPUS_MANAGERS.npm;
393
- const runtime = { next: "16.4.0", react: "19.3.0", "react-dom": "19.3.0" };
390
+ const runtime = { next: "16.4.1", react: "19.3.0", "react-dom": "19.3.0" };
394
391
  const postcss = options.postcss === true;
395
392
  return Object.freeze({
396
393
  "ohmyhost.yaml": corpusConfiguration("npm", ".open-next/assets", "edge"),
@@ -15,8 +15,15 @@ export const PINNED_BUN_VERSION = "1.2.22";
15
15
  * upper bound: later patches, minors and majors reach the real build, which reports adapter failures.
16
16
  * Denied ranges are half-open [introduced, fixed); a null fix denies every later release.
17
17
  *
18
- * next: GHSA-4jqv-mc3x-m676 and GHSA-mcj8-r9mp-w47p (fixed in 15.5.27 and 16.3.8); GHSA-cjq9-62q9-8jv4,
19
- * GHSA-f87g-xv8r-7p7x, GHSA-3w37-wq28-93x7, GHSA-h694-7cp9-m8p3 and GHSA-39w2-rjm5-chcv (16.3.8).
18
+ * next: GHSA-2464-fhr4-w9xw (critical, remote code execution through the Image Optimization API with
19
+ * SVG and sharp before 0.35.5) and GHSA-p3m5-59mp-jv5r (high, denial of service through Server
20
+ * Actions), both fixed in 15.5.28 and 16.4.1; GHSA-673q-mjm6-r5xr (critical, remote code execution
21
+ * through `use cache` functions that return typed arrays, Next.js 16 only, fixed in 16.4.1).
22
+ * There is no 15.x floor: the pinned OpenNext has refused Next.js 15 since 21 October 2026, so every 15.x
23
+ * release is refused with the minimum 16.4.1 instead of sending a customer to 15.5.28 and a failing build.
24
+ * Earlier floors: GHSA-4jqv-mc3x-m676 and GHSA-mcj8-r9mp-w47p (fixed in 15.5.27 and 16.3.8);
25
+ * GHSA-cjq9-62q9-8jv4, GHSA-f87g-xv8r-7p7x, GHSA-3w37-wq28-93x7, GHSA-h694-7cp9-m8p3 and
26
+ * GHSA-39w2-rjm5-chcv (16.3.8).
20
27
  * @tanstack/react-start: GHSA-qx66-fv34-fjm8 (fixed in 1.168.60).
21
28
  * vite: GHSA-64vr-g452-qvp3 (fixed in 5.4.6), the only advisory affecting `vite build` output.
22
29
  * @type {FrameworkVersionPolicy}
@@ -27,7 +34,7 @@ export const FRAMEWORK_VERSION_POLICY = Object.freeze({
27
34
  floors: Object.freeze(["14.4.0"]),
28
35
  denied: Object.freeze([]),
29
36
  }),
30
- next: Object.freeze({ floors: Object.freeze(["15.5.27", "16.3.8"]), denied: Object.freeze([]) }),
37
+ next: Object.freeze({ floors: Object.freeze(["16.4.1"]), denied: Object.freeze([]) }),
31
38
  vite: Object.freeze({ floors: Object.freeze(["5.4.6"]), denied: Object.freeze([]) }),
32
39
  "@tanstack/react-start": Object.freeze({
33
40
  floors: Object.freeze(["1.168.60"]),
@@ -209,6 +209,9 @@ function normalizeEgressOrigins(value: readonly string[] | undefined): readonly
209
209
  function forbiddenEgressHostname(hostname: string): boolean {
210
210
  const normalized = hostname.toLowerCase();
211
211
  return (
212
+ /(?:^|\.)ohmyho-(?:mail|storage)-gateway-[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*\.workers\.dev\.?$/u.test(
213
+ normalized,
214
+ ) ||
212
215
  normalized === "localhost" ||
213
216
  normalized.endsWith(".localhost") ||
214
217
  normalized.endsWith(".local") ||
@@ -111,8 +111,8 @@ const failures = {
111
111
  "The built Worker could not be loaded: top-level code throws or calls fetch, timers or random values outside a handler, an import cannot be resolved, a Worker module such as src/ohmyhost/companion.ts has no default export, or the bundle is too large. Fix that, save or push the corrected version and plan its commit; replaying this operation cannot pass. If none of this applies, report the operation ID through feedback.",
112
112
  ],
113
113
  storage_jurisdiction_conflict: [
114
- "storage.jurisdiction must equal the project's hosting region.",
115
- "Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', save or push the corrected version, then plan its commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
114
+ "storage.jurisdiction must match the project's recorded storage jurisdiction.",
115
+ "Read 'Storage jurisdiction' in 'ohmyhost project context --project ULID --json' and set storage.jurisdiction in ohmyhost.yaml to that recorded value (us or eu). Existing R2 buckets and reservations keep their jurisdiction after a project-region migration; only new storage without a recorded bucket uses the project region. If context reports unavailable, report the request ID through feedback rather than guessing. Save or push the corrected version and plan its commit; for promotion, deploy it to Dev and promote again. Do not delete or recreate the project to move files.",
116
116
  ],
117
117
  provider_state_absent: [
118
118
  "Reconciliation confirmed that the required deployment resources are absent.",
@@ -3,3 +3,11 @@ export type SaasCnameTarget = "customers.omh.st" | "customers.ohmyho.st";
3
3
  export function isSaasCnameTarget(value: unknown): value is SaasCnameTarget {
4
4
  return value === "customers.omh.st" || value === "customers.ohmyho.st";
5
5
  }
6
+
7
+ export {
8
+ classifyPlatformGatewayHostname,
9
+ createPinnedStorageGatewayOrigin,
10
+ parsePinnedStorageGatewayOrigin,
11
+ type PinnedStorageGatewayOrigin,
12
+ type StorageGatewayOriginScope,
13
+ } from "./storage-gateway-origin.js";
@@ -120,6 +120,20 @@ export const PROBLEM_POLICY = {
120
120
  restAction:
121
121
  "Performance compute needs active Paid and usable credits or grace; repeating unchanged does not help. Read 'ohmyhost credits account --organization ULID --json'. On Free, the Owner may start Paid with 'ohmyhost billing checkout --offer paid'; without usable credits, they may add credits. Until then, keep standard compute.",
122
122
  },
123
+ compute_paid_required: {
124
+ status: 403,
125
+ retryable: false,
126
+ action:
127
+ "Database sizes above 256 MiB require active Paid access. Read the account status; keep the existing database until the owner chooses an eligible size or Paid access.",
128
+ title: "Database size requires Paid access",
129
+ },
130
+ compute_storage_shrink_requires_migration: {
131
+ status: 409,
132
+ retryable: false,
133
+ action:
134
+ "This size includes less storage than the current volume. Keep the current database and plan an explicit data migration to a fresh smaller volume; do not retry the resize or delete data automatically.",
135
+ title: "Smaller storage requires a data migration",
136
+ },
123
137
  compute_performance_unavailable: {
124
138
  status: 503,
125
139
  retryable: false,
@@ -327,10 +341,10 @@ export const PROBLEM_POLICY = {
327
341
  status: 409,
328
342
  retryable: false,
329
343
  action:
330
- "Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', save or push the corrected version, then plan its commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
344
+ "Read 'Storage jurisdiction' in 'ohmyhost project context --project ULID --json' and set storage.jurisdiction in ohmyhost.yaml to that recorded value (us or eu). Existing R2 buckets and reservations keep their jurisdiction after a project-region migration; only new storage without a recorded bucket uses the project region. If context reports unavailable, report the request ID through feedback rather than guessing. Save or push the corrected version and plan its commit; for promotion, deploy it to Dev and promote again. Do not delete or recreate the project to move files.",
331
345
  title: "Storage jurisdiction conflict",
332
346
  restAction:
333
- "Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', save or push the corrected version, then plan its commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
347
+ "Read 'Storage jurisdiction' in 'ohmyhost project context --project ULID --json' and set storage.jurisdiction in ohmyhost.yaml to that recorded value (us or eu). Existing R2 buckets and reservations keep their jurisdiction after a project-region migration; only new storage without a recorded bucket uses the project region. If context reports unavailable, report the request ID through feedback rather than guessing. Save or push the corrected version and plan its commit; for promotion, deploy it to Dev and promote again. Do not delete or recreate the project to move files.",
334
348
  },
335
349
  shared_data_requires_promotion: {
336
350
  status: 409,