@amerged/ohmyhost-mcp 0.1.11 → 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.
@@ -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
  | {
@@ -132,6 +133,15 @@ export type ProductCliCommand =
132
133
  readonly idempotencyKey: string;
133
134
  readonly credentialStore: "native";
134
135
  }
136
+ | {
137
+ /** Moves a project to a chosen address once its gateways serve it. */
138
+ readonly kind: "project-handle-set";
139
+ readonly projectId: string;
140
+ readonly handle: string;
141
+ readonly ifMatch: string;
142
+ readonly idempotencyKey: string;
143
+ readonly credentialStore: "native";
144
+ }
135
145
  | {
136
146
  /** Asks whether a project address is free; the candidate is sent exactly as the agent typed it. */
137
147
  readonly kind: "project-handle-check";
@@ -388,15 +398,19 @@ export type ProductCliCommand =
388
398
  readonly credentialStore: "native";
389
399
  }
390
400
  | {
391
- readonly kind: "mail-domain-set";
392
- readonly projectId: string;
393
- readonly domain: string;
394
- readonly idempotencyKey: string;
395
- readonly credentialStore: "native";
396
- }
397
- | {
398
- readonly kind: "mail-domain-status";
399
- 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";
400
414
  readonly credentialStore: "native";
401
415
  }
402
416
  | {
@@ -494,6 +508,7 @@ export type RecognizedCommandLabel =
494
508
  | "project status"
495
509
  | "project dev-access create"
496
510
  | "project handle check"
511
+ | "project handle set"
497
512
  | "database write"
498
513
  | "database query"
499
514
  | "database access create"
@@ -524,6 +539,14 @@ export type RecognizedCommandLabel =
524
539
  | "function runs"
525
540
  | "secret set"
526
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"
527
550
  | "mail domain set"
528
551
  | "mail domain status";
529
552
 
@@ -585,6 +608,8 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
585
608
  if (value === "project" && values[0] === "status") return "project status";
586
609
  if (value === "project" && values[0] === "handle" && values[1] === "check")
587
610
  return "project handle check";
611
+ if (value === "project" && values[0] === "handle" && values[1] === "set")
612
+ return "project handle set";
588
613
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
589
614
  return "project dev-access create";
590
615
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -627,6 +652,20 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
627
652
  if (values[0] === "delete") return "secret delete";
628
653
  }
629
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
+ }
630
669
  if (value === "mail" && values[0] === "domain") {
631
670
  if (values[1] === "set") return "mail domain set";
632
671
  if (values[1] === "status") return "mail domain status";
@@ -1054,6 +1093,24 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1054
1093
  }
1055
1094
  if (first === "project") {
1056
1095
  const subcommand = values.shift();
1096
+ if (subcommand === "handle" && values[0] === "set") {
1097
+ values.shift();
1098
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1099
+ const options = parseOptions(values, [
1100
+ "--project",
1101
+ "--handle",
1102
+ "--if-match",
1103
+ "--idempotency-key",
1104
+ ]);
1105
+ return {
1106
+ kind: "project-handle-set",
1107
+ projectId: options["--project"],
1108
+ handle: options["--handle"],
1109
+ ifMatch: options["--if-match"],
1110
+ idempotencyKey: options["--idempotency-key"],
1111
+ credentialStore,
1112
+ };
1113
+ }
1057
1114
  if (subcommand === "handle" && values.shift() === "check") {
1058
1115
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1059
1116
  const options = parseOptions(values, ["--handle"]);
@@ -1694,34 +1751,82 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1694
1751
  }
1695
1752
  throw new InvalidCommandError();
1696
1753
  }
