@amerged/ohmyhost-cli 0.1.13 → 0.1.14

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.
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  selectWorkspace,
48
48
  selectedWorkspaceId,
49
49
  workspaceOf
50
- } from "./chunk-ZWAAQEOZ.js";
50
+ } from "./chunk-U32PJQHH.js";
51
51
  import {
52
52
  CredentialStoreIntegrityError,
53
53
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -75,7 +75,6 @@ import {
75
75
  type PublicEnvironmentSecret,
76
76
  parseOperation,
77
77
  parseOperationEvent,
78
- parseProjectMailDomain,
79
78
  parseProjectPage,
80
79
  parseProjectStatus,
81
80
  parseProjectDatabaseQuery,
@@ -370,9 +369,24 @@ const HELP_USAGE = Object.freeze({
370
369
  "printf '%s' \"$SECRET_VALUE\" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY --stdin [--wait] --json",
371
370
  "secret delete":
372
371
  "ohmyhost secret delete NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--wait] --json",
372
+ "mail setup":
373
+ "ohmyhost mail setup --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
374
+ "mail status": "ohmyhost mail status --project ULID --environment ULID --json",
375
+ "mail webhook set":
376
+ "ohmyhost mail webhook set --project ULID --environment ULID --url HTTPS_URL --idempotency-key KEY --json",
377
+ "mail webhook verify":
378
+ "ohmyhost mail webhook verify --project ULID --environment ULID --idempotency-key KEY --json",
379
+ "mail webhook disable":
380
+ "ohmyhost mail webhook disable --project ULID --environment ULID --idempotency-key KEY --json",
381
+ "mail messages list":
382
+ "ohmyhost mail messages list --project ULID --environment ULID [--after ULID] --json",
383
+ "mail messages get":
384
+ "ohmyhost mail messages get --project ULID --environment ULID --message ULID --json",
385
+ "mail messages retry":
386
+ "ohmyhost mail messages retry --project ULID --environment ULID --message ULID --idempotency-key KEY --json",
373
387
  "mail domain set":
374
- "ohmyhost mail domain set --project ULID --domain DOMAIN --idempotency-key KEY --json",
375
- "mail domain status": "ohmyhost mail domain status --project ULID --json",
388
+ "ohmyhost mail domain set --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
389
+ "mail domain status": "ohmyhost mail domain status --project ULID --environment ULID --json",
376
390
  } satisfies Readonly<Record<CustomerCommandLabel, string>>);
377
391
 
378
392
  const HELP_COMMANDS = Object.freeze(
@@ -807,9 +821,11 @@ export class ProductCli {
807
821
  if (command.kind === "secret-list") return await this.listEnvironmentSecrets(command, signal);
808
822
  if (command.kind === "function-runs") return await this.listFunctionRuns(command, signal);
809
823
  if (command.kind === "secret-set") return await this.putEnvironmentSecret(command, signal);
810
- if (command.kind === "mail-domain-set")
811
- return await this.configureMailDomain(command, signal);
812
- if (command.kind === "mail-domain-status") return await this.getMailDomain(command, signal);
824
+ if (command.kind === "managed-mail") {
825
+ const api = await this.authenticatedApi(command.credentialStore, signal);
826
+ if (!api.managedMail) throw new Error("Mail unavailable");
827
+ return success(command.label, { mail: await api.managedMail(command.request) });
828
+ }
813
829
  return await this.deleteEnvironmentSecret(command, signal);
814
830
  } catch (error) {
815
831
  if (error instanceof UserTokenFileError)
@@ -1763,30 +1779,6 @@ export class ProductCli {
1763
1779
  );
1764
1780
  }
1765
1781
 
1766
- private async configureMailDomain(
1767
- command: Extract<ProductCliCommand, { kind: "mail-domain-set" }>,
1768
- signal: AbortSignal,
1769
- ): Promise<CliResult> {
1770
- const api = await this.authenticatedApi(command.credentialStore, signal);
1771
- const mailDomain = parseProjectMailDomain(
1772
- await api.configureMailDomain({
1773
- projectId: command.projectId,
1774
- domain: command.domain,
1775
- idempotencyKey: command.idempotencyKey,
1776
- }),
1777
- );
1778
- return success("mail domain set", { status: mailDomain.status, mail_domain: mailDomain });
1779
- }
1780
-
1781
- private async getMailDomain(
1782
- command: Extract<ProductCliCommand, { kind: "mail-domain-status" }>,
1783
- signal: AbortSignal,
1784
- ): Promise<CliResult> {
1785
- const api = await this.authenticatedApi(command.credentialStore, signal);
1786
- const mailDomain = parseProjectMailDomain(await api.getMailDomain(command.projectId));
1787
- return success("mail domain status", { status: mailDomain.status, mail_domain: mailDomain });
1788
- }
1789
-
1790
1782
  private async waitForOperation(
1791
1783
  api: ProductApi,
1792
1784
  initial: ReturnType<typeof parseOperation>,
@@ -2178,8 +2170,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2178
2170
  if (command.kind === "function-runs") return "function runs";
2179
2171
  if (command.kind === "secret-set") return "secret set";
2180
2172
  if (command.kind === "secret-delete") return "secret delete";
2181
- if (command.kind === "mail-domain-set") return "mail domain set";
2182
- if (command.kind === "mail-domain-status") return "mail domain status";
2173
+ if (command.kind === "managed-mail") return command.label;
2183
2174
  return command.kind;
2184
2175
  };
2185
2176
 
@@ -1,3 +1,4 @@
1
+ import type { ManagedMailCommand } from "@ohmyhost/sdk-ts";
1
2
  import { USER_API_KEY_ID } from "@ohmyhost/contracts/user-api-keys";
2
3
  export type ProductCliCommand =
3
4
  | {
@@ -397,15 +398,19 @@ export type ProductCliCommand =
397
398
  readonly credentialStore: "native";
398
399
  }
399
400
  | {
400
- readonly kind: "mail-domain-set";
401
- readonly projectId: string;
402
- readonly domain: string;
403
- readonly idempotencyKey: string;
404
- readonly credentialStore: "native";
405
- }
406
- | {
407
- readonly kind: "mail-domain-status";
408
- readonly projectId: string;
401
+ readonly kind: "managed-mail";
402
+ readonly request: ManagedMailCommand;
403
+ readonly label:
404
+ | "mail domain set"
405
+ | "mail domain status"
406
+ | "mail setup"
407
+ | "mail status"
408
+ | "mail webhook set"
409
+ | "mail webhook verify"
410
+ | "mail webhook disable"
411
+ | "mail messages list"
412
+ | "mail messages get"
413
+ | "mail messages retry";
409
414
  readonly credentialStore: "native";
410
415
  }
411
416
  | {
@@ -534,6 +539,14 @@ export type RecognizedCommandLabel =
534
539
  | "function runs"
535
540
  | "secret set"
536
541
  | "secret delete"
542
+ | "mail setup"
543
+ | "mail status"
544
+ | "mail webhook set"
545
+ | "mail webhook verify"
546
+ | "mail webhook disable"
547
+ | "mail messages list"
548
+ | "mail messages get"
549
+ | "mail messages retry"
537
550
  | "mail domain set"
538
551
  | "mail domain status";
539
552
 
@@ -639,6 +652,20 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
639
652
  if (values[0] === "delete") return "secret delete";
640
653
  }
641
654
  if (value === "function" && values[0] === "runs") return "function runs";
655
+ if (value === "mail") {
656
+ if (values[0] === "setup") return "mail setup";
657
+ if (values[0] === "status") return "mail status";
658
+ if (values[0] === "webhook") {
659
+ if (values[1] === "set") return "mail webhook set";
660
+ if (values[1] === "verify") return "mail webhook verify";
661
+ if (values[1] === "disable") return "mail webhook disable";
662
+ }
663
+ if (values[0] === "messages") {
664
+ if (values[1] === "list") return "mail messages list";
665
+ if (values[1] === "get") return "mail messages get";
666
+ if (values[1] === "retry") return "mail messages retry";
667
+ }
668
+ }
642
669
  if (value === "mail" && values[0] === "domain") {
643
670
  if (values[1] === "set") return "mail domain set";
644
671
  if (values[1] === "status") return "mail domain status";
@@ -1724,34 +1751,82 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1724
1751
  }
1725
1752
  throw new InvalidCommandError();
1726
1753
  }
1727
- if (first === "mail" && values[0] === "domain" && values[1] === "set") {
1754
+ if (first === "mail") {
1728
1755
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1729
- values.splice(0, 2);
1730
- const options = parseOptions(values, ["--project", "--domain", "--idempotency-key"]);
1731
- if (
1732
- !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(
1733
- options["--domain"],
1734
- )
1735
- ) {
1736
- throw new InvalidCommandError();
1737
- }
1738
- return {
1739
- kind: "mail-domain-set",
1740
- projectId: options["--project"],
1741
- domain: options["--domain"],
1742
- idempotencyKey: options["--idempotency-key"],
1743
- credentialStore,
1744
- };
1745
- }
1746
- if (first === "mail" && values[0] === "domain" && values[1] === "status") {
1747
- if (yes || wait || follow || stdin) throw new InvalidCommandError();
1748
- values.splice(0, 2);
1749
- const options = parseOptions(values, ["--project"]);
1750
- return {
1751
- kind: "mail-domain-status",
1752
- projectId: options["--project"],
1753
- credentialStore,
1756
+ const area = values.shift();
1757
+ const sub =
1758
+ area === "webhook" || area === "messages" || area === "domain" ? values.shift() : undefined;
1759
+ const options = parseOptionalOptions(values, [
1760
+ "--project",
1761
+ "--environment",
1762
+ "--domain",
1763
+ "--sending",
1764
+ "--receiving",
1765
+ "--url",
1766
+ "--idempotency-key",
1767
+ "--message",
1768
+ "--after",
1769
+ ]);
1770
+ const required = (key: keyof typeof options) => {
1771
+ const value = options[key];
1772
+ if (!value) throw new InvalidCommandError();
1773
+ return value;
1754
1774
  };
1775
+ const scope = { projectId: required("--project"), environmentId: required("--environment") };
1776
+ let request: ManagedMailCommand,
1777
+ label: Extract<ProductCliCommand, { kind: "managed-mail" }>["label"];
1778
+ if (area === "setup" || (area === "domain" && sub === "set")) {
1779
+ const sending = required("--sending"),
1780
+ receiving = required("--receiving");
1781
+ if (!["true", "false"].includes(sending) || !["true", "false"].includes(receiving))
1782
+ throw new InvalidCommandError();
1783
+ request = {
1784
+ ...scope,
1785
+ action: "configure",
1786
+ domain: required("--domain"),
1787
+ sending: sending === "true",
1788
+ receiving: receiving === "true",
1789
+ idempotencyKey: required("--idempotency-key"),
1790
+ };
1791
+ label = area === "domain" ? "mail domain set" : "mail setup";
1792
+ } else if (area === "status" || (area === "domain" && sub === "status")) {
1793
+ request = { ...scope, action: "status" };
1794
+ label = area === "domain" ? "mail domain status" : "mail status";
1795
+ } else if (area === "webhook" && sub === "set") {
1796
+ request = {
1797
+ ...scope,
1798
+ action: "webhook_set",
1799
+ url: required("--url"),
1800
+ idempotencyKey: required("--idempotency-key"),
1801
+ };
1802
+ label = "mail webhook set";
1803
+ } else if (area === "webhook" && (sub === "verify" || sub === "disable")) {
1804
+ request = {
1805
+ ...scope,
1806
+ action: sub === "verify" ? "webhook_verify" : "webhook_disable",
1807
+ idempotencyKey: required("--idempotency-key"),
1808
+ };
1809
+ label = sub === "verify" ? "mail webhook verify" : "mail webhook disable";
1810
+ } else if (area === "messages" && sub === "list") {
1811
+ request = {
1812
+ ...scope,
1813
+ action: "messages_list",
1814
+ ...(options["--after"] ? { after: options["--after"] } : {}),
1815
+ };
1816
+ label = "mail messages list";
1817
+ } else if (area === "messages" && sub === "get") {
1818
+ request = { ...scope, action: "message_get", messageId: required("--message") };
1819
+ label = "mail messages get";
1820
+ } else if (area === "messages" && sub === "retry") {
1821
+ request = {
1822
+ ...scope,
1823
+ action: "message_retry",
1824
+ messageId: required("--message"),
1825
+ idempotencyKey: required("--idempotency-key"),
1826
+ };
1827
+ label = "mail messages retry";
1828
+ } else throw new InvalidCommandError();
1829
+ return { kind: "managed-mail", request, label, credentialStore };
1755
1830
  }
1756
1831
  throw new InvalidCommandError();
1757
1832
  };
@@ -1,3 +1,4 @@
1
+ import { executeManagedMail, type ManagedMailCommand } from "@ohmyhost/sdk-ts";
1
2
  import {
2
3
  assertProjectDatabaseAccess,
3
4
  assertProjectDatabaseAccessCredential,
@@ -53,7 +54,6 @@ import {
53
54
  type ProjectCreditBudget,
54
55
  applyPaidProjectDomain,
55
56
  configureProjectDomains,
56
- configureProjectMailDomain,
57
57
  configureProjectSourceAutoDeploy,
58
58
  createCloudflareDnsAuthorization,
59
59
  createProjectDevAccessTicket,
@@ -74,7 +74,6 @@ import {
74
74
  getAccountProfile,
75
75
  getCurrentIdentity,
76
76
  getDeployment,
77
- getProjectMailDomain,
78
77
  getProjectSourceAutoDeploy,
79
78
  getProjectStatus,
80
79
  getPaidProjectDomain,
@@ -113,7 +112,6 @@ import {
113
112
  type DeleteEnvironmentSecretResult,
114
113
  type OhMyHostClient,
115
114
  type ProviderReconciliationAttempt,
116
- type ProjectMailDomain,
117
115
  type CurrentIdentity,
118
116
  type DevAccessTicket,
119
117
  type ProjectPage,
@@ -375,12 +373,7 @@ export interface ProductApi {
375
373
  readonly name: string;
376
374
  readonly idempotencyKey: string;
377
375
  }): Promise<DeleteEnvironmentSecretResult>;
378
- configureMailDomain(input: {
379
- readonly projectId: string;
380
- readonly domain: string;
381
- readonly idempotencyKey: string;
382
- }): Promise<ProjectMailDomain>;
383
- getMailDomain(projectId: string): Promise<ProjectMailDomain>;
376
+ managedMail?(input: ManagedMailCommand): Promise<unknown>;
384
377
  }
385
378
 
386
379
  export class GeneratedSdkProductApi implements ProductApi {
@@ -1094,22 +1087,7 @@ export class GeneratedSdkProductApi implements ProductApi {
1094
1087
  );
1095
1088
  }
1096
1089
 
1097
- public async configureMailDomain(input: {
1098
- readonly projectId: string;
1099
- readonly domain: string;
1100
- readonly idempotencyKey: string;
1101
- }): Promise<ProjectMailDomain> {
1102
- return configureProjectMailDomain(
1103
- {
1104
- "Idempotency-Key": input.idempotencyKey,
1105
- project_id: input.projectId,
1106
- configureProjectMailDomainRequest: { domain: input.domain },
1107
- },
1108
- { client: this.#client },
1109
- );
1110
- }
1111
-
1112
- public async getMailDomain(projectId: string): Promise<ProjectMailDomain> {
1113
- return getProjectMailDomain({ project_id: projectId }, { client: this.#client });
1090
+ public managedMail(input: ManagedMailCommand) {
1091
+ return executeManagedMail(this.#client, input);
1114
1092
  }
1115
1093
  }
@@ -50,7 +50,7 @@ const MAXIMUM_SOURCE_BYTES = 2 * 1024 * 1024;
50
50
  const SOURCE_MODULE_PATTERN = /\.(?:[cm]?[jt]s|[jt]sx)$/u;
51
51
  const STORAGE_CLIENT_CALL_PATTERN = /\bcreatePrivateStorageClient\s*\(/u;
52
52
  // Both database mistakes of 2026-09-18 are visible in the source before anything is built: one read
53
- // a Hyperdrive connection string, the other opened a socket driver. Neither can work in a Worker, so
53
+ // a retired database binding, the other opened a socket driver. Neither can work in a customer Worker, so
54
54
  // naming them here costs a customer minutes instead of a deployment that fails its health check.
55
55
  const DATABASE_PRIVATE_BINDING_PATTERN =
56
56
  /\bHYPERDRIVE\b|\bconnectionString\b|\bDATABASE_URL\b|postgres(?:ql)?:\/\//u;
@@ -549,7 +549,7 @@ async function databaseContractBlockers(
549
549
  if (DATABASE_PRIVATE_BINDING_PATTERN.test(source)) {
550
550
  blockers.push({
551
551
  code: "database_binding_private",
552
- message: `${path} reaches for a managed database connection string. Hyperdrive, DATABASE_URL and postgres:// URLs are platform-private and never reach a customer Worker. Call the database with createPrivateDatabaseClient from "@ohmyhost/customer-runtime/database".`,
552
+ message: `${path} reaches for 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".`,
553
553
  });
554
554
  }
555
555
  if (DATABASE_SOCKET_DRIVER_PATTERN.test(source)) {
@@ -1304,100 +1304,6 @@ function validDelivery(input: Readonly<Record<string, unknown>>): boolean {
1304
1304
  );
1305
1305
  }
1306
1306
 
1307
- export type PublicProjectMailDomain =
1308
- | Readonly<{ domain: string; status: "configured"; configured_at: string }>
1309
- | Readonly<{
1310
- domain: string;
1311
- status: "delegation_required" | "ready" | "verification_pending" | "verification_failed";
1312
- observed_at?: string;
1313
- verification_issue?:
1314
- | "identity_missing"
1315
- | "identity_verification_failed"
1316
- | "dkim_verification_failed"
1317
- | "dkim_records_changed"
1318
- | "tenant_association_missing"
1319
- | null;
1320
- next_check_after_seconds?: 3600 | null;
1321
- configured_at: string;
1322
- name_servers: readonly [string, string, string, string];
1323
- change_id: string;
1324
- }>;
1325
-
1326
- export const parseProjectMailDomain = (value: unknown): PublicProjectMailDomain => {
1327
- const input = exactRecord(value, [
1328
- "domain",
1329
- "status",
1330
- "configured_at",
1331
- "name_servers",
1332
- "change_id",
1333
- "observed_at",
1334
- "verification_issue",
1335
- "next_check_after_seconds",
1336
- ]);
1337
- if (
1338
- typeof input["domain"] !== "string" ||
1339
- !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(
1340
- input["domain"],
1341
- ) ||
1342
- !isIsoInstant(input["configured_at"])
1343
- ) {
1344
- throw new ResponseContractError();
1345
- }
1346
- if (input["status"] === "configured") {
1347
- if (
1348
- input["name_servers"] !== undefined ||
1349
- input["change_id"] !== undefined ||
1350
- input["observed_at"] !== undefined ||
1351
- input["verification_issue"] !== undefined ||
1352
- input["next_check_after_seconds"] !== undefined
1353
- ) {
1354
- throw new ResponseContractError();
1355
- }
1356
- return input as unknown as PublicProjectMailDomain;
1357
- }
1358
- if (
1359
- !["delegation_required", "ready", "verification_pending", "verification_failed"].includes(
1360
- String(input["status"]),
1361
- ) ||
1362
- !Array.isArray(input["name_servers"]) ||
1363
- input["name_servers"].length !== 4 ||
1364
- new Set(input["name_servers"]).size !== 4 ||
1365
- input["name_servers"].some(
1366
- (nameServer) =>
1367
- typeof nameServer !== "string" ||
1368
- !/^ns-[0-9]{1,4}\.awsdns-[0-9]{1,2}\.(?:com|net|org|co\.uk)$/u.test(nameServer),
1369
- ) ||
1370
- typeof input["change_id"] !== "string" ||
1371
- !/^C[A-Z0-9]{8,31}$/u.test(input["change_id"])
1372
- ) {
1373
- throw new ResponseContractError();
1374
- }
1375
- if (input["observed_at"] !== undefined) {
1376
- if (
1377
- ![
1378
- null,
1379
- "identity_missing",
1380
- "identity_verification_failed",
1381
- "dkim_verification_failed",
1382
- "dkim_records_changed",
1383
- "tenant_association_missing",
1384
- ].includes(input["verification_issue"] as string | null)
1385
- )
1386
- throw new ResponseContractError();
1387
- if (
1388
- !isIsoInstant(input["observed_at"]) ||
1389
- input["next_check_after_seconds"] !== (input["status"] === "ready" ? null : 3600)
1390
- )
1391
- throw new ResponseContractError();
1392
- } else if (
1393
- input["next_check_after_seconds"] !== undefined ||
1394
- input["verification_issue"] !== undefined ||
1395
- ["verification_pending", "verification_failed"].includes(String(input["status"]))
1396
- )
1397
- throw new ResponseContractError();
1398
- return input as unknown as PublicProjectMailDomain;
1399
- };
1400
-
1401
1307
  export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unknown>> => {
1402
1308
  const input = exactRecord(value, [
1403
1309
  "id",
@@ -208,9 +208,9 @@ const FRAMEWORK_CONVERSION_DETAILS: Readonly<
208
208
  > = Object.freeze({
209
209
  database_binding_private: Object.freeze({
210
210
  render: (path: string) =>
211
- `"${path}" reads a managed database connection string. Hyperdrive, 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.`,
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.`,
212
212
  pattern:
213
- /^"([^"]+)" reads a managed database connection string\. Hyperdrive, 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,
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,
214
214
  }),
215
215
  database_driver_unsupported: Object.freeze({
216
216
  render: (path: string) =>