@amerged/ohmyhost-mcp 0.1.23 → 0.1.25

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-P44YTAKK.js → chunk-L5A5FEGY.js} +25 -24
  3. package/dist/index.js +1 -1
  4. package/dist/stdio-main.js +724 -266
  5. package/package.json +2 -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} +324 -142
  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} +90 -36
  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 +28 -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} +76 -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 +113 -4
  40. package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +201 -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,28 @@
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 managedCustomerAuthDatabaseMessage(path: string): string {
8
+ 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.`;
9
+ }
10
+
11
+ /** Managed auth needs its real database; Mail remains an independent optional capability. */
12
+ export function managedCustomerAuthAdmissionIssue(
13
+ capabilities: ManagedCustomerAuthCapabilities | null | undefined,
14
+ ): Readonly<{
15
+ code: "managed_auth_database_required";
16
+ message: string;
17
+ }> | null {
18
+ if (capabilities?.auth?.provider !== "better-auth") {
19
+ return null;
20
+ }
21
+ if (capabilities.database?.enabled !== true) {
22
+ return Object.freeze({
23
+ code: "managed_auth_database_required" as const,
24
+ message: managedCustomerAuthDatabaseMessage("ohmyhost.yaml"),
25
+ });
26
+ }
27
+ return null;
28
+ }
@@ -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,7 @@
1
+ import { PINNED_BUN_VERSION } from "./framework-build-script.mjs";
2
+ import { managedCustomerAuthDatabaseMessage } from "./customer-auth-admission.js";
3
+ export { managedCustomerAuthAdmissionIssue } from "./customer-auth-admission.js";
4
+
1
5
  export type PackageManagerName = "bun" | "npm" | "pnpm" | "yarn";
2
6
 
3
7
  export type FrameworkAdmissionReason =
@@ -6,7 +10,8 @@ export type FrameworkAdmissionReason =
6
10
  | "invalid_package_manager"
7
11
  | "missing_lockfile"
8
12
  | "missing_package_manager"
9
- | "package_manager_lockfile_mismatch";
13
+ | "package_manager_lockfile_mismatch"
14
+ | "package_manager_version_unsupported";
10
15
 
11
16
  export class FrameworkAdmissionError extends Error {
12
17
  public readonly code = "framework_admission_failed";
@@ -88,6 +93,9 @@ export function resolvePackageManager(input: PackageManagerInput): PackageManage
88
93
  if (!contract.lockfiles.includes(lockfile)) {
89
94
  throw new FrameworkAdmissionError("package_manager_lockfile_mismatch");
90
95
  }
96
+ if (manager === "bun" && version !== PINNED_BUN_VERSION) {
97
+ throw new FrameworkAdmissionError("package_manager_version_unsupported");
98
+ }
91
99
  return Object.freeze({
92
100
  manager,
93
101
  version,
@@ -187,17 +195,24 @@ function versionAdmission(
187
195
  }
188
196
 
189
197
  export type FrameworkConversionDiagnosticCode =
198
+ | "managed_auth_database_required"
190
199
  | "database_binding_private"
191
200
  | "database_driver_unsupported"
192
201
  | "source_syntax_invalid"
193
202
  | "worker_module_default_export_required"
194
- | "scheduled_handler_required";
203
+ | "scheduled_handler_required"
204
+ | "framework_base_path_unsupported"
205
+ | "tanstack_prerender_required"
206
+ | "tanstack_vite_plugins_required"
207
+ | "next_cache_components_unsupported";
195
208
 
196
209
  export interface FrameworkConversionDiagnostic {
197
210
  readonly code: FrameworkConversionDiagnosticCode;
198
211
  readonly path: string;
199
212
  }
200
213
 
214
+ const PUSH_AND_PLAN = "Commit and push the change, then plan the new commit.";
215
+
201
216
  // Detail texts are the customer-facing sentence for one diagnostic; parseFrameworkConversionDetail
202
217
  // recognizes exactly these sentences so a client can map a detail back to its typed diagnostic.
203
218
  const FRAMEWORK_CONVERSION_DETAILS: Readonly<
@@ -206,38 +221,84 @@ const FRAMEWORK_CONVERSION_DETAILS: Readonly<
206
221
  Readonly<{ render(path: string): string; pattern: RegExp }>
207
222
  >
208
223
  > = Object.freeze({
224
+ managed_auth_database_required: Object.freeze({
225
+ render: managedCustomerAuthDatabaseMessage,
226
+ pattern:
227
+ /^"([^"]+)" 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,
228
+ }),
209
229
  database_binding_private: Object.freeze({
210
230
  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.`,
231
+ `"${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
232
  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,
233
+ /^"([^"]+)" 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
234
  }),
215
235
  database_driver_unsupported: Object.freeze({
216
236
  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.`,
237
+ `"${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
238
  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,
239
+ /^"([^"]+)" 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
240
  }),
221
241
  source_syntax_invalid: Object.freeze({
222
242
  render: (path: string) =>
223
- `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit the change, then plan again.`,
243
+ `Invalid JavaScript or TypeScript in "${path}". Fix the file, commit and push the change, then plan the new commit.`,
224
244
  pattern:
225
- /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit the change, then plan again\.$/u,
245
+ /^Invalid JavaScript or TypeScript in "([^"]+)"\. Fix the file, commit and push the change, then plan the new commit\.$/u,
226
246
  }),
227
247
  worker_module_default_export_required: Object.freeze({
228
248
  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.`,
249
+ `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
250
  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,
251
+ /^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
252
  }),
233
253
  scheduled_handler_required: Object.freeze({
234
254
  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.`,
255
+ `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.`,
256
+ pattern:
257
+ /^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,
258
+ }),
259
+ framework_base_path_unsupported: Object.freeze({
260
+ render: (path: string) =>
261
+ `"${path}" sets a Vite base. ohmyho.st serves the app at the root origin: remove base or set base: "/". ${PUSH_AND_PLAN}`,
262
+ pattern:
263
+ /^"([^"]+)" 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,
264
+ }),
265
+ tanstack_prerender_required: Object.freeze({
266
+ render: (path: string) =>
267
+ `"${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}`,
236
268
  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,
269
+ /^"([^"]+)" 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,
270
+ }),
271
+ tanstack_vite_plugins_required: Object.freeze({
272
+ render: (path: string) =>
273
+ `"${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}`,
274
+ pattern:
275
+ /^"([^"]+)" 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,
276
+ }),
277
+ next_cache_components_unsupported: Object.freeze({
278
+ render: (path: string) =>
279
+ `"${path}" sets cacheComponents: true, which ohmyho.st does not support; remove it or set it to false. ${PUSH_AND_PLAN}`,
280
+ pattern:
281
+ /^"([^"]+)" 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
282
  }),
