@amerged/ohmyhost-mcp 0.1.22 → 0.1.24

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 (87) hide show
  1. package/README.md +5 -2
  2. package/dist/{chunk-NLBFEH6H.js → chunk-EEFUMPKG.js} +25 -24
  3. package/dist/index.js +1 -1
  4. package/dist/stdio-main.js +734 -269
  5. package/package.json +1 -1
  6. package/src/apps/mcp/package.json +47 -0
  7. package/src/apps/mcp/{local-client.ts → src/local-client.ts} +69 -12
  8. package/src/apps/mcp/{local-server.ts → src/local-server.ts} +327 -145
  9. package/src/apps/product-cli/package.json +45 -0
  10. package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
  11. package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
  12. package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
  13. package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
  14. package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
  15. package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
  16. package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +79 -29
  17. package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
  18. package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
  19. package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
  20. package/src/packages/agent-skills/package.json +24 -0
  21. package/src/packages/agent-skills/src/generated-skill-resources.ts +165 -0
  22. package/src/packages/agent-skills/{index.ts → src/index.ts} +1 -1
  23. package/src/packages/contracts/package.json +137 -0
  24. package/src/packages/contracts/src/browser-capabilities.ts +133 -0
  25. package/src/packages/contracts/src/customer-auth-admission.ts +36 -0
  26. package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
  27. package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +88 -13
  28. package/src/packages/contracts/src/framework-build-script.mjs +22 -0
  29. package/src/packages/contracts/src/framework-build-script.ts +1 -0
  30. package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
  31. package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
  32. package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
  33. package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
  34. package/src/packages/contracts/src/problem-policy.ts +498 -0
  35. package/src/packages/contracts/src/project-data-changes.ts +90 -0
  36. package/src/packages/contracts/src/secret-names.ts +27 -0
  37. package/src/packages/sdk-ts/package.json +21 -0
  38. package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
  39. package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +114 -5
  40. package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +209 -10
  41. package/src/packages/workos-auth-contracts/package.json +39 -0
  42. package/src/packages/agent-skills/generated-skill-resources.ts +0 -165
  43. /package/src/apps/mcp/{index.ts → src/index.ts} +0 -0
  44. /package/src/apps/mcp/{stdio-main.ts → src/stdio-main.ts} +0 -0
  45. /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
  46. /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
  47. /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
  48. /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
  49. /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
  50. /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
  51. /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
  52. /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
  53. /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
  54. /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
  55. /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
  56. /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
  57. /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
  58. /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
  59. /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
  60. /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
  61. /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
  62. /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  63. /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
  64. /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
  65. /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
  66. /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
  67. /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
  68. /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
  69. /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
  70. /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
  71. /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
  72. /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
  73. /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
  74. /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
  75. /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
  76. /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
  77. /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
  78. /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
  79. /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
  80. /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
  81. /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
  82. /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
  83. /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
  84. /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
  85. /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
  86. /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  87. /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { GENERATED_SKILL_RESOURCES } from "./generated-skill-resources.js";
2
2
 
3
- export const OHMYHOST_SKILL_INSTALL_SOURCE = "amerged/docs";
3
+ export const OHMYHOST_SKILL_INSTALL_SOURCE = "amerged-org/docs";
4
4
 
