@amerged/ohmyhost-cli 0.1.23 → 0.1.25
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 +3 -2
- package/dist/{chunk-74DROG4F.js → chunk-YOEEWHSQ.js} +985 -304
- package/dist/index.js +13 -1
- package/package.json +1 -1
- package/src/apps/product-cli/package.json +45 -0
- package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
- package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
- package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
- package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
- package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
- package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
- package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +90 -36
- package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
- package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
- package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
- package/src/packages/contracts/package.json +137 -0
- package/src/packages/contracts/src/browser-capabilities.ts +133 -0
- package/src/packages/contracts/src/customer-auth-admission.ts +28 -0
- package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
- package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +76 -13
- package/src/packages/contracts/src/framework-build-script.mjs +22 -0
- package/src/packages/contracts/src/framework-build-script.ts +1 -0
- package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
- package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
- package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
- package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
- package/src/packages/contracts/src/problem-policy.ts +498 -0
- package/src/packages/contracts/src/project-data-changes.ts +90 -0
- package/src/packages/contracts/src/secret-names.ts +27 -0
- package/src/packages/sdk-ts/package.json +21 -0
- package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
- package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +113 -4
- package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +201 -10
- package/src/packages/workos-auth-contracts/package.json +39 -0
- /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
- /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
- /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
- /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
- /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
- /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
- /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
- /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
- /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
- /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
- /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
- /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
- /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
- /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
- /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
- /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
- /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
- /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
- /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
- /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
- /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/** Canonical public status, retry policy and recovery text shared by the API and clients. */
|
|
2
|
+
export const PROBLEM_POLICY = {
|
|
3
|
+
cloudflare_zone_not_bound: {
|
|
4
|
+
status: 409,
|
|
5
|
+
retryable: false,
|
|
6
|
+
action:
|
|
7
|
+
"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.",
|
|
8
|
+
title: "Cloudflare zone is not bound to this project",
|
|
9
|
+
},
|
|
10
|
+
github_connection_required: {
|
|
11
|
+
status: 409,
|
|
12
|
+
retryable: false,
|
|
13
|
+
action:
|
|
14
|
+
"This workspace has no GitHub connection yet. Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' for it, complete its browser link, then repeat the original source link. If the project belongs to another account, use that account's login instead; error.acting_as in the CLI and MCP names the login this ran as.",
|
|
15
|
+
title: "GitHub connection required",
|
|
16
|
+
},
|
|
17
|
+
github_connection_revoked: {
|
|
18
|
+
status: 409,
|
|
19
|
+
retryable: false,
|
|
20
|
+
action:
|
|
21
|
+
"The workspace's GitHub App installation was removed or can no longer be used. Run 'ohmyhost github connect --organization ULID --idempotency-key NEW_KEY --json' with a new key (an earlier key only replays the old connection), send the customer its authorization_url, repeat that command with the same new key until status is connected, then repeat the original source link.",
|
|
22
|
+
title: "GitHub connection revoked",
|
|
23
|
+
restAction:
|
|
24
|
+
"The workspace's GitHub App installation was removed or can no longer be used. Run 'ohmyhost github connect --organization ULID --idempotency-key NEW_KEY --json' with a new key (an earlier key only replays the old connection), send the customer its authorization_url, repeat that command with the same new key until status is connected, then repeat the original source link.",
|
|
25
|
+
},
|
|
26
|
+
repository_not_installed: {
|
|
27
|
+
status: 403,
|
|
28
|
+
retryable: false,
|
|
29
|
+
action:
|
|
30
|
+
"Read 'ohmyhost github status --organization ULID --json', open connection.settings_url and add this repository to the installation. Then repeat the original source link and key.",
|
|
31
|
+
title: "Repository access required",
|
|
32
|
+
},
|
|
33
|
+
database_write_pending: {
|
|
34
|
+
status: 409,
|
|
35
|
+
retryable: false,
|
|
36
|
+
action:
|
|
37
|
+
"A database write of this project is still running, so the project cannot be deleted yet. Read latest_operation from 'ohmyhost project status --project ULID --json' until it is no longer running; writes are short. Then repeat the deletion, planning it again if its confirmation expired.",
|
|
38
|
+
title: "Database write in progress",
|
|
39
|
+
restAction:
|
|
40
|
+
"A database write of this project is still running, so the project cannot be deleted yet. Read latest_operation from 'ohmyhost project status --project ULID --json' until it is no longer running; writes are short. Then repeat the deletion, planning it again if its confirmation expired.",
|
|
41
|
+
},
|
|
42
|
+
cloudflare_authorization_closed: {
|
|
43
|
+
status: 409,
|
|
44
|
+
retryable: false,
|
|
45
|
+
action:
|
|
46
|
+
"Read Cloudflare DNS status. Reuse a valid grant for the requested zone; otherwise request fresh authorization with a new idempotency key. Do not replay the callback.",
|
|
47
|
+
title: "Cloudflare authorization closed",
|
|
48
|
+
},
|
|
49
|
+
billing_recharge_conflict: {
|
|
50
|
+
status: 409,
|
|
51
|
+
retryable: false,
|
|
52
|
+
action:
|
|
53
|
+
"The auto-recharge settings changed since you read them, or this key was used with other settings. Run 'ohmyhost billing recharge get --organization ULID --json'. If it already shows the requested change, stop. Otherwise, only if the Owner still wants it, send the change again with the returned revision and a new idempotency key. Never re-enable charges without the Owner's approval.",
|
|
54
|
+
title: "Auto-recharge settings conflict",
|
|
55
|
+
restAction:
|
|
56
|
+
"The auto-recharge settings changed since you read them, or this key was used with other settings. Run 'ohmyhost billing recharge get --organization ULID --json'. If it already shows the requested change, stop. Otherwise, only if the Owner still wants it, send the change again with the returned revision and a new idempotency key. Never re-enable charges without the Owner's approval.",
|
|
57
|
+
},
|
|
58
|
+
billing_purchase_conflict: {
|
|
59
|
+
status: 409,
|
|
60
|
+
retryable: false,
|
|
61
|
+
action:
|
|
62
|
+
"This workspace already has Paid or an open Paid checkout, this key belongs to a checkout with a different offer or pack count, or, for 'billing portal', the workspace has never started a checkout. Read the original checkout with 'ohmyhost billing status --organization ULID --checkout CHECKOUT_ULID --json'; manage an existing Paid subscription with 'ohmyhost billing portal'. Never start a second purchase after an uncertain one.",
|
|
63
|
+
title: "Billing purchase conflict",
|
|
64
|
+
restAction:
|
|
65
|
+
"This workspace already has Paid or an open Paid checkout, this key belongs to a checkout with a different offer or pack count, or, for 'billing portal', the workspace has never started a checkout. Read the original checkout with 'ohmyhost billing status --organization ULID --checkout CHECKOUT_ULID --json'; manage an existing Paid subscription with 'ohmyhost billing portal'. Never start a second purchase after an uncertain one.",
|
|
66
|
+
},
|
|
67
|
+
compute_performance_paid_required: {
|
|
68
|
+
status: 403,
|
|
69
|
+
retryable: false,
|
|
70
|
+
action:
|
|
71
|
+
"Performance compute needs active Paid and usable credits or grace; repeating unchanged does not help. Read 'ohmyhost credits account --organization ULID --json'. On Free, the Owner may start Paid with 'ohmyhost billing checkout --offer paid'; without usable credits, they may add credits. Until then, keep standard compute.",
|
|
72
|
+
title: "Performance requires Paid access",
|
|
73
|
+
restAction:
|
|
74
|
+
"Performance compute needs active Paid and usable credits or grace; repeating unchanged does not help. Read 'ohmyhost credits account --organization ULID --json'. On Free, the Owner may start Paid with 'ohmyhost billing checkout --offer paid'; without usable credits, they may add credits. Until then, keep standard compute.",
|
|
75
|
+
},
|
|
76
|
+
compute_performance_unavailable: {
|
|
77
|
+
status: 503,
|
|
78
|
+
retryable: false,
|
|
79
|
+
action:
|
|
80
|
+
"Choose standard compute or report inactive performance pricing through feedback. Do not repeatedly resubmit.",
|
|
81
|
+
title: "Performance pricing unavailable",
|
|
82
|
+
},
|
|
83
|
+
compute_change_pending: {
|
|
84
|
+
status: 409,
|
|
85
|
+
retryable: false,
|
|
86
|
+
action:
|
|
87
|
+
"A database size change of this project is still running. Every 60 seconds read latest_operation from 'ohmyhost project status --project ULID --json', or 'ohmyhost operation get OPERATION_ID --json' for the change you started, until it ends, then repeat this request. Do not submit another size change or a deletion meanwhile.",
|
|
88
|
+
title: "Database size change running",
|
|
89
|
+
restAction:
|
|
90
|
+
"A database size change of this project is still running. Every 60 seconds read latest_operation from 'ohmyhost project status --project ULID --json', or 'ohmyhost operation get OPERATION_ID --json' for the change you started, until it ends, then repeat this request. Do not submit another size change or a deletion meanwhile.",
|
|
91
|
+
},
|
|
92
|
+
compute_change_conflict: {
|
|
93
|
+
status: 409,
|
|
94
|
+
retryable: false,
|
|
95
|
+
action:
|
|
96
|
+
"Read the original operation and current database compute. Preserve data and report unresolved conflicts through feedback with the operation ID.",
|
|
97
|
+
title: "Database size change conflict",
|
|
98
|
+
},
|
|
99
|
+
database_access_limit: {
|
|
100
|
+
status: 409,
|
|
101
|
+
retryable: false,
|
|
102
|
+
action:
|
|
103
|
+
"List the project's database credentials and revoke one you no longer need, then request access again.",
|
|
104
|
+
title: "Database access limit reached",
|
|
105
|
+
},
|
|
106
|
+
project_notes_conflict: {
|
|
107
|
+
status: 409,
|
|
108
|
+
retryable: false,
|
|
109
|
+
action:
|
|
110
|
+
"Read project context, merge the current notes and retry with its version and a new idempotency key.",
|
|
111
|
+
title: "Project notes changed",
|
|
112
|
+
},
|
|
113
|
+
invalid_request: {
|
|
114
|
+
status: 400,
|
|
115
|
+
retryable: false,
|
|
116
|
+
action:
|
|
117
|
+
"The server rejected this request; repeating it unchanged fails again. Check every argument against the command usage or the tool schema. For 'mail webhook verify' and mail receiving, the app's webhook route must answer the signed test with 2xx using the installed signing secret; receiving needs a verified webhook. For 'database query', send one read-only SELECT or WITH statement that runs on the current schema.",
|
|
118
|
+
title: "Invalid request",
|
|
119
|
+
restAction:
|
|
120
|
+
"Read detail for the rejected field or condition, correct it and send the corrected request; repeating it unchanged fails again.",
|
|
121
|
+
},
|
|
122
|
+
unauthenticated: {
|
|
123
|
+
status: 401,
|
|
124
|
+
retryable: false,
|
|
125
|
+
action:
|
|
126
|
+
"The login or token was not accepted; repeating unchanged does not help. If OHMYHOST_TOKEN is set, it was revoked, mistyped or belongs to a different platform than OHMYHOST_ENVIRONMENT selects (a platform setting, not the project's dev or prod): ask the customer for a current token from the portal's API Tokens page, or unset it and run 'ohmyhost login --json'. Otherwise run 'ohmyhost login --json', then repeat the request.",
|
|
127
|
+
title: "Authentication required",
|
|
128
|
+
restAction:
|
|
129
|
+
"The login or token was not accepted; repeating unchanged does not help. If OHMYHOST_TOKEN is set, it was revoked, mistyped or belongs to a different platform than OHMYHOST_ENVIRONMENT selects (a platform setting, not the project's dev or prod): ask the customer for a current token from the portal's API Tokens page, or unset it and run 'ohmyhost login --json'. Otherwise run 'ohmyhost login --json', then repeat the request.",
|
|
130
|
+
},
|
|
131
|
+
forbidden: {
|
|
132
|
+
status: 403,
|
|
133
|
+
retryable: false,
|
|
134
|
+
action:
|
|
135
|
+
"This login may not do this; repeating unchanged does not help. error.acting_as names the login used: use a workspace this login belongs to ('ohmyhost whoami --json'), or the login of the account that owns it. For 'organization create', pass --source exactly as the Signup source on the portal Profile page, or omit it when that page shows Direct.",
|
|
136
|
+
title: "Forbidden",
|
|
137
|
+
restAction:
|
|
138
|
+
"This identity may not do this; repeating unchanged does not help. Use a workspace this identity belongs to. A new workspace must preserve the signup source already recorded for this user; a scoped API token may create another workspace through the ordinary authorized flow.",
|
|
139
|
+
},
|
|
140
|
+
organization_required: {
|
|
141
|
+
status: 409,
|
|
142
|
+
retryable: false,
|
|
143
|
+
action:
|
|
144
|
+
"Run 'ohmyhost organization list --json' and select one with 'ohmyhost organization use --organization ULID --json', or create the first workspace.",
|
|
145
|
+
title: "Workspace selection required",
|
|
146
|
+
},
|
|
147
|
+
resource_not_found: {
|
|
148
|
+
status: 404,
|
|
149
|
+
retryable: false,
|
|
150
|
+
action:
|
|
151
|
+
"Check the resource identifier and the account: a project is visible only to its own organization, and error.acting_as in the CLI and MCP names the login this ran as. Use the login of the account that owns it.",
|
|
152
|
+
title: "Resource not found",
|
|
153
|
+
},
|
|
154
|
+
idempotency_key_reused: {
|
|
155
|
+
status: 409,
|
|
156
|
+
retryable: false,
|
|
157
|
+
action:
|
|
158
|
+
"This key was already used for a request with different arguments or from another login. To repeat that original request, send exactly its original arguments with this key from the same login; for a new request, use a new key. Never pair a new key with a request whose first attempt had an uncertain outcome: read its result first.",
|
|
159
|
+
title: "Idempotency key reused",
|
|
160
|
+
restAction:
|
|
161
|
+
"This key was already used for a request with different arguments or from another login. To repeat that original request, send exactly its original arguments with this key from the same login; for a new request, use a new key. Never pair a new key with a request whose first attempt had an uncertain outcome: read its result first.",
|
|
162
|
+
},
|
|
163
|
+
project_handle_unavailable: {
|
|
164
|
+
status: 503,
|
|
165
|
+
retryable: true,
|
|
166
|
+
action: "Retry project creation with the same idempotency key.",
|
|
167
|
+
title: "Project handle unavailable",
|
|
168
|
+
},
|
|
169
|
+
project_handle_taken: {
|
|
170
|
+
status: 409,
|
|
171
|
+
retryable: false,
|
|
172
|
+
action:
|
|
173
|
+
"Another project uses this address. Run 'ohmyhost project handle check --handle NAME --json' and offer the customer its alternatives; rename only to the one they choose.",
|
|
174
|
+
title: "Project address is taken",
|
|
175
|
+
restAction:
|
|
176
|
+
"Another project uses this address. Run 'ohmyhost project handle check --handle NAME --json' and offer the customer its alternatives; rename only to the one they choose.",
|
|
177
|
+
},
|
|
178
|
+
project_handle_invalid: {
|
|
179
|
+
status: 409,
|
|
180
|
+
retryable: false,
|
|
181
|
+
action:
|
|
182
|
+
"Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words.",
|
|
183
|
+
title: "Project address is invalid",
|
|
184
|
+
},
|
|
185
|
+
project_handle_unchanged: {
|
|
186
|
+
status: 409,
|
|
187
|
+
retryable: false,
|
|
188
|
+
action: "The project already answers on this address; no rename is needed.",
|
|
189
|
+
title: "Project address is unchanged",
|
|
190
|
+
},
|
|
191
|
+
project_rename_blocked: {
|
|
192
|
+
status: 409,
|
|
193
|
+
retryable: true,
|
|
194
|
+
action:
|
|
195
|
+
"Wait for the running operation to finish, read the project again, then rename with the new ETag.",
|
|
196
|
+
title: "Project address change blocked",
|
|
197
|
+
},
|
|
198
|
+
project_identity_unavailable: {
|
|
199
|
+
status: 503,
|
|
200
|
+
retryable: true,
|
|
201
|
+
action: "Retry project creation with the same idempotency key.",
|
|
202
|
+
title: "Project identity unavailable",
|
|
203
|
+
},
|
|
204
|
+
deployment_plan_expired: {
|
|
205
|
+
status: 409,
|
|
206
|
+
retryable: false,
|
|
207
|
+
action: "Run 'ohmyhost plan' again, then deploy the new plan.",
|
|
208
|
+
title: "Deployment plan expired",
|
|
209
|
+
},
|
|
210
|
+
deployment_plan_incompatible: {
|
|
211
|
+
status: 409,
|
|
212
|
+
retryable: false,
|
|
213
|
+
action: "Run 'ohmyhost plan' again with the current source revision.",
|
|
214
|
+
title: "Deployment plan incompatible",
|
|
215
|
+
},
|
|
216
|
+
confirmation_expired: {
|
|
217
|
+
status: 409,
|
|
218
|
+
retryable: false,
|
|
219
|
+
action: "Plan the destructive action again before confirming it.",
|
|
220
|
+
title: "Confirmation expired",
|
|
221
|
+
},
|
|
222
|
+
confirmation_invalid: {
|
|
223
|
+
status: 409,
|
|
224
|
+
retryable: false,
|
|
225
|
+
action:
|
|
226
|
+
"The confirmation token does not match this action or the project's current state. A deletion is also refused while a SQL export of the project is being captured, for up to about ten minutes ('ohmyhost export get'). Then create a new plan and confirm with its confirmation_token and resource_etag before the token expires.",
|
|
227
|
+
title: "Confirmation invalid",
|
|
228
|
+
restAction:
|
|
229
|
+
"The confirmation token does not match this action or the project's current state. A deletion is also refused while a SQL export of the project is being captured, for up to about ten minutes ('ohmyhost export get'). Then create a new plan and confirm with its confirmation_token and resource_etag before the token expires.",
|
|
230
|
+
},
|
|
231
|
+
etag_mismatch: {
|
|
232
|
+
status: 409,
|
|
233
|
+
retryable: false,
|
|
234
|
+
action:
|
|
235
|
+
"The project changed after you read its ETag. For a rename, read etag from 'ohmyhost project status --project ULID --json' and repeat 'ohmyhost project handle set' with it. For a rollback, promotion or deletion, create a new plan and confirm with its resource_etag and confirmation_token.",
|
|
236
|
+
title: "Resource changed",
|
|
237
|
+
restAction:
|
|
238
|
+
"The project changed after you read its ETag. For a rename, read etag from 'ohmyhost project status --project ULID --json' and repeat 'ohmyhost project handle set' with it. For a rollback, promotion or deletion, create a new plan and confirm with its resource_etag and confirmation_token.",
|
|
239
|
+
},
|
|
240
|
+
promotion_source_stale: {
|
|
241
|
+
status: 409,
|
|
242
|
+
retryable: false,
|
|
243
|
+
action: "Read the current dev deployment, then plan promotion again.",
|
|
244
|
+
title: "Promotion source changed",
|
|
245
|
+
},
|
|
246
|
+
promotion_target_stale: {
|
|
247
|
+
status: 409,
|
|
248
|
+
retryable: false,
|
|
249
|
+
action: "Read the current prod status, then plan promotion again.",
|
|
250
|
+
title: "Promotion target changed",
|
|
251
|
+
},
|
|
252
|
+
promotion_invalid_target: {
|
|
253
|
+
status: 409,
|
|
254
|
+
retryable: false,
|
|
255
|
+
action: "Select the current succeeded dev deployment and target prod.",
|
|
256
|
+
title: "Promotion target invalid",
|
|
257
|
+
},
|
|
258
|
+
mail_capacity_unavailable: {
|
|
259
|
+
status: 503,
|
|
260
|
+
retryable: true,
|
|
261
|
+
action:
|
|
262
|
+
"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.",
|
|
263
|
+
title: "Mail capacity unavailable",
|
|
264
|
+
},
|
|
265
|
+
mail_domain_conflict: {
|
|
266
|
+
status: 409,
|
|
267
|
+
retryable: false,
|
|
268
|
+
action:
|
|
269
|
+
"Use the sender domain already configured for this project, or retire it with 'ohmyhost mail domain delete' (repeat while the removal is pending) and then set up the new domain; a domain registered to another project cannot be set up.",
|
|
270
|
+
title: "Mail domain conflict",
|
|
271
|
+
},
|
|
272
|
+
mail_domain_required: {
|
|
273
|
+
status: 409,
|
|
274
|
+
retryable: false,
|
|
275
|
+
action:
|
|
276
|
+
"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.",
|
|
277
|
+
title: "Mail domain required",
|
|
278
|
+
},
|
|
279
|
+
storage_jurisdiction_conflict: {
|
|
280
|
+
status: 409,
|
|
281
|
+
retryable: false,
|
|
282
|
+
action:
|
|
283
|
+
"Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', commit and push, then plan the new commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
|
|
284
|
+
title: "Storage jurisdiction conflict",
|
|
285
|
+
restAction:
|
|
286
|
+
"Set storage.jurisdiction in ohmyhost.yaml to the project's region (us or eu) shown by 'ohmyhost project status', commit and push, then plan the new commit (for a promotion, deploy it to Dev and promote again); the unchanged commit fails again. A project cannot move its files between regions; do not delete or recreate the project to change it.",
|
|
287
|
+
},
|
|
288
|
+
shared_data_requires_promotion: {
|
|
289
|
+
status: 409,
|
|
290
|
+
retryable: false,
|
|
291
|
+
action:
|
|
292
|
+
"Plan and deploy this commit to Dev, then run 'ohmyhost deployment promote plan' to reach Prod.",
|
|
293
|
+
title: "Shared data requires promotion",
|
|
294
|
+
},
|
|
295
|
+
production_deployment_required: {
|
|
296
|
+
status: 409,
|
|
297
|
+
retryable: false,
|
|
298
|
+
action:
|
|
299
|
+
"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.",
|
|
300
|
+
title: "Production deployment required",
|
|
301
|
+
},
|
|
302
|
+
framework_conversion_required: {
|
|
303
|
+
status: 409,
|
|
304
|
+
retryable: false,
|
|
305
|
+
action:
|
|
306
|
+
"Read detail for the file, reason and required change. Run ohmyhost init --dry-run --json at that commit, apply its blockers and the matching ohmyho.st framework Skill, then commit and push. Plan the new commit; repeating the unchanged source cannot fix an admission error.",
|
|
307
|
+
title: "Framework conversion required",
|
|
308
|
+
restAction:
|
|
309
|
+
"Read detail for the file, reason and required change. Run ohmyhost init --dry-run --json at that commit, apply its blockers and the matching ohmyho.st framework Skill, then commit and push. Plan the new commit; repeating the unchanged source cannot fix an admission error.",
|
|
310
|
+
},
|
|
311
|
+
repository_configuration_missing: {
|
|
312
|
+
status: 409,
|
|
313
|
+
retryable: false,
|
|
314
|
+
action:
|
|
315
|
+
"The planned commit has no ohmyhost.yaml at the Git repository root. If the repository has none, run 'ohmyhost init --json' at the root (--root DIR when the app lives in a subdirectory, --region eu for an EU project). Otherwise move the existing file to the root and set applicationRoot to the app directory, for example website. Commit and push, then plan the new commit.",
|
|
316
|
+
title: "Repository configuration missing",
|
|
317
|
+
restAction:
|
|
318
|
+
"The planned commit has no ohmyhost.yaml at the Git repository root. If the repository has none, run 'ohmyhost init --json' at the root (--root DIR when the app lives in a subdirectory, --region eu for an EU project). Otherwise move the existing file to the root and set applicationRoot to the app directory, for example website. Commit and push, then plan the new commit.",
|
|
319
|
+
},
|
|
320
|
+
migration_filename_noncanonical: {
|
|
321
|
+
status: 409,
|
|
322
|
+
retryable: false,
|
|
323
|
+
action:
|
|
324
|
+
"Every file in the migrations directory (database.migrations in ohmyhost.yaml) must be named YYYYMMDDHHMMSS_name.sql: 14 digits, an underscore, then lowercase letters, digits, _ or -. Rename each file that does not match and move any other file, such as README.md or .gitkeep, out of that directory. Commit and push, then plan the new commit. Never rename a migration a deployment already applied.",
|
|
325
|
+
title: "Migration filename noncanonical",
|
|
326
|
+
restAction:
|
|
327
|
+
"Every file in the migrations directory (database.migrations in ohmyhost.yaml) must be named YYYYMMDDHHMMSS_name.sql: 14 digits, an underscore, then lowercase letters, digits, _ or -. Rename each file that does not match and move any other file, such as README.md or .gitkeep, out of that directory. Commit and push, then plan the new commit. Never rename a migration a deployment already applied.",
|
|
328
|
+
},
|
|
329
|
+
environment_secret_mutation_blocked: {
|
|
330
|
+
status: 409,
|
|
331
|
+
retryable: true,
|
|
332
|
+
action: "Wait for the active deployment or rollback to finish, then retry unchanged.",
|
|
333
|
+
title: "Environment secret mutation blocked",
|
|
334
|
+
},
|
|
335
|
+
workers_runtime_incompatible: {
|
|
336
|
+
status: 409,
|
|
337
|
+
retryable: false,
|
|
338
|
+
action:
|
|
339
|
+
"A Next.js file under app/, pages/, middleware or proxy imports a native addon (.node) or a Node.js module that does not run on Workers: child_process, cluster, dgram, domain, http2, inspector, readline, repl, sqlite, trace_events, tty, v8, vm, wasi, worker_threads or _stream_wrap, with or without node:. Remove or replace that import, commit and push, then plan the new commit.",
|
|
340
|
+
title: "Workers runtime incompatible",
|
|
341
|
+
restAction:
|
|
342
|
+
"A Next.js file under app/, pages/, middleware or proxy imports a native addon (.node) or a Node.js module that does not run on Workers: child_process, cluster, dgram, domain, http2, inspector, readline, repl, sqlite, trace_events, tty, v8, vm, wasi, worker_threads or _stream_wrap, with or without node:. Remove or replace that import, commit and push, then plan the new commit.",
|
|
343
|
+
},
|
|
344
|
+
payload_too_large: {
|
|
345
|
+
status: 413,
|
|
346
|
+
retryable: false,
|
|
347
|
+
action: "Reduce the request size.",
|
|
348
|
+
title: "Payload too large",
|
|
349
|
+
},
|
|
350
|
+
project_export_not_ready: {
|
|
351
|
+
status: 409,
|
|
352
|
+
retryable: false,
|
|
353
|
+
action:
|
|
354
|
+
"No database of this project is ready, so no export was accepted. A database is created when a commit whose ohmyhost.yaml sets database.enabled: true is deployed; request the export again after that deployment succeeds ('ohmyhost operation get OPERATION_ID --json'). A project without a database has no SQL export.",
|
|
355
|
+
title: "Project database not ready for export",
|
|
356
|
+
restAction:
|
|
357
|
+
"No database of this project is ready, so no export was accepted. A database is created when a commit whose ohmyhost.yaml sets database.enabled: true is deployed; request the export again after that deployment succeeds ('ohmyhost operation get OPERATION_ID --json'). A project without a database has no SQL export.",
|
|
358
|
+
},
|
|
359
|
+
powered_by_flag_required: {
|
|
360
|
+
status: 409,
|
|
361
|
+
retryable: false,
|
|
362
|
+
action:
|
|
363
|
+
"Keep the flag on, remove the custom domain with 'ohmyhost domain paid delete', or buy Paid, then switch the flag off.",
|
|
364
|
+
title: "Powered-by flag required",
|
|
365
|
+
},
|
|
366
|
+
rate_limited: {
|
|
367
|
+
status: 429,
|
|
368
|
+
retryable: true,
|
|
369
|
+
action:
|
|
370
|
+
"Too many requests. Wait retry_after_seconds, or 60 seconds when none is shown, then repeat the same request with the same idempotency key. SQL exports allow one request per project per rolling 24 hours: poll the export already requested with 'ohmyhost export get EXPORT_ID --project ULID --json' instead of creating another.",
|
|
371
|
+
title: "Rate limited",
|
|
372
|
+
restAction:
|
|
373
|
+
"Wait the Retry-After interval (also retry_after_seconds), then repeat the same request with the same idempotency key.",
|
|
374
|
+
},
|
|
375
|
+
insufficient_organization_credits: {
|
|
376
|
+
status: 402,
|
|
377
|
+
retryable: false,
|
|
378
|
+
action:
|
|
379
|
+
"Add organization credits or wait for reservations to settle, then retry the same request.",
|
|
380
|
+
title: "Insufficient organization credits",
|
|
381
|
+
},
|
|
382
|
+
project_budget_exceeded: {
|
|
383
|
+
status: 402,
|
|
384
|
+
retryable: false,
|
|
385
|
+
action:
|
|
386
|
+
"This project's Stop budget for the current UTC month is reached; nothing was started. Show the customer 'ohmyhost budget get --project ULID --json'. Only on their request raise the limit or switch the mode to continue with 'ohmyhost budget set', then retry the same request; otherwise wait for the next UTC month.",
|
|
387
|
+
title: "Project budget exceeded",
|
|
388
|
+
restAction:
|
|
389
|
+
"This project's Stop budget for the current UTC month is reached; nothing was started. Show the customer 'ohmyhost budget get --project ULID --json'. Only on their request raise the limit or switch the mode to continue with 'ohmyhost budget set', then retry the same request; otherwise wait for the next UTC month.",
|
|
390
|
+
},
|
|
391
|
+
paid_plan_required: {
|
|
392
|
+
status: 402,
|
|
393
|
+
retryable: false,
|
|
394
|
+
action:
|
|
395
|
+
"Ask the organization Owner to activate Paid through billing checkout, then retry. Top-ups and signup credits do not activate Paid. For a website domain alone, a Free workspace may instead show the powered-by flag on the project.",
|
|
396
|
+
title: "Paid plan required",
|
|
397
|
+
},
|
|
398
|
+
interactive_login_required: {
|
|
399
|
+
status: 403,
|
|
400
|
+
retryable: false,
|
|
401
|
+
action:
|
|
402
|
+
"Unset OHMYHOST_TOKEN and run ohmyhost login before managing tokens. Existing token files remain unchanged.",
|
|
403
|
+
title: "Interactive login required",
|
|
404
|
+
},
|
|
405
|
+
api_key_creation_uncertain: {
|
|
406
|
+
status: 503,
|
|
407
|
+
retryable: true,
|
|
408
|
+
action:
|
|
409
|
+
"Repeat the original token name and Idempotency-Key to observe its result; do not blindly create another key.",
|
|
410
|
+
title: "Token creation unconfirmed",
|
|
411
|
+
},
|
|
412
|
+
api_key_permissions_unavailable: {
|
|
413
|
+
status: 503,
|
|
414
|
+
retryable: false,
|
|
415
|
+
action:
|
|
416
|
+
"Report the platform user-key permission configuration through feedback. Repeated login or creation will not fix it.",
|
|
417
|
+
title: "Token permissions unavailable",
|
|
418
|
+
},
|
|
419
|
+
reconciliation_exhausted: {
|
|
420
|
+
status: 409,
|
|
421
|
+
retryable: false,
|
|
422
|
+
action:
|
|
423
|
+
"Stop retries and submit feedback with this operation ID. A platform recovery review is required.",
|
|
424
|
+
title: "Recovery attempts exhausted",
|
|
425
|
+
},
|
|
426
|
+
service_unavailable: {
|
|
427
|
+
status: 503,
|
|
428
|
+
retryable: true,
|
|
429
|
+
action:
|
|
430
|
+
"The service could not complete or confirm this request; it may already have taken effect. Wait, then repeat exactly the same request with the same idempotency key, waiting longer each time; never switch to a new key. If 'ohmyhost plan' keeps failing, check that the commit is pushed to the linked repository and that 'ohmyhost init --dry-run --json' at that commit reports no blockers. If it still fails, report the request_id through feedback.",
|
|
431
|
+
title: "Service unavailable",
|
|
432
|
+
restAction:
|
|
433
|
+
"The service could not complete or confirm this request; it may already have taken effect. Wait, then repeat exactly the same request with the same idempotency key, waiting longer each time; never switch to a new key. If 'ohmyhost plan' keeps failing, check that the commit is pushed to the linked repository and that 'ohmyhost init --dry-run --json' at that commit reports no blockers. If it still fails, report the request_id through feedback.",
|
|
434
|
+
},
|
|
435
|
+
source_commit_not_found: {
|
|
436
|
+
status: 409,
|
|
437
|
+
retryable: false,
|
|
438
|
+
action:
|
|
439
|
+
"The requested commit is not in the linked repository. Push that exact commit to the repository named by project status, or select a commit already there, then run 'ohmyhost plan' with its full SHA. Repeating the unchanged unavailable commit cannot work.",
|
|
440
|
+
restAction:
|
|
441
|
+
"The requested commit is not in the linked repository. Push that exact commit to the repository named by project status, or select a commit already there, then run 'ohmyhost plan' with its full SHA. Repeating the unchanged unavailable commit cannot work.",
|
|
442
|
+
title: "Source commit not found",
|
|
443
|
+
},
|
|
444
|
+
domain_hostname_taken: {
|
|
445
|
+
status: 409,
|
|
446
|
+
retryable: false,
|
|
447
|
+
action:
|
|
448
|
+
"A live project connection already holds this hostname. Remove it from the project you manage with 'ohmyhost domain paid delete --project ULID --hostname HOST --yes --json', then apply it to the new project. Repeating while that connection remains cannot work; report the request_id if you cannot manage its owning project.",
|
|
449
|
+
restAction:
|
|
450
|
+
"A live project connection already holds this hostname. Remove it from the project you manage with 'ohmyhost domain paid delete --project ULID --hostname HOST --yes --json', then apply it to the new project. Repeating while that connection remains cannot work; report the request_id if you cannot manage its owning project.",
|
|
451
|
+
title: "Domain hostname is already connected",
|
|
452
|
+
},
|
|
453
|
+
data_change_blocked: {
|
|
454
|
+
status: 409,
|
|
455
|
+
retryable: true,
|
|
456
|
+
action:
|
|
457
|
+
"An operation of this project is still running. Wait until 'ohmyhost operation get OPERATION_ULID --json' shows it finished, plan the data change again and confirm it with the new ETag and token.",
|
|
458
|
+
restAction:
|
|
459
|
+
"An operation of this project is still running. Wait until 'ohmyhost operation get OPERATION_ULID --json' shows it finished, plan the data change again and confirm it with the new ETag and token.",
|
|
460
|
+
title: "Data change blocked",
|
|
461
|
+
},
|
|
462
|
+
data_change_in_progress: {
|
|
463
|
+
status: 409,
|
|
464
|
+
retryable: true,
|
|
465
|
+
action:
|
|
466
|
+
"A data change of this project is running. Wait until its operation has finished ('ohmyhost project status --project ULID --json'), then repeat the request unchanged.",
|
|
467
|
+
restAction:
|
|
468
|
+
"A data change of this project is running. Wait until its operation has finished ('ohmyhost project status --project ULID --json'), then repeat the request unchanged.",
|
|
469
|
+
title: "Data change in progress",
|
|
470
|
+
},
|
|
471
|
+
data_change_not_applicable: {
|
|
472
|
+
status: 409,
|
|
473
|
+
retryable: false,
|
|
474
|
+
action:
|
|
475
|
+
"Read 'ohmyhost project status --project ULID --json'. Isolation applies to shared projects; returning to shared needs a deployed Prod database, and reset_dev applies to isolated projects. If detail names an old database or storage binding, deploy the affected environment again before planning the change. Repeating unchanged does not help.",
|
|
476
|
+
restAction:
|
|
477
|
+
"Read 'ohmyhost project status --project ULID --json'. Isolation applies to shared projects; returning to shared needs a deployed Prod database, and reset_dev applies to isolated projects. If detail names an old database or storage binding, deploy the affected environment again before planning the change. Repeating unchanged does not help.",
|
|
478
|
+
title: "Data change is not applicable",
|
|
479
|
+
},
|
|
480
|
+
rollback_target_data_changed: {
|
|
481
|
+
status: 409,
|
|
482
|
+
retryable: false,
|
|
483
|
+
action:
|
|
484
|
+
"This deployment used a data area its environment no longer has. Deploy that commit again with a fresh plan instead of rolling back to its old bindings; repeating this rollback cannot work.",
|
|
485
|
+
restAction:
|
|
486
|
+
"This deployment used a data area its environment no longer has. Deploy that commit again with a fresh plan instead of rolling back to its old bindings; repeating this rollback cannot work.",
|
|
487
|
+
title: "Rollback data area changed",
|
|
488
|
+
},
|
|
489
|
+
project_domain_delete_required: {
|
|
490
|
+
status: 409,
|
|
491
|
+
retryable: false,
|
|
492
|
+
title: "Remove the project domain first",
|
|
493
|
+
action:
|
|
494
|
+
"This project still has a live or pending custom-domain connection. Read 'ohmyhost domain paid status --project ULID --json', remove that connection with 'ohmyhost domain paid delete --project ULID --hostname HOST --yes --json', then create a fresh project deletion plan. Repeating while it remains connected cannot work.",
|
|
495
|
+
},
|
|
496
|
+
} as const;
|
|
497
|
+
|
|
498
|
+
export type ProblemCode = keyof typeof PROBLEM_POLICY;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const PROJECT_DATA_CHANGE_KINDS = [
|
|
2
|
+
"isolate_prod_keeps_data",
|
|
3
|
+
"isolate_dev_keeps_data",
|
|
4
|
+
"share_prod_keeps_data",
|
|
5
|
+
"reset_dev",
|
|
6
|
+
] as const;
|
|
7
|
+
export type ProjectDataChangeKind = (typeof PROJECT_DATA_CHANGE_KINDS)[number];
|
|
8
|
+
export interface ProjectDataChangePlan {
|
|
9
|
+
readonly action: "data_change";
|
|
10
|
+
readonly project_id: string;
|
|
11
|
+
readonly change: ProjectDataChangeKind;
|
|
12
|
+
readonly data_mode_before: "shared" | "isolated";
|
|
13
|
+
readonly data_mode_after: "shared" | "isolated";
|
|
14
|
+
readonly effects: readonly string[];
|
|
15
|
+
readonly destructive_effects: readonly string[];
|
|
16
|
+
readonly redeploy_environments: readonly ("dev" | "prod")[];
|
|
17
|
+
readonly risks: readonly string[];
|
|
18
|
+
readonly resource_etag: string;
|
|
19
|
+
readonly confirmation_token: string;
|
|
20
|
+
readonly created_at: string;
|
|
21
|
+
readonly expires_at: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A data change uses exactly the plan the owner reviewed; no loose destructive plan reader. */
|
|
25
|
+
export function parseProjectDataChangePlan(value: unknown): ProjectDataChangePlan {
|
|
26
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) invalid();
|
|
27
|
+
const input = value as Record<string, unknown>;
|
|
28
|
+
if (
|
|
29
|
+
Object.keys(input).sort().join(",") !==
|
|
30
|
+
"action,change,confirmation_token,created_at,data_mode_after,data_mode_before,destructive_effects,effects,expires_at,project_id,redeploy_environments,resource_etag,risks" ||
|
|
31
|
+
input["action"] !== "data_change" ||
|
|
32
|
+
typeof input["project_id"] !== "string" ||
|
|
33
|
+
!/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(input["project_id"]) ||
|
|
34
|
+
!isProjectDataChangeKind(input["change"]) ||
|
|
35
|
+
!["shared", "isolated"].includes(String(input["data_mode_before"])) ||
|
|
36
|
+
input["data_mode_after"] !==
|
|
37
|
+
(input["change"] === "share_prod_keeps_data" ? "shared" : "isolated") ||
|
|
38
|
+
input["data_mode_before"] !==
|
|
39
|
+
(input["change"].startsWith("isolate_") ? "shared" : "isolated") ||
|
|
40
|
+
!Array.isArray(input["redeploy_environments"]) ||
|
|
41
|
+
input["redeploy_environments"].length !== 1 ||
|
|
42
|
+
input["redeploy_environments"][0] !==
|
|
43
|
+
(input["change"] === "isolate_dev_keeps_data" ? "prod" : "dev") ||
|
|
44
|
+
!planStrings(input["effects"]) ||
|
|
45
|
+
!planStrings(input["destructive_effects"]) ||
|
|
46
|
+
!planStrings(input["risks"]) ||
|
|
47
|
+
typeof input["resource_etag"] !== "string" ||
|
|
48
|
+
!/^"sha256-[a-f0-9]{64}"$/u.test(input["resource_etag"]) ||
|
|
49
|
+
typeof input["confirmation_token"] !== "string" ||
|
|
50
|
+
!/^[A-Za-z0-9_-]{43}$/u.test(input["confirmation_token"]) ||
|
|
51
|
+
!instant(input["created_at"]) ||
|
|
52
|
+
!instant(input["expires_at"]) ||
|
|
53
|
+
Date.parse(input["expires_at"]) - Date.parse(input["created_at"]) !== 600_000
|
|
54
|
+
)
|
|
55
|
+
invalid();
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
...input,
|
|
58
|
+
effects: Object.freeze([...input["effects"]]),
|
|
59
|
+
destructive_effects: Object.freeze([...input["destructive_effects"]]),
|
|
60
|
+
risks: Object.freeze([...input["risks"]]),
|
|
61
|
+
redeploy_environments: Object.freeze([...input["redeploy_environments"]]),
|
|
62
|
+
}) as unknown as ProjectDataChangePlan;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isProjectDataChangeKind(value: unknown): value is ProjectDataChangeKind {
|
|
66
|
+
return typeof value === "string" && PROJECT_DATA_CHANGE_KINDS.some((kind) => kind === value);
|
|
67
|
+
}
|
|
68
|
+
function planStrings(value: unknown): value is string[] {
|
|
69
|
+
return (
|
|
70
|
+
Array.isArray(value) &&
|
|
71
|
+
value.length <= 32 &&
|
|
72
|
+
value.every(
|
|
73
|
+
(text) =>
|
|
74
|
+
typeof text === "string" &&
|
|
75
|
+
text.length > 0 &&
|
|
76
|
+
text.length <= 2_000 &&
|
|
77
|
+
!/[\p{Cc}\p{Cf}]/u.test(text),
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function instant(value: unknown): value is string {
|
|
82
|
+
return (
|
|
83
|
+
typeof value === "string" &&
|
|
84
|
+
Number.isFinite(Date.parse(value)) &&
|
|
85
|
+
new Date(value).toISOString() === value
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function invalid(): never {
|
|
89
|
+
throw new TypeError("Project data change plan is invalid");
|
|
90
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Customer names and the platform's installed keys share one rule in API, CLI and MCP. */
|
|
2
|
+
export const USER_SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/u;
|
|
3
|
+
const FIXED_PLATFORM_NAMES = new Set([
|
|
4
|
+
"BETTER_AUTH_SECRET",
|
|
5
|
+
"BETTER_AUTH_URL",
|
|
6
|
+
"DATABASE_URL",
|
|
7
|
+
"HYPERDRIVE",
|
|
8
|
+
"ASSETS",
|
|
9
|
+
"FILES",
|
|
10
|
+
"IMAGES",
|
|
11
|
+
"STORAGE",
|
|
12
|
+
]);
|
|
13
|
+
const INSTALLED_PLATFORM_KEYS = new Set([
|
|
14
|
+
"OHMYHOST_STORAGE_KEY",
|
|
15
|
+
"OHMYHOST_MAIL_KEY",
|
|
16
|
+
"BETTER_AUTH_SECRET",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function isUserSecretName(
|
|
20
|
+
value: unknown,
|
|
21
|
+
action: "set" | "delete" = "set",
|
|
22
|
+
): value is string {
|
|
23
|
+
if (typeof value !== "string" || !USER_SECRET_NAME_PATTERN.test(value)) return false;
|
|
24
|
+
return action === "delete"
|
|
25
|
+
? !INSTALLED_PLATFORM_KEYS.has(value)
|
|
26
|
+
: !value.startsWith("OHMYHOST_") && !FIXED_PLATFORM_NAMES.has(value);
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ohmyhost/sdk-ts",
|
|
3
|
+
"version": "0.1.25",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc --project tsconfig.build.json",
|
|
15
|
+
"check": "pnpm --dir ../.. sdk-ts:check",
|
|
16
|
+
"generate": "pnpm --dir ../.. sdk-ts:generate",
|
|
17
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
18
|
+
"test": "pnpm run check && vitest run --root ../.. packages/sdk-ts"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0"
|
|
21
|
+
}
|