@amerged/ohmyhost-mcp 0.1.10

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 (72) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +11 -0
  4. package/THIRD_PARTY_NOTICES.md +474 -0
  5. package/dist/chunk-7MV2EVXV.js +30779 -0
  6. package/dist/index.js +9 -0
  7. package/dist/stdio-main.js +5607 -0
  8. package/package.json +32 -0
  9. package/src/apps/mcp/index.ts +30 -0
  10. package/src/apps/mcp/local-client.ts +891 -0
  11. package/src/apps/mcp/local-server.ts +1512 -0
  12. package/src/apps/mcp/stdio-main.ts +26 -0
  13. package/src/apps/product-cli/bin.ts +24 -0
  14. package/src/apps/product-cli/cli.ts +2271 -0
  15. package/src/apps/product-cli/command.ts +1811 -0
  16. package/src/apps/product-cli/composition.ts +823 -0
  17. package/src/apps/product-cli/credential-store.ts +149 -0
  18. package/src/apps/product-cli/device-token-expiry.ts +89 -0
  19. package/src/apps/product-cli/file-credential-store.ts +166 -0
  20. package/src/apps/product-cli/index.ts +23 -0
  21. package/src/apps/product-cli/product-api.ts +1082 -0
  22. package/src/apps/product-cli/project-link-store.ts +30 -0
  23. package/src/apps/product-cli/psql-session.ts +29 -0
  24. package/src/apps/product-cli/repository-init.ts +1622 -0
  25. package/src/apps/product-cli/runtime-contract.ts +1747 -0
  26. package/src/apps/product-cli/user-token-file.ts +214 -0
  27. package/src/apps/product-cli/workspace.ts +92 -0
  28. package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
  29. package/src/packages/agent-skills/index.ts +37 -0
  30. package/src/packages/contracts/application-root.ts +80 -0
  31. package/src/packages/contracts/billing.ts +172 -0
  32. package/src/packages/contracts/credit-pricing.ts +214 -0
  33. package/src/packages/contracts/database-access.ts +257 -0
  34. package/src/packages/contracts/database-compute.ts +72 -0
  35. package/src/packages/contracts/database-migration-admission.ts +235 -0
  36. package/src/packages/contracts/feedback.ts +96 -0
  37. package/src/packages/contracts/framework-admission.ts +280 -0
  38. package/src/packages/contracts/framework-family.ts +18 -0
  39. package/src/packages/contracts/functions.ts +76 -0
  40. package/src/packages/contracts/index.ts +245 -0
  41. package/src/packages/contracts/operation-deployment-result.ts +165 -0
  42. package/src/packages/contracts/operation-failure.ts +150 -0
  43. package/src/packages/contracts/platform-origins.ts +5 -0
  44. package/src/packages/contracts/project-context.ts +59 -0
  45. package/src/packages/contracts/project-exports.ts +102 -0
  46. package/src/packages/contracts/user-api-keys.ts +98 -0
  47. package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
  48. package/src/packages/sdk-ts/generated/client/index.ts +27 -0
  49. package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
  50. package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
  51. package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
  52. package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
  53. package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
  54. package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
  55. package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
  56. package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
  57. package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
  58. package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
  59. package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
  60. package/src/packages/sdk-ts/generated/index.ts +578 -0
  61. package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
  62. package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
  63. package/src/packages/sdk-ts/index.ts +7 -0
  64. package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
  65. package/src/packages/workos-auth-contracts/index.ts +8 -0
  66. package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
  67. package/src/packages/workos-auth-contracts/organizations.ts +113 -0
  68. package/src/packages/workos-auth-contracts/parsing.ts +75 -0
  69. package/src/packages/workos-auth-contracts/session-status.ts +53 -0
  70. package/src/packages/workos-auth-contracts/tokens.ts +216 -0
  71. package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
  72. package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