5
5
  export interface OhmyhostSkillResource {
6
6
  readonly skillName: string;
@@ -0,0 +1,137 @@
1
+ {
2
+ "name": "@ohmyhost/contracts",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ "./operation-failure": {
9
+ "source": "./src/operation-failure.ts",
10
+ "types": "./dist/operation-failure.d.ts",
11
+ "import": "./dist/operation-failure.js"
12
+ },
13
+ "./billing": {
14
+ "source": "./src/billing.ts",
15
+ "types": "./dist/billing.d.ts",
16
+ "import": "./dist/billing.js"
17
+ },
18
+ "./credit-pricing": {
19
+ "source": "./src/credit-pricing.ts",
20
+ "types": "./dist/credit-pricing.d.ts",
21
+ "import": "./dist/credit-pricing.js"
22
+ },
23
+ "./platform-origins": {
24
+ "source": "./src/platform-origins.ts",
25
+ "types": "./dist/platform-origins.d.ts",
26
+ "import": "./dist/platform-origins.js"
27
+ },
28
+ ".": {
29
+ "source": "./src/index.ts",
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./application-root": {
34
+ "source": "./src/application-root.ts",
35
+ "types": "./dist/application-root.d.ts",
36
+ "import": "./dist/application-root.js"
37
+ },
38
+ "./database-migration-admission": {
39
+ "source": "./src/database-migration-admission.ts",
40
+ "types": "./dist/database-migration-admission.d.ts",
41
+ "import": "./dist/database-migration-admission.js"
42
+ },
43
+ "./framework-family": {
44
+ "source": "./src/framework-family.ts",
45
+ "types": "./dist/framework-family.d.ts",
46
+ "import": "./dist/framework-family.js"
47
+ },
48
+ "./operation-deployment-result": {
49
+ "source": "./src/operation-deployment-result.ts",
50
+ "types": "./dist/operation-deployment-result.d.ts",
51
+ "import": "./dist/operation-deployment-result.js"
52
+ },
53
+ "./operation-data-change-result": {
54
+ "source": "./src/operation-data-change-result.ts",
55
+ "types": "./dist/operation-data-change-result.d.ts",
56
+ "import": "./dist/operation-data-change-result.js"
57
+ },
58
+ "./framework-admission": {
59
+ "source": "./src/framework-admission.ts",
60
+ "types": "./dist/framework-admission.d.ts",
61
+ "import": "./dist/framework-admission.js"
62
+ },
63
+ "./framework-build-script": {
64
+ "source": "./src/framework-build-script.ts",
65
+ "types": "./dist/framework-build-script.d.ts",
66
+ "import": "./dist/framework-build-script.js"
67
+ },
68
+ "./browser-capabilities": {
69
+ "source": "./src/browser-capabilities.ts",
70
+ "types": "./dist/browser-capabilities.d.ts",
71
+ "import": "./dist/browser-capabilities.js"
72
+ },
73
+ "./openapi.yaml": "./openapi.yaml",
74
+ "./ohmyhost.schema.json": "./schemas/ohmyhost.schema.json",
75
+ "./feedback": {
76
+ "source": "./src/feedback.ts",
77
+ "types": "./dist/feedback.d.ts",
78
+ "import": "./dist/feedback.js"
79
+ },
80
+ "./user-api-keys": {
81
+ "source": "./src/user-api-keys.ts",
82
+ "types": "./dist/user-api-keys.d.ts",
83
+ "import": "./dist/user-api-keys.js"
84
+ },
85
+ "./project-exports": {
86
+ "source": "./src/project-exports.ts",
87
+ "types": "./dist/project-exports.d.ts",
88
+ "import": "./dist/project-exports.js"
89
+ },
90
+ "./project-context": {
91
+ "source": "./src/project-context.ts",
92
+ "types": "./dist/project-context.d.ts",
93
+ "import": "./dist/project-context.js"
94
+ },
95
+ "./database-compute": {
96
+ "source": "./src/database-compute.ts",
97
+ "types": "./dist/database-compute.d.ts",
98
+ "import": "./dist/database-compute.js"
99
+ },
100
+ "./database-access": {
101
+ "source": "./src/database-access.ts",
102
+ "types": "./dist/database-access.d.ts",
103
+ "import": "./dist/database-access.js"
104
+ },
105
+ "./functions": {
106
+ "source": "./src/functions.ts",
107
+ "types": "./dist/functions.d.ts",
108
+ "import": "./dist/functions.js"
109
+ },
110
+ "./problem-policy": {
111
+ "source": "./src/problem-policy.ts",
112
+ "types": "./dist/problem-policy.d.ts",
113
+ "import": "./dist/problem-policy.js"
114
+ },
115
+ "./secret-names": {
116
+ "source": "./src/secret-names.ts",
117
+ "types": "./dist/secret-names.d.ts",
118
+ "import": "./dist/secret-names.js"
119
+ },
120
+ "./project-data-changes": {
121
+ "source": "./src/project-data-changes.ts",
122
+ "types": "./dist/project-data-changes.d.ts",
123
+ "import": "./dist/project-data-changes.js"
124
+ }
125
+ },
126
+ "scripts": {
127
+ "build": "tsc --project tsconfig.build.json",
128
+ "typecheck": "tsc --project tsconfig.json --noEmit",
129
+ "test": "vitest run --root ../.. packages/contracts/test",
130
+ "validate": "pnpm run typecheck && pnpm run test"
131
+ },
132
+ "dependencies": {
133
+ "ajv": "8.18.0",
134
+ "yaml": "2.8.3"
135
+ },
136
+ "license": "Apache-2.0"
137
+ }
@@ -0,0 +1,133 @@
1
+ const RESOURCE_GROUPS = [
2
+ "connect",
3
+ "scripts",
4
+ "styles",
5
+ "images",
6
+ "fonts",
7
+ "frames",
8
+ "frameAncestors",
9
+ ] as const;
10
+ type BrowserResourceGroup = (typeof RESOURCE_GROUPS)[number];
11
+
12
+ export type BrowserPolicy = Readonly<
13
+ Record<BrowserResourceGroup, readonly string[]> & {
14
+ workers: Readonly<{ self: boolean; blob: boolean }>;
15
+ cors: Readonly<{ origins: readonly string[]; credentials: boolean }>;
16
+ }
17
+ >;
18
+
19
+ const POLICY_KEYS = [...RESOURCE_GROUPS, "workers", "cors"];
20
+ const MAX_ORIGINS = 16;
21
+
22
+ /** Source declarations may omit capabilities; each omission retains the restrictive default. */
23
+ export function normalizeBrowserPolicy(value: unknown): BrowserPolicy {
24
+ const input = value === undefined ? {} : policyRecord(value, POLICY_KEYS);
25
+ const workers =
26
+ input["workers"] === undefined ? {} : policyRecord(input["workers"], ["self", "blob"]);
27
+ const cors =
28
+ input["cors"] === undefined ? {} : policyRecord(input["cors"], ["origins", "credentials"]);
29
+ const resources = Object.fromEntries(
30
+ RESOURCE_GROUPS.map((group) => [group, normalizeOrigins(input[group], group === "connect")]),
31
+ ) as Record<BrowserResourceGroup, readonly string[]>;
32
+ return Object.freeze({
33
+ ...resources,
34
+ workers: Object.freeze({
35
+ self: normalizeBoolean(workers["self"]),
36
+ blob: normalizeBoolean(workers["blob"]),
37
+ }),
38
+ cors: Object.freeze({
39
+ origins: normalizeOrigins(cors["origins"], false),
40
+ credentials: normalizeBoolean(cors["credentials"]),
41
+ }),
42
+ });
43
+ }
44
+
45
+ /** Private routing and artifact custody accept the complete normalized policy only. */
46
+ export function parseBrowserPolicy(value: unknown): BrowserPolicy {
47
+ const input = policyRecord(value, POLICY_KEYS, true);
48
+ const workers = policyRecord(input["workers"], ["self", "blob"], true);
49
+ const cors = policyRecord(input["cors"], ["origins", "credentials"], true);
50
+ if (
51
+ typeof workers["self"] !== "boolean" ||
52
+ typeof workers["blob"] !== "boolean" ||
53
+ typeof cors["credentials"] !== "boolean"
54
+ )
55
+ throw invalidPolicy();
56
+ const normalized = normalizeBrowserPolicy(input);
57
+ for (const group of RESOURCE_GROUPS) {
58
+ if (!sameOrigins(input[group], normalized[group])) throw invalidPolicy();
59
+ }
60
+ if (!sameOrigins(cors["origins"], normalized.cors.origins)) throw invalidPolicy();
61
+ return normalized;
62
+ }
63
+
64
+ function policyRecord(
65
+ value: unknown,
66
+ keys: readonly string[],
67
+ complete = false,
68
+ ): Record<string, unknown> {
69
+ if (
70
+ typeof value !== "object" ||
71
+ value === null ||
72
+ Array.isArray(value) ||
73
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
74
+ Object.keys(value).some((key) => !keys.includes(key)) ||
75
+ (complete && Object.keys(value).length !== keys.length)
76
+ )
77
+ throw invalidPolicy();
78
+ return value as Record<string, unknown>;
79
+ }
80
+
81
+ function normalizeBoolean(value: unknown): boolean {
82
+ if (value === undefined) return false;
83
+ if (typeof value !== "boolean") throw invalidPolicy();
84
+ return value;
85
+ }
86
+
87
+ function normalizeOrigins(value: unknown, allowWebSocket: boolean): readonly string[] {
88
+ if (value === undefined) return Object.freeze([]);
89
+ if (!Array.isArray(value) || value.length > MAX_ORIGINS) throw invalidPolicy();
90
+ const origins: string[] = [];
91
+ for (const origin of value) {
92
+ if (!browserOrigin(origin, allowWebSocket) || origins.includes(origin)) throw invalidPolicy();
93
+ origins.push(origin);
94
+ }
95
+ return Object.freeze(origins.sort());
96
+ }
97
+
98
+ function browserOrigin(value: unknown, allowWebSocket: boolean): value is string {
99
+ if (typeof value !== "string" || value.length > 256) return false;
100
+ try {
101
+ const url = new URL(value);
102
+ return (
103
+ (url.protocol === "https:" || (allowWebSocket && url.protocol === "wss:")) &&
104
+ url.origin === value &&
105
+ url.username === "" &&
106
+ url.password === "" &&
107
+ url.pathname === "/" &&
108
+ url.search === "" &&
109
+ url.hash === "" &&
110
+ url.hostname.includes(".") &&
111
+ !url.hostname.endsWith(".local") &&
112
+ !url.hostname.endsWith(".internal") &&
113
+ !/^\d+(?:\.\d+){3}$/u.test(url.hostname) &&
114
+ url.hostname
115
+ .split(".")
116
+ .every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label))
117
+ );
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ function sameOrigins(value: unknown, expected: readonly string[]): boolean {
124
+ return (
125
+ Array.isArray(value) &&
126
+ value.length === expected.length &&
127
+ value.every((origin, index) => origin === expected[index])
128
+ );
129
+ }
130
+
131
+ function invalidPolicy(): TypeError {
132
+ return new TypeError("Invalid browser capability policy");
133
+ }
@@ -0,0 +1,36 @@
1
+ export interface ManagedCustomerAuthCapabilities {
2
+ readonly auth?: Readonly<{ provider: string }>;
3
+ readonly database?: Readonly<{ enabled: boolean }>;
4
+ readonly mail?: Readonly<{ enabled: boolean }>;
5
+ }
6
+
7
+ export function managedCustomerAuthMailMessage(path: string): string {
8
+ return `"${path}" selects the managed Better Auth bridge, which sends verification and password-reset email through ohmyhost Mail. Set mail.enabled: true and set up a verified mail sender, or set auth.provider: none and keep the application-owned Better Auth configuration and sender. Commit and push the change, then plan the new commit.`;
9
+ }
10
+
11
+ export function managedCustomerAuthDatabaseMessage(path: string): string {
12
+ return `"${path}" selects the managed Better Auth bridge, which requires the managed PostgreSQL database. Set database.enabled: true with an admitted migrations path, or set auth.provider: none and keep the application-owned authentication configuration. Commit and push the change, then plan the new commit.`;
13
+ }
14
+
15
+ /** Dependency inventory never selects the managed bridge; only the explicit auth capability does. */
16
+ export function managedCustomerAuthAdmissionIssue(
17
+ capabilities: ManagedCustomerAuthCapabilities | null | undefined,
18
+ ): Readonly<{
19
+ code: "managed_auth_database_required" | "managed_auth_mail_required";
20
+ message: string;
21
+ }> | null {
22
+ if (capabilities?.auth?.provider !== "better-auth") {
23
+ return null;
24
+ }
25
+ if (capabilities.database?.enabled !== true) {
26
+ return Object.freeze({
27
+ code: "managed_auth_database_required" as const,
28
+ message: managedCustomerAuthDatabaseMessage("ohmyhost.yaml"),
29
+ });
30
+ }
31
+ if (capabilities.mail?.enabled === true) return null;
32
+ return Object.freeze({
33
+ code: "managed_auth_mail_required" as const,
34
+ message: managedCustomerAuthMailMessage("ohmyhost.yaml"),
35
+ });
36
+ }
@@ -105,7 +105,13 @@ export interface ProjectDatabaseAccess {
105
105
  readonly issued_at: string;
106
106
  readonly expires_at: string;
107
107
  readonly revoked_at: string | null;
108
- readonly revocation_reason: "principal" | "expired" | "budget" | "creation_failed" | null;
108
+ readonly revocation_reason:
109
+ | "principal"
110
+ | "expired"
111
+ | "budget"
112
+ | "creation_failed"
113
+ | "data_change"
114
+ | null;
109
115
  }
