@amerged/ohmyhost-mcp 0.1.13 → 0.1.15

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,3 +5,4 @@ export {
5
5
  type Config as OhMyHostClientConfig,
6
6
  } from "./generated/client/index.js";
7
7
  export * from "./generated/index.js";
8
+ export * from "./managed-mail.js";
@@ -0,0 +1,195 @@
1
+ import type { ManagedMailCommand } from "./managed-mail.js";
2
+ const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
3
+ /** Exact response validation is shared by CLI and MCP; malformed data never becomes agent guidance. */
4
+ export function assertManagedMailResponse(value: unknown, input: ManagedMailCommand): void {
5
+ const r = object(value);
6
+ if (
7
+ input.action === "configure" ||
8
+ input.action === "status" ||
9
+ input.action === "webhook_verify"
10
+ ) {
11
+ exact(
12
+ r,
13
+ [
14
+ "domain_id",
15
+ "domain",
16
+ "environment_id",
17
+ "sending",
18
+ "receiving",
19
+ "status",
20
+ "configured_at",
21
+ "observed_at",
22
+ "dns_records",
23
+ "webhook",
24
+ "next_check_after_seconds",
25
+ ],
26
+ ["dns"],
27
+ );
28
+ id(r["domain_id"]);
29
+ domain(r["domain"]);
30
+ if (r["environment_id"] !== input.environmentId) invalid();
31
+ for (const key of ["sending", "receiving"])
32
+ if (!["disabled", "pending", "ready", "failed"].includes(String(r[key]))) invalid();
33
+ if (!["ready", "verification_pending"].includes(String(r["status"]))) invalid();
34
+ time(r["configured_at"]);
35
+ if (r["observed_at"] !== null) time(r["observed_at"]);
36
+ records(r["dns_records"]);
37
+ if (r["next_check_after_seconds"] !== null && r["next_check_after_seconds"] !== 60) invalid();
38
+ if (r["webhook"] !== null) {
39
+ const hook = object(r["webhook"]);
40
+ exact(hook, ["url", "verified"]);
41
+ https(hook["url"]);
42
+ if (typeof hook["verified"] !== "boolean") invalid();
43
+ }
44
+ if (r["dns"] !== undefined) {
45
+ const dns = object(r["dns"]);
46
+ exact(dns, ["status", "records"]);
47
+ if (!["configured", "action_required", "conflict"].includes(String(dns["status"]))) invalid();
48
+ records(dns["records"]);
49
+ }
50
+ return;
51
+ }
52
+ if (input.action === "webhook_set") {
53
+ exact(r, ["url", "signing_secret", "status"]);
54
+ if (
55
+ r["url"] !== input.url ||
56
+ r["status"] !== "verification_required" ||
57
+ typeof r["signing_secret"] !== "string" ||
58
+ !/^whsec_[A-Za-z0-9+/]{43}=$/u.test(r["signing_secret"])
59
+ )
60
+ invalid();
61
+ return;
62
+ }
63
+ if (input.action === "webhook_disable") {
64
+ exact(r, ["status"]);
65
+ if (r["status"] !== "disabled") invalid();
66
+ return;
67
+ }
68
+ if (input.action === "messages_list") {
69
+ exact(r, ["items"]);
70
+ if (!Array.isArray(r["items"]) || r["items"].length > 50) invalid();
71
+ for (const value of r["items"]) {
72
+ const item = object(value);
73
+ exact(item, ["message_id", "received_at", "state", "attempts"]);
74
+ id(item["message_id"]);
75
+ time(item["received_at"]);
76
+ if (
77
+ !["pending", "delivering", "delivered", "delivery_failed", "expired"].includes(
78
+ String(item["state"]),
79
+ ) ||
80
+ !Number.isInteger(item["attempts"]) ||
81
+ Number(item["attempts"]) < 0 ||
82
+ Number(item["attempts"]) > 4
83
+ )
84
+ invalid();
85
+ }
86
+ return;
87
+ }
88
+ if (input.action === "message_retry") {
89
+ exact(r, ["attempted"], ["delivered"]);
90
+ if (
91
+ typeof r["attempted"] !== "boolean" ||
92
+ (r["delivered"] !== undefined && typeof r["delivered"] !== "boolean")
93
+ )
94
+ invalid();
95
+ return;
96
+ }
97
+ if (input.action !== "message_get") invalid();
98
+ if (r["status"] === "expired") {
99
+ exact(r, ["status"]);
100
+ return;
101
+ }
102
+ exact(r, [
103
+ "id",
104
+ "domain",
105
+ "received_at",
106
+ "expires_at",
107
+ "from",
108
+ "to",
109
+ "subject",
110
+ "text",
111
+ "html",
112
+ "message_id",
113
+ "attachments",
114
+ ]);
115
+ if (r["id"] !== input.messageId) invalid();
116
+ domain(r["domain"]);
117
+ time(r["received_at"]);
118
+ time(r["expires_at"]);
119
+ if (Date.parse(String(r["expires_at"])) - Date.parse(String(r["received_at"])) !== 72 * 3600000)
120
+ invalid();
121
+ for (const key of ["from", "subject", "message_id"]) if (typeof r[key] !== "string") invalid();
122
+ for (const key of ["text", "html"]) if (r[key] !== null && typeof r[key] !== "string") invalid();
123
+ if (
124
+ !Array.isArray(r["to"]) ||
125
+ r["to"].some(
126
+ (v) => typeof v !== "string" || v.slice(v.lastIndexOf("@") + 1).toLowerCase() !== r["domain"],
127
+ )
128
+ )
129
+ invalid();
130
+ if (!Array.isArray(r["attachments"])) invalid();
131
+ for (const value of r["attachments"]) {
132
+ const a = object(value);
133
+ exact(a, ["id", "filename", "content_type", "download_path"]);
134
+ if (
135
+ typeof a["id"] !== "string" ||
136
+ !/^[A-Za-z0-9_-]{1,128}$/u.test(a["id"]) ||
137
+ (a["filename"] !== null && typeof a["filename"] !== "string") ||
138
+ typeof a["content_type"] !== "string" ||
139
+ a["download_path"] !==
140
+ `/v1/projects/${input.projectId}/environments/${input.environmentId}/mail/messages/${input.messageId}/attachments/${a["id"]}`
141
+ )
142
+ invalid();
143
+ }
144
+ }
145
+ function object(v: unknown): Record<string, unknown> {
146
+ if (typeof v !== "object" || v === null || Array.isArray(v)) invalid();
147
+ return v as Record<string, unknown>;
148
+ }
149
+ function exact(
150
+ v: Record<string, unknown>,
151
+ required: readonly string[],
152
+ optional: readonly string[] = [],
153
+ ) {
154
+ if (
155
+ required.some((k) => !(k in v)) ||
156
+ Object.keys(v).some((k) => !required.includes(k) && !optional.includes(k))
157
+ )
158
+ invalid();
159
+ }
160
+ function id(v: unknown) {
161
+ if (typeof v !== "string" || !ULID.test(v)) invalid();
162
+ }
163
+ function domain(v: unknown) {
164
+ if (
165
+ typeof v !== "string" ||
166
+ !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(v)
167
+ )
168
+ invalid();
169
+ }
170
+ function time(v: unknown) {
171
+ if (typeof v !== "string" || !Number.isFinite(Date.parse(v)) || new Date(v).toISOString() !== v)
172
+ invalid();
173
+ }
174
+ function https(v: unknown) {
175
+ if (typeof v !== "string" || !v.startsWith("https://")) invalid();
176
+ }
177
+ function records(v: unknown) {
178
+ if (!Array.isArray(v)) invalid();
179
+ for (const value of v) {
180
+ const r = object(value);
181
+ exact(r, ["type", "name", "value", "purpose", "status"], ["priority"]);
182
+ if (
183
+ !["TXT", "CNAME", "MX"].includes(String(r["type"])) ||
184
+ ["name", "value", "purpose", "status"].some((k) => typeof r[k] !== "string") ||
185
+ (r["type"] === "MX" &&
186
+ (!Number.isInteger(r["priority"]) ||
187
+ Number(r["priority"]) < 0 ||
188
+ Number(r["priority"]) > 65535))
189
+ )
190
+ invalid();
191
+ }
192
+ }
193
+ function invalid(): never {
194
+ throw new TypeError("Mail response contract invalid");
195
+ }
@@ -0,0 +1,85 @@
1
+ import { assertManagedMailResponse } from "./managed-mail-response.js";
2
+ import type { Client } from "./generated/client/index.js";
3
+ import {
4
+ configureManagedMailDomain,
5
+ getManagedMailDomain,
6
+ prepareManagedMailWebhook,
7
+ verifyManagedMailWebhook,
8
+ disableManagedMailReceiving,
9
+ listManagedMailMessages,
10
+ getManagedMailMessage,
11
+ retryManagedMailMessage,
12
+ } from "./generated/sdk.gen.js";
13
+ export type ManagedMailCommand = { projectId: string; environmentId: string } & (
14
+ | {
15
+ action: "configure";
16
+ domain: string;
17
+ sending: boolean;
18
+ receiving: boolean;
19
+ idempotencyKey: string;
20
+ }
21
+ | { action: "status" }
22
+ | { action: "webhook_set"; url: string; idempotencyKey: string }
23
+ | { action: "webhook_verify" | "webhook_disable"; idempotencyKey: string }
24
+ | { action: "messages_list"; after?: string }
25
+ | { action: "message_get"; messageId: string }
26
+ | { action: "message_retry"; messageId: string; idempotencyKey: string }
27
+ );
28
+ /** All client surfaces call the same generated REST operations. */
29
+ function requestManagedMail(client: Client, input: ManagedMailCommand) {
30
+ const scope = { project_id: input.projectId, environment_id: input.environmentId };
31
+ switch (input.action) {
32
+ case "configure":
33
+ return configureManagedMailDomain(
34
+ {
35
+ ...scope,
36
+ "Idempotency-Key": input.idempotencyKey,
37
+ managedMailDomainRequest: {
38
+ domain: input.domain,
39
+ sending: input.sending,
40
+ receiving: input.receiving,
41
+ },
42
+ },
43
+ { client },
44
+ );
45
+ case "status":
46
+ return getManagedMailDomain(scope, { client });
47
+ case "webhook_set":
48
+ return prepareManagedMailWebhook(
49
+ {
50
+ ...scope,
51
+ "Idempotency-Key": input.idempotencyKey,
52
+ managedMailWebhookRequest: { url: input.url },
53
+ },
54
+ { client },
55
+ );
56
+ case "webhook_verify":
57
+ return verifyManagedMailWebhook(
58
+ { ...scope, "Idempotency-Key": input.idempotencyKey },
59
+ { client },
60
+ );
61
+ case "webhook_disable":
62
+ return disableManagedMailReceiving(
63
+ { ...scope, "Idempotency-Key": input.idempotencyKey },
64
+ { client },
65
+ );
66
+ case "messages_list":
67
+ return listManagedMailMessages(
68
+ { ...scope, ...(input.after ? { after: input.after } : {}) },
69
+ { client },
70
+ );
71
+ case "message_get":
72
+ return getManagedMailMessage({ ...scope, message_id: input.messageId }, { client });
73
+ case "message_retry":
74
+ return retryManagedMailMessage(
75
+ { ...scope, message_id: input.messageId, "Idempotency-Key": input.idempotencyKey },
76
+ { client },
77
+ );
78
+ }
79
+ }
80
+
81
+ export async function executeManagedMail(client: Client, input: ManagedMailCommand) {
82
+ const result = await requestManagedMail(client, input);
83
+ assertManagedMailResponse(result, input);
84
+ return result;
85
+ }