@@ -0,0 +1,18 @@
1
+ export type FrameworkFamilyClassification =
2
+ | "ambiguous"
3
+ | "nextjs"
4
+ | "tanstack-start"
5
+ | "unknown"
6
+ | "vite";
7
+
8
+ export function classifyFrameworkFamily(
9
+ dependencies: ReadonlySet<string>,
10
+ ): FrameworkFamilyClassification {
11
+ const next = dependencies.has("next");
12
+ const tanStack = dependencies.has("@tanstack/react-start");
13
+ const vite = dependencies.has("vite");
14
+ if (next && (tanStack || vite)) return "ambiguous";
15
+ if (next) return "nextjs";
16
+ if (tanStack) return "tanstack-start";
17
+ return vite ? "vite" : "unknown";
18
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Declared cron schedules for the Worker module of a project environment.
3
+ * Five-field UTC cron, at most eight unique entries, minimum five-minute interval.
4
+ */
5
+ export function normalizeCrons(value: readonly unknown[]): readonly string[] {
6
+ if (!Array.isArray(value) || value.length === 0 || value.length > 8) {
7
+ throw new TypeError("ohmyhost.yaml functions.crons must list one to eight schedules");
8
+ }
9
+ const crons = value.map((entry) => {
10
+ if (typeof entry !== "string") throw new TypeError("ohmyhost.yaml cron is invalid");
11
+ return normalizeCron(entry);
12
+ });
13
+ const sorted = [...crons].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
14
+ for (let index = 1; index < sorted.length; index += 1) {
15
+ if (sorted[index - 1] === sorted[index]) {
16
+ throw new TypeError("ohmyhost.yaml functions.crons must be unique");
17
+ }
18
+ }
19
+ return Object.freeze(sorted);
20
+ }
21
+
22
+ export function normalizeCron(value: string): string {
23
+ if (value !== value.trim() || value.includes(" ")) {
24
+ throw new TypeError("ohmyhost.yaml cron is invalid");
25
+ }
26
+ const fields = value.split(" ");
27
+ if (fields.length !== 5) throw new TypeError("ohmyhost.yaml cron is invalid");
28
+ const [minute, hour, day, month, weekday] = fields;
29
+ if (
30
+ minute === undefined ||
31
+ hour === undefined ||
32
+ day === undefined ||
33
+ month === undefined ||
34
+ weekday === undefined ||
35
+ !validCronMinute(minute) ||
36
+ !validCronField(hour, 0, 23) ||
37
+ !validCronField(day, 1, 31) ||
38
+ !validCronField(month, 1, 12) ||
39
+ !validCronField(weekday, 0, 6)
40
+ ) {
41
+ throw new TypeError("ohmyhost.yaml cron is invalid");
42
+ }
43
+ return value;
44
+ }
45
+
46
+ function validCronMinute(value: string): boolean {
47
+ const step = /^\*\/([1-9]|[1-5][0-9])$/u.exec(value);
48
+ if (step !== null) return Number(step[1]) >= 5;
49
+ return value !== "*" && validCronField(value, 0, 59);
50
+ }
51
+
52
+ function validCronField(value: string, minimum: number, maximum: number): boolean {
53
+ if (value === "*") return true;
54
+ if (!/^(?:0|[1-9][0-9]?)$/u.test(value)) return false;
55
+ const parsed = Number(value);
56
+ return parsed >= minimum && parsed <= maximum;
57
+ }
58
+
59
+ /** Whether a validated five-field UTC cron fires in the minute that contains `at`. */
60
+ export function isCronDue(cron: string, at: Date): boolean {
61
+ const fields = normalizeCron(cron).split(" ");
62
+ const [minute, hour, day, month, weekday] = fields as [string, string, string, string, string];
63
+ const utcMinute = at.getUTCMinutes();
64
+ const minuteDue = minute.startsWith("*/")
65
+ ? utcMinute % Number(minute.slice(2)) === 0
66
+ : Number(minute) === utcMinute;
67
+ const matches = (field: string, value: number): boolean =>
68
+ field === "*" || Number(field) === value;
69
+ return (
70
+ minuteDue &&
71
+ matches(hour, at.getUTCHours()) &&
72
+ matches(day, at.getUTCDate()) &&
73
+ matches(month, at.getUTCMonth() + 1) &&
74
+ matches(weekday, at.getUTCDay())
75
+ );
76
+ }
@@ -0,0 +1,245 @@
1
+ import { Ajv2020, type ErrorObject } from "ajv/dist/2020.js";
2
+ import { parse as parseYaml } from "yaml";
3
+
4
+ import schema from "../schemas/ohmyhost.schema.json" with { type: "json" };
5
+ import { normalizeApplicationRoot, type ApplicationRoot } from "./application-root.js";
6
+ import { normalizeCrons } from "./functions.js";
7
+
8
+ export * from "./application-root.js";
9
+ export * from "./framework-admission.js";
10
+ export * from "./operation-deployment-result.js";
11
+ export * from "./operation-failure.js";
12
+ export * from "./functions.js";
13
+
14
+ export type OhmyhostRuntimeMode = "static" | "edge" | "functions" | "auto";
15
+
16
+ /** Build settings of an application; the functions runtime installs dependencies only. */
17
+ export type OhmyhostApplicationBuild = Readonly<{
18
+ install: string;
19
+ command: string;
20
+ output: string;
21
+ }>;
22
+ export type OhmyhostFunctionsBuild = Readonly<{ install: string }>;
23
+
24
+ export interface OhmyhostConfig {
25
+ readonly version: 1;
26
+ readonly project: string;
27
+ readonly applicationRoot: ApplicationRoot;
28
+ readonly build: OhmyhostApplicationBuild | OhmyhostFunctionsBuild;
29
+ readonly runtime: Readonly<{
30
+ mode: OhmyhostRuntimeMode;
31
+ healthcheck: string;
32
+ egress: Readonly<{ allow: readonly string[] }>;
33
+ }>;
34
+ readonly database?: Readonly<{
35
+ enabled: boolean;
36
+ migrations?: string;
37
+ }>;
38
+ readonly auth?: Readonly<{
39
+ provider: "none" | "better-auth";
40
+ }>;
41
+ readonly mail?: Readonly<{
42
+ enabled: boolean;
43
+ }>;
44
+ readonly storage?: Readonly<{
45
+ provider: "r2";
46
+ /** Must equal the project's hosting region (`us` by default, `eu` when the project was created there). */
47
+ jurisdiction: "us" | "eu";
48
+ public: false;
49
+ }>;
50
+ readonly functions?: Readonly<{ crons: readonly string[] }>;
51
+ readonly credits?: Readonly<{
52
+ monthly_budget: number;
53
+ auto_recharge?: Readonly<{
54
+ enabled: boolean;
55
+ threshold?: number;
56
+ amount_usd_micros?: number;
57
+ monthly_cap_usd_micros?: number;
58
+ }>;
59
+ }>;
60
+ }
61
+
62
+ const validateOhmyhostConfig = new Ajv2020({
63
+ allErrors: true,
64
+ strict: true,
65
+ }).compile<OhmyhostConfig>(schema);
66
+
67
+ export function parseOhmyhostConfig(value: unknown): OhmyhostConfig {
68
+ if (!validateOhmyhostConfig(value)) {
69
+ throw new TypeError(`ohmyhost.yaml is invalid: ${formatErrors(validateOhmyhostConfig.errors)}`);
70
+ }
71
+
72
+ const input = value as OhmyhostConfig;
73
+ const config: OhmyhostConfig = {
74
+ version: 1,
75
+ project: input.project,
76
+ applicationRoot: normalizeApplicationRoot(input.applicationRoot),
77
+ build:
78
+ input.runtime.mode === "functions"
79
+ ? Object.freeze({ install: input.build.install })
80
+ : Object.freeze({
81
+ install: input.build.install,
82
+ command: (input.build as OhmyhostApplicationBuild).command,
83
+ output: (input.build as OhmyhostApplicationBuild).output,
84
+ }),
85
+ runtime: Object.freeze({
86
+ mode: input.runtime.mode,
87
+ healthcheck: normalizeHealthcheck(input.runtime.healthcheck),
88
+ egress: Object.freeze({
89
+ allow: normalizeEgressOrigins(input.runtime.egress?.allow),
90
+ }),
91
+ }),
92
+ ...(input.database === undefined
93
+ ? {}
94
+ : {
95
+ database: Object.freeze({
96
+ enabled: input.database.enabled,
97
+ ...(input.database.migrations === undefined
98
+ ? {}
99
+ : { migrations: input.database.migrations }),
100
+ }),
101
+ }),
102
+ ...(input.auth === undefined ? {} : { auth: Object.freeze({ provider: input.auth.provider }) }),
103
+ ...(input.mail === undefined ? {} : { mail: Object.freeze({ enabled: input.mail.enabled }) }),
104
+ ...(input.storage === undefined
105
+ ? {}
106
+ : {
107
+ storage: Object.freeze({
108
+ provider: input.storage.provider,
109
+ jurisdiction: input.storage.jurisdiction,
110
+ public: input.storage.public,
111
+ }),
112
+ }),
113
+ ...(input.functions === undefined
114
+ ? {}
115
+ : {
116
+ functions: Object.freeze({ crons: normalizeCrons(input.functions.crons) }),
117
+ }),
118
+ ...(input.credits === undefined
119
+ ? {}
120
+ : {
121
+ credits: Object.freeze({
122
+ monthly_budget: input.credits.monthly_budget,
123
+ ...(input.credits.auto_recharge === undefined
124
+ ? {}
125
+ : {
126
+ auto_recharge: Object.freeze({
127
+ enabled: input.credits.auto_recharge.enabled,
128
+ ...(input.credits.auto_recharge.threshold === undefined
129
+ ? {}
130
+ : { threshold: input.credits.auto_recharge.threshold }),
131
+ ...(input.credits.auto_recharge.amount_usd_micros === undefined
132
+ ? {}
133
+ : { amount_usd_micros: input.credits.auto_recharge.amount_usd_micros }),
134
+ ...(input.credits.auto_recharge.monthly_cap_usd_micros === undefined
135
+ ? {}
136
+ : {
137
+ monthly_cap_usd_micros:
138
+ input.credits.auto_recharge.monthly_cap_usd_micros,
139
+ }),
140
+ }),
141
+ }),
142
+ }),
143
+ }),
144
+ };
145
+
146
+ return Object.freeze(config);
147
+ }
148
+
149
+ function normalizeHealthcheck(value: string | undefined): string {
150
+ const candidate = value ?? "/";
151
+ if (
152
+ candidate !== candidate.trim() ||
153
+ !candidate.startsWith("/") ||
154
+ candidate.startsWith("//") ||
155
+ candidate.includes("\\") ||
156
+ candidate.includes("?") ||
157
+ candidate.includes("#") ||
158
+ candidate.split("/").some((segment) => segment === "." || segment === "..")
159
+ ) {
160
+ throw new TypeError("ohmyhost.yaml runtime healthcheck is invalid");
161
+ }
162
+ return candidate;
163
+ }
164
+
165
+ function normalizeEgressOrigins(value: readonly string[] | undefined): readonly string[] {
166
+ const origins = [...(value ?? [])];
167
+ for (const origin of origins) {
168
+ let parsed: URL;
169
+ try {
170
+ parsed = new URL(origin);
171
+ } catch {
172
+ throw new TypeError("ohmyhost.yaml runtime egress origin is invalid");
173
+ }
174
+ if (
175
+ parsed.protocol !== "https:" ||
176
+ parsed.username !== "" ||
177
+ parsed.password !== "" ||
178
+ parsed.origin !== origin ||
179
+ parsed.pathname !== "/" ||
180
+ parsed.search !== "" ||
181
+ parsed.hash !== "" ||
182
+ forbiddenEgressHostname(parsed.hostname)
183
+ ) {
184
+ throw new TypeError("ohmyhost.yaml runtime egress origin is invalid");
185
+ }
186
+ }
187
+ origins.sort();
188
+ return Object.freeze(origins);
189
+ }
190
+
191
+ function forbiddenEgressHostname(hostname: string): boolean {
192
+ const normalized = hostname.toLowerCase();
193
+ if (
194
+ normalized === "localhost" ||
195
+ normalized.endsWith(".localhost") ||
196
+ normalized.endsWith(".local") ||
197
+ 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)
213
+ );
214
+ }
215
+
216
+ /** The application build of a static or edge configuration. */
217
+ export function requireApplicationBuild(config: OhmyhostConfig): OhmyhostApplicationBuild {
218
+ if (config.runtime.mode === "functions" || !("command" in config.build)) {
219
+ throw new TypeError(
220
+ "ohmyhost.yaml build.command and build.output require an application runtime",
221
+ );
222
+ }
223
+ return config.build;
224
+ }
225
+
226
+ export function parseOhmyhostConfigYaml(source: string): OhmyhostConfig {
227
+ if (typeof source !== "string") {
228
+ throw new TypeError("ohmyhost.yaml must be a string");
229
+ }
230
+
231
+ let value: unknown;
232
+ try {
233
+ value = parseYaml(source) as unknown;
234
+ } catch {
235
+ throw new TypeError("ohmyhost.yaml is not valid YAML");
236
+ }
237
+
238
+ return parseOhmyhostConfig(value);
239
+ }
240
+
241
+ function formatErrors(errors: ErrorObject[] | null | undefined): string {
242
+ return (errors ?? [])
243
+ .map(({ instancePath, message }) => `${instancePath || "/"} ${message ?? "is invalid"}`)
244
+ .join("; ");
245
+ }
@@ -0,0 +1,165 @@
1
+ export const OPERATION_DEPLOYMENT_RESULT_SCHEMA_VERSION =
2
+ "ohmyhost.operation-deployment-result/v2" as const;
3
+
4
+ export interface OperationDeploymentResult {
5
+ readonly schemaVersion: typeof OPERATION_DEPLOYMENT_RESULT_SCHEMA_VERSION;
6
+ readonly operationId: string;
7
+ readonly deploymentId: string;
8
+ readonly planId: string;
9
+ readonly artifactSha256: string;
10
+ readonly url: string;
11
+ }
12
+
13
+ export function createOperationDeploymentResult(
14
+ input: Omit<OperationDeploymentResult, "schemaVersion">,
15
+ ): OperationDeploymentResult {
16
+ return parseOperationDeploymentResult({
17
+ schemaVersion: OPERATION_DEPLOYMENT_RESULT_SCHEMA_VERSION,
18
+ ...input,
19
+ });
20
+ }
21
+
22
+ export function parseOperationDeploymentResult(value: unknown): OperationDeploymentResult {
23
+ const result = exactRecord(value, [
24
+ "schemaVersion",
25
+ "operationId",
26
+ "deploymentId",
27
+ "planId",
28
+ "artifactSha256",
29
+ "url",
30
+ ]);
31
+ if (result["schemaVersion"] !== OPERATION_DEPLOYMENT_RESULT_SCHEMA_VERSION) invalid();
32
+ return Object.freeze({
33
+ schemaVersion: OPERATION_DEPLOYMENT_RESULT_SCHEMA_VERSION,
34
+ operationId: ulid(result["operationId"]),
35
+ deploymentId: ulid(result["deploymentId"]),
36
+ planId: ulid(result["planId"]),
37
+ artifactSha256: digest(result["artifactSha256"]),
38
+ url: httpsUrl(result["url"]),
39
+ });
40
+ }
41
+
42
+ function exactRecord(value: unknown, keys: readonly string[]): Record<string, unknown> {
43
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid();
44
+ const actual = Object.keys(value).sort();
45
+ const expected = [...keys].sort();
46
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
47
+ invalid();
48
+ }
49
+ return value as Record<string, unknown>;
50
+ }
51
+
52
+ function ulid(value: unknown): string {
53
+ if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value)) invalid();
54
+ return value;
55
+ }
56
+
57
+ function digest(value: unknown): string {
58
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) invalid();
59
+ return value;
60
+ }
61
+
62
+ function httpsUrl(value: unknown): string {
63
+ if (typeof value !== "string" || value.length > 2048) invalid();
64
+ let parsed: URL;
65
+ try {
66
+ parsed = new URL(value);
67
+ } catch {
68
+ invalid();
69
+ }
70
+ if (
71
+ parsed.protocol !== "https:" ||
72
+ parsed.username !== "" ||
73
+ parsed.password !== "" ||
74
+ parsed.search !== "" ||
75
+ parsed.hash !== "" ||
76
+ parsed.hostname === ""
77
+ ) {
78
+ invalid();
79
+ }
80
+ return value;
81
+ }
82
+
83
+ function invalid(): never {
84
+ throw new TypeError("Operation deployment result is invalid");
85
+ }
86
+
87
+ const deploymentProgressActions = {
88
+ queued: "The deployment is queued. Poll this same operation; do not submit another deployment.",
89
+ building:
90
+ "The build has not yet completed. Poll this same operation and read deployment logs if it fails.",
91
+ publishing:
92
+ "The build has completed. Runtime preparation or publication is still running; poll this same operation.",
93
+ waiting_for_mail:
94
+ "Read 'ohmyhost mail domain status --project PROJECT --json' now and follow its verification issue and DNS records. Keep this operation; do not rebuild or repeat correct DNS changes.",
95
+ mail_status_unavailable:
96
+ "Mail readiness could not be observed. Read 'ohmyhost mail domain status --project PROJECT --json' and report a persistent error; keep this operation.",
97
+ } as const;
98
+ export type DeploymentProgressPhase = keyof typeof deploymentProgressActions;
99
+ export interface OperationDeploymentProgress {
100
+ readonly build_completed_at?: string;
101
+ readonly phase: DeploymentProgressPhase;
102
+ readonly project_id: string;
103
+ readonly deployment_id: string;
104
+ readonly observed_at: string;
105
+ readonly next_poll_after_seconds: 60;
106
+ readonly suggested_action: string;
107
+ }
108
+ export function createOperationDeploymentProgress(input: {
109
+ build_completed_at?: string;
110
+ phase: DeploymentProgressPhase;
111
+ project_id: string;
112
+ deployment_id: string;
113
+ observed_at: string;
114
+ }): OperationDeploymentProgress {
115
+ const project = ulid(input.project_id);
116
+ ulid(input.deployment_id);
117
+ if (
118
+ !Object.hasOwn(deploymentProgressActions, input.phase) ||
119
+ !Number.isFinite(Date.parse(input.observed_at)) ||
120
+ new Date(input.observed_at).toISOString() !== input.observed_at
121
+ )
122
+ invalid();
123
+ if (
124
+ input.build_completed_at !== undefined &&
125
+ (!Number.isFinite(Date.parse(input.build_completed_at)) ||
126
+ new Date(input.build_completed_at).toISOString() !== input.build_completed_at)
127
+ )
128
+ invalid();
129
+ return Object.freeze({
130
+ ...(input.build_completed_at === undefined
131
+ ? {}
132
+ : { build_completed_at: input.build_completed_at }),
133
+ phase: input.phase,
134
+ project_id: project,
135
+ deployment_id: input.deployment_id,
136
+ observed_at: input.observed_at,
137
+ next_poll_after_seconds: 60 as const,
138
+ suggested_action: deploymentProgressActions[input.phase].replace("PROJECT", project),
139
+ });
140
+ }
141
+ export function parseOperationDeploymentProgress(value: unknown): OperationDeploymentProgress {
142
+ const keys = [
143
+ "phase",
144
+ "project_id",
145
+ "deployment_id",
146
+ "observed_at",
147
+ "next_poll_after_seconds",
148
+ "suggested_action",
149
+ ];
150
+ if (typeof value === "object" && value !== null && Object.hasOwn(value, "build_completed_at"))
151
+ keys.push("build_completed_at");
152
+ const row = exactRecord(value, keys);
153
+ const parsed = createOperationDeploymentProgress({
154
+ ...(row["build_completed_at"] === undefined
155
+ ? {}
156
+ : { build_completed_at: row["build_completed_at"] as string }),
157
+ phase: row["phase"] as DeploymentProgressPhase,
158
+ project_id: row["project_id"] as string,
159
+ deployment_id: row["deployment_id"] as string,
160
+ observed_at: row["observed_at"] as string,
161
+ });
162
+ if (row["next_poll_after_seconds"] !== 60 || row["suggested_action"] !== parsed.suggested_action)
163
+ invalid();
164
+ return parsed;
165
+ }
@@ -0,0 +1,150 @@
1
+ const failures = {
2
+ operation_abandoned: [
3
+ "The operation stopped renewing its execution lease and was ended.",
4
+ "Inspect project status and deployment diagnostics before creating a fresh plan. Keep this operation ID; the expired execution cannot resume.",
5
+ ],
6
+ database_write_rejected: [
7
+ "The database rejected this write; it was not committed.",
8
+ "Inspect the schema and parameter types with database query, then correct the statement and use a new key. Do not reset the database.",
9
+ ],
10
+ database_write_unavailable: [
11
+ "The database write could not start.",
12
+ "Read project status and database compute. This operation did not write data; after fixing availability, submit a new confirmed request.",
13
+ ],
14
+ database_write_outcome_unknown: [
15
+ "The database write outcome could not be confirmed.",
16
+ "Do not repeat the write with a new key. Inspect the target data with a read query and retain this operation ID. The same key only reads this receipt.",
17
+ ],
18
+ database_compute_failed: [
19
+ "The database size change failed at the provider.",
20
+ "Read the current database compute and report this operation ID through feedback. Preserve data; do not reset or redeploy the database to repair its size.",
21
+ ],
22
+ database_compute_plan_changed: [
23
+ "Paid access ended before the database size change started.",
24
+ "Read the organization's plan and current database compute, then confirm a new standard-size request. This operation did not resize the database.",
25
+ ],
26
+ database_compute_rejected: [
27
+ "The database size change could not be started.",
28
+ "Read current compute and report this operation ID through feedback. Preserve existing data. After the provider problem is resolved, submit a new confirmed size change.",
29
+ ],
30
+
31
+ recovery_dispatch_failed: [
32
+ "The export executor could not confirm completion after bounded delivery attempts.",
33
+ "Report the export operation ID through feedback. Check the original status before requesting another export after next_request_at.",
34
+ ],
35
+ recovery_job_failed: [
36
+ "The SQL export could not be completed.",
37
+ "Inspect the export status and report its operation ID through feedback. A new export is available after next_request_at.",
38
+ ],
39
+ recovery_input_expired: [
40
+ "The export did not start before its temporary password input expired.",
41
+ "The password input was discarded. Request a new export after next_request_at and report the delayed operation ID.",
42
+ ],
43
+ recovery_database_unavailable: [
44
+ "A read-only database snapshot could not be obtained.",
45
+ "Check database readiness through project status. Preserve existing data and report the export operation ID.",
46
+ ],
47
+ recovery_checksum_mismatch: [
48
+ "The stored export did not match its verified ciphertext receipt.",
49
+ "Do not use this archive. Report the export operation ID; request a new export after next_request_at.",
50
+ ],
51
+ recovery_scope_unavailable: [
52
+ "The project was frozen, transferred or deleted before the export completed.",
53
+ "Check the current project and organization access before requesting another export.",
54
+ ],
55
+ recovery_archive_too_large: [
56
+ "The SQL export exceeds the current 256 MiB plaintext limit.",
57
+ "Report the required export size through feedback. No incomplete archive is offered for download.",
58
+ ],
59
+ operation_failed: [
60
+ "The operation failed.",
61
+ "Inspect the operation events and deployment diagnostics. Include the operation ID when reporting the issue.",
62
+ ],
63
+ build_not_started: [
64
+ "The deployment ended without starting a build.",
65
+ "Create a fresh deployment plan and submit it once with a new idempotency key. Replaying this terminal operation cannot start a build. Report the operation ID if this happens again.",
66
+ ],
67
+ build_failed: [
68
+ "The build did not produce a verified deployable artifact.",
69
+ "Inspect the deployment build diagnostics and verify the install and build commands for the admitted commit. Correct the reported failure before planning a new deployment.",
70
+ ],
71
+ insufficient_organization_credits: [
72
+ "The organization does not have sufficient available credits for this operation.",
73
+ "Ask the organization owner to top up credits, then create a new deployment plan. Preserve existing data; do not buy a second subscription or repeatedly reconcile this terminal operation.",
74
+ ],
75
+ paid_plan_required: [
76
+ "Transactional mail requires an active Paid plan for this organization.",
77
+ "Activate or renew Paid for the organization, then create a new deployment plan. Replaying this terminal operation cannot enable mail. Do not change application code or reset its database.",
78
+ ],
79
+ database_migration_failed: [
80
+ "The database migrations could not be applied.",
81
+ "Inspect the migration diagnostics and verify the canonical SQL migrations on PostgreSQL. Preserve production data; do not reset the database to retry a deployment.",
82
+ ],
83
+ runtime_candidate_failed: [
84
+ "The deployed candidate failed its runtime checks.",
85
+ "Inspect deployment runtime diagnostics, required secrets and the health-check route before creating a new deployment plan.",
86
+ ],
87
+ runtime_candidate_rejected: [
88
+ "The runtime rejected the built Worker script.",
89
+ "Check that src/ohmyhost/worker.ts exports its handlers as the default export and imports no framework-only modules, then plan the new commit. Replaying this terminal operation cannot stage the same script.",
90
+ ],
91
+ storage_jurisdiction_conflict: [
92
+ "storage.jurisdiction must equal the project's hosting region.",
93
+ "Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status and plan again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
94
+ ],
95
+ provider_state_absent: [
96
+ "Reconciliation confirmed that the required deployment resources are absent.",
97
+ "Create a fresh deployment plan. The original operation is terminal; do not repeatedly reconcile it.",
98
+ ],
99
+ } as const;
100
+
101
+ export interface PublicOperationFailure {
102
+ readonly code: keyof typeof failures;
103
+ readonly message: string;
104
+ readonly retryable: false;
105
+ readonly suggested_action: string;
106
+ }
107
+
108
+ export interface OperationReconciliationObservation {
109
+ readonly state: "required" | "pending";
110
+ readonly attempt_id: string | null;
111
+ readonly observed_at: string;
112
+ }
113
+
114
+ export interface PublicOperationReconciliation extends OperationReconciliationObservation {
115
+ readonly suggested_action: string;
116
+ }
117
+
118
+ /** Reconciliation is an observation, not a terminal operation event or provider error. */
119
+ export function publicOperationReconciliation(
120
+ observation: OperationReconciliationObservation,
121
+ ): PublicOperationReconciliation {
122
+ if (
123
+ !["required", "pending"].includes(observation.state) ||
124
+ !Number.isFinite(Date.parse(observation.observed_at)) ||
125
+ (observation.state === "required"
126
+ ? observation.attempt_id !== null
127
+ : typeof observation.attempt_id !== "string" ||
128
+ !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(observation.attempt_id))
129
+ )
130
+ throw new TypeError("Operation reconciliation observation is invalid");
131
+ return Object.freeze({
132
+ state: observation.state,
133
+ attempt_id: observation.attempt_id,
134
+ observed_at: observation.observed_at,
135
+ suggested_action:
136
+ observation.state === "required"
137
+ ? "Confirm reconciliation of this same operation; preserve its existing build and resources. Do not submit another deployment."
138
+ : "Reconciliation is running. Poll this same operation after 60 seconds; do not start another reconciliation or deployment.",
139
+ });
140
+ }
141
+
142
+ /** Render only reviewed text; never return the internal/provider error message. */
143
+ export function publicOperationFailure(internalCode: string): PublicOperationFailure {
144
+ const normalized = internalCode.toLowerCase();
145
+ const code = Object.hasOwn(failures, normalized)
146
+ ? (normalized as keyof typeof failures)
147
+ : "operation_failed";
148
+ const [message, suggested_action] = failures[code];
149
+ return Object.freeze({ code, message, retryable: false, suggested_action });
150
+ }
@@ -0,0 +1,5 @@
1
+ export type SaasCnameTarget = "customers.omh.st" | "customers.ohmyho.st";
2
+
3
+ export function isSaasCnameTarget(value: unknown): value is SaasCnameTarget {
4
+ return value === "customers.omh.st" || value === "customers.ohmyho.st";
5
+ }