@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.
- package/README.md +1 -1
- package/dist/{chunk-QT3HUPU3.js → chunk-S2LJAGEP.js} +18 -9
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +591 -102
- package/package.json +1 -1
- package/src/apps/mcp/local-client.ts +20 -14
- package/src/apps/mcp/local-server.ts +197 -47
- package/src/apps/product-cli/cli.ts +48 -32
- package/src/apps/product-cli/command.ts +140 -35
- package/src/apps/product-cli/composition.ts +5 -3
- package/src/apps/product-cli/product-api.ts +28 -26
- package/src/apps/product-cli/repository-init.ts +27 -3
- package/src/apps/product-cli/runtime-contract.ts +41 -100
- package/src/packages/agent-skills/generated-skill-resources.ts +17 -8
- package/src/packages/contracts/framework-admission.ts +2 -2
- package/src/packages/sdk-ts/generated/index.ts +71 -16
- package/src/packages/sdk-ts/generated/sdk.gen.ts +500 -96
- package/src/packages/sdk-ts/generated/types.gen.ts +741 -162
- package/src/packages/sdk-ts/index.ts +1 -0
- package/src/packages/sdk-ts/managed-mail-response.ts +195 -0
- package/src/packages/sdk-ts/managed-mail.ts +85 -0
|
@@ -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
|
|
392
|
-
readonly
|
|
393
|
-
readonly
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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"
|
|
1754
|
+
if (first === "mail") {
|
|
1698
1755
|
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1699
|
-
values.
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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 === "
|
|
433
|
-
? "
|
|
434
|
-
:
|
|
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
|
-
|
|
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
|
|
1074
|
-
|
|
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
|
|
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.
|
|
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-
|
|
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-
|
|
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"],
|
|
884
|
-
!/^[a-
|
|
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
|
-
|
|
929
|
-
!/^
|
|
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:
|