@amerged/ohmyhost-cli 0.1.23 → 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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +3 -2
  3. package/dist/{chunk-74DROG4F.js → chunk-QIJJFWW6.js} +993 -301
  4. package/dist/index.js +13 -1
  5. package/package.json +1 -1
  6. package/src/apps/product-cli/package.json +45 -0
  7. package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
  8. package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
  9. package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
  10. package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
  11. package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
  12. package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
  13. package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +79 -29
  14. package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
  15. package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
  16. package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
  17. package/src/packages/contracts/package.json +137 -0
  18. package/src/packages/contracts/src/browser-capabilities.ts +133 -0
  19. package/src/packages/contracts/src/customer-auth-admission.ts +36 -0
  20. package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
  21. package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +88 -13
  22. package/src/packages/contracts/src/framework-build-script.mjs +22 -0
  23. package/src/packages/contracts/src/framework-build-script.ts +1 -0
  24. package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
  25. package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
  26. package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
  27. package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
  28. package/src/packages/contracts/src/problem-policy.ts +498 -0
  29. package/src/packages/contracts/src/project-data-changes.ts +90 -0
  30. package/src/packages/contracts/src/secret-names.ts +27 -0
  31. package/src/packages/sdk-ts/package.json +21 -0
  32. package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
  33. package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +112 -3
  34. package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +201 -10
  35. package/src/packages/workos-auth-contracts/package.json +39 -0
  36. /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
  37. /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
  38. /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
  39. /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
  40. /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
  41. /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
  42. /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
  43. /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
  44. /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
  45. /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
  46. /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
  47. /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
  48. /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
  49. /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
  50. /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
  51. /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
  52. /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
  53. /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  54. /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
  55. /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
  56. /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
  57. /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
  58. /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
  59. /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
  60. /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
  61. /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
  62. /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
  63. /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
  64. /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
  65. /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
  66. /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
  67. /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
  68. /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
  69. /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
  70. /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
  71. /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
  72. /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
  73. /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
  74. /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
  75. /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
  76. /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
  77. /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
  78. /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
@@ -23,23 +23,28 @@ export class UserTokenFileError extends Error {
23
23
  | "token_value_unavailable",
24
24
  readonly keyId?: string,
25
25
  ) {
26
- super(
27
- {
28
- token_file_not_ignored:
29
- "Add the selected env file to .gitignore or choose a location outside the repository. No token was created or written.",
30
- token_file_unsafe:
31
- "Choose a private .env file owned by you. Symlinks, shared files and files larger than 64 KiB are not accepted.",
32
- token_file_has_token:
33
- "The selected file already contains OHMYHOST_TOKEN. Use that token or choose a new file; existing credentials were preserved.",
34
- token_file_changed:
35
- "The environment file changed during creation. It was not overwritten. List tokens to inspect the issued key before creating another.",
36
- token_value_unavailable:
37
- "This token was already issued and its full value cannot be retrieved. Use the previously saved file, or explicitly revoke the listed key before creating a replacement.",
38
- }[code] + (keyId ? ` Token ID: ${keyId}.` : ""),
39
- );
26
+ super(tokenFileAction(code, keyId));
40
27
  }
41
28
  }
42
29
 
30
+ /** What to do next; once a key was issued, the answer names it so it can be revoked. */
31
+ const tokenFileAction = (code: UserTokenFileError["code"], keyId: string | undefined): string => {
32
+ switch (code) {
33
+ case "token_file_not_ignored":
34
+ return "Add the selected env file to .gitignore or choose a location outside the repository. No token was created or written.";
35
+ case "token_file_unsafe":
36
+ return "Choose an env file whose name starts or ends with .env, for example .env.local: a regular file you own, not a symlink or hard link, at most 64 KiB.";
37
+ case "token_file_has_token":
38
+ return "The selected file already contains OHMYHOST_TOKEN. Use that token or choose a new file; existing credentials were preserved.";
39
+ case "token_file_changed":
40
+ return keyId === undefined
41
+ ? "The env file changed while it was being read; no key was issued. Run the same command again."
42
+ : `The env file changed while the token was being saved, so its value may not be in the file; key ${keyId} was issued. Revoke it with 'ohmyhost token revoke --organization ULID --key ${keyId} --yes --json' (MCP token_revoke), then create a new one with a new idempotency key.`;
43
+ case "token_value_unavailable":
44
+ return `A key was already issued for this idempotency key, and its value is shown only once. Use the file it was saved to, or revoke key ${String(keyId)} (ohmyhost token revoke, MCP token_revoke) and create a replacement with a new idempotency key.`;
45
+ }
46
+ };
47
+
43
48
  /** Prepare before the API call; append only OHMYHOST_TOKEN and never return its value to a chat. */