110
116
  export interface ProjectDatabaseAccessCredential extends ProjectDatabaseAccess {
111
117
  readonly connection_uri: string;
@@ -219,7 +225,9 @@ function assertAccessFields(value: Record<string, unknown>): void {
219
225
  (revoked !== null && !timestamp(revoked)) ||
220
226
  (revoked === null) !== (reason === null) ||
221
227
  (reason !== null &&
222
- !["principal", "expired", "budget", "creation_failed"].includes(String(reason))) ||
228
+ !["principal", "expired", "budget", "creation_failed", "data_change"].includes(
229
+ String(reason),
230
+ )) ||
223
231
  (value["state"] === "revoked") !== (revoked !== null)
224
232
  )
225
233
  throw new TypeError("Invalid database access");
@@ -1,3 +1,13 @@
1
+ import { PINNED_BUN_VERSION } from "./framework-build-script.mjs";
2
+ import {
3
+ managedCustomerAuthMailMessage,
4
+ managedCustomerAuthDatabaseMessage,
5
+ } from "./customer-auth-admission.js";
6
+ export {
7
+ managedCustomerAuthAdmissionIssue,
8
+ managedCustomerAuthMailMessage,
9
+ } from "./customer-auth-admission.js";
10
+
1
11
  export type PackageManagerName = "bun" | "npm" | "pnpm" | "yarn";
