@amerged/ohmyhost-cli 0.1.15 → 0.1.17

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.
@@ -57,6 +57,10 @@ 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,
@@ -131,6 +135,7 @@ export interface ProjectCreateInput {
131
135
  readonly organizationId: string;
132
136
  readonly name: string;
133
137
  readonly dataMode?: "shared" | "isolated";
138
+ readonly devAccessMode?: "protected" | "public";
134
139
  readonly region?: "us" | "eu";
135
140
  readonly idempotencyKey: string;
136
141
  }
@@ -257,6 +262,20 @@ export interface ProductApi {
257
262
  readonly idempotencyKey: string;
258
263
  }): Promise<PaidDomain>;
259
264
  createDevAccessTicket(projectId: string): Promise<DevAccessTicket>;
265
+ ensureDevShareLink(projectId: string): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
266
+ rotateDevShareLink(
267
+ projectId: string,
268
+ idempotencyKey: string,
269
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
270
+ revokeDevShareLink(
271
+ projectId: string,
272
+ idempotencyKey: string,
273
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
274
+ setDevAccessMode(
275
+ projectId: string,
276
+ mode: "protected" | "public",
277
+ idempotencyKey: string,
278
+ ): Promise<import("@ohmyhost/sdk-ts").DevAccessState>;
260
279
  submitFeedback(input: {
261
280
  report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
262
281
  idempotencyKey: string;
@@ -298,6 +317,7 @@ export interface ProductApi {
298
317
  planDeployment(input: {
299
318
  readonly projectId: string;
300
319
  readonly commitSha: string;
320
+ readonly environment: "dev" | "prod";
301
321
  }): Promise<DeploymentPlan>;
302
322
  createDeployment(input: {
303
323
  readonly projectId: string;
@@ -736,6 +756,35 @@ export class GeneratedSdkProductApi implements ProductApi {
736
756
  );
737
757
  }
738
758
 
759
+ public async ensureDevShareLink(projectId: string) {
760
+ return ensureProjectDevShareLink({ project_id: projectId }, { client: this.#client });
761
+ }
762
+ public async rotateDevShareLink(projectId: string, idempotencyKey: string) {
763
+ return rotateProjectDevShareLink(
764
+ { project_id: projectId, "Idempotency-Key": idempotencyKey },
765
+ { client: this.#client },
766
+ );
767
+ }
768
+ public async revokeDevShareLink(projectId: string, idempotencyKey: string) {
769
+ return revokeProjectDevShareLink(
770
+ { project_id: projectId, "Idempotency-Key": idempotencyKey },
771
+ { client: this.#client },
772
+ );
773
+ }
774
+ public async setDevAccessMode(
775
+ projectId: string,
776
+ mode: "protected" | "public",
777
+ idempotencyKey: string,
778
+ ) {
779
+ return setProjectDevAccessMode(
780
+ {
781
+ project_id: projectId,
782
+ "Idempotency-Key": idempotencyKey,
783
+ setDevAccessModeRequest: { mode },
784
+ },
785
+ { client: this.#client },
786
+ );
787
+ }
739
788
  public async createDevAccessTicket(projectId: string): Promise<DevAccessTicket> {
740
789
  return createProjectDevAccessTicket({ project_id: projectId }, { client: this.#client });
741
790
  }
@@ -760,6 +809,7 @@ export class GeneratedSdkProductApi implements ProductApi {
760
809
  organization_id: input.organizationId,
761
810
  name: input.name,
762
811
  ...(input.dataMode === undefined ? {} : { data_mode: input.dataMode }),
812
+ ...(input.devAccessMode === undefined ? {} : { dev_access_mode: input.devAccessMode }),
763
813
  ...(input.region === undefined ? {} : { region: input.region }),
764
814
  },
765
815
  },
@@ -880,9 +930,13 @@ export class GeneratedSdkProductApi implements ProductApi {
880
930
  public async planDeployment(input: {
881
931
  readonly projectId: string;
882
932
  readonly commitSha: string;
933
+ readonly environment: "dev" | "prod";
883
934
  }): Promise<DeploymentPlan> {
884
935
  return planDeployment(
885
- { project_id: input.projectId, planDeploymentRequest: { commit_sha: input.commitSha } },
936
+ {
937
+ project_id: input.projectId,
938
+ planDeploymentRequest: { commit_sha: input.commitSha, environment: input.environment },
939
+ },
886
940
  { client: this.#client },
887
941
  );
888
942
  }
@@ -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
- public constructor(private readonly directory: string) {}
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
  }
@@ -339,7 +339,8 @@ export async function initializeRepository(
339
339
  }),
340
340
  });
341
341
  const targetBetterAuth = existingConfiguration?.auth?.provider === "better-auth";
342
- const targetMail = targetBetterAuth || existingConfiguration?.mail?.enabled === true;
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 &&
@@ -21,12 +21,13 @@ import {
21
21
  } from "@ohmyhost/contracts/operation-failure";
22
22
  export type { PublicOperationFailure } from "@ohmyhost/contracts/operation-failure";
23
23
 
24
- const PROBLEM_POLICY = {
24
+ /** Exported for the documentation build, which publishes one page per code. */
25
+ export const PROBLEM_POLICY = {
25
26
  cloudflare_zone_not_bound: {
26
27
  status: 409,
27
28
  retryable: false,
28
29
  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 domain set'. Then request Cloudflare authorization for that domain's zone.",
30
+ "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
31
  },
31
32
  github_connection_required: {
32
33
  status: 409,
@@ -197,6 +198,12 @@ const PROBLEM_POLICY = {
197
198
  retryable: false,
198
199
  action: "Select the current succeeded dev deployment and target prod.",
199
200
  },
201
+ mail_capacity_unavailable: {
202
+ status: 503,
203
+ retryable: true,
204
+ action:
205
+ "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.",
206
+ },
200
207
  mail_domain_conflict: {
201
208
  status: 409,
202
209
  retryable: false,
@@ -205,7 +212,8 @@ const PROBLEM_POLICY = {
205
212
  mail_domain_required: {
206
213
  status: 409,
207
214
  retryable: false,
208
- action: "Run 'ohmyhost mail domain set' for this project, then plan again.",
215
+ action:
216
+ "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
217
  },
210
218
  storage_jurisdiction_conflict: {
211
219
  status: 409,
@@ -213,6 +221,18 @@ const PROBLEM_POLICY = {
213
221
  action:
214
222
  "Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status, then plan again.",
215
223
  },
224
+ shared_data_requires_promotion: {
225
+ status: 409,
226
+ retryable: false,
227
+ action:
228
+ "Plan and deploy this commit to Dev, then run 'ohmyhost deployment promote plan' to reach Prod.",
229
+ },
230
+ production_deployment_required: {
231
+ status: 409,
232
+ retryable: false,
233
+ action:
234
+ "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.",
235
+ },
216
236
  framework_conversion_required: {
217
237
  status: 409,
218
238
  retryable: false,
@@ -308,6 +328,8 @@ export class ResponseContractError extends Error {
308
328
 
309
329
  export interface SafeProblem {
310
330
  readonly requestId: string;
331
+ /** The error's documentation page, when the server names one on docs.ohmyho.st. */
332
+ readonly docsUrl?: string;
311
333
  readonly retryAfterSeconds?: number;
312
334
  readonly code: ProblemCode;
313
335
  readonly retryable: boolean;
@@ -690,6 +712,28 @@ export const parseCloudflareDnsAuthorizationStatus = (
690
712
  return input as unknown as PublicCloudflareDnsAuthorizationStatus;
691
713
  };
692
714
 
715
+ export interface PublicDevAccessState {
716
+ readonly project_id: string;
717
+ readonly mode: "protected" | "public";
718
+ readonly share_url: string | null;
719
+ }
720
+
721
+ export const parseDevAccessState = (value: unknown): PublicDevAccessState => {
722
+ const input = exactRecord(value, ["project_id", "mode", "share_url"]);
723
+ if (
724
+ !isUlid(input["project_id"]) ||
725
+ (input["mode"] !== "protected" && input["mode"] !== "public") ||
726
+ (input["mode"] === "public" && input["share_url"] !== null) ||
727
+ (input["share_url"] !== null &&
728
+ (typeof input["share_url"] !== "string" ||
729
+ !/^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(
730
+ input["share_url"],
731
+ )))
732
+ )
733
+ throw new ResponseContractError();
734
+ return input as unknown as PublicDevAccessState;
735
+ };
736
+
693
737
  export const parseDevAccessTicket = (value: unknown): PublicDevAccessTicket => {
694
738
  const input = exactRecord(value, [
695
739
  "ticket_id",
@@ -943,6 +987,7 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
943
987
  "etag",
944
988
  "handle",
945
989
  "region",
990
+ "dev_access_mode",
946
991
  "lifecycle",
947
992
  "source",
948
993
  "default_environment",
@@ -961,6 +1006,7 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
961
1006
  !boundedString(handle, 3, 59) ||
962
1007
  !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(handle) ||
963
1008
  (input["region"] !== "us" && input["region"] !== "eu") ||
1009
+ (input["dev_access_mode"] !== "protected" && input["dev_access_mode"] !== "public") ||
964
1010
  (input["lifecycle"] !== "active" &&
965
1011
  input["lifecycle"] !== "deleting" &&
966
1012
  input["lifecycle"] !== "deleted") ||
@@ -1308,6 +1354,7 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
1308
1354
  const input = exactRecord(value, [
1309
1355
  "id",
1310
1356
  "project_id",
1357
+ "environment",
1311
1358
  "commit_sha",
1312
1359
  "source_digest",
1313
1360
  "build_plan_digest",
@@ -1329,6 +1376,7 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
1329
1376
  "currency",
1330
1377
  "rate_card_version",
1331
1378
  "credit_micros",
1379
+ "credits",
1332
1380
  "provider_cost_micros",
1333
1381
  "scope",
1334
1382
  "reserved_seconds",
@@ -1336,6 +1384,7 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
1336
1384
  if (
1337
1385
  !isUlid(input["id"]) ||
1338
1386
  !isUlid(input["project_id"]) ||
1387
+ (input["environment"] !== "dev" && input["environment"] !== "prod") ||
1339
1388
  !isCommitSha(input["commit_sha"]) ||
1340
1389
  !isDigest(input["source_digest"]) ||
1341
1390
  !isDigest(input["build_plan_digest"]) ||
@@ -1355,6 +1404,8 @@ export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unk
1355
1404
  !boundedString(cost["currency"], 1, 16) ||
1356
1405
  !boundedString(cost["rate_card_version"], 1, 128) ||
1357
1406
  !decimal(cost["credit_micros"]) ||
1407
+ typeof cost["credits"] !== "string" ||
1408
+ !/^[0-9]+(?:\.[0-9]{1,6})?$/u.test(cost["credits"]) ||
1358
1409
  !decimal(cost["provider_cost_micros"]) ||
1359
1410
  cost["scope"] !== "build_compute" ||
1360
1411
  !decimal(cost["reserved_seconds"]) ||
@@ -1497,8 +1548,10 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1497
1548
  retryAfter > 86400)
1498
1549
  )
1499
1550
  throw new ResponseContractError();
1551
+ const docsUrl = String(input["type"]);
1500
1552
  return {
1501
1553
  requestId: input["request_id"],
1554
+ ...(docsUrl.startsWith("https://docs.ohmyho.st/errors/") ? { docsUrl } : {}),
1502
1555
  code: code as ProblemCode,
1503
1556
  retryable: policy.retryable,
1504
1557
  suggestedAction:
@@ -22,9 +22,11 @@ export {
22
22
  createProjectExport,
23
23
  createUserApiKey,
24
24
  deleteEnvironmentSecret,
25
+ deleteManagedMailDomain,
25
26
  deletePaidProjectDomain,
26
27
  deleteProject,
27
28
  disableManagedMailReceiving,
29
+ ensureProjectDevShareLink,
28
30
  getAccountProfile,
29
31
  getBillingCheckout,
30
32
  getBillingRecharge,
@@ -77,9 +79,12 @@ export {
77
79
  retryManagedMailMessage,
78
80
  revokeCurrentSession,
79
81
  revokeProjectDatabaseAccess,
82
+ revokeProjectDevShareLink,
80
83
  revokeUserApiKey,
81
84
  rollbackDeployment,
85
+ rotateProjectDevShareLink,
82
86
  setProjectCreditBudget,
87
+ setProjectDevAccessMode,
83
88
  setProjectNotes,
84
89
  streamDeploymentLogs,
85
90
  streamOperationEvents,
@@ -220,6 +225,11 @@ export type {
220
225
  DeleteEnvironmentSecretResponse,
221
226
  DeleteEnvironmentSecretResponses,
222
227
  DeleteEnvironmentSecretResult,
228
+ DeleteManagedMailDomainData,
229
+ DeleteManagedMailDomainError,
230
+ DeleteManagedMailDomainErrors,
231
+ DeleteManagedMailDomainResponse,
232
+ DeleteManagedMailDomainResponses,
223
233
  DeletePaidProjectDomainData,
224
234
  DeletePaidProjectDomainError,
225
235
  DeletePaidProjectDomainErrors,
@@ -239,12 +249,18 @@ export type {
239
249
  DeploymentPage,
240
250
  DeploymentPlan,
241
251
  DeploymentPlanWritable,
252
+ DevAccessState,
242
253
  DevAccessTicket,
243
254
  DisableManagedMailReceivingData,
244
255
  DisableManagedMailReceivingError,
245
256
  DisableManagedMailReceivingErrors,
246
257
  DisableManagedMailReceivingResponse,
247
258
  DisableManagedMailReceivingResponses,
259
+ EnsureProjectDevShareLinkData,
260
+ EnsureProjectDevShareLinkError,
261
+ EnsureProjectDevShareLinkErrors,
262
+ EnsureProjectDevShareLinkResponse,
263
+ EnsureProjectDevShareLinkResponses,
248
264
  EnvironmentId,
249
265
  EnvironmentSecret,
250
266
  EnvironmentSecretPage,
@@ -450,6 +466,7 @@ export type {
450
466
  ManagedMailDisabled,
451
467
  ManagedMailDnsRecord,
452
468
  ManagedMailDomain,
469
+ ManagedMailDomainDeleted,
453
470
  ManagedMailDomainRequest,
454
471
  ManagedMailExpired,
455
472
  ManagedMailMessagePage,
@@ -575,6 +592,11 @@ export type {
575
592
  RevokeProjectDatabaseAccessErrors,
576
593
  RevokeProjectDatabaseAccessResponse,
577
594
  RevokeProjectDatabaseAccessResponses,
595
+ RevokeProjectDevShareLinkData,
596
+ RevokeProjectDevShareLinkError,
597
+ RevokeProjectDevShareLinkErrors,
598
+ RevokeProjectDevShareLinkResponse,
599
+ RevokeProjectDevShareLinkResponses,
578
600
  RevokeUserApiKeyData,
579
601
  RevokeUserApiKeyError,
580
602
  RevokeUserApiKeyErrors,
@@ -585,13 +607,24 @@ export type {
585
607
  RollbackDeploymentErrors,
586
608
  RollbackDeploymentResponse,
587
609
  RollbackDeploymentResponses,
610
+ RotateProjectDevShareLinkData,
611
+ RotateProjectDevShareLinkError,
612
+ RotateProjectDevShareLinkErrors,
613
+ RotateProjectDevShareLinkResponse,
614
+ RotateProjectDevShareLinkResponses,
588
615
  SecretName,
616
+ SetDevAccessModeRequest,
589
617
  SetProjectCreditBudgetData,
590
618
  SetProjectCreditBudgetError,
591
619
  SetProjectCreditBudgetErrors,
592
620
  SetProjectCreditBudgetRequest,
593
621
  SetProjectCreditBudgetResponse,
594
622
  SetProjectCreditBudgetResponses,
623
+ SetProjectDevAccessModeData,
624
+ SetProjectDevAccessModeError,
625
+ SetProjectDevAccessModeErrors,
626
+ SetProjectDevAccessModeResponse,
627
+ SetProjectDevAccessModeResponses,
595
628
  SetProjectNotesData,
596
629
  SetProjectNotesError,
597
630
  SetProjectNotesErrors,