@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.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-LU7ONYZG.js → chunk-EWKJGJQU.js} +1379 -258
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/apps/product-cli/package.json +2 -2
- package/src/apps/product-cli/src/cli.ts +37 -1
- package/src/apps/product-cli/src/command.ts +228 -2
- package/src/apps/product-cli/src/composition.ts +4 -0
- package/src/apps/product-cli/src/product-api.ts +11 -1
- package/src/apps/product-cli/src/repository-init.ts +181 -9
- package/src/packages/contracts/package.json +6 -1
- package/src/packages/contracts/src/framework-admission-corpus.ts +15 -18
- package/src/packages/contracts/src/framework-build-script.mjs +10 -3
- package/src/packages/contracts/src/index.ts +3 -0
- package/src/packages/contracts/src/problem-policy.ts +2 -2
- package/src/packages/contracts/src/shared-mail.ts +236 -0
- package/src/packages/sdk-ts/package.json +2 -2
- package/src/packages/sdk-ts/src/generated/index.ts +112 -0
- package/src/packages/sdk-ts/src/generated/sdk.gen.ts +870 -0
- package/src/packages/sdk-ts/src/generated/shared-mail-response.ts +238 -0
- package/src/packages/sdk-ts/src/generated/types.gen.ts +1207 -0
- package/src/packages/sdk-ts/src/index.ts +2 -0
- package/src/packages/sdk-ts/src/shared-mail-response.ts +1 -0
- package/src/packages/sdk-ts/src/shared-mail.ts +225 -0
- package/src/packages/workos-auth-contracts/package.json +1 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ohmyhost/product-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"private": true,
|
|
5
5
|
"ohmyhost": {
|
|
6
6
|
"deployment": "production",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc --project tsconfig.build.json",
|
|
35
|
-
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
35
|
+
"typecheck": "tsc --project tsconfig.json --noEmit --incremental --tsBuildInfoFile typecheck.tsbuildinfo",
|
|
36
36
|
"test": "vitest run --root ../.. apps/product-cli/test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
@@ -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 {
|
|
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
|
}
|
|
@@ -75,6 +75,8 @@ export type RepositoryFramework = Exclude<FrameworkFamilyClassification, "ambigu
|
|
|
75
75
|
const MAXIMUM_SOURCE_BYTES = 2 * 1024 * 1024;
|
|
76
76
|
const SOURCE_MODULE_PATTERN = /\.(?:[cm]?[jt]s|[jt]sx)$/u;
|
|
77
77
|
const STORAGE_CLIENT_CALL_PATTERN = /\bcreatePrivateStorageClient\s*\(/u;
|
|
78
|
+
const GATEWAY_BINDING_NAME = /^OHMYHOST_(?:MAIL|STORAGE)_GATEWAY$/u;
|
|
79
|
+
const GATEWAY_WORD_TOKEN = /[\p{ID_Start}_$][\p{ID_Continue}$]*/uy;
|
|
78
80
|
// The catalog names a module only by a path of its grammar; a file name with a space, for example,
|
|
79
81
|
// is not one, so its blocker carries the path beside a sentence that does not quote it.
|
|
80
82
|
const UNNAMED_MODULE_MESSAGES: Readonly<Record<DatabaseContractCode, string>> = Object.freeze({
|
|
@@ -89,15 +91,19 @@ function callsStorageClient(source: string): boolean {
|
|
|
89
91
|
return STORAGE_CLIENT_CALL_PATTERN.test(sourceWithoutComments(source));
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
+
// Global fetch reaches the private storage gateway using these values and its environment key.
|
|
95
|
+
// No raw bucket binding is handed to customer code.
|
|
94
96
|
const STORAGE_RUNTIME_BINDINGS = Object.freeze([
|
|
95
97
|
"OHMYHOST_ENVIRONMENT_ID",
|
|
96
98
|
"OHMYHOST_PROJECT_ID",
|
|
97
|
-
"OHMYHOST_STORAGE_GATEWAY",
|
|
98
99
|
"OHMYHOST_STORAGE_GATEWAY_URL",
|
|
99
100
|
"OHMYHOST_STORAGE_KEY",
|
|
100
101
|
]);
|
|
102
|
+
const MAIL_RUNTIME_BINDINGS = Object.freeze([
|
|
103
|
+
"OHMYHOST_MAIL_GATEWAY_URL",
|
|
104
|
+
"OHMYHOST_MAIL_KEY",
|
|
105
|
+
"OHMYHOST_PROJECT_ID",
|
|
106
|
+
]);
|
|
101
107
|
// The runtime release is published unchanged as `@amerged/ohmyhost-runtime`; this pinned alias keeps
|
|
102
108
|
// its import name and, unlike the website archive each release removes, stays installable.
|
|
103
109
|
const CUSTOMER_RUNTIME_SOURCE = `npm:@amerged/ohmyhost-runtime@${cliPackage.version}`;
|
|
@@ -105,6 +111,7 @@ const CUSTOMER_RUNTIME_SOURCE = `npm:@amerged/ohmyhost-runtime@${cliPackage.vers
|
|
|
105
111
|
/** A catalog code, which planning answers with the same sentence, or a check only init runs. */
|
|
106
112
|
export type RepositoryInitBlockerCode =
|
|
107
113
|
| FrameworkConversionDiagnosticCode
|
|
114
|
+
| "gateway_binding_unsupported"
|
|
108
115
|
| "migration_filename_noncanonical"
|
|
109
116
|
| "migration_sql_not_admitted"
|
|
110
117
|
| "storage_client_missing";
|
|
@@ -507,7 +514,7 @@ interface SourceCheckContext {
|
|
|
507
514
|
/**
|
|
508
515
|
* Every blocker of a source with a valid or proposed configuration, in the order planning judges:
|
|
509
516
|
* migration filenames, the shared source rules, the Worker module handlers and the database
|
|
510
|
-
* contract; init's own storage client and migration SQL checks come last.
|
|
517
|
+
* contract; init's own gateway binding, storage client and migration SQL checks come last.
|
|
511
518
|
*/
|
|
512
519
|
async function judgeSource(
|
|
513
520
|
context: SourceCheckContext &
|
|
@@ -528,6 +535,7 @@ async function judgeSource(
|
|
|
528
535
|
...source.findings.map((finding) => requiredCatalogBlocker(finding, context.applicationRoot)),
|
|
529
536
|
...(await workerModuleBlockers(context)),
|
|
530
537
|
...(await databaseContractBlockers(context, context.provider)),
|
|
538
|
+
...(await gatewayBindingBlockers(context)),
|
|
531
539
|
...(await storageClientBlockers(context)),
|
|
532
540
|
...(await migrationSqlBlockers(context, context.provider)),
|
|
533
541
|
],
|
|
@@ -621,6 +629,168 @@ async function workerModuleBlockers(context: SourceCheckContext): Promise<Reposi
|
|
|
621
629
|
);
|
|
622
630
|
}
|
|
623
631
|
|
|
632
|
+
/** Keep literal text inert, but scan executable template interpolations as ordinary code. */
|
|
633
|
+
function gatewaySourceTokens(source: string): readonly string[] {
|
|
634
|
+
const tokens: string[] = [];
|
|
635
|
+
let cursor = 0;
|
|
636
|
+
const quoted = (quote: string) => {
|
|
637
|
+
const start = cursor++;
|
|
638
|
+
while (cursor < source.length) {
|
|
639
|
+
const character = source[cursor++];
|
|
640
|
+
if (character === "\\") cursor++;
|
|
641
|
+
else if (character === quote) break;
|
|
642
|
+
}
|
|
643
|
+
tokens.push(source.slice(start, cursor));
|
|
644
|
+
};
|
|
645
|
+
const code = (interpolation: boolean): void => {
|
|
646
|
+
let braces = 0;
|
|
647
|
+
while (cursor < source.length) {
|
|
648
|
+
const character = source[cursor] as string;
|
|
649
|
+
if (interpolation && character === "}" && braces === 0) {
|
|
650
|
+
cursor++;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (character === '"' || character === "'") {
|
|
654
|
+
quoted(character);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (character === "`") {
|
|
658
|
+
cursor++;
|
|
659
|
+
while (cursor < source.length) {
|
|
660
|
+
const literal = source[cursor++];
|
|
661
|
+
if (literal === "\\") cursor++;
|
|
662
|
+
else if (literal === "`") break;
|
|
663
|
+
else if (literal === "$" && source[cursor] === "{") {
|
|
664
|
+
cursor++;
|
|
665
|
+
tokens.push("(");
|
|
666
|
+
code(true);
|
|
667
|
+
tokens.push(")");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (/\s/u.test(character)) {
|
|
673
|
+
cursor++;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (character === "{") braces++;
|
|
677
|
+
if (character === "}") braces--;
|
|
678
|
+
GATEWAY_WORD_TOKEN.lastIndex = cursor;
|
|
679
|
+
const word = GATEWAY_WORD_TOKEN.exec(source)?.[0];
|
|
680
|
+
const token =
|
|
681
|
+
word ??
|
|
682
|
+
(["?.", "=>"].includes(source.slice(cursor, cursor + 2))
|
|
683
|
+
? source.slice(cursor, cursor + 2)
|
|
684
|
+
: character);
|
|
685
|
+
tokens.push(token);
|
|
686
|
+
cursor += token.length;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
code(false);
|
|
690
|
+
return tokens;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Static value accesses only: declarations, literal messages and object-literal keys are inert. */
|
|
694
|
+
function usesGatewayBinding(source: string): boolean {
|
|
695
|
+
const tokens = gatewaySourceTokens(sourceWithoutComments(source));
|
|
696
|
+
const objects: boolean[] = [];
|
|
697
|
+
let declaration: "type" | "interface" | "annotation" | undefined;
|
|
698
|
+
let declarationDepth = 0;
|
|
699
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
700
|
+
const token = tokens[index] as string;
|
|
701
|
+
const previous = tokens[index - 1];
|
|
702
|
+
const next = tokens[index + 1];
|
|
703
|
+
if (declaration !== undefined) {
|
|
704
|
+
if (
|
|
705
|
+
declarationDepth === 0 &&
|
|
706
|
+
(token === ";" || /^(?:export|const|let|var|function|class|type|interface)$/u.test(token))
|
|
707
|
+
) {
|
|
708
|
+
declaration = undefined;
|
|
709
|
+
if (token === ";") continue;
|
|
710
|
+
} else {
|
|
711
|
+
if (token === "{" || token === "(" || token === "[") declarationDepth++;
|
|
712
|
+
if (token === "}" || token === ")" || token === "]") {
|
|
713
|
+
declarationDepth--;
|
|
714
|
+
if (declaration !== "type" && declarationDepth === 0) declaration = undefined;
|
|
715
|
+
}
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (
|
|
720
|
+
(token === "type" || token === "interface") &&
|
|
721
|
+
next !== undefined &&
|
|
722
|
+
/^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u.test(next) &&
|
|
723
|
+
previous !== "." &&
|
|
724
|
+
previous !== "?."
|
|
725
|
+
) {
|
|
726
|
+
declaration = token;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
// An inline variable type is not an object binding pattern; resume at its initializer.
|
|
730
|
+
if (token === "{" && previous === ":" && /^(?:const|let|var)$/u.test(tokens[index - 3] ?? "")) {
|
|
731
|
+
declaration = "annotation";
|
|
732
|
+
declarationDepth = 1;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (token === "{") objects.push(false);
|
|
736
|
+
if (token === "}") {
|
|
737
|
+
const gateway = objects.pop();
|
|
738
|
+
let after = index + 1;
|
|
739
|
+
if (tokens[after] === ":") {
|
|
740
|
+
after++;
|
|
741
|
+
while (after < tokens.length && !/^(?:=|;|\{|\})$/u.test(tokens[after] as string)) after++;
|
|
742
|
+
}
|
|
743
|
+
if (gateway && tokens[after] === "=") return true;
|
|
744
|
+
}
|
|
745
|
+
const quoted = token.startsWith('"') || token.startsWith("'");
|
|
746
|
+
const name = quoted ? token.slice(1, -1) : token;
|
|
747
|
+
if (!GATEWAY_BINDING_NAME.test(name)) continue;
|
|
748
|
+
if (!quoted && (previous === "." || previous === "?.")) return true;
|
|
749
|
+
const receiver = tokens[index - (tokens[index - 2] === "?." ? 3 : 2)] ?? "";
|
|
750
|
+
if (
|
|
751
|
+
quoted &&
|
|
752
|
+
previous === "[" &&
|
|
753
|
+
next === "]" &&
|
|
754
|
+
(/^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u.test(receiver) ||
|
|
755
|
+
receiver === ")" ||
|
|
756
|
+
receiver === "]") &&
|
|
757
|
+
!/^(?:return|throw|yield|case|new|typeof|void|delete|await|in|of)$/u.test(receiver)
|
|
758
|
+
)
|
|
759
|
+
return true;
|
|
760
|
+
if (
|
|
761
|
+
objects.length > 0 &&
|
|
762
|
+
(previous === "{" || previous === ",") &&
|
|
763
|
+
(next === ":" || next === "," || next === "}" || next === "=")
|
|
764
|
+
)
|
|
765
|
+
objects[objects.length - 1] = true;
|
|
766
|
+
}
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Init names legacy mail/storage bindings before a customer builds code whose gateway calls the
|
|
772
|
+
* egress policy cannot serve. This local guidance does not change planning's admission policy.
|
|
773
|
+
*/
|
|
774
|
+
async function gatewayBindingBlockers(
|
|
775
|
+
context: SourceCheckContext,
|
|
776
|
+
): Promise<RepositoryInitBlocker[]> {
|
|
777
|
+
const blockers: RepositoryInitBlocker[] = [];
|
|
778
|
+
for (const path of context.files) {
|
|
779
|
+
if (!SOURCE_MODULE_PATTERN.test(path) || /\.d\.[cm]?ts$/u.test(path)) continue;
|
|
780
|
+
const absolute = resolve(context.applicationDirectory, path);
|
|
781
|
+
const information = await stat(absolute);
|
|
782
|
+
if (!information.isFile() || information.size > MAXIMUM_SOURCE_BYTES) continue;
|
|
783
|
+
if (!usesGatewayBinding(await readFile(absolute, "utf8"))) continue;
|
|
784
|
+
blockers.push({
|
|
785
|
+
code: "gateway_binding_unsupported",
|
|
786
|
+
path: repositoryPath(context.applicationRoot, path),
|
|
787
|
+
message:
|
|
788
|
+
"Mail and storage gateway Service Bindings are unsupported. In server code, create the client from its OHMYHOST_MAIL_GATEWAY_URL or OHMYHOST_STORAGE_GATEWAY_URL, environment key and project ID, plus the environment ID for storage. Use the global fetch with fetch: (request) => fetch(request), and capabilityFetch: (request) => fetch(request) for storage signed URLs.",
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return blockers;
|
|
792
|
+
}
|
|
793
|
+
|
|
624
794
|
/**
|
|
625
795
|
* Declared storage is reached only through the private gateway client. A storage-enabled app whose
|
|
626
796
|
* source never constructs that client deploys green and fails at its first upload, so name it here.
|
|
@@ -641,7 +811,7 @@ async function storageClientBlockers(
|
|
|
641
811
|
code: "storage_client_missing",
|
|
642
812
|
path: null,
|
|
643
813
|
message:
|
|
644
|
-
'ohmyhost.yaml declares storage, but no application source calls createPrivateStorageClient. Import it from "@ohmyhost/customer-runtime/storage" and build it in server code from
|
|
814
|
+
'ohmyhost.yaml declares storage, but no application source calls createPrivateStorageClient. Import it from "@ohmyhost/customer-runtime/storage" and build it in server code from OHMYHOST_STORAGE_GATEWAY_URL, OHMYHOST_PROJECT_ID, OHMYHOST_ENVIRONMENT_ID and OHMYHOST_STORAGE_KEY, with the global fetch as fetch: (request) => fetch(request) and capabilityFetch: (request) => fetch(request). Reserve an upload, PUT the bytes to the returned signed URL, complete it, and serve reads with createSignedRead. There is no FILES bucket binding.',
|
|
645
815
|
},
|
|
646
816
|
];
|
|
647
817
|
}
|
|
@@ -1418,10 +1588,12 @@ function runtimeContract(capabilities: RuntimeCapabilities): Readonly<{
|
|
|
1418
1588
|
return Object.freeze({
|
|
1419
1589
|
bindings: Object.freeze(
|
|
1420
1590
|
[
|
|
1421
|
-
...(
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1591
|
+
...new Set([
|
|
1592
|
+
...(capabilities.auth ? ["BETTER_AUTH_SECRET"] : []),
|
|
1593
|
+
...(capabilities.storage ? STORAGE_RUNTIME_BINDINGS : []),
|
|
1594
|
+
...(capabilities.database ? ["OHMYHOST_DATABASE"] : []),
|
|
1595
|
+
...(capabilities.mail ? MAIL_RUNTIME_BINDINGS : []),
|
|
1596
|
+
]),
|
|
1425
1597
|
].sort(),
|
|
1426
1598
|
),
|
|
1427
1599
|
packages: Object.freeze({
|