2
12
 
3
13
  export type FrameworkAdmissionReason =
@@ -6,7 +16,8 @@ export type FrameworkAdmissionReason =
6
16
  | "invalid_package_manager"
7
17
  | "missing_lockfile"
8
18
  | "missing_package_manager"
9
- | "package_manager_lockfile_mismatch";
19
+ | "package_manager_lockfile_mismatch"
20
+ | "package_manager_version_unsupported";
10
21
 
11
22
  export class FrameworkAdmissionError extends Error {
12
23
  public readonly code = "framework_admission_failed";
@@ -88,6 +99,9 @@ export function resolvePackageManager(input: PackageManagerInput): PackageManage
88
99
  if (!contract.lockfiles.includes(lockfile)) {
89
100
  throw new FrameworkAdmissionError("package_manager_lockfile_mismatch");
90
101
  }
102
+ if (manager === "bun" && version !== PINNED_BUN_VERSION) {
103
+ throw new FrameworkAdmissionError("package_manager_version_unsupported");
104
+ }
91
105
  return Object.freeze({
92
106
  manager,
93
107
  version,
@@ -187,17 +201,25 @@ function versionAdmission(
187
201
  }
188
202
 
189
203
  export type FrameworkConversionDiagnosticCode =
204
+ | "managed_auth_database_required"
205
+ | "managed_auth_mail_required"
190
206
  | "database_binding_private"
191
207
  | "database_driver_unsupported"
192
208
  | "source_syntax_invalid"
193
209
  | "worker_module_default_export_required"
194
- | "scheduled_handler_required";
210
+ | "scheduled_handler_required"
211
+ | "framework_base_path_unsupported"
212
+ | "tanstack_prerender_required"
213
+ | "tanstack_vite_plugins_required"
214
+ | "next_cache_components_unsupported";
195
215
 
196
216
  export interface FrameworkConversionDiagnostic {
197
217
  readonly code: FrameworkConversionDiagnosticCode;
198
218
  readonly path: string;
199
219
  }
200
220
 
221
+ const PUSH_AND_PLAN = "Commit and push the change, then plan the new commit.";
222
+
201
223
  // Detail texts are the customer-facing sentence for one diagnostic; parseFrameworkConversionDetail
202
224
  // recognizes exactly these sentences so a client can map a detail back to its typed diagnostic.
203
225
  const FRAMEWORK_CONVERSION_DETAILS: Readonly<
@@ -206,38 +228,89 @@ const FRAMEWORK_CONVERSION_DETAILS: Readonly<
206
228
  Readonly<{ render(path: string): string; pattern: RegExp }>
207
229
  >
208
230
  > = Object.freeze({
231
+ managed_auth_database_required: Object.freeze({
232
+ render: managedCustomerAuthDatabaseMessage,
233
+ pattern:
234
+ /^"([^"]+)" selects the managed Better Auth bridge, which requires the managed PostgreSQL database\. Set database\.enabled: true with an admitted migrations path, or set auth\.provider: none and keep the application-owned authentication configuration\. Commit and push the change, then plan the new commit\.$/u,
235
+ }),
236
+ managed_auth_mail_required: Object.freeze({
237
+ render: managedCustomerAuthMailMessage,
238
+ pattern:
239
+ /^"([^"]+)" selects the managed Better Auth bridge, which sends verification and password-reset email through ohmyhost Mail\. Set mail\.enabled: true and set up a verified mail sender, or set auth\.provider: none and keep the application-owned Better Auth configuration and sender\. Commit and push the change, then plan the new commit\.$/u,
240
+ }),
209
241
  database_binding_private: Object.freeze({
210
242
  render: (path: string) =>
211
- `"${path}" reads a managed database connection string. HYPERDRIVE is retired; DATABASE_URL and postgres:// URLs never reach a customer Worker. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". Commit the change, then plan again.`,
243
+ `"${path}" reads a database connection string (DATABASE_URL, HYPERDRIVE, connectionString or a postgres:// URL); none reaches a customer Worker. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". ${PUSH_AND_PLAN}`,
212
244
  pattern:
213
- /^"([^"]+)" reads a managed database connection string\. HYPERDRIVE is retired; DATABASE_URL and postgres:\/\/ URLs never reach a customer Worker\. Call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit the change, then plan again\.$/u,
245
+ /^"([^"]+)" reads a database connection string \(DATABASE_URL, HYPERDRIVE, connectionString or a postgres:\/\/ URL\); none reaches a customer Worker\. Call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit and push the change, then plan the new commit\.$/u,
214
246
  }),