44
49
  export async function prepareUserTokenFile(output: string) {
45
50
  const path = resolve(output);
@@ -77,28 +82,34 @@ export async function prepareUserTokenFile(output: string) {
77
82
  const result = parseUserApiKeyCreation(value);
78
83
  if (result.value === null)
79
84
  throw new UserTokenFileError("token_value_unavailable", result.key.id);
80
- const current = await lstat(path);
81
- if (
82
- !current.isFile() ||
83
- current.isSymbolicLink() ||
84
- current.nlink !== 1 ||
85
- current.ino !== identity.ino ||
86
- current.dev !== identity.dev ||
87
- (await tokenFileText(handle)) !== initial
88
- )
89
- throw new UserTokenFileError("token_file_changed");
90
- await handle.appendFile(`\nOHMYHOST_TOKEN=${result.value}\n`, "utf8");
91
- await handle.sync();
92
- if (parseEnv(await tokenFileText(handle))["OHMYHOST_TOKEN"] !== result.value)
93
- throw new UserTokenFileError("token_file_changed");
94
- const saved = await lstat(path);
95
- if (
96
- !saved.isFile() ||
97
- saved.isSymbolicLink() ||
98
- saved.ino !== identity.ino ||
99
- saved.dev !== identity.dev
100
- )
85
+ try {
86
+ const current = await lstat(path);
87
+ if (
88
+ !current.isFile() ||
89
+ current.isSymbolicLink() ||
90
+ current.nlink !== 1 ||
91
+ current.ino !== identity.ino ||
92
+ current.dev !== identity.dev ||
93
+ (await tokenFileText(handle)) !== initial
94
+ )
95
+ throw new UserTokenFileError("token_file_changed");
96
+ await handle.appendFile(`\nOHMYHOST_TOKEN=${result.value}\n`, "utf8");
97
+ await handle.sync();
98
+ if (parseEnv(await tokenFileText(handle))["OHMYHOST_TOKEN"] !== result.value)
99
+ throw new UserTokenFileError("token_file_changed");
100
+ const saved = await lstat(path);
101
+ if (
102
+ !saved.isFile() ||
103
+ saved.isSymbolicLink() ||
104
+ saved.ino !== identity.ino ||
105
+ saved.dev !== identity.dev
106
+ )
107
+ throw new UserTokenFileError("token_file_changed");
108
+ } catch {
109
+ // The file was checked before the key was issued, so any refusal now means it changed,
110
+ // and the answer names the issued key so it can be revoked.
101
111
  throw new UserTokenFileError("token_file_changed", result.key.id);
112
+ }
102
113
  return { request_id: result.request_id, key: result.key, token_file: path };
103
114
  },
104
115
  close: () => handle.close(),
@@ -119,8 +119,14 @@ export const selectWorkspace = async (
119
119
  session.profile.userId,
120
120
  organization.id,
121
121
  );
122
+ // That user's existing login in the organization keeps its name; binding under this login's alias
123
+ // would silently rename it for every agent that selects it by name.
124
+ const existing = (await session.profiles.list()).find(
125
+ (saved) => saved.userId === session.profile.userId && saved.organizationId === organization.id,
126
+ );
122
127
  const bound: LocalProfile = {
123
128
  ...session.profile,
129
+ name: existing?.name ?? session.profile.name,
124
130
  organizationId: organization.id,
125
131
  organizationName: organization.name,
126
132
  };
@@ -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";