239
283
  });
240
284
 
285
+ /**
286
+ * An adapter rule that `init` does not check. It stays a TypeError, so every existing caller that
287
+ * treats an adapter refusal as a TypeError keeps working, and carries the typed diagnostic whose
288
+ * detail names the file and its fix.
289
+ */
290
+ export class FrameworkConversionDiagnosticError extends TypeError {
291
+ public readonly diagnostic: FrameworkConversionDiagnostic;
292
+
293
+ public constructor(diagnostic: unknown) {
294
+ const parsed = parseFrameworkConversionDiagnostic(diagnostic);
295
+ if (parsed === null) throw new TypeError("Framework conversion diagnostic is invalid");
296
+ super(FRAMEWORK_CONVERSION_DETAILS[parsed.code].render(parsed.path));
297
+ this.name = "FrameworkConversionDiagnosticError";
298
+ this.diagnostic = parsed;
299
+ }
300
+ }
301
+
241
302
  function isFrameworkConversionDiagnosticCode(
242
303
  value: unknown,
243
304
  ): value is FrameworkConversionDiagnosticCode {
@@ -256,7 +317,9 @@ export function parseFrameworkConversionDiagnostic(
256
317
  !isFrameworkConversionDiagnosticCode(code) ||
257
318
  typeof path !== "string" ||
258
319
  path.length > 240 ||
259
- !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path) ||
320
+ (code === "managed_auth_database_required"
321
+ ? path !== "ohmyhost.yaml"
322
+ : !/^[A-Za-z0-9_@()[\]./-]+\.(?:[cm]?[jt]sx?)$/u.test(path)) ||
260
323
  path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
261
324
  )
262
325
  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