215
247
  database_driver_unsupported: Object.freeze({
216
248
  render: (path: string) =>
217
- `"${path}" imports a PostgreSQL socket driver. Outbound connect() is disabled for customer Workers, so it can never open a connection; call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". Commit the change, then plan again.`,
249
+ `"${path}" imports a database socket driver (pg, postgres, pg-native or mysql2); customer Workers cannot open sockets. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database". ${PUSH_AND_PLAN}`,
218
250
  pattern:
219
- /^"([^"]+)" imports a PostgreSQL socket driver\. Outbound connect\(\) is disabled for customer Workers, so it can never open a connection; call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit the change, then plan again\.$/u,
251
+ /^"([^"]+)" imports a database socket driver \(pg, postgres, pg-native or mysql2\); customer Workers cannot open sockets\. Call the database with createPrivateDatabaseClient from "@ohmyhost\/customer-runtime\/database"\. Commit and push the change, then plan the new commit\.$/u,
220
252
  }),
221
253
  source_syntax_invalid: Object.freeze({
222
254
  render: (path: string) =>
223
- `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit the change, then plan again.`,
255
+ `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit and push the change, then plan the new commit.`,
224
256
  pattern:
225
- /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit the change, then plan again\.$/u,
257
+ /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit and push the change, then plan the new commit\.$/u,
226
258
  }),