1697
- if (first === "mail" && values[0] === "domain" && values[1] === "set") {
1754
+ if (first === "mail") {
1698
1755
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1699
- values.splice(0, 2);
1700
- const options = parseOptions(values, ["--project", "--domain", "--idempotency-key"]);
1701
- if (
1702
- !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(
1703
- options["--domain"],
1704
- )
1705
- ) {
1706
- throw new InvalidCommandError();
1707
- }
1708
- return {
1709
- kind: "mail-domain-set",
1710
- projectId: options["--project"],
1711
- domain: options["--domain"],
1712
- idempotencyKey: options["--idempotency-key"],
1713
- credentialStore,
1714
- };
1715
- }
1716
- if (first === "mail" && values[0] === "domain" && values[1] === "status") {
1717
- if (yes || wait || follow || stdin) throw new InvalidCommandError();
1718
- values.splice(0, 2);
1719
- const options = parseOptions(values, ["--project"]);
1720
- return {
1721
- kind: "mail-domain-status",
1722
- projectId: options["--project"],
1723
- 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;
1724
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 };
1725
1830
  }
1726
1831
  throw new InvalidCommandError();
1727
1832
  };
@@ -429,9 +429,11 @@ const runOfflineInit = async (
429
429
  false,
430
430
  error instanceof RepositoryInitConflictError
431
431
  ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
432
- : error instanceof RepositoryInitError && error.code === "project_name_invalid"
433
- ? "Pass a lowercase deployment slug with '--project', then rerun init."
434
- : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
432
+ : error instanceof RepositoryInitError && error.code === "repository_root_required"
433
+ ? "Run ohmyhost init from the Git repository root with --root set to the application directory (for example --root website); commit the root ohmyhost.yaml before planning."
434
+ : error instanceof RepositoryInitError && error.code === "project_name_invalid"
435
+ ? "Pass a lowercase deployment slug with '--project', then rerun init."
436
+ : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
435
437
  );
436
438
  io.writeStdout(result.stdout);
437
439
  return result.exitCode;
