@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.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +27 -1
- package/dist/{chunk-CHAFMFTK.js → chunk-KQS5IMSO.js} +7606 -2403
- package/dist/index.js +1 -1
- package/dist/licenses/pgcf-bridge-LICENSE +201 -0
- package/dist/licenses/pgcf-bridge-NOTICE +1 -0
- package/dist/stdio-main.js +3853 -34
- package/package.json +1 -1
- package/src/apps/mcp/package.json +4 -4
- package/src/apps/mcp/src/local-client.ts +15 -9
- package/src/apps/mcp/src/local-server.ts +36 -18
- package/src/apps/product-cli/package.json +4 -3
- package/src/apps/product-cli/src/cli.ts +7 -5
- package/src/apps/product-cli/src/command.ts +18 -5
- package/src/apps/product-cli/src/product-api.ts +22 -15
- package/src/apps/product-cli/src/psql-session.ts +54 -19
- package/src/apps/product-cli/src/repository-init.ts +181 -9
- package/src/packages/agent-skills/package.json +1 -1
- package/src/packages/agent-skills/src/generated-skill-resources.ts +16 -16
- package/src/packages/contracts/package.json +6 -1
- package/src/packages/contracts/src/credit-pricing.ts +7 -1
- package/src/packages/contracts/src/database-access.ts +53 -12
- package/src/packages/contracts/src/database-compute.ts +66 -11
- package/src/packages/contracts/src/database-sizes.ts +114 -0
- package/src/packages/contracts/src/framework-admission-corpus.ts +15 -18
- package/src/packages/contracts/src/framework-build-script.mjs +10 -3
- package/src/packages/contracts/src/index.ts +3 -0
- package/src/packages/contracts/src/operation-failure.ts +2 -2
- package/src/packages/contracts/src/platform-origins.ts +8 -0
- package/src/packages/contracts/src/problem-policy.ts +16 -2
- package/src/packages/contracts/src/storage-gateway-origin.ts +93 -0
- package/src/packages/postgres-bridge/LICENSE +201 -0
- package/src/packages/postgres-bridge/NOTICE +1 -0
- package/src/packages/postgres-bridge/package.json +26 -0
- package/src/packages/postgres-bridge/src/auth.ts +159 -0
- package/src/packages/postgres-bridge/src/bridge.ts +168 -0
- package/src/packages/postgres-bridge/src/index.ts +2 -0
- package/src/packages/postgres-bridge/src/options.ts +49 -0
- package/src/packages/sdk-ts/package.json +2 -2
- package/src/packages/sdk-ts/src/generated/index.ts +1 -0
- package/src/packages/sdk-ts/src/generated/sdk.gen.ts +9 -8
- package/src/packages/sdk-ts/src/generated/types.gen.ts +91 -24
- package/src/packages/sdk-ts/src/index.ts +1 -0
- package/src/packages/sdk-ts/src/managed-mail-attachment.ts +107 -0
- 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
|
|
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
|
|
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,
|
|
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, [...
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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["
|
|
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:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
158
|
-
nextSource({ declared: "16.
|
|
159
|
-
versionFinding("package-lock.json", "next", "16.
|
|
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.
|
|
163
|
-
nextSource({ declared: "15.5.
|
|
164
|
-
versionFinding("package.json", "next", "15.5.
|
|
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.
|
|
168
|
-
nextSource({ declared: "^16.3.0", resolved: "16.
|
|
169
|
-
versionFinding("package-lock.json", "next", "16.
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
19
|
-
*
|
|
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(["
|
|
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
|
|
115
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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,
|