227
259
  worker_module_default_export_required: Object.freeze({
228
260
  render: (path: string) =>
229
- `Worker module "${path}" has no default export. Export default { fetch, scheduled } with the handlers ohmyho.st invokes; named exports are never called. Commit the change, then plan again.`,
261
+ `Worker module "${path}" has no default export. Export default { fetch, scheduled } with the handlers ohmyho.st invokes; named exports are never called. ${PUSH_AND_PLAN}`,
230
262
  pattern:
231
- /^Worker module "([^"]+)" has no default export\. Export default \{ fetch, scheduled \} with the handlers ohmyho\.st invokes; named exports are never called\. Commit the change, then plan again\.$/u,
263
+ /^Worker module "([^"]+)" has no default export\. Export default \{ fetch, scheduled \} with the handlers ohmyho\.st invokes; named exports are never called\. Commit and push the change, then plan the new commit\.$/u,
232
264
  }),
233
265
  scheduled_handler_required: Object.freeze({
234
266
  render: (path: string) =>
235
- `functions.crons is declared but "${path}" exports no scheduled handler. Add scheduled(controller, env, ctx) to its default export, commit the change, then plan again.`,
267
+ `functions.crons is declared but "${path}" exports no scheduled handler. Add scheduled(controller, env, ctx) to its default export, commit and push the change, then plan the new commit.`,
268
+ pattern:
269
+ /^functions\.crons is declared but "([^"]+)" exports no scheduled handler\. Add scheduled\(controller, env, ctx\) to its default export, commit and push the change, then plan the new commit\.$/u,
270
+ }),
271
+ framework_base_path_unsupported: Object.freeze({
272
+ render: (path: string) =>
273
+ `"${path}" sets a Vite base. ohmyho.st serves the app at the root origin: remove base or set base: "/". ${PUSH_AND_PLAN}`,
236
274
  pattern:
237
- /^functions\.crons is declared but "([^"]+)" exports no scheduled handler\. Add scheduled\(controller, env, ctx\) to its default export, commit the change, then plan again\.$/u,
275
+ /^"([^"]+)" sets a Vite base\. ohmyho\.st serves the app at the root origin: remove base or set base: "\/"\. Commit and push the change, then plan the new commit\.$/u,
276
+ }),
277
+ tanstack_prerender_required: Object.freeze({
278
+ render: (path: string) =>
279
+ `"${path}" does not call tanstackStart({ prerender: { enabled: true } }), which the static TanStack Start runtime needs; enable prerender, or set runtime.mode: edge with the Cloudflare Vite plugin. ${PUSH_AND_PLAN}`,
280
+ pattern:
281
+ /^"([^"]+)" does not call tanstackStart\(\{ prerender: \{ enabled: true \} \}\), which the static TanStack Start runtime needs; enable prerender, or set runtime\.mode: edge with the Cloudflare Vite plugin\. Commit and push the change, then plan the new commit\.$/u,
282
+ }),
283
+ tanstack_vite_plugins_required: Object.freeze({
284
+ render: (path: string) =>
285
+ `"${path}" does not follow the TanStack Start plugin contract: import tanstackStart from "@tanstack/react-start/plugin/vite"; on runtime edge also import cloudflare from "@cloudflare/vite-plugin" and call cloudflare({ viteEnvironment: { name: "ssr" } }) before tanstackStart(); on runtime static do not import the Cloudflare plugin. ${PUSH_AND_PLAN}`,
286
+ pattern:
287
+ /^"([^"]+)" does not follow the TanStack Start plugin contract: import tanstackStart from "@tanstack\/react-start\/plugin\/vite"; on runtime edge also import cloudflare from "@cloudflare\/vite-plugin" and call cloudflare\(\{ viteEnvironment: \{ name: "ssr" \} \}\) before tanstackStart\(\); on runtime static do not import the Cloudflare plugin\. Commit and push the change, then plan the new commit\.$/u,
288
+ }),
289
+ next_cache_components_unsupported: Object.freeze({
290
+ render: (path: string) =>
291
+ `"${path}" sets cacheComponents: true, which ohmyho.st does not support; remove it or set it to false. ${PUSH_AND_PLAN}`,
292
+ pattern:
293
+ /^"([^"]+)" sets cacheComponents: true, which ohmyho\.st does not support; remove it or set it to false\. Commit and push the change, then plan the new commit\.$/u,
238
294
  }),
