@amerged/ohmyhost-cli 0.1.15 → 0.1.18
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/bin.js +1 -1
- package/dist/{chunk-3BEMNNWB.js → chunk-MLABIGQ6.js} +506 -65
- package/dist/index.js +7 -1
- package/package.json +1 -1
- package/src/apps/product-cli/cli.ts +97 -10
- package/src/apps/product-cli/command.ts +155 -10
- package/src/apps/product-cli/composition.ts +47 -3
- package/src/apps/product-cli/index.ts +1 -0
- package/src/apps/product-cli/product-api.ts +73 -2
- package/src/apps/product-cli/project-link-store.ts +45 -2
- package/src/apps/product-cli/repository-init.ts +6 -5
- package/src/apps/product-cli/runtime-contract.ts +60 -4
- package/src/packages/contracts/feedback.ts +212 -32
- package/src/packages/sdk-ts/generated/index.ts +42 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +282 -3
- package/src/packages/sdk-ts/generated/types.gen.ts +438 -10
- package/src/packages/sdk-ts/managed-mail-response.ts +7 -0
- package/src/packages/sdk-ts/managed-mail.ts +7 -1
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
} from "./credential-store.js";
|
|
45
45
|
import { GeneratedSdkProductApi, type ProductApi } from "./product-api.js";
|
|
46
46
|
import { workspaceOf, type WorkspaceSession } from "./workspace.js";
|
|
47
|
-
import { OwnerOnlyProjectLinkStore } from "./project-link-store.js";
|
|
47
|
+
import { OwnerOnlyProjectLinkStore, readLinkedProjectId } from "./project-link-store.js";
|
|
48
48
|
import {
|
|
49
49
|
initializeRepository,
|
|
50
50
|
RepositoryInitBlockedError,
|
|
@@ -67,6 +67,8 @@ export interface ProductCliIo {
|
|
|
67
67
|
|
|
68
68
|
export interface ProductCliCompositionOverrides {
|
|
69
69
|
readonly credentialStore?: CredentialStore;
|
|
70
|
+
/** Directory whose project link supplies a missing --project; defaults to the process directory. */
|
|
71
|
+
readonly workingDirectory?: string;
|
|
70
72
|
readonly readSecretValue?: (signal: AbortSignal) => Promise<string>;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -221,7 +223,16 @@ export const runProductionCli = async (
|
|
|
221
223
|
const diagnostics = new StderrDiagnosticSink(io);
|
|
222
224
|
let command: ProductCliCommand;
|
|
223
225
|
try {
|
|
224
|
-
|
|
226
|
+
const parsed = await parseWithLinkedProject(argv, async () => {
|
|
227
|
+
const profile = resolveProductCliEnvironment(environment);
|
|
228
|
+
return readLinkedProjectId(
|
|
229
|
+
join(overrides.workingDirectory ?? process.cwd(), profile.linkDirectory),
|
|
230
|
+
profile.apiOrigin,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
command = parsed.command;
|
|
234
|
+
if (parsed.linkedProjectId !== null)
|
|
235
|
+
io.writeStderr(`ohmyhost: using linked project ${parsed.linkedProjectId}\n`);
|
|
225
236
|
} catch (error) {
|
|
226
237
|
const result =
|
|
227
238
|
error instanceof InvalidCommandError
|
|
@@ -344,7 +355,10 @@ export const runProductionCli = async (
|
|
|
344
355
|
signal,
|
|
345
356
|
},
|
|
346
357
|
),
|
|
347
|
-
projectLinkStore: new OwnerOnlyProjectLinkStore(
|
|
358
|
+
projectLinkStore: new OwnerOnlyProjectLinkStore(
|
|
359
|
+
join(process.cwd(), profile.linkDirectory),
|
|
360
|
+
process.cwd(),
|
|
361
|
+
),
|
|
348
362
|
productApiOrigin: profile.apiOrigin,
|
|
349
363
|
tokenRefresher: new PublicWorkosTokenRefresher(environment),
|
|
350
364
|
tokenRevoker: lifecycle,
|
|
@@ -588,6 +602,36 @@ export class GeneratedSdkSessionRevoker implements TokenRevoker {
|
|
|
588
602
|
}
|
|
589
603
|
}
|
|
590
604
|
|
|
605
|
+
/**
|
|
606
|
+
* A command run inside a linked directory may omit --project; the link supplies it. The
|
|
607
|
+
* link is read only when the command needs a project it did not name.
|
|
608
|
+
*/
|
|
609
|
+
export async function parseWithLinkedProject(
|
|
610
|
+
argv: readonly string[],
|
|
611
|
+
linkedProjectId: () => Promise<string | null>,
|
|
612
|
+
): Promise<{ readonly command: ProductCliCommand; readonly linkedProjectId: string | null }> {
|
|
613
|
+
try {
|
|
614
|
+
return { command: parseProductCliCommand(argv), linkedProjectId: null };
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (!(error instanceof InvalidCommandError) || argv.includes("--project")) throw error;
|
|
617
|
+
let projectId: string | null;
|
|
618
|
+
try {
|
|
619
|
+
projectId = await linkedProjectId();
|
|
620
|
+
} catch {
|
|
621
|
+
projectId = null;
|
|
622
|
+
}
|
|
623
|
+
if (projectId === null) throw error;
|
|
624
|
+
try {
|
|
625
|
+
return {
|
|
626
|
+
command: parseProductCliCommand([...argv, "--project", projectId]),
|
|
627
|
+
linkedProjectId: projectId,
|
|
628
|
+
};
|
|
629
|
+
} catch {
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
591
635
|
class StderrDiagnosticSink implements DiagnosticSink {
|
|
592
636
|
public constructor(private readonly io: ProductCliIo) {}
|
|
593
637
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type UserApiKeyCreation,
|
|
25
25
|
type UserApiKeyPage,
|
|
26
26
|
} from "@ohmyhost/contracts/user-api-keys";
|
|
27
|
-
import { parseFeedbackReceipt } from "@ohmyhost/contracts/feedback";
|
|
27
|
+
import { parseFeedbackReceipt, parseFeedbackStatus } from "@ohmyhost/contracts/feedback";
|
|
28
28
|
import {
|
|
29
29
|
getProjectContext,
|
|
30
30
|
setProjectNotes,
|
|
@@ -57,9 +57,14 @@ import {
|
|
|
57
57
|
configureProjectSourceAutoDeploy,
|
|
58
58
|
createCloudflareDnsAuthorization,
|
|
59
59
|
createProjectDevAccessTicket,
|
|
60
|
+
ensureProjectDevShareLink,
|
|
61
|
+
rotateProjectDevShareLink,
|
|
62
|
+
revokeProjectDevShareLink,
|
|
63
|
+
setProjectDevAccessMode,
|
|
60
64
|
createDeployment,
|
|
61
65
|
createProject,
|
|
62
66
|
submitFeedback,
|
|
67
|
+
getFeedback,
|
|
63
68
|
createOrganization,
|
|
64
69
|
type Organization,
|
|
65
70
|
type GithubConnectionAuthorization,
|
|
@@ -131,6 +136,7 @@ export interface ProjectCreateInput {
|
|
|
131
136
|
readonly organizationId: string;
|
|
132
137
|
readonly name: string;
|
|
133
138
|
readonly dataMode?: "shared" | "isolated";
|
|
139
|
+
readonly devAccessMode?: "protected" | "public";
|
|
134
140
|
readonly region?: "us" | "eu";
|
|
135
141
|
readonly idempotencyKey: string;
|
|
136
142
|
}
|
|
@@ -257,10 +263,28 @@ export interface ProductApi {
|
|
|
257
263
|
readonly idempotencyKey: string;
|
|
258
264
|
}): Promise<PaidDomain>;
|
|
259
265
|
createDevAccessTicket(projectId: string): Promise<DevAccessTicket>;
|
|
266
|
+
ensureDevShareLink(projectId: string): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
|
|
267
|
+
rotateDevShareLink(
|
|
268
|
+
projectId: string,
|
|
269
|
+
idempotencyKey: string,
|
|
270
|
+
): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
|
|
271
|
+
revokeDevShareLink(
|
|
272
|
+
projectId: string,
|
|
273
|
+
idempotencyKey: string,
|
|
274
|
+
): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
|
|
275
|
+
setDevAccessMode(
|
|
276
|
+
projectId: string,
|
|
277
|
+
mode: "protected" | "public",
|
|
278
|
+
idempotencyKey: string,
|
|
279
|
+
): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
|
|
260
280
|
submitFeedback(input: {
|
|
261
281
|
report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
|
|
262
282
|
idempotencyKey: string;
|
|
263
283
|
}): Promise<import("@ohmyhost/sdk-ts").FeedbackReceipt>;
|
|
284
|
+
getFeedback(
|
|
285
|
+
feedbackId: string,
|
|
286
|
+
cursor?: string,
|
|
287
|
+
): Promise<import("@ohmyhost/sdk-ts").FeedbackStatus>;
|
|
264
288
|
createProject(input: ProjectCreateInput): Promise<Operation>;
|
|
265
289
|
getOperation(operationId: string): Promise<Operation>;
|
|
266
290
|
reconcileOperation(input: {
|
|
@@ -298,6 +322,7 @@ export interface ProductApi {
|
|
|
298
322
|
planDeployment(input: {
|
|
299
323
|
readonly projectId: string;
|
|
300
324
|
readonly commitSha: string;
|
|
325
|
+
readonly environment: "dev" | "prod";
|
|
301
326
|
}): Promise<DeploymentPlan>;
|
|
302
327
|
createDeployment(input: {
|
|
303
328
|
readonly projectId: string;
|
|
@@ -736,6 +761,35 @@ export class GeneratedSdkProductApi implements ProductApi {
|
|
|
736
761
|
);
|
|
737
762
|
}
|
|
738
763
|
|
|
764
|
+
public async ensureDevShareLink(projectId: string) {
|
|
765
|
+
return ensureProjectDevShareLink({ project_id: projectId }, { client: this.#client });
|
|
766
|
+
}
|
|
767
|
+
public async rotateDevShareLink(projectId: string, idempotencyKey: string) {
|
|
768
|
+
return rotateProjectDevShareLink(
|
|
769
|
+
{ project_id: projectId, "Idempotency-Key": idempotencyKey },
|
|
770
|
+
{ client: this.#client },
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
public async revokeDevShareLink(projectId: string, idempotencyKey: string) {
|
|
774
|
+
return revokeProjectDevShareLink(
|
|
775
|
+
{ project_id: projectId, "Idempotency-Key": idempotencyKey },
|
|
776
|
+
{ client: this.#client },
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
public async setDevAccessMode(
|
|
780
|
+
projectId: string,
|
|
781
|
+
mode: "protected" | "public",
|
|
782
|
+
idempotencyKey: string,
|
|
783
|
+
) {
|
|
784
|
+
return setProjectDevAccessMode(
|
|
785
|
+
{
|
|
786
|
+
project_id: projectId,
|
|
787
|
+
"Idempotency-Key": idempotencyKey,
|
|
788
|
+
setDevAccessModeRequest: { mode },
|
|
789
|
+
},
|
|
790
|
+
{ client: this.#client },
|
|
791
|
+
);
|
|
792
|
+
}
|
|
739
793
|
public async createDevAccessTicket(projectId: string): Promise<DevAccessTicket> {
|
|
740
794
|
return createProjectDevAccessTicket({ project_id: projectId }, { client: this.#client });
|
|
741
795
|
}
|
|
@@ -752,6 +806,18 @@ export class GeneratedSdkProductApi implements ProductApi {
|
|
|
752
806
|
);
|
|
753
807
|
}
|
|
754
808
|
|
|
809
|
+
public async getFeedback(
|
|
810
|
+
feedbackId: string,
|
|
811
|
+
cursor?: string,
|
|
812
|
+
): Promise<import("@ohmyhost/sdk-ts").FeedbackStatus> {
|
|
813
|
+
return parseFeedbackStatus(
|
|
814
|
+
await getFeedback(
|
|
815
|
+
{ feedback_id: feedbackId, ...(cursor === undefined ? {} : { cursor }) },
|
|
816
|
+
{ client: this.#client },
|
|
817
|
+
),
|
|
818
|
+
) as import("@ohmyhost/sdk-ts").FeedbackStatus;
|
|
819
|
+
}
|
|
820
|
+
|
|
755
821
|
public async createProject(input: ProjectCreateInput): Promise<Operation> {
|
|
756
822
|
return createProject(
|
|
757
823
|
{
|
|
@@ -760,6 +826,7 @@ export class GeneratedSdkProductApi implements ProductApi {
|
|
|
760
826
|
organization_id: input.organizationId,
|
|
761
827
|
name: input.name,
|
|
762
828
|
...(input.dataMode === undefined ? {} : { data_mode: input.dataMode }),
|
|
829
|
+
...(input.devAccessMode === undefined ? {} : { dev_access_mode: input.devAccessMode }),
|
|
763
830
|
...(input.region === undefined ? {} : { region: input.region }),
|
|
764
831
|
},
|
|
765
832
|
},
|
|
@@ -880,9 +947,13 @@ export class GeneratedSdkProductApi implements ProductApi {
|
|
|
880
947
|
public async planDeployment(input: {
|
|
881
948
|
readonly projectId: string;
|
|
882
949
|
readonly commitSha: string;
|
|
950
|
+
readonly environment: "dev" | "prod";
|
|
883
951
|
}): Promise<DeploymentPlan> {
|
|
884
952
|
return planDeployment(
|
|
885
|
-
{
|
|
953
|
+
{
|
|
954
|
+
project_id: input.projectId,
|
|
955
|
+
planDeploymentRequest: { commit_sha: input.commitSha, environment: input.environment },
|
|
956
|
+
},
|
|
886
957
|
{ client: this.#client },
|
|
887
958
|
);
|
|
888
959
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
1
|
+
import { appendFile, chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
export interface ProjectLinkMetadata {
|
|
@@ -14,8 +14,32 @@ export interface ProjectLinkStore {
|
|
|
14
14
|
save(metadata: ProjectLinkMetadata): Promise<void>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** The project this directory was linked to for this API, or null when there is no usable link. */
|
|
18
|
+
export async function readLinkedProjectId(
|
|
19
|
+
directory: string,
|
|
20
|
+
apiOrigin: string,
|
|
21
|
+
): Promise<string | null> {
|
|
22
|
+
try {
|
|
23
|
+
const link = JSON.parse(await readFile(join(directory, "link.json"), "utf8")) as unknown;
|
|
24
|
+
if (typeof link !== "object" || link === null) return null;
|
|
25
|
+
const { version, api_origin, project_id } = link as Record<string, unknown>;
|
|
26
|
+
return version === 1 &&
|
|
27
|
+
api_origin === apiOrigin &&
|
|
28
|
+
typeof project_id === "string" &&
|
|
29
|
+
/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(project_id)
|
|
30
|
+
? project_id
|
|
31
|
+
: null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
export class OwnerOnlyProjectLinkStore implements ProjectLinkStore {
|
|
18
|
-
|
|
38
|
+
/** repositoryRoot: when it is a Git working tree, the link directory is kept out of Git. */
|
|
39
|
+
public constructor(
|
|
40
|
+
private readonly directory: string,
|
|
41
|
+
private readonly repositoryRoot?: string,
|
|
42
|
+
) {}
|
|
19
43
|
|
|
20
44
|
public async save(metadata: ProjectLinkMetadata): Promise<void> {
|
|
21
45
|
await mkdir(this.directory, { mode: 0o700, recursive: true });
|
|
@@ -26,5 +50,24 @@ export class OwnerOnlyProjectLinkStore implements ProjectLinkStore {
|
|
|
26
50
|
await chmod(temporary, 0o600);
|
|
27
51
|
await rename(temporary, target);
|
|
28
52
|
await chmod(target, 0o600);
|
|
53
|
+
if (this.repositoryRoot !== undefined) await ignoreLinkDirectory(this.repositoryRoot);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function ignoreLinkDirectory(root: string): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
await stat(join(root, ".git"));
|
|
60
|
+
} catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const file = join(root, ".gitignore");
|
|
64
|
+
let current = "";
|
|
65
|
+
try {
|
|
66
|
+
current = await readFile(file, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
current = "";
|
|
29
69
|
}
|
|
70
|
+
const lines = current.split(/\r?\n/u).map((line) => line.trim());
|
|
71
|
+
if (lines.some((line) => /^\/?\.ohmyhost\/?$/u.test(line))) return;
|
|
72
|
+
await appendFile(file, `${current === "" || current.endsWith("\n") ? "" : "\n"}/.ohmyhost/\n`);
|
|
30
73
|
}
|
|
@@ -73,9 +73,9 @@ const STORAGE_RUNTIME_BINDINGS = Object.freeze([
|
|
|
73
73
|
"OHMYHOST_STORAGE_GATEWAY_URL",
|
|
74
74
|
"OHMYHOST_STORAGE_KEY",
|
|
75
75
|
]);
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
const CUSTOMER_RUNTIME_SOURCE = `
|
|
76
|
+
// The runtime release is published unchanged as `@amerged/ohmyhost-runtime`; this pinned alias keeps
|
|
77
|
+
// its import name and, unlike the website archive each release removes, stays installable.
|
|
78
|
+
const CUSTOMER_RUNTIME_SOURCE = `npm:@amerged/ohmyhost-runtime@${cliPackage.version}`;
|
|
79
79
|
const SCHEDULED_HANDLER_PATTERN = /\bscheduled\s*(?:\(|:|,|\})/u;
|
|
80
80
|
// The platform imports the module's default export; handlers that are only named exports are
|
|
81
81
|
// never invoked, so init blocks them before a build or a runtime rejection would.
|
|
@@ -139,7 +139,7 @@ export interface RepositoryInitInventory {
|
|
|
139
139
|
|
|
140
140
|
export interface RepositoryInitRuntimePackages {
|
|
141
141
|
readonly betterAuth?: "1.7.1";
|
|
142
|
-
/** Exact install source:
|
|
142
|
+
/** Exact install source: an npm alias pinned to the published runtime of this client version. */
|
|
143
143
|
readonly customerRuntime?: string;
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -339,7 +339,8 @@ export async function initializeRepository(
|
|
|
339
339
|
}),
|
|
340
340
|
});
|
|
341
341
|
const targetBetterAuth = existingConfiguration?.auth?.provider === "better-auth";
|
|
342
|
-
|
|
342
|
+
// Mail stays an explicit choice: Better Auth or a detected mail package never enables it.
|
|
343
|
+
const targetMail = existingConfiguration?.mail?.enabled === true;
|
|
343
344
|
const targetStorage = r2 || supabaseStorage;
|
|
344
345
|
const configuredCompanionRequired =
|
|
345
346
|
existingConfiguration !== null &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFeedbackSubmissionDetail } from "@ohmyhost/contracts/feedback";
|
|
1
2
|
import { parseFrameworkConversionDetail } from "@ohmyhost/contracts/framework-admission";
|
|
2
3
|
import {
|
|
3
4
|
parseOperationDeploymentProgress,
|
|
@@ -21,12 +22,13 @@ import {
|
|
|
21
22
|
} from "@ohmyhost/contracts/operation-failure";
|
|
22
23
|
export type { PublicOperationFailure } from "@ohmyhost/contracts/operation-failure";
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
/** Exported for the documentation build, which publishes one page per code. */
|
|
26
|
+
export const PROBLEM_POLICY = {
|
|
25
27
|
cloudflare_zone_not_bound: {
|
|
26
28
|
status: 409,
|
|
27
29
|
retryable: false,
|
|
28
30
|
action:
|
|
29
|
-
"Configure the project domain first with 'ohmyhost domain paid plan' then 'ohmyhost domain paid apply', or configure its sender domain with 'ohmyhost mail
|
|
31
|
+
"Configure the project domain first with 'ohmyhost domain paid plan' then 'ohmyhost domain paid apply', or configure its sender domain with 'ohmyhost mail setup'. Then request Cloudflare authorization for that domain's zone.",
|
|
30
32
|
},
|
|
31
33
|
github_connection_required: {
|
|
32
34
|
status: 409,
|
|
@@ -197,6 +199,12 @@ const PROBLEM_POLICY = {
|
|
|
197
199
|
retryable: false,
|
|
198
200
|
action: "Select the current succeeded dev deployment and target prod.",
|
|
199
201
|
},
|
|
202
|
+
mail_capacity_unavailable: {
|
|
203
|
+
status: 503,
|
|
204
|
+
retryable: true,
|
|
205
|
+
action:
|
|
206
|
+
"Report this request ID through feedback. The stored mail setup continues automatically once ohmyho.st has capacity; follow mail status and keep the original idempotency key.",
|
|
207
|
+
},
|
|
200
208
|
mail_domain_conflict: {
|
|
201
209
|
status: 409,
|
|
202
210
|
retryable: false,
|
|
@@ -205,7 +213,8 @@ const PROBLEM_POLICY = {
|
|
|
205
213
|
mail_domain_required: {
|
|
206
214
|
status: 409,
|
|
207
215
|
retryable: false,
|
|
208
|
-
action:
|
|
216
|
+
action:
|
|
217
|
+
"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.",
|
|
209
218
|
},
|
|
210
219
|
storage_jurisdiction_conflict: {
|
|
211
220
|
status: 409,
|
|
@@ -213,6 +222,18 @@ const PROBLEM_POLICY = {
|
|
|
213
222
|
action:
|
|
214
223
|
"Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status, then plan again.",
|
|
215
224
|
},
|
|
225
|
+
shared_data_requires_promotion: {
|
|
226
|
+
status: 409,
|
|
227
|
+
retryable: false,
|
|
228
|
+
action:
|
|
229
|
+
"Plan and deploy this commit to Dev, then run 'ohmyhost deployment promote plan' to reach Prod.",
|
|
230
|
+
},
|
|
231
|
+
production_deployment_required: {
|
|
232
|
+
status: 409,
|
|
233
|
+
retryable: false,
|
|
234
|
+
action:
|
|
235
|
+
"Deploy to Prod first: run 'ohmyhost plan --environment prod' and deploy that plan, or promote the current Dev deployment with 'ohmyhost deployment promote plan'. Then plan the domain again.",
|
|
236
|
+
},
|
|
216
237
|
framework_conversion_required: {
|
|
217
238
|
status: 409,
|
|
218
239
|
retryable: false,
|
|
@@ -308,6 +329,8 @@ export class ResponseContractError extends Error {
|
|
|
308
329
|
|
|
309
330
|
export interface SafeProblem {
|
|
310
331
|
readonly requestId: string;
|
|
332
|
+
/** The error's documentation page, when the server names one on docs.ohmyho.st. */
|
|
333
|
+
readonly docsUrl?: string;
|
|
311
334
|
readonly retryAfterSeconds?: number;
|
|
312
335
|
readonly code: ProblemCode;
|
|
313
336
|
readonly retryable: boolean;
|
|
@@ -690,6 +713,28 @@ export const parseCloudflareDnsAuthorizationStatus = (
|
|
|
690
713
|
return input as unknown as PublicCloudflareDnsAuthorizationStatus;
|
|
691
714
|
};
|
|
692
715
|
|
|
716
|
+
export interface PublicDevAccessState {
|
|
717
|
+
readonly project_id: string;
|
|
718
|
+
readonly mode: "protected" | "public";
|
|
719
|
+
readonly share_url: string | null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export const parseDevAccessState = (value: unknown): PublicDevAccessState => {
|
|
723
|
+
const input = exactRecord(value, ["project_id", "mode", "share_url"]);
|
|
724
|
+
if (
|
|
725
|
+
!isUlid(input["project_id"]) ||
|
|
726
|
+
(input["mode"] !== "protected" && input["mode"] !== "public") ||
|
|
727
|
+
(input["mode"] === "public" && input["share_url"] !== null) ||
|
|
728
|
+
(input["share_url"] !== null &&
|
|
729
|
+
(typeof input["share_url"] !== "string" ||
|
|
730
|
+
!/^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(
|
|
731
|
+
input["share_url"],
|
|
732
|
+
)))
|
|
733
|
+
)
|
|
734
|
+
throw new ResponseContractError();
|
|
735
|
+
return input as unknown as PublicDevAccessState;
|
|
736
|
+
};
|
|
737
|
+
|
|
693
738
|
export const parseDevAccessTicket = (value: unknown): PublicDevAccessTicket => {
|
|
694
739
|
const input = exactRecord(value, [
|
|
695
740
|
"ticket_id",
|
|
@@ -943,6 +988,7 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
|
|
|
943
988
|
"etag",
|
|
944
989
|
"handle",
|
|
945
990
|
"region",
|
|
991
|
+
"dev_access_mode",
|
|
946
992
|
"lifecycle",
|
|
947
993
|
"source",
|
|
948
994
|
"default_environment",
|
|
@@ -961,6 +1007,7 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
|
|
|
961
1007
|
!boundedString(handle, 3, 59) ||
|
|
962
1008
|
!/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(handle) ||
|
|
963
1009
|
(input["region"] !== "us" && input["region"] !== "eu") ||
|
|
1010
|
+
(input["dev_access_mode"] !== "protected" && input["dev_access_mode"] !== "public") ||
|
|
964
1011
|
(input["lifecycle"] !== "active" &&
|
|
965
1012
|
input["lifecycle"] !== "deleting" &&
|
|
966
1013
|
input["lifecycle"] !== "deleted") ||
|
|
@@ -1308,6 +1355,7 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
|
|
|
1308
1355
|
const input = exactRecord(value, [
|
|
1309
1356
|
"id",
|
|
1310
1357
|
"project_id",
|
|
1358
|
+
"environment",
|
|
1311
1359
|
"commit_sha",
|
|
1312
1360
|
"source_digest",
|
|
1313
1361
|
"build_plan_digest",
|
|
@@ -1329,6 +1377,7 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
|
|
|
1329
1377
|
"currency",
|
|
1330
1378
|
"rate_card_version",
|
|
1331
1379
|
"credit_micros",
|
|
1380
|
+
"credits",
|
|
1332
1381
|
"provider_cost_micros",
|
|
1333
1382
|
"scope",
|
|
1334
1383
|
"reserved_seconds",
|
|
@@ -1336,6 +1385,7 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
|
|
|
1336
1385
|
if (
|
|
1337
1386
|
!isUlid(input["id"]) ||
|
|
1338
1387
|
!isUlid(input["project_id"]) ||
|
|
1388
|
+
(input["environment"] !== "dev" && input["environment"] !== "prod") ||
|
|
1339
1389
|
!isCommitSha(input["commit_sha"]) ||
|
|
1340
1390
|
!isDigest(input["source_digest"]) ||
|
|
1341
1391
|
!isDigest(input["build_plan_digest"]) ||
|
|
@@ -1355,6 +1405,8 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
|
|
|
1355
1405
|
!boundedString(cost["currency"], 1, 16) ||
|
|
1356
1406
|
!boundedString(cost["rate_card_version"], 1, 128) ||
|
|
1357
1407
|
!decimal(cost["credit_micros"]) ||
|
|
1408
|
+
typeof cost["credits"] !== "string" ||
|
|
1409
|
+
!/^[0-9]+(?:\.[0-9]{1,6})?$/u.test(cost["credits"]) ||
|
|
1358
1410
|
!decimal(cost["provider_cost_micros"]) ||
|
|
1359
1411
|
cost["scope"] !== "build_compute" ||
|
|
1360
1412
|
!decimal(cost["reserved_seconds"]) ||
|
|
@@ -1497,14 +1549,18 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
|
|
|
1497
1549
|
retryAfter > 86400)
|
|
1498
1550
|
)
|
|
1499
1551
|
throw new ResponseContractError();
|
|
1552
|
+
const docsUrl = String(input["type"]);
|
|
1500
1553
|
return {
|
|
1501
1554
|
requestId: input["request_id"],
|
|
1555
|
+
...(docsUrl.startsWith("https://docs.ohmyho.st/errors/") ? { docsUrl } : {}),
|
|
1502
1556
|
code: code as ProblemCode,
|
|
1503
1557
|
retryable: policy.retryable,
|
|
1504
1558
|
suggestedAction:
|
|
1505
1559
|
(code === "framework_conversion_required"
|
|
1506
1560
|
? (parseFrameworkConversionDetail(input["detail"]) ?? policy.action)
|
|
1507
|
-
:
|
|
1561
|
+
: code === "invalid_request"
|
|
1562
|
+
? (parseFeedbackSubmissionDetail(input["detail"]) ?? policy.action)
|
|
1563
|
+
: policy.action) +
|
|
1508
1564
|
(retryAfter === undefined ? "" : ` Retry after ${String(retryAfter)} seconds.`),
|
|
1509
1565
|
...(retryAfter === undefined ? {} : { retryAfterSeconds: retryAfter as number }),
|
|
1510
1566
|
};
|