@amerged/ohmyhost-cli 0.1.36 → 0.1.38

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.
@@ -5,6 +5,11 @@
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "exports": {
8
+ "./shared-mail": {
9
+ "source": "./src/shared-mail.ts",
10
+ "types": "./dist/shared-mail.d.ts",
11
+ "import": "./dist/shared-mail.js"
12
+ },
8
13
  "./mcp-source-codec": {
9
14
  "source": "./src/mcp-source-codec.ts",
10
15
  "types": "./dist/mcp-source-codec.d.ts",
@@ -145,7 +150,7 @@
145
150
  },
146
151
  "scripts": {
147
152
  "build": "tsc --project tsconfig.build.json",
148
- "typecheck": "tsc --project tsconfig.json --noEmit",
153
+ "typecheck": "tsc --project tsconfig.json --noEmit --incremental --tsBuildInfoFile typecheck.tsbuildinfo",
149
154
  "test": "vitest run --root ../.. packages/contracts/test",
150
155
  "validate": "pnpm run typecheck && pnpm run test"
151
156
  },
@@ -48,7 +48,7 @@ const CORPUS_NEXT_PAGE = `export default function Home() {
48
48
  return <main>Hello from ohmyho.st</main>;
49
49
  }
50
50
  `;
51
- // The config `create-next-app@16.4.0 --yes --no-tailwind` writes: Cache Components with partial
51
+ // The config `create-next-app@16.4.1 --yes --no-tailwind` writes: Cache Components with partial
52
52
  // prefetching.
53
53
  const CREATE_NEXT_APP_CONFIG = `import type { NextConfig } from "next";
54
54
 
@@ -124,10 +124,7 @@ export const FRAMEWORK_ADMISSION_CONFORMANCE_CORPUS: Readonly<{
124
124
  Readonly<{ diagnostic: FrameworkConversionDiagnostic }>)[];
125
125
  }> = Object.freeze({
126
126
  admitted: Object.freeze([
127
- corpusCase(
128
- "Next.js 16.3.8 with npm on the edge runtime (the exact customer source)",
129
- nextSource(),
130
- ),
127
+ corpusCase("Next.js 16.4.1 with npm on the edge runtime", nextSource()),
131
128
  corpusCase("Next.js 17.0.0, a later stable major", nextSource({ declared: "17.0.0" })),
132
129
  corpusCase(
133
130
  "Vite ^8.2.0 resolved to 8.3.4 by pnpm-lock.yaml",
@@ -147,26 +144,26 @@ export const FRAMEWORK_ADMISSION_CONFORMANCE_CORPUS: Readonly<{
147
144
  Object.freeze({
148
145
  ...corpusCase(
149
146
  "Next.js ^16.3.0 with bun.lock, which ohmyho.st does not resolve",
150
- nextSource({ declared: "^16.3.0", resolved: "16.4.0", manager: "bun" }),
147
+ nextSource({ declared: "^16.3.0", resolved: "16.4.1", manager: "bun" }),
151
148
  ),
152
149
  packages: Object.freeze(["next"] as const),
153
150
  }),
154
151
  ]),
155
152
  refused: Object.freeze([
156
153
  refusedCase(
157
- "Next.js 16.3.7 below the 16.x security minimum",
158
- nextSource({ declared: "16.3.7" }),
159
- versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
154
+ "Next.js 16.4.0 below the 16.x security minimum",
155
+ nextSource({ declared: "16.4.0" }),
156
+ versionFinding("package-lock.json", "next", "16.4.0", "16.4.1"),
160
157
  ),
161
158
  refusedCase(
162
- "Next.js 15.5.26 declared with yarn below the 15.x security minimum",
163
- nextSource({ declared: "15.5.26", manager: "yarn" }),
164
- versionFinding("package.json", "next", "15.5.26", "15.5.27"),
159
+ "Next.js 15.5.28 declared with yarn, because no 15.x release is admitted",
160
+ nextSource({ declared: "15.5.28", manager: "yarn" }),
161
+ versionFinding("package.json", "next", "15.5.28", "16.4.1"),
165
162
  ),
166
163
  refusedCase(
167
- "Next.js ^16.3.0 resolved to 16.3.7 by package-lock.json",
168
- nextSource({ declared: "^16.3.0", resolved: "16.3.7" }),
169
- versionFinding("package-lock.json", "next", "16.3.7", "16.3.8"),
164
+ "Next.js ^16.3.0 resolved to 16.4.0 by package-lock.json",
165
+ nextSource({ declared: "^16.3.0", resolved: "16.4.0" }),
166
+ versionFinding("package-lock.json", "next", "16.4.0", "16.4.1"),
170
167
  ),
171
168
  refusedCase(
172
169
  "TanStack Start 1.168.59 resolved by pnpm-lock.yaml below its security minimum",
@@ -354,7 +351,7 @@ function nextSource(
354
351
  ): Readonly<Record<string, string>> {
355
352
  const manager = options.manager ?? "npm";
356
353
  const [packageManager, lockfile] = CORPUS_MANAGERS[manager];
357
- const next = options.declared ?? "16.3.8";
354
+ const next = options.declared ?? "16.4.1";
358
355
  const react = { react: "19.1.2", "react-dom": "19.1.2" };
359
356
  const runtime = options.field === "devDependencies" ? react : { next, ...react };
360
357
  return Object.freeze({
@@ -380,7 +377,7 @@ function nextSource(
380
377
  }
381
378
 
382
379
  /**
383
- * The next.config.ts and Tailwind CSS files and packages `create-next-app@16.4.0 --yes` writes, in
380
+ * The next.config.ts and Tailwind CSS files and packages `create-next-app@16.4.1 --yes` writes, in
384
381
  * an App Router source with the packageManager, lockfile and ohmyhost.yaml ohmyho.st requires;
385
382
  * `tailwind: false` is its --no-tailwind output. On Turbopack, its default bundler, it adds only the
386
383
  * Turbopack rule for Tailwind CSS; `postcss` adds the postcss.config.mjs and @tailwindcss/postcss it
@@ -390,7 +387,7 @@ function createNextAppSource(
390
387
  options: Readonly<{ tailwind: boolean; postcss?: boolean }>,
391
388
  ): Readonly<Record<string, string>> {
392
389
  const [packageManager, lockfile] = CORPUS_MANAGERS.npm;
393
- const runtime = { next: "16.4.0", react: "19.3.0", "react-dom": "19.3.0" };
390
+ const runtime = { next: "16.4.1", react: "19.3.0", "react-dom": "19.3.0" };
394
391
  const postcss = options.postcss === true;
395
392
  return Object.freeze({
396
393
  "ohmyhost.yaml": corpusConfiguration("npm", ".open-next/assets", "edge"),
@@ -15,8 +15,15 @@ export const PINNED_BUN_VERSION = "1.2.22";
15
15
  * upper bound: later patches, minors and majors reach the real build, which reports adapter failures.
16
16
  * Denied ranges are half-open [introduced, fixed); a null fix denies every later release.
17
17
  *
18
- * next: GHSA-4jqv-mc3x-m676 and GHSA-mcj8-r9mp-w47p (fixed in 15.5.27 and 16.3.8); GHSA-cjq9-62q9-8jv4,
19
- * GHSA-f87g-xv8r-7p7x, GHSA-3w37-wq28-93x7, GHSA-h694-7cp9-m8p3 and GHSA-39w2-rjm5-chcv (16.3.8).
18
+ * next: GHSA-2464-fhr4-w9xw (critical, remote code execution through the Image Optimization API with
19
+ * SVG and sharp before 0.35.5) and GHSA-p3m5-59mp-jv5r (high, denial of service through Server
20
+ * Actions), both fixed in 15.5.28 and 16.4.1; GHSA-673q-mjm6-r5xr (critical, remote code execution
21
+ * through `use cache` functions that return typed arrays, Next.js 16 only, fixed in 16.4.1).
22
+ * There is no 15.x floor: the pinned OpenNext has refused Next.js 15 since 21 October 2026, so every 15.x
23
+ * release is refused with the minimum 16.4.1 instead of sending a customer to 15.5.28 and a failing build.
24
+ * Earlier floors: GHSA-4jqv-mc3x-m676 and GHSA-mcj8-r9mp-w47p (fixed in 15.5.27 and 16.3.8);
25
+ * GHSA-cjq9-62q9-8jv4, GHSA-f87g-xv8r-7p7x, GHSA-3w37-wq28-93x7, GHSA-h694-7cp9-m8p3 and
26
+ * GHSA-39w2-rjm5-chcv (16.3.8).
20
27
  * @tanstack/react-start: GHSA-qx66-fv34-fjm8 (fixed in 1.168.60).
21
28
  * vite: GHSA-64vr-g452-qvp3 (fixed in 5.4.6), the only advisory affecting `vite build` output.
22
29
  * @type {FrameworkVersionPolicy}
@@ -27,7 +34,7 @@ export const FRAMEWORK_VERSION_POLICY = Object.freeze({
27
34
  floors: Object.freeze(["14.4.0"]),
28
35
  denied: Object.freeze([]),
29
36
  }),
30
- next: Object.freeze({ floors: Object.freeze(["15.5.27", "16.3.8"]), denied: Object.freeze([]) }),
37
+ next: Object.freeze({ floors: Object.freeze(["16.4.1"]), denied: Object.freeze([]) }),
31
38
  vite: Object.freeze({ floors: Object.freeze(["5.4.6"]), denied: Object.freeze([]) }),
32
39
  "@tanstack/react-start": Object.freeze({
33
40
  floors: Object.freeze(["1.168.60"]),
@@ -209,6 +209,9 @@ function normalizeEgressOrigins(value: readonly string[] | undefined): readonly
209
209
  function forbiddenEgressHostname(hostname: string): boolean {
210
210
  const normalized = hostname.toLowerCase();
211
211
  return (
212
+ /^ohmyho-(?:mail|storage)-gateway-[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*\.workers\.dev\.?$/u.test(
213
+ normalized,
214
+ ) ||
212
215
  normalized === "localhost" ||
213
216
  normalized.endsWith(".localhost") ||
214
217
  normalized.endsWith(".local") ||
@@ -313,14 +313,14 @@ export const PROBLEM_POLICY = {
313
313
  status: 409,
314
314
  retryable: false,
315
315
  action:
316
- "Use the sender domain already configured for this project, or retire it with 'ohmyhost mail domain delete' (repeat while the removal is pending) and then set up the new domain; a domain registered to another project cannot be set up.",
316
+ "Read the workspace sender resource and project binding. Reuse requires explicit domain-use authority, target-project editing and a current verified application identity with the same registrable main domain. Read the current revision before changing settings. Detach a shared project binding explicitly; legacy domain deletion cannot retire a resource used by another project.",
317
317
  title: "Mail domain conflict",
318
318
  },
319
319
  mail_domain_required: {
320
320
  status: 409,
321
321
  retryable: false,
322
322
  action:
323
- "This app declares mail.enabled. Configure the project's own sender domain with 'ohmyhost mail setup' using its Prod environment ID, or set mail.enabled to false if the app sends no mail, then plan again.",
323
+ "This app declares mail.enabled. Configure a sender with 'ohmyhost mail setup' or explicitly attach an authorized workspace sender using the project's Prod environment ID. Reuse first requires proof of the intended application hostname with the same registrable main domain; declare and verify that identity before mail-gated deployment. Then plan again.",
324
324
  title: "Mail domain required",
325
325
  },
326
326
  storage_jurisdiction_conflict: {
@@ -0,0 +1,236 @@
1
+ export interface SharedMailResponseContext {
2
+ readonly action: string;
3
+ readonly organizationId?: string;
4
+ readonly projectId?: string;
5
+ readonly environmentId?: string;
6
+ readonly mailDomainId?: string;
7
+ }
8
+ const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
9
+ const HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u;
10
+
11
+ /** Strict validation keeps provider data and out-of-scope resources out of client guidance. */
12
+ export function assertSharedMailResponse(value: unknown, input: SharedMailResponseContext): void {
13
+ const v = object(value);
14
+ if (input.action === "domains_list") {
15
+ exact(v, ["items"]);
16
+ if (!Array.isArray(v["items"]) || v["items"].length > 100) invalid();
17
+ for (const item of v["items"])
18
+ assertSharedMailResponse(item, {
19
+ ...input,
20
+ action: "domain_get",
21
+ mailDomainId: String(object(item)["domain_id"]),
22
+ });
23
+ return;
24
+ }
25
+ if (["domain_get", "domain_create", "domain_retire"].includes(input.action)) {
26
+ exact(v, [
27
+ "domain_id",
28
+ "organization_id",
29
+ "domain",
30
+ "registrable_domain",
31
+ "revision",
32
+ "status",
33
+ "sending",
34
+ "receiving",
35
+ "configured_at",
36
+ "observed_at",
37
+ "dns_records",
38
+ "bindings_count",
39
+ ]);
40
+ id(v["domain_id"]);
41
+ id(v["organization_id"]);
42
+ if (
43
+ !("organizationId" in input) ||
44
+ v["organization_id"] !== input.organizationId ||
45
+ ("mailDomainId" in input && v["domain_id"] !== input.mailDomainId)
46
+ )
47
+ invalid();
48
+ host(v["domain"]);
49
+ host(v["registrable_domain"]);
50
+ positive(v["revision"]);
51
+ member(v["status"], ["active", "retiring", "retired"]);
52
+ for (const k of ["sending", "receiving"])
53
+ member(v[k], ["disabled", "pending", "ready", "failed"]);
54
+ time(v["configured_at"]);
55
+ if (v["observed_at"] !== null) time(v["observed_at"]);
56
+ if (!Number.isSafeInteger(v["bindings_count"]) || Number(v["bindings_count"]) < 0) invalid();
57
+ if (!Array.isArray(v["dns_records"])) invalid();
58
+ for (const entry of v["dns_records"]) {
59
+ const r = object(entry);
60
+ exact(r, ["type", "name", "value", "purpose", "status"], ["priority"]);
61
+ member(r["type"], ["TXT", "MX", "CNAME"]);
62
+ for (const k of ["name", "value", "purpose", "status"])
63
+ if (typeof r[k] !== "string" || /[\r\n\0]/u.test(r[k])) invalid();
64
+ if (
65
+ r["priority"] !== undefined &&
66
+ (!Number.isSafeInteger(r["priority"]) ||
67
+ Number(r["priority"]) < 0 ||
68
+ Number(r["priority"]) > 65535)
69
+ )
70
+ invalid();
71
+ if (r["type"] === "MX" && r["priority"] === undefined) invalid();
72
+ }
73
+ return;
74
+ }
75
+ if (["binding_get", "binding_set", "binding_detach"].includes(input.action)) {
76
+ exact(v, [
77
+ "mail_domain_id",
78
+ "organization_id",
79
+ "project_id",
80
+ "environment_id",
81
+ "revision",
82
+ "sending",
83
+ "sender_addresses",
84
+ "default_from",
85
+ "default_reply_to",
86
+ "legacy",
87
+ "status",
88
+ "application_domain_identity",
89
+ ]);
90
+ for (const k of ["mail_domain_id", "organization_id", "project_id", "environment_id"]) id(v[k]);
91
+ if (
92
+ !("projectId" in input) ||
93
+ !("environmentId" in input) ||
94
+ v["project_id"] !== input.projectId ||
95
+ v["environment_id"] !== input.environmentId ||
96
+ ("mailDomainId" in input && v["mail_domain_id"] !== input.mailDomainId)
97
+ )
98
+ invalid();
99
+ positive(v["revision"]);
100
+ for (const k of ["sending", "legacy"]) if (typeof v[k] !== "boolean") invalid();
101
+ member(v["status"], ["active", "detached", "suspended"]);
102
+ if (
103
+ !Array.isArray(v["sender_addresses"]) ||
104
+ v["sender_addresses"].length > 32 ||
105
+ new Set(v["sender_addresses"]).size !== v["sender_addresses"].length
106
+ )
107
+ invalid();
108
+ for (const a of v["sender_addresses"]) mailbox(a);
109
+ for (const k of ["default_from", "default_reply_to"]) if (v[k] !== null) mailbox(v[k]);
110
+ if (
111
+ !v["legacy"] &&
112
+ (!v["sender_addresses"].includes(v["default_from"]) || v["sender_addresses"].length === 0)
113
+ )
114
+ invalid();
115
+ if (v["application_domain_identity"] !== null) {
116
+ const identity = object(v["application_domain_identity"]);
117
+ exact(identity, ["hostname", "registrable_domain", "revision", "status"]);
118
+ host(identity["hostname"]);
119
+ host(identity["registrable_domain"]);
120
+ positive(identity["revision"]);
121
+ member(identity["status"], ["pending", "verified", "expired", "revoked"]);
122
+ }
123
+ return;
124
+ }
125
+ if (
126
+ ["identity_get", "identity_declare", "identity_verify", "identity_revoke"].includes(
127
+ input.action,
128
+ )
129
+ ) {
130
+ exact(v, [
131
+ "organization_id",
132
+ "project_id",
133
+ "hostname",
134
+ "registrable_domain",
135
+ "status",
136
+ "revision",
137
+ "challenge",
138
+ "verified_at",
139
+ "expires_at",
140
+ ]);
141
+ id(v["organization_id"]);
142
+ id(v["project_id"]);
143
+ if (!("projectId" in input) || v["project_id"] !== input.projectId) invalid();
144
+ host(v["hostname"]);
145
+ host(v["registrable_domain"]);
146
+ positive(v["revision"]);
147
+ member(v["status"], ["pending", "verified", "expired", "revoked"]);
148
+ const challenge = object(v["challenge"]);
149
+ exact(challenge, ["type", "name", "value"]);
150
+ if (
151
+ challenge["type"] !== "TXT" ||
152
+ typeof challenge["name"] !== "string" ||
153
+ challenge["name"] !== `_omh-project.${v["hostname"]}` ||
154
+ typeof challenge["value"] !== "string" ||
155
+ challenge["value"].length < 16 ||
156
+ challenge["value"].length > 255 ||
157
+ /[\r\n\0]/u.test(challenge["value"])
158
+ )
159
+ invalid();
160
+ if (v["verified_at"] !== null) time(v["verified_at"]);
161
+ time(v["expires_at"]);
162
+ return;
163
+ }
164
+ if (input.action === "domain_dns_authorize" || input.action === "identity_authorize") {
165
+ exact(v, ["authorization_id", "authorization_url", "expires_at"]);
166
+ id(v["authorization_id"]);
167
+ time(v["expires_at"]);
168
+ if (
169
+ typeof v["authorization_url"] !== "string" ||
170
+ !/^https:\/\/dash\.cloudflare\.com\/oauth2\/auth\?/u.test(v["authorization_url"])
171
+ )
172
+ invalid();
173
+ return;
174
+ }
175
+ exact(v, ["status", "zone", "scopes", "expires_at"]);
176
+ member(v["status"], ["not_authorized", "pending", "authorized", "expired", "revoked"]);
177
+ if (
178
+ !Array.isArray(v["scopes"]) ||
179
+ v["scopes"].length > 2 ||
180
+ new Set(v["scopes"]).size !== v["scopes"].length ||
181
+ v["scopes"].some((s) => s !== "dns.write" && s !== "zone.read")
182
+ )
183
+ invalid();
184
+ if (v["zone"] !== null) host(v["zone"]);
185
+ if (v["expires_at"] !== null) time(v["expires_at"]);
186
+ if (
187
+ v["status"] === "not_authorized" &&
188
+ (v["zone"] !== null || v["expires_at"] !== null || v["scopes"].length)
189
+ )
190
+ invalid();
191
+ }
192
+ function object(v: unknown): Record<string, unknown> {
193
+ if (v === null || typeof v !== "object" || Array.isArray(v)) invalid();
194
+ return v as Record<string, unknown>;
195
+ }
196
+ function exact(
197
+ v: Record<string, unknown>,
198
+ required: readonly string[],
199
+ optional: readonly string[] = [],
200
+ ) {
201
+ if (
202
+ required.some((k) => !(k in v)) ||
203
+ Object.keys(v).some((k) => !required.includes(k) && !optional.includes(k))
204
+ )
205
+ invalid();
206
+ }
207
+ function member(v: unknown, values: readonly string[]) {
208
+ if (typeof v !== "string" || !values.includes(v)) invalid();
209
+ }
210
+ function id(v: unknown) {
211
+ if (typeof v !== "string" || !ULID.test(v)) invalid();
212
+ }
213
+ function host(v: unknown) {
214
+ if (typeof v !== "string" || !HOST.test(v)) invalid();
215
+ }
216
+ function positive(v: unknown) {
217
+ if (!Number.isSafeInteger(v) || Number(v) < 1) invalid();
218
+ }
219
+ function time(v: unknown) {
220
+ if (typeof v !== "string" || !Number.isFinite(Date.parse(v)) || new Date(v).toISOString() !== v)
221
+ invalid();
222
+ }
223
+ function mailbox(v: unknown) {
224
+ if (
225
+ typeof v !== "string" ||
226
+ v.length > 254 ||
227
+ !/^[A-Za-z0-9.!#$%&'*+\-/=?^_`{|}~]+@[a-z0-9.-]+\.[a-z]{2,63}$/u.test(v) ||
228
+ v.startsWith(".") ||
229
+ v.includes("..") ||
230
+ v.includes(".@")
231
+ )
232
+ invalid();
233
+ }
234
+ function invalid(): never {
235
+ throw new TypeError("Shared mail response contract invalid");
236
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/sdk-ts",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,7 +14,7 @@
14
14
  "build": "tsc --project tsconfig.build.json",
15
15
  "check": "pnpm --dir ../.. sdk-ts:check",
16
16
  "generate": "pnpm --dir ../.. sdk-ts:generate",
17
- "typecheck": "tsc --project tsconfig.json --noEmit",
17
+ "typecheck": "tsc --project tsconfig.json --noEmit --incremental --tsBuildInfoFile typecheck.tsbuildinfo",
18
18
  "test": "pnpm run check && vitest run --root ../.. packages/sdk-ts"
19
19
  },
20
20
  "license": "Apache-2.0"