239
295
  });
240
296
 
297
+ /**
298
+ * An adapter rule that `init` does not check. It stays a TypeError, so every existing caller that
299
+ * treats an adapter refusal as a TypeError keeps working, and carries the typed diagnostic whose
300
+ * detail names the file and its fix.
301
+ */
302
+ export class FrameworkConversionDiagnosticError extends TypeError {
303
+ public readonly diagnostic: FrameworkConversionDiagnostic;
304
+
305
+ public constructor(diagnostic: unknown) {
306
+ const parsed = parseFrameworkConversionDiagnostic(diagnostic);
307
+ if (parsed === null) throw new TypeError("Framework conversion diagnostic is invalid");
308
+ super(FRAMEWORK_CONVERSION_DETAILS[parsed.code].render(parsed.path));
309
+ this.name = "FrameworkConversionDiagnosticError";
310
+ this.diagnostic = parsed;
311
+ }
312
+ }
313
+
241
314
  function isFrameworkConversionDiagnosticCode(
242
315
  value: unknown,
243
316
  ): value is FrameworkConversionDiagnosticCode {
@@ -256,7 +329,9 @@ export function parseFrameworkConversionDiagnostic(
256
329
  !isFrameworkConversionDiagnosticCode(code) ||
257
330
  typeof path !== "string" ||
258
331
  path.length > 240 ||
259
- !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path) ||
332
+ (code === "managed_auth_mail_required" || code === "managed_auth_database_required"
333
+ ? path !== "ohmyhost.yaml"
334
+ : !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path)) ||
260
335
  path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
261
336
  )
262
337
  return null;
@@ -0,0 +1,22 @@
1
+ export const PINNED_BUN_VERSION = "1.2.22";
2
+
3
+ const TYPESCRIPT_BUILD_STAGE = /^tsc(?:[ \t]+(?:--noEmit|-b|--build))?$/u;
4
+
5
+ /**
6
+ * The same source is consumed by local admission, framework adapters and the build sandbox.
7
+ * @param {"nextjs" | "tanstack-start" | "vite"} framework
8
+ * @param {unknown} script
9
+ * @returns {boolean}
10
+ */
11
+ export function isAdmittedFrameworkBuildScript(framework, script) {
12
+ if (typeof script !== "string") return false;
13
+ if (framework === "nextjs") return script === "next build" || script === "next build --webpack";
14
+ if (framework !== "vite" && framework !== "tanstack-start") return false;
15
+ const stages = script.trim().split(/[ \t]*&&[ \t]*/u);
16
+ if (stages.length === 1) return stages[0] === "vite build";
17
+ if (stages.length !== 2) return false;
18
+ return (
19
+ (stages[0] === "vite build" && TYPESCRIPT_BUILD_STAGE.test(stages[1] ?? "")) ||
20
+ (TYPESCRIPT_BUILD_STAGE.test(stages[0] ?? "") && stages[1] === "vite build")
21
+ );
22
+ }
@@ -0,0 +1 @@
1
+ export { isAdmittedFrameworkBuildScript, PINNED_BUN_VERSION } from "./framework-build-script.mjs";
@@ -4,12 +4,18 @@ import { parse as parseYaml } from "yaml";
4
4
  import schema from "../schemas/ohmyhost.schema.json" with { type: "json" };