@@ -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,
@@ -83,6 +82,7 @@ import {
83
82
  listEnvironmentSecrets,
84
83
  listFunctionRuns,
85
84
  listProjects,
85
+ changeProjectHandle,
86
86
  getProjectHandleAvailability,
87
87
  planDeployment,
88
88
  planPaidProjectDomain,
@@ -112,7 +112,6 @@ import {
112
112
  type DeleteEnvironmentSecretResult,
113
113
  type OhMyHostClient,
114
114
  type ProviderReconciliationAttempt,
115
- type ProjectMailDomain,
116
115
  type CurrentIdentity,
117
116
  type DevAccessTicket,
118
117
  type ProjectPage,
@@ -211,6 +210,12 @@ export interface ProductApi {
211
210
  getAccountProfile(): Promise<readonly PublicWorkspace[]>;
212
211
  listProjects(input: { readonly cursor?: string; readonly limit: number }): Promise<ProjectPage>;
213
212
  checkProjectHandle(input: { readonly handle: string }): Promise<ProjectHandleAvailability>;
213
+ changeProjectHandle(input: {
214
+ readonly projectId: string;
215
+ readonly handle: string;
216
+ readonly ifMatch: string;
217
+ readonly idempotencyKey: string;
218
+ }): Promise<Operation>;
214
219
  getProjectStatus(projectId: string): Promise<ProjectStatus>;
215
220
  writeProjectDatabase(
216
221
  input: ProjectDatabaseWriteRequest & { projectId: string; idempotencyKey: string },
@@ -368,12 +373,7 @@ export interface ProductApi {
368
373
  readonly name: string;
369
374
  readonly idempotencyKey: string;
370
375
  }): Promise<DeleteEnvironmentSecretResult>;
371
- configureMailDomain(input: {
372
- readonly projectId: string;
373
- readonly domain: string;
374
- readonly idempotencyKey: string;
375
- }): Promise<ProjectMailDomain>;
376
- getMailDomain(projectId: string): Promise<ProjectMailDomain>;
376
+ managedMail?(input: ManagedMailCommand): Promise<unknown>;
377
377
  }
378
378
 
379
379
  export class GeneratedSdkProductApi implements ProductApi {
@@ -585,6 +585,23 @@ export class GeneratedSdkProductApi implements ProductApi {
585
585
  return listProjects(input, { client: this.#client });
586
586
  }
587
587
 
588
+ public async changeProjectHandle(input: {
589
+ readonly projectId: string;
590
+ readonly handle: string;
591
+ readonly ifMatch: string;
592
+ readonly idempotencyKey: string;
593
+ }): Promise<Operation> {
594
+ return changeProjectHandle(
595
+ {
596
+ project_id: input.projectId,
597
+ "Idempotency-Key": input.idempotencyKey,
598
+ "If-Match": input.ifMatch,
599
+ handle: input.handle,
600
+ },
601
+ { client: this.#client },
602
+ );
603
+ }
604
+
588
605
  public async checkProjectHandle(input: {
589
606
  readonly handle: string;
590
607
  }): Promise<ProjectHandleAvailability> {
@@ -1070,22 +1087,7 @@ export class GeneratedSdkProductApi implements ProductApi {
1070
1087
  );
1071
1088
  }
1072
1089
 
1073
- public async configureMailDomain(input: {
1074
- readonly projectId: string;
1075
- readonly domain: string;
1076
- readonly idempotencyKey: string;
1077
- }): Promise<ProjectMailDomain> {
1078
- return configureProjectMailDomain(
1079
- {
1080
- "Idempotency-Key": input.idempotencyKey,
1081
- project_id: input.projectId,
1082
- configureProjectMailDomainRequest: { domain: input.domain },
1083
- },
1084
- { client: this.#client },
1085
- );
1086
- }
1087
-
1088
- public async getMailDomain(projectId: string): Promise<ProjectMailDomain> {
1089
- return getProjectMailDomain({ project_id: projectId }, { client: this.#client });
1090
+ public managedMail(input: ManagedMailCommand) {
1091
+ return executeManagedMail(this.#client, input);
1090
1092
  }
1091
1093
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, open, readFile, readdir, stat } from "node:fs/promises";
3
- import { posix, resolve } from "node:path";
3
+ import { dirname, posix, resolve } from "node:path";
4
4
 
5
5
  import {
6
6
  normalizeApplicationRoot,
@@ -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)) {
@@ -876,6 +876,30 @@ async function assertRepositoryRoot(root: string): Promise<void> {
876
876
  if (!information.isDirectory()) {
877
877
  throw new RepositoryInitError("repository_not_directory", "Repository path is not a directory");
878
878
  }
879
+ // A Git worktree uses a .git file; never read or follow that file's target.
880
+ for (let ancestor = root; ; ancestor = dirname(ancestor)) {
881
+ let marker;
882
+ try {
883
+ marker = await lstat(resolve(ancestor, ".git"));
884
+ } catch (error) {
885
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
886
+ throw new RepositoryInitError(
887
+ "repository_unreadable",
888
+ "Could not determine the Git repository root",
889
+ );
890
+ }
891
+ }
892
+ if (marker !== undefined) {
893
+ if (ancestor !== root) {
894
+ throw new RepositoryInitError(
895
+ "repository_root_required",
896
+ "Run ohmyhost init from the Git repository root and select the application directory with --root; ohmyhost.yaml must be at the repository root.",
897
+ );
898
+ }
899
+ return;
900
+ }
901
+ if (dirname(ancestor) === ancestor) return;
902
+ }
879
903
  }
880
904
 
881
905
  async function collectRepositoryFiles(root: string): Promise<string[]> {
@@ -129,6 +129,29 @@ const PROBLEM_POLICY = {
129
129
  retryable: true,
130
130
  action: "Retry project creation with the same idempotency key.",
131
131
  },
132
+ project_handle_taken: {
133
+ status: 409,
134
+ retryable: false,
135
+ action:
136
+ "Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns.",
137
+ },
138
+ project_handle_invalid: {
139
+ status: 409,
140
+ retryable: false,
141
+ action:
142
+ "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words.",
143
+ },
144
+ project_handle_unchanged: {
145
+ status: 409,
146
+ retryable: false,
147
+ action: "The project already answers on this address; no rename is needed.",
148
+ },
149
+ project_rename_blocked: {
150
+ status: 409,
151
+ retryable: true,
152
+ action:
153
+ "Wait for the running operation to finish, read the project again, then rename with the new ETag.",
154
+ },
132
155
  project_identity_unavailable: {
133
156
  status: 503,
134
157
  retryable: true,
@@ -196,6 +219,12 @@ const PROBLEM_POLICY = {
196
219
  action:
197
220
  "Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit.",
198
221
  },
222
+ repository_configuration_missing: {
223
+ status: 409,
224
+ retryable: false,
225
+ action:
226
+ "Place ohmyhost.yaml at the Git repository root and set applicationRoot to the application directory (for example website). Commit and push the change, then plan the new commit.",
227
+ },
199
228
  migration_filename_noncanonical: {
200
229
  status: 409,
201
230
  retryable: false,
@@ -278,6 +307,7 @@ export class ResponseContractError extends Error {
278
307
  }
279
308
 
280
309
  export interface SafeProblem {
310
+ readonly requestId: string;
281
311
  readonly retryAfterSeconds?: number;
282
312
  readonly code: ProblemCode;
283
313
  readonly retryable: boolean;
@@ -676,9 +706,9 @@ export const parseDevAccessTicket = (value: unknown): PublicDevAccessTicket => {
676
706
  !isUlid(input["project_id"]) ||
677
707
  !isUlid(input["environment_id"]) ||
678
708
  typeof origin !== "string" ||
679
- !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$/u.test(origin) ||
709
+ !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st$/u.test(origin) ||
680
710
  typeof redeemUrl !== "string" ||
681
- !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(
711
+ !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(
682
712
  redeemUrl,
683
713
  ) ||
684
714
  new URL(redeemUrl).origin !== origin ||
@@ -880,8 +910,8 @@ const parseProject = (value: unknown): PublicProject => {
880
910
  !isUlid(input["organization_id"]) ||
881
911
  !boundedString(input["name"], 1, 128) ||
882
912
  input["name"] !== input["name"].trim() ||
883
- !boundedString(input["handle"], 5, 59) ||
884
- !/^[a-z]+-[a-z]+-[a-z]+$/u.test(input["handle"]) ||
913
+ !boundedString(input["handle"], 3, 59) ||
914
+ !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(input["handle"]) ||
885
915
  (input["data_mode"] !== "shared" && input["data_mode"] !== "isolated") ||
886
916
  (input["region"] !== "us" && input["region"] !== "eu") ||
887
917
  !isIsoInstant(input["created_at"])
@@ -910,6 +940,7 @@ export const parseProjectPage = (
910
940
  export const parseProjectStatus = (value: unknown): Readonly<Record<string, unknown>> => {
911
941
  const input = exactRecord(value, [
912
942
  "project_id",
943
+ "etag",
913
944
  "handle",
914
945
  "region",
915
946
  "lifecycle",
@@ -925,8 +956,10 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
925
956
  const handle = input["handle"];
926
957
  if (
927
958
  !isUlid(input["project_id"]) ||
928
- !boundedString(handle, 5, 59) ||
929
- !/^[a-z]+-[a-z]+-[a-z]+$/u.test(handle) ||
959
+ typeof input["etag"] !== "string" ||
960
+ !/^"sha256-[a-f0-9]{64}"$/u.test(input["etag"]) ||
961
+ !boundedString(handle, 3, 59) ||
962
+ !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(handle) ||
930
963
  (input["region"] !== "us" && input["region"] !== "eu") ||
931
964
  (input["lifecycle"] !== "active" &&
932
965
  input["lifecycle"] !== "deleting" &&
@@ -1271,100 +1304,6 @@ function validDelivery(input: Readonly<Record<string, unknown>>): boolean {
1271
1304
  );
1272
1305
  }
1273
1306
 
1274
- export type PublicProjectMailDomain =
1275
- | Readonly<{ domain: string; status: "configured"; configured_at: string }>
1276
- | Readonly<{
1277
- domain: string;
1278
- status: "delegation_required" | "ready" | "verification_pending" | "verification_failed";
1279
- observed_at?: string;
1280
- verification_issue?:
1281
- | "identity_missing"
1282
- | "identity_verification_failed"
1283
- | "dkim_verification_failed"
1284
- | "dkim_records_changed"
1285
- | "tenant_association_missing"
1286
- | null;
1287
- next_check_after_seconds?: 3600 | null;
1288
- configured_at: string;
1289
- name_servers: readonly [string, string, string, string];
1290
- change_id: string;
1291
- }>;
1292
-
1293
- export const parseProjectMailDomain = (value: unknown): PublicProjectMailDomain => {
1294
- const input = exactRecord(value, [
1295
- "domain",
1296
- "status",
1297
- "configured_at",
1298
- "name_servers",
1299
- "change_id",
1300
- "observed_at",
1301
- "verification_issue",
1302
- "next_check_after_seconds",
1303
- ]);
1304
- if (
1305
- typeof input["domain"] !== "string" ||
1306
- !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(
1307
- input["domain"],
1308
- ) ||
1309
- !isIsoInstant(input["configured_at"])
1310
- ) {
1311
- throw new ResponseContractError();
1312
- }
1313
- if (input["status"] === "configured") {
1314
- if (
1315
- input["name_servers"] !== undefined ||
1316
- input["change_id"] !== undefined ||
1317
- input["observed_at"] !== undefined ||
1318
- input["verification_issue"] !== undefined ||
1319
- input["next_check_after_seconds"] !== undefined
1320
- ) {
1321
- throw new ResponseContractError();
1322
- }
1323
- return input as unknown as PublicProjectMailDomain;
1324
- }
1325
- if (
1326
- !["delegation_required", "ready", "verification_pending", "verification_failed"].includes(
1327
- String(input["status"]),
1328
- ) ||
1329
- !Array.isArray(input["name_servers"]) ||
1330
- input["name_servers"].length !== 4 ||
1331
- new Set(input["name_servers"]).size !== 4 ||
1332
- input["name_servers"].some(
1333
- (nameServer) =>
1334
- typeof nameServer !== "string" ||
1335
- !/^ns-[0-9]{1,4}\.awsdns-[0-9]{1,2}\.(?:com|net|org|co\.uk)$/u.test(nameServer),
1336
- ) ||
1337
- typeof input["change_id"] !== "string" ||
1338
- !/^C[A-Z0-9]{8,31}$/u.test(input["change_id"])
1339
- ) {
1340
- throw new ResponseContractError();
1341
- }
1342
- if (input["observed_at"] !== undefined) {
1343
- if (
1344
- ![
1345
- null,
1346
- "identity_missing",
1347
- "identity_verification_failed",
1348
- "dkim_verification_failed",
1349
- "dkim_records_changed",
1350
- "tenant_association_missing",
1351
- ].includes(input["verification_issue"] as string | null)
1352
- )
1353
- throw new ResponseContractError();
1354
- if (
1355
- !isIsoInstant(input["observed_at"]) ||
1356
- input["next_check_after_seconds"] !== (input["status"] === "ready" ? null : 3600)
1357
- )
1358
- throw new ResponseContractError();
1359
- } else if (
1360
- input["next_check_after_seconds"] !== undefined ||
1361
- input["verification_issue"] !== undefined ||
1362
- ["verification_pending", "verification_failed"].includes(String(input["status"]))
1363
- )
1364
- throw new ResponseContractError();
1365
- return input as unknown as PublicProjectMailDomain;
1366
- };
1367
-
1368
1307
  export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unknown>> => {
1369
1308
  const input = exactRecord(value, [
1370
1309
  "id",
@@ -1540,6 +1479,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1540
1479
  !isHttpsUrl(input["type"]) ||
1541
1480
  !boundedString(input["title"], 1, 160) ||
1542
1481
  !boundedString(input["request_id"], 1, 128) ||
1482
+ /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(input["request_id"]) ||
1543
1483
  typeof input["retryable"] !== "boolean" ||
1544
1484
  !boundedString(input["suggested_action"], 1, 500) ||
1545
1485
  (input["detail"] !== undefined && !boundedString(input["detail"], 1, 2_000)) ||
@@ -1558,6 +1498,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1558
1498
  )
1559
1499
  throw new ResponseContractError();
1560
1500
  return {
1501
+ requestId: input["request_id"],
1561
1502
  code: code as ProblemCode,
1562
1503
  retryable: policy.retryable,
1563
1504
  suggestedAction: