@amerged/ohmyhost-cli 0.1.36 → 0.1.37

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
@@ -99,7 +99,7 @@ import {
99
99
  selectWorkspace,
100
100
  verifiedUserId,
101
101
  workspaceOf
102
- } from "./chunk-LU7ONYZG.js";
102
+ } from "./chunk-IHXNADOQ.js";
103
103
  import {
104
104
  CredentialStoreIntegrityError,
105
105
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",
@@ -470,6 +470,37 @@ const HELP_USAGE = Object.freeze({
470
470
  "printf '%s' \"$SECRET_VALUE\" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--profile-user USER_ID --profile-organization ULID | --token-user USER_ID --token-organization ULID] --stdin [--wait] --json (with --profile-user and --profile-organization the saved login that runs it must belong to that user and organization; with --token-user and --token-organization it runs only with an OHMYHOST_TOKEN of that user and organization, never with a saved login)",
471
471
  "secret delete":
472
472
  "ohmyhost secret delete NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--wait] --json",
473
+ "mail workspace list": "ohmyhost mail workspace list --organization ULID --json",
474
+ "mail workspace create":
475
+ "ohmyhost mail workspace create --organization ULID --domain DOMAIN --sending true --idempotency-key KEY --json",
476
+ "mail workspace status":
477
+ "ohmyhost mail workspace status --organization ULID --mail-domain ULID --json",
478
+ "mail workspace retire":
479
+ "ohmyhost mail workspace retire --organization ULID --mail-domain ULID --expected-revision N --idempotency-key KEY --yes --json",
480
+ "mail workspace dns-authorize":
481
+ "ohmyhost mail workspace dns-authorize --organization ULID --mail-domain ULID --expected-revision N --zone DOMAIN --idempotency-key KEY --json",
482
+ "mail workspace dns-revoke":
483
+ "ohmyhost mail workspace dns-revoke --organization ULID --mail-domain ULID --expected-revision N --idempotency-key KEY --yes --json",
484
+ "mail workspace dns-status":
485
+ "ohmyhost mail workspace dns-status --organization ULID --mail-domain ULID --json",
486
+ "mail binding get": "ohmyhost mail binding get --project ULID --environment PROD_ULID --json",
487
+ "mail binding set":
488
+ "ohmyhost mail binding set --project ULID --environment PROD_ULID --mail-domain ULID --sending true --sender-addresses contact@mail.example.com,contracts@mail.example.com --default-from contracts@mail.example.com [--reply-to owner@example.com] --expected-revision N --idempotency-key KEY --json",
489
+ "mail binding detach":
490
+ "ohmyhost mail binding detach --project ULID --environment PROD_ULID --expected-revision N --idempotency-key KEY --yes --json",
491
+ "mail identity get": "ohmyhost mail identity get --project ULID --json",
492
+ "mail identity declare":
493
+ "ohmyhost mail identity declare --project ULID --hostname sign.example.com --idempotency-key KEY --json",
494
+ "mail identity verify":
495
+ "ohmyhost mail identity verify --project ULID --expected-revision N --idempotency-key KEY --json",
496
+ "mail identity revoke":
497
+ "ohmyhost mail identity revoke --project ULID --expected-revision N --idempotency-key KEY --yes --json",
498
+ "mail identity authorize":
499
+ "ohmyhost mail identity authorize --project ULID --expected-revision N --zone example.com --idempotency-key KEY --json",
500
+ "mail identity authorization-revoke":
501
+ "ohmyhost mail identity authorization-revoke --project ULID --expected-revision N --idempotency-key KEY --yes --json",
502
+ "mail identity authorization-status":
503
+ "ohmyhost mail identity authorization-status --project ULID --json",
473
504
  "mail setup":
474
505
  "ohmyhost mail setup --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
475
506
  "mail status": "ohmyhost mail status --project ULID --environment ULID --json",
@@ -1114,6 +1145,11 @@ export class ProductCli {
1114
1145
  if (command.kind === "secret-list") return await this.listEnvironmentSecrets(command, signal);
1115
1146
  if (command.kind === "function-runs") return await this.listFunctionRuns(command, signal);
1116
1147
  if (command.kind === "secret-set") return await this.putEnvironmentSecret(command, signal);
1148
+ if (command.kind === "shared-mail") {
1149
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1150
+ if (!api.sharedMail) throw new Error("Shared mail unavailable");
1151
+ return success(command.label, { mail: await api.sharedMail(command.request) });
1152
+ }
1117
1153
  if (command.kind === "managed-mail") {
1118
1154
  const api = await this.authenticatedApi(command.credentialStore, signal);
1119
1155
  if (!api.managedMail) throw new Error("Mail unavailable");
@@ -3120,7 +3156,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
3120
3156
  if (command.kind === "function-runs") return "function runs";
3121
3157
  if (command.kind === "secret-set") return "secret set";
3122
3158
  if (command.kind === "secret-delete") return "secret delete";
3123
- if (command.kind === "managed-mail") return command.label;
3159
+ if (command.kind === "shared-mail" || command.kind === "managed-mail") return command.label;
3124
3160
  if (command.kind === "profile-list") return "profile list";
3125
3161
  return command.kind;
3126
3162
  };
@@ -3,7 +3,7 @@ import {
3
3
  isProjectDataChangeKind,
4
4
  type ProjectDataChangeKind,
5
5
  } from "@ohmyhost/contracts/project-data-changes";
6
- import type { ManagedMailCommand } from "@ohmyhost/sdk-ts";
6
+ import type { ManagedMailCommand, SharedMailCommand } from "@ohmyhost/sdk-ts";
7
7
  import { USER_API_KEY_ID } from "@ohmyhost/contracts/user-api-keys";
8
8
  import { WORKOS_USER_API_KEY_PATTERN } from "@ohmyhost/workos-auth-contracts/user-api-keys";
9
9
  import { isProfileName, isProfileUserId } from "./profile-store.js";
@@ -536,6 +536,12 @@ export type ProductCliCommand =
536
536
  readonly wait: boolean;
537
537
  readonly credentialStore: "native";
538
538
  }
539
+ | {
540
+ readonly kind: "shared-mail";
541
+ readonly request: SharedMailCommand;
542
+ readonly label: SharedMailCommandLabel;
543
+ readonly credentialStore: "native";
544
+ }
539
545
  | {
540
546
  readonly kind: "managed-mail";
541
547
  readonly request: ManagedMailCommand;
@@ -902,7 +908,27 @@ export type RecognizedCommandLabel =
902
908
  | "mail messages retry"
903
909
  | "mail domain set"
904
910
  | "mail domain status"
905
- | "mail domain delete";
911
+ | "mail domain delete"
912
+ | SharedMailCommandLabel;
913
+
914
+ export type SharedMailCommandLabel =
915
+ | "mail workspace list"
916
+ | "mail workspace create"
917
+ | "mail workspace status"
918
+ | "mail workspace retire"
919
+ | "mail workspace dns-authorize"
920
+ | "mail workspace dns-status"
921
+ | "mail workspace dns-revoke"
922
+ | "mail binding get"
923
+ | "mail binding set"
924
+ | "mail binding detach"
925
+ | "mail identity get"
926
+ | "mail identity declare"
927
+ | "mail identity verify"
928
+ | "mail identity revoke"
929
+ | "mail identity authorize"
930
+ | "mail identity authorization-status"
931
+ | "mail identity authorization-revoke";
906
932
 
907
933
  export const recognizedCommandLabel = (argv: readonly string[]): RecognizedCommandLabel => {
908
934
  const values = [...argv];
@@ -1052,6 +1078,11 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
1052
1078
  }
1053
1079
  if (value === "function" && values[0] === "runs") return "function runs";
1054
1080
  if (value === "mail") {
1081
+ if (["workspace", "binding", "identity"].includes(values[0] ?? "")) {
1082
+ const candidate = `mail ${values[0]} ${values[1]}`;
1083
+ if (SHARED_MAIL_LABELS.includes(candidate as SharedMailCommandLabel))
1084
+ return candidate as SharedMailCommandLabel;
1085
+ }
1055
1086
  if (values[0] === "setup") return "mail setup";
1056
1087
  if (values[0] === "status") return "mail status";
1057
1088
  if (values[0] === "webhook") {
@@ -2328,6 +2359,10 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
2328
2359
  }
2329
2360
  throw new InvalidCommandError();
2330
2361
  }
2362
+ if (first === "mail" && ["workspace", "binding", "identity"].includes(values[0] ?? "")) {
2363
+ if (wait || follow || stdin) throw new InvalidCommandError();
2364
+ return parseSharedMailCommand(values, yes);
2365
+ }
2331
2366
  if (first === "mail") {
2332
2367
  const area = values.shift();
2333
2368
  const sub =
@@ -2538,3 +2573,194 @@ const validPaidHostname = (value: string): boolean =>
2538
2573
  !value.endsWith(".omh.st") &&
2539
2574
  value !== "ohmyho.st" &&
2540
2575
  !value.endsWith(".ohmyho.st");
2576
+
2577
+ const SHARED_MAIL_LABELS: readonly SharedMailCommandLabel[] = [
2578
+ "mail workspace list",
2579
+ "mail workspace create",
2580
+ "mail workspace status",
2581
+ "mail workspace retire",
2582
+ "mail workspace dns-authorize",
2583
+ "mail workspace dns-status",
2584
+ "mail workspace dns-revoke",
2585
+ "mail binding get",
2586
+ "mail binding set",
2587
+ "mail binding detach",
2588
+ "mail identity get",
2589
+ "mail identity declare",
2590
+ "mail identity verify",
2591
+ "mail identity revoke",
2592
+ "mail identity authorize",
2593
+ "mail identity authorization-status",
2594
+ "mail identity authorization-revoke",
2595
+ ];
2596
+
2597
+ /** New workspace sender operations stay separate from the exact legacy project mail contract. */
2598
+ function parseSharedMailCommand(values: string[], yes: boolean): ProductCliCommand {
2599
+ const area = values.shift(),
2600
+ operation = values.shift();
2601
+ const label = `mail ${area} ${operation}` as SharedMailCommandLabel;
2602
+ if (!SHARED_MAIL_LABELS.includes(label)) throw new InvalidCommandError();
2603
+ const destructive =
2604
+ operation === "retire" ||
2605
+ operation === "detach" ||
2606
+ operation === "revoke" ||
2607
+ operation === "dns-revoke" ||
2608
+ operation === "authorization-revoke";
2609
+ if (yes !== destructive) throw new InvalidCommandError();
2610
+ const allowed = area === "workspace" ? ["--organization"] : ["--project"];
2611
+ const writing = [
2612
+ "create",
2613
+ "retire",
2614
+ "dns-authorize",
2615
+ "dns-revoke",
2616
+ "set",
2617
+ "detach",
2618
+ "declare",
2619
+ "verify",
2620
+ "revoke",
2621
+ "authorize",
2622
+ "authorization-revoke",
2623
+ ].includes(operation ?? "");
2624
+ if (area === "workspace" && operation !== "list" && operation !== "create")
2625
+ allowed.push("--mail-domain");
2626
+ if (area === "binding") allowed.push("--environment");
2627
+ if (operation === "create") allowed.push("--domain", "--sending");
2628
+ if (area === "binding" && operation === "set")
2629
+ allowed.push(
2630
+ "--mail-domain",
2631
+ "--sending",
2632
+ "--sender-addresses",
2633
+ "--default-from",
2634
+ "--reply-to",
2635
+ );
2636
+ if (operation === "declare") allowed.push("--hostname");
2637
+ if (operation === "authorize" || operation === "dns-authorize") allowed.push("--zone");
2638
+ if (writing) allowed.push("--idempotency-key");
2639
+ if (writing && operation !== "create" && operation !== "declare")
2640
+ allowed.push("--expected-revision");
2641
+ const options = parseOptionalOptions(values, allowed);
2642
+ const required = (key: keyof typeof options) => {
2643
+ const value = options[key];
2644
+ if (!value) throw new InvalidCommandError();
2645
+ return value;
2646
+ };
2647
+ const revision = () => {
2648
+ const value = required("--expected-revision");
2649
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(value) || !Number.isSafeInteger(Number(value)))
2650
+ throw new InvalidCommandError();
2651
+ return Number(value);
2652
+ };
2653
+ const sending = () => {
2654
+ const value = required("--sending");
2655
+ if (value !== "true" && value !== "false") throw new InvalidCommandError();
2656
+ return value === "true";
2657
+ };
2658
+ const idempotencyKey = () => required("--idempotency-key");
2659
+ let request: SharedMailCommand;
2660
+ if (area === "workspace") {
2661
+ const scope = { organizationId: required("--organization") };
2662
+ if (operation === "list") request = { ...scope, action: "domains_list" };
2663
+ else if (operation === "create") {
2664
+ const enabled = sending();
2665
+ if (!enabled) throw new InvalidCommandError();
2666
+ request = {
2667
+ ...scope,
2668
+ action: "domain_create",
2669
+ domain: required("--domain"),
2670
+ sending: enabled,
2671
+ idempotencyKey: idempotencyKey(),
2672
+ };
2673
+ } else {
2674
+ const target = { ...scope, mailDomainId: required("--mail-domain") };
2675
+ if (operation === "status") request = { ...target, action: "domain_get" };
2676
+ else if (operation === "dns-status") request = { ...target, action: "domain_dns_status" };
2677
+ else if (operation === "dns-revoke")
2678
+ request = {
2679
+ ...target,
2680
+ action: "domain_dns_revoke",
2681
+ expectedRevision: revision(),
2682
+ idempotencyKey: idempotencyKey(),
2683
+ };
2684
+ else if (operation === "retire")
2685
+ request = {
2686
+ ...target,
2687
+ action: "domain_retire",
2688
+ expectedRevision: revision(),
2689
+ idempotencyKey: idempotencyKey(),
2690
+ };
2691
+ else
2692
+ request = {
2693
+ ...target,
2694
+ action: "domain_dns_authorize",
2695
+ expectedRevision: revision(),
2696
+ zone: required("--zone"),
2697
+ idempotencyKey: idempotencyKey(),
2698
+ };
2699
+ }
2700
+ } else if (area === "binding") {
2701
+ const scope = { projectId: required("--project"), environmentId: required("--environment") };
2702
+ if (operation === "get") request = { ...scope, action: "binding_get" };
2703
+ else if (operation === "detach")
2704
+ request = {
2705
+ ...scope,
2706
+ action: "binding_detach",
2707
+ expectedRevision: revision(),
2708
+ idempotencyKey: idempotencyKey(),
2709
+ };
2710
+ else {
2711
+ const senderAddresses = required("--sender-addresses").split(",");
2712
+ if (
2713
+ senderAddresses.length > 32 ||
2714
+ senderAddresses.some((address) => !address || address.trim() !== address) ||
2715
+ new Set(senderAddresses).size !== senderAddresses.length
2716
+ )
2717
+ throw new InvalidCommandError();
2718
+ request = {
2719
+ ...scope,
2720
+ action: "binding_set",
2721
+ mailDomainId: required("--mail-domain"),
2722
+ sending: sending(),
2723
+ senderAddresses,
2724
+ defaultFrom: required("--default-from"),
2725
+ defaultReplyTo: options["--reply-to"] ?? null,
2726
+ expectedRevision: revision(),
2727
+ idempotencyKey: idempotencyKey(),
2728
+ };
2729
+ }
2730
+ } else {
2731
+ const scope = { projectId: required("--project") };
2732
+ if (operation === "get") request = { ...scope, action: "identity_get" };
2733
+ else if (operation === "authorization-status")
2734
+ request = { ...scope, action: "identity_authorization_status" };
2735
+ else if (operation === "authorization-revoke")
2736
+ request = {
2737
+ ...scope,
2738
+ action: "identity_authorization_revoke",
2739
+ expectedRevision: revision(),
2740
+ idempotencyKey: idempotencyKey(),
2741
+ };
2742
+ else if (operation === "declare")
2743
+ request = {
2744
+ ...scope,
2745
+ action: "identity_declare",
2746
+ hostname: required("--hostname"),
2747
+ idempotencyKey: idempotencyKey(),
2748
+ };
2749
+ else if (operation === "authorize")
2750
+ request = {
2751
+ ...scope,
2752
+ action: "identity_authorize",
2753
+ zone: required("--zone"),
2754
+ expectedRevision: revision(),
2755
+ idempotencyKey: idempotencyKey(),
2756
+ };
2757
+ else
2758
+ request = {
2759
+ ...scope,
2760
+ action: operation === "verify" ? "identity_verify" : "identity_revoke",
2761
+ expectedRevision: revision(),
2762
+ idempotencyKey: idempotencyKey(),
2763
+ };
2764
+ }
2765
+ return { kind: "shared-mail", request, label, credentialStore: "native" };
2766
+ }
@@ -528,6 +528,8 @@ const readTokenIdentity = async (input: {
528
528
  /** The organization a command names explicitly; `organization use` names its target instead. */
529
529
  const commandOrganizationId = (command: ProductCliCommand): string | undefined => {
530
530
  if (command.kind === "organization-use" || command.kind === "login") return undefined;
531
+ if (command.kind === "shared-mail" && "organizationId" in command.request)
532
+ return command.request.organizationId;
531
533
  if (command.kind === "feedback-submit") return command.report.organization_id;
532
534
  return "organizationId" in command && typeof command.organizationId === "string"
533
535
  ? command.organizationId
@@ -535,6 +537,8 @@ const commandOrganizationId = (command: ProductCliCommand): string | undefined =
535
537
  };
536
538
 
537
539
  const commandProjectId = (command: ProductCliCommand): string | undefined => {
540
+ if (command.kind === "shared-mail")
541
+ return "projectId" in command.request ? command.request.projectId : undefined;
538
542
  if (command.kind === "managed-mail") return command.request.projectId;
539
543
  return "projectId" in command && typeof command.projectId === "string"
540
544
  ? command.projectId
@@ -1,4 +1,9 @@
1
- import { executeManagedMail, type ManagedMailCommand } from "@ohmyhost/sdk-ts";
1
+ import {
2
+ executeManagedMail,
3
+ executeSharedMail,
4
+ type ManagedMailCommand,
5
+ type SharedMailCommand,
6
+ } from "@ohmyhost/sdk-ts";
2
7
  import type {
3
8
  ProjectDataChangeKind,
4
9
  ProjectDataChangePlan,
@@ -434,6 +439,7 @@ export interface ProductApi {
434
439
  readonly name: string;
435
440
  readonly idempotencyKey: string;
436
441
  }): Promise<DeleteEnvironmentSecretResult>;
442
+ sharedMail?(input: SharedMailCommand): Promise<unknown>;
437
443
  managedMail?(input: ManagedMailCommand): Promise<unknown>;
438
444
  }
439
445
 
@@ -1237,6 +1243,10 @@ export class GeneratedSdkProductApi implements ProductApi {
1237
1243
  );
1238
1244
  }
1239
1245
 
1246
+ public sharedMail(input: SharedMailCommand) {
1247
+ return executeSharedMail(this.#client, input);
1248
+ }
1249
+
1240
1250
  public managedMail(input: ManagedMailCommand) {
1241
1251
  return executeManagedMail(this.#client, input);
1242
1252
  }
@@ -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",
@@ -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
+ }