5
5
  import { normalizeApplicationRoot, type ApplicationRoot } from "./application-root.js";
6
6
  import { normalizeCrons } from "./functions.js";
7
+ import { normalizeBrowserPolicy, type BrowserPolicy } from "./browser-capabilities.js";
7
8
 
8
9
  export * from "./application-root.js";
9
10
  export * from "./framework-admission.js";
10
11
  export * from "./operation-deployment-result.js";
12
+ export * from "./operation-data-change-result.js";
11
13
  export * from "./operation-failure.js";
12
14
  export * from "./functions.js";
15
+ export * from "./browser-capabilities.js";
16
+ export * from "./problem-policy.js";
17
+ export * from "./secret-names.js";
18
+ export * from "./project-data-changes.js";
13
19
 
14
20
  export type OhmyhostRuntimeMode = "static" | "edge" | "functions" | "auto";
15
21
 
@@ -30,6 +36,7 @@ export interface OhmyhostConfig {
30
36
  mode: OhmyhostRuntimeMode;
31
37
  healthcheck: string;
32
38
  egress: Readonly<{ allow: readonly string[] }>;
39
+ browser?: BrowserPolicy;
33
40
  }>;
34
41
  readonly database?: Readonly<{
35
42
  enabled: boolean;
@@ -88,6 +95,9 @@ export function parseOhmyhostConfig(value: unknown): OhmyhostConfig {
88
95
  egress: Object.freeze({
89
96
  allow: normalizeEgressOrigins(input.runtime.egress?.allow),
90
97
  }),
98
+ ...(input.runtime.browser === undefined
99
+ ? {}
100
+ : { browser: normalizeBrowserPolicy(input.runtime.browser) }),
91
101
  }),
92
102
  ...(input.database === undefined
93
103
  ? {}
@@ -173,6 +183,7 @@ function normalizeEgressOrigins(value: readonly string[] | undefined): readonly
173
183
  }
174
184
  if (
175
185
  parsed.protocol !== "https:" ||
186
+ parsed.port !== "" ||
176
187
  parsed.username !== "" ||
177
188
  parsed.password !== "" ||
178
189
  parsed.origin !== origin ||
@@ -188,28 +199,18 @@ function normalizeEgressOrigins(value: readonly string[] | undefined): readonly
188
199
  return Object.freeze(origins);
189
200
  }
190
201
 
202
+ // Egress is routed and staged by DNS name on the default HTTPS port: an IP literal (public or
203
+ // private, v4 or v6) can never become a working origin, and a local name never resolves.
191
204
  function forbiddenEgressHostname(hostname: string): boolean {
192
205
  const normalized = hostname.toLowerCase();
193
- if (
206
+ return (
194
207
  normalized === "localhost" ||
195
208
  normalized.endsWith(".localhost") ||
196
209
  normalized.endsWith(".local") ||
197
210
  normalized.endsWith(".internal") ||
198
- normalized === "0.0.0.0" ||
199
- normalized === "::1"
200
- ) {
201
- return true;
202
- }
203
- const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(normalized);
204
- if (ipv4 === null) return normalized.startsWith("[") || normalized.endsWith("]");
205
- const octets = ipv4.slice(1).map(Number);
206
- if (octets.some((octet) => octet > 255)) return true;
207
- return (
208
- octets[0] === 10 ||
209
- octets[0] === 127 ||
210
- (octets[0] === 169 && octets[1] === 254) ||
211
- (octets[0] === 172 && (octets[1] ?? 0) >= 16 && (octets[1] ?? 0) <= 31) ||
212
- (octets[0] === 192 && octets[1] === 168)
211
+ /^\d{1,3}(?:\.\d{1,3}){3}$/u.test(normalized) ||
212
+ normalized.startsWith("[") ||
213
+ normalized.endsWith("]")
213
214
  );
214
215
  }
215
216