@amerged/ohmyhost-mcp 0.1.10
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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +11 -0
- package/THIRD_PARTY_NOTICES.md +474 -0
- package/dist/chunk-7MV2EVXV.js +30779 -0
- package/dist/index.js +9 -0
- package/dist/stdio-main.js +5607 -0
- package/package.json +32 -0
- package/src/apps/mcp/index.ts +30 -0
- package/src/apps/mcp/local-client.ts +891 -0
- package/src/apps/mcp/local-server.ts +1512 -0
- package/src/apps/mcp/stdio-main.ts +26 -0
- package/src/apps/product-cli/bin.ts +24 -0
- package/src/apps/product-cli/cli.ts +2271 -0
- package/src/apps/product-cli/command.ts +1811 -0
- package/src/apps/product-cli/composition.ts +823 -0
- package/src/apps/product-cli/credential-store.ts +149 -0
- package/src/apps/product-cli/device-token-expiry.ts +89 -0
- package/src/apps/product-cli/file-credential-store.ts +166 -0
- package/src/apps/product-cli/index.ts +23 -0
- package/src/apps/product-cli/product-api.ts +1082 -0
- package/src/apps/product-cli/project-link-store.ts +30 -0
- package/src/apps/product-cli/psql-session.ts +29 -0
- package/src/apps/product-cli/repository-init.ts +1622 -0
- package/src/apps/product-cli/runtime-contract.ts +1747 -0
- package/src/apps/product-cli/user-token-file.ts +214 -0
- package/src/apps/product-cli/workspace.ts +92 -0
- package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
- package/src/packages/agent-skills/index.ts +37 -0
- package/src/packages/contracts/application-root.ts +80 -0
- package/src/packages/contracts/billing.ts +172 -0
- package/src/packages/contracts/credit-pricing.ts +214 -0
- package/src/packages/contracts/database-access.ts +257 -0
- package/src/packages/contracts/database-compute.ts +72 -0
- package/src/packages/contracts/database-migration-admission.ts +235 -0
- package/src/packages/contracts/feedback.ts +96 -0
- package/src/packages/contracts/framework-admission.ts +280 -0
- package/src/packages/contracts/framework-family.ts +18 -0
- package/src/packages/contracts/functions.ts +76 -0
- package/src/packages/contracts/index.ts +245 -0
- package/src/packages/contracts/operation-deployment-result.ts +165 -0
- package/src/packages/contracts/operation-failure.ts +150 -0
- package/src/packages/contracts/platform-origins.ts +5 -0
- package/src/packages/contracts/project-context.ts +59 -0
- package/src/packages/contracts/project-exports.ts +102 -0
- package/src/packages/contracts/user-api-keys.ts +98 -0
- package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
- package/src/packages/sdk-ts/generated/client/index.ts +27 -0
- package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
- package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
- package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
- package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
- package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
- package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
- package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
- package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
- package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
- package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
- package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
- package/src/packages/sdk-ts/generated/index.ts +578 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
- package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
- package/src/packages/sdk-ts/index.ts +7 -0
- package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
- package/src/packages/workos-auth-contracts/index.ts +8 -0
- package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
- package/src/packages/workos-auth-contracts/organizations.ts +113 -0
- package/src/packages/workos-auth-contracts/parsing.ts +75 -0
- package/src/packages/workos-auth-contracts/session-status.ts +53 -0
- package/src/packages/workos-auth-contracts/tokens.ts +216 -0
- package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
- package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
|
@@ -0,0 +1,1747 @@
|
|
|
1
|
+
import { parseFrameworkConversionDetail } from "@ohmyhost/contracts/framework-admission";
|
|
2
|
+
import {
|
|
3
|
+
parseOperationDeploymentProgress,
|
|
4
|
+
type OperationDeploymentProgress,
|
|
5
|
+
} from "@ohmyhost/contracts/operation-deployment-result";
|
|
6
|
+
import type {
|
|
7
|
+
GithubConnectionAuthorization,
|
|
8
|
+
GithubOrganizationConnection,
|
|
9
|
+
GithubOrganizationConnectionStatus,
|
|
10
|
+
ProblemDetails,
|
|
11
|
+
OrganizationCredits,
|
|
12
|
+
ProjectCreditBudget,
|
|
13
|
+
} from "@ohmyhost/sdk-ts";
|
|
14
|
+
import { isSaasCnameTarget, type SaasCnameTarget } from "@ohmyhost/contracts/platform-origins";
|
|
15
|
+
import { assertPublishedCreditRateCards } from "@ohmyhost/contracts/credit-pricing";
|
|
16
|
+
import {
|
|
17
|
+
publicOperationFailure,
|
|
18
|
+
publicOperationReconciliation,
|
|
19
|
+
type PublicOperationFailure,
|
|
20
|
+
type PublicOperationReconciliation,
|
|
21
|
+
} from "@ohmyhost/contracts/operation-failure";
|
|
22
|
+
export type { PublicOperationFailure } from "@ohmyhost/contracts/operation-failure";
|
|
23
|
+
|
|
24
|
+
const PROBLEM_POLICY = {
|
|
25
|
+
cloudflare_zone_not_bound: {
|
|
26
|
+
status: 409,
|
|
27
|
+
retryable: false,
|
|
28
|
+
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
|
+
},
|
|
31
|
+
github_connection_required: {
|
|
32
|
+
status: 409,
|
|
33
|
+
retryable: false,
|
|
34
|
+
action:
|
|
35
|
+
"Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' for this workspace, complete its browser link, then repeat the original source link.",
|
|
36
|
+
},
|
|
37
|
+
github_connection_revoked: {
|
|
38
|
+
status: 409,
|
|
39
|
+
retryable: false,
|
|
40
|
+
action:
|
|
41
|
+
"Run 'ohmyhost github connect --organization ULID --idempotency-key KEY --json' to restore this workspace's GitHub connection, then repeat the original source link.",
|
|
42
|
+
},
|
|
43
|
+
repository_not_installed: {
|
|
44
|
+
status: 403,
|
|
45
|
+
retryable: false,
|
|
46
|
+
action:
|
|
47
|
+
"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.",
|
|
48
|
+
},
|
|
49
|
+
database_write_pending: {
|
|
50
|
+
status: 409,
|
|
51
|
+
retryable: false,
|
|
52
|
+
action: "Poll the original database-write operation before transfer or deletion.",
|
|
53
|
+
},
|
|
54
|
+
cloudflare_authorization_closed: {
|
|
55
|
+
status: 409,
|
|
56
|
+
retryable: false,
|
|
57
|
+
action:
|
|
58
|
+
"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.",
|
|
59
|
+
},
|
|
60
|
+
billing_recharge_conflict: {
|
|
61
|
+
status: 409,
|
|
62
|
+
retryable: false,
|
|
63
|
+
action:
|
|
64
|
+
"Read billing recharge get and the current revision. Preserve the original key after uncertainty; do not start a checkout or automatically re-enable charges.",
|
|
65
|
+
},
|
|
66
|
+
billing_purchase_conflict: {
|
|
67
|
+
status: 409,
|
|
68
|
+
retryable: false,
|
|
69
|
+
action:
|
|
70
|
+
"Read the original checkout and use billing portal to manage the existing subscription. Do not create another purchase after uncertainty.",
|
|
71
|
+
},
|
|
72
|
+
compute_performance_paid_required: {
|
|
73
|
+
status: 403,
|
|
74
|
+
retryable: false,
|
|
75
|
+
action: "Check Paid service and usable credits or grace, or choose standard compute.",
|
|
76
|
+
},
|
|
77
|
+
compute_performance_unavailable: {
|
|
78
|
+
status: 503,
|
|
79
|
+
retryable: false,
|
|
80
|
+
action:
|
|
81
|
+
"Choose standard compute or report inactive performance pricing through feedback. Do not repeatedly resubmit.",
|
|
82
|
+
},
|
|
83
|
+
compute_change_pending: {
|
|
84
|
+
status: 409,
|
|
85
|
+
retryable: false,
|
|
86
|
+
action:
|
|
87
|
+
"Poll the existing operation after 60 seconds. Do not submit another size change, deletion or transfer while it runs.",
|
|
88
|
+
},
|
|
89
|
+
compute_change_conflict: {
|
|
90
|
+
status: 409,
|
|
91
|
+
retryable: false,
|
|
92
|
+
action:
|
|
93
|
+
"Read the original operation and current database compute. Preserve data and report unresolved conflicts through feedback with the operation ID.",
|
|
94
|
+
},
|
|
95
|
+
database_access_limit: {
|
|
96
|
+
status: 409,
|
|
97
|
+
retryable: false,
|
|
98
|
+
action:
|
|
99
|
+
"List the project's database credentials and revoke one you no longer need, then request access again.",
|
|
100
|
+
},
|
|
101
|
+
project_notes_conflict: {
|
|
102
|
+
status: 409,
|
|
103
|
+
retryable: false,
|
|
104
|
+
action:
|
|
105
|
+
"Read project context, merge the current notes and retry with its version and a new idempotency key.",
|
|
106
|
+
},
|
|
107
|
+
invalid_request: { status: 400, retryable: false, action: "Check the request and retry." },
|
|
108
|
+
unauthenticated: {
|
|
109
|
+
status: 401,
|
|
110
|
+
retryable: false,
|
|
111
|
+
action:
|
|
112
|
+
"Check OHMYHOST_TOKEN and the selected environment. Replace invalid tokens, or unset OHMYHOST_TOKEN and run 'ohmyhost login'.",
|
|
113
|
+
},
|
|
114
|
+
forbidden: { status: 403, retryable: false, action: "Check organization access." },
|
|
115
|
+
organization_required: {
|
|
116
|
+
status: 409,
|
|
117
|
+
retryable: false,
|
|
118
|
+
action:
|
|
119
|
+
"Run 'ohmyhost organization list --json' and select one with 'ohmyhost organization use --organization ULID --json', or create the first workspace.",
|
|
120
|
+
},
|
|
121
|
+
resource_not_found: { status: 404, retryable: false, action: "Check the resource identifier." },
|
|
122
|
+
idempotency_key_reused: {
|
|
123
|
+
status: 409,
|
|
124
|
+
retryable: false,
|
|
125
|
+
action: "Use a new idempotency key for a different request.",
|
|
126
|
+
},
|
|
127
|
+
project_handle_unavailable: {
|
|
128
|
+
status: 503,
|
|
129
|
+
retryable: true,
|
|
130
|
+
action: "Retry project creation with the same idempotency key.",
|
|
131
|
+
},
|
|
132
|
+
project_identity_unavailable: {
|
|
133
|
+
status: 503,
|
|
134
|
+
retryable: true,
|
|
135
|
+
action: "Retry project creation with the same idempotency key.",
|
|
136
|
+
},
|
|
137
|
+
deployment_plan_expired: {
|
|
138
|
+
status: 409,
|
|
139
|
+
retryable: false,
|
|
140
|
+
action: "Run 'ohmyhost plan' again, then deploy the new plan.",
|
|
141
|
+
},
|
|
142
|
+
deployment_plan_incompatible: {
|
|
143
|
+
status: 409,
|
|
144
|
+
retryable: false,
|
|
145
|
+
action: "Run 'ohmyhost plan' again with the current source revision.",
|
|
146
|
+
},
|
|
147
|
+
confirmation_expired: {
|
|
148
|
+
status: 409,
|
|
149
|
+
retryable: false,
|
|
150
|
+
action: "Plan the destructive action again before confirming it.",
|
|
151
|
+
},
|
|
152
|
+
confirmation_invalid: {
|
|
153
|
+
status: 409,
|
|
154
|
+
retryable: false,
|
|
155
|
+
action: "Use the confirmation token from a fresh matching plan.",
|
|
156
|
+
},
|
|
157
|
+
etag_mismatch: {
|
|
158
|
+
status: 409,
|
|
159
|
+
retryable: false,
|
|
160
|
+
action: "Refresh the resource, plan the action again, and retry.",
|
|
161
|
+
},
|
|
162
|
+
promotion_source_stale: {
|
|
163
|
+
status: 409,
|
|
164
|
+
retryable: false,
|
|
165
|
+
action: "Read the current dev deployment, then plan promotion again.",
|
|
166
|
+
},
|
|
167
|
+
promotion_target_stale: {
|
|
168
|
+
status: 409,
|
|
169
|
+
retryable: false,
|
|
170
|
+
action: "Read the current prod status, then plan promotion again.",
|
|
171
|
+
},
|
|
172
|
+
promotion_invalid_target: {
|
|
173
|
+
status: 409,
|
|
174
|
+
retryable: false,
|
|
175
|
+
action: "Select the current succeeded dev deployment and target prod.",
|
|
176
|
+
},
|
|
177
|
+
mail_domain_conflict: {
|
|
178
|
+
status: 409,
|
|
179
|
+
retryable: false,
|
|
180
|
+
action: "Read and use the sender domain already configured for this project.",
|
|
181
|
+
},
|
|
182
|
+
mail_domain_required: {
|
|
183
|
+
status: 409,
|
|
184
|
+
retryable: false,
|
|
185
|
+
action: "Run 'ohmyhost mail domain set' for this project, then plan again.",
|
|
186
|
+
},
|
|
187
|
+
storage_jurisdiction_conflict: {
|
|
188
|
+
status: 409,
|
|
189
|
+
retryable: false,
|
|
190
|
+
action:
|
|
191
|
+
"Set storage.jurisdiction in ohmyhost.yaml to the project's hosting region shown by project status, then plan again.",
|
|
192
|
+
},
|
|
193
|
+
framework_conversion_required: {
|
|
194
|
+
status: 409,
|
|
195
|
+
retryable: false,
|
|
196
|
+
action:
|
|
197
|
+
"Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit.",
|
|
198
|
+
},
|
|
199
|
+
migration_filename_noncanonical: {
|
|
200
|
+
status: 409,
|
|
201
|
+
retryable: false,
|
|
202
|
+
action:
|
|
203
|
+
"Rename every migration to YYYYMMDDHHMMSS_name.sql, commit the change, then plan again.",
|
|
204
|
+
},
|
|
205
|
+
environment_secret_mutation_blocked: {
|
|
206
|
+
status: 409,
|
|
207
|
+
retryable: true,
|
|
208
|
+
action: "Wait for the active deployment or rollback to finish, then retry unchanged.",
|
|
209
|
+
},
|
|
210
|
+
workers_runtime_incompatible: {
|
|
211
|
+
status: 409,
|
|
212
|
+
retryable: false,
|
|
213
|
+
action: "Remove or replace the reported module, commit the change, then create a new plan.",
|
|
214
|
+
},
|
|
215
|
+
payload_too_large: { status: 413, retryable: false, action: "Reduce the request size." },
|
|
216
|
+
project_export_not_ready: {
|
|
217
|
+
status: 409,
|
|
218
|
+
retryable: false,
|
|
219
|
+
action: "Finish database provisioning, then request the export again. No backup was accepted.",
|
|
220
|
+
},
|
|
221
|
+
rate_limited: { status: 429, retryable: true, action: "Retry after the server delay." },
|
|
222
|
+
insufficient_organization_credits: {
|
|
223
|
+
status: 402,
|
|
224
|
+
retryable: false,
|
|
225
|
+
action:
|
|
226
|
+
"Add organization credits or wait for reservations to settle, then retry the same request.",
|
|
227
|
+
},
|
|
228
|
+
project_budget_exceeded: {
|
|
229
|
+
status: 402,
|
|
230
|
+
retryable: false,
|
|
231
|
+
action: "Raise the project's stop budget or select continue, then retry the same request.",
|
|
232
|
+
},
|
|
233
|
+
paid_plan_required: {
|
|
234
|
+
status: 402,
|
|
235
|
+
retryable: false,
|
|
236
|
+
action:
|
|
237
|
+
"Ask the organization Owner to activate Paid through billing checkout, then retry. Top-ups and signup credits do not activate Paid.",
|
|
238
|
+
},
|
|
239
|
+
interactive_login_required: {
|
|
240
|
+
status: 403,
|
|
241
|
+
retryable: false,
|
|
242
|
+
action:
|
|
243
|
+
"Unset OHMYHOST_TOKEN and run ohmyhost login before managing tokens. Existing token files remain unchanged.",
|
|
244
|
+
},
|
|
245
|
+
api_key_creation_uncertain: {
|
|
246
|
+
status: 503,
|
|
247
|
+
retryable: true,
|
|
248
|
+
action:
|
|
249
|
+
"Repeat the original token name and Idempotency-Key to observe its result; do not blindly create another key.",
|
|
250
|
+
},
|
|
251
|
+
api_key_permissions_unavailable: {
|
|
252
|
+
status: 503,
|
|
253
|
+
retryable: false,
|
|
254
|
+
action:
|
|
255
|
+
"Report the platform user-key permission configuration through feedback. Repeated login or creation will not fix it.",
|
|
256
|
+
},
|
|
257
|
+
reconciliation_exhausted: {
|
|
258
|
+
status: 409,
|
|
259
|
+
retryable: false,
|
|
260
|
+
action:
|
|
261
|
+
"Stop retries and submit feedback with this operation ID. A platform recovery review is required.",
|
|
262
|
+
},
|
|
263
|
+
service_unavailable: { status: 503, retryable: true, action: "Retry later." },
|
|
264
|
+
} as const satisfies Readonly<
|
|
265
|
+
Record<
|
|
266
|
+
ProblemDetails["code"],
|
|
267
|
+
{ readonly status: number; readonly retryable: boolean; readonly action: string }
|
|
268
|
+
>
|
|
269
|
+
>;
|
|
270
|
+
|
|
271
|
+
type ProblemCode = keyof typeof PROBLEM_POLICY;
|
|
272
|
+
|
|
273
|
+
export class ResponseContractError extends Error {
|
|
274
|
+
public constructor() {
|
|
275
|
+
super("The server response does not match the public contract.");
|
|
276
|
+
this.name = "ResponseContractError";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface SafeProblem {
|
|
281
|
+
readonly retryAfterSeconds?: number;
|
|
282
|
+
readonly code: ProblemCode;
|
|
283
|
+
readonly retryable: boolean;
|
|
284
|
+
readonly suggestedAction: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface PublicCurrentIdentity {
|
|
288
|
+
readonly actor_id: string;
|
|
289
|
+
readonly organization_ids: readonly string[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** One workspace the signed-in user belongs to, with the provider identity a selection needs. */
|
|
293
|
+
export interface PublicWorkspace {
|
|
294
|
+
readonly id: string;
|
|
295
|
+
readonly workos_id: string;
|
|
296
|
+
readonly name: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface PublicDevAccessTicket {
|
|
300
|
+
readonly ticket_id: string;
|
|
301
|
+
readonly project_id: string;
|
|
302
|
+
readonly environment_id: string;
|
|
303
|
+
readonly origin: string;
|
|
304
|
+
readonly redeem_url: string;
|
|
305
|
+
readonly expires_at: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface PublicProjectDatabaseQuery {
|
|
309
|
+
readonly environment: "dev" | "prod";
|
|
310
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
311
|
+
readonly row_count: number;
|
|
312
|
+
readonly truncated: boolean;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface PublicPaidDomainPlan {
|
|
316
|
+
readonly status: "planned";
|
|
317
|
+
readonly hostname: string;
|
|
318
|
+
readonly environment: "prod";
|
|
319
|
+
readonly cname_target: SaasCnameTarget;
|
|
320
|
+
readonly effects: readonly string[];
|
|
321
|
+
readonly risks: readonly string[];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface PublicPaidDomain {
|
|
325
|
+
readonly status:
|
|
326
|
+
| "not_configured"
|
|
327
|
+
| "pending"
|
|
328
|
+
| "active"
|
|
329
|
+
| "reconciliation_required"
|
|
330
|
+
| "deleted"
|
|
331
|
+
| "suspended";
|
|
332
|
+
readonly suspension_reason:
|
|
333
|
+
| "paid_plan_required"
|
|
334
|
+
| "insufficient_organization_credits"
|
|
335
|
+
| "project_budget_exceeded"
|
|
336
|
+
| null;
|
|
337
|
+
readonly hostname: string | null;
|
|
338
|
+
readonly environment: "prod";
|
|
339
|
+
readonly cname_target: SaasCnameTarget;
|
|
340
|
+
readonly custom_hostname_status: string | null;
|
|
341
|
+
readonly ssl_status: string | null;
|
|
342
|
+
readonly validation_records: readonly Readonly<Record<string, unknown>>[];
|
|
343
|
+
readonly url: string | null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const parsePaidDomainPlan = (value: unknown): PublicPaidDomainPlan => {
|
|
347
|
+
const input = exactRecord(value, [
|
|
348
|
+
"cname_target",
|
|
349
|
+
"effects",
|
|
350
|
+
"environment",
|
|
351
|
+
"hostname",
|
|
352
|
+
"risks",
|
|
353
|
+
"status",
|
|
354
|
+
]);
|
|
355
|
+
if (
|
|
356
|
+
input["status"] !== "planned" ||
|
|
357
|
+
input["environment"] !== "prod" ||
|
|
358
|
+
!isSaasCnameTarget(input["cname_target"]) ||
|
|
359
|
+
!isPaidHostname(input["hostname"]) ||
|
|
360
|
+
!shortStringArray(input["effects"], 8, 512) ||
|
|
361
|
+
!shortStringArray(input["risks"], 8, 512)
|
|
362
|
+
) {
|
|
363
|
+
throw new ResponseContractError();
|
|
364
|
+
}
|
|
365
|
+
return input as unknown as PublicPaidDomainPlan;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
export const parsePaidDomain = (value: unknown): PublicPaidDomain => {
|
|
369
|
+
const input = exactRecord(value, [
|
|
370
|
+
"cname_target",
|
|
371
|
+
"custom_hostname_status",
|
|
372
|
+
"environment",
|
|
373
|
+
"hostname",
|
|
374
|
+
"ssl_status",
|
|
375
|
+
"status",
|
|
376
|
+
"suspension_reason",
|
|
377
|
+
"url",
|
|
378
|
+
"validation_records",
|
|
379
|
+
]);
|
|
380
|
+
const status = input["status"];
|
|
381
|
+
const hostname = input["hostname"];
|
|
382
|
+
const records = input["validation_records"];
|
|
383
|
+
if (
|
|
384
|
+
![
|
|
385
|
+
"not_configured",
|
|
386
|
+
"pending",
|
|
387
|
+
"active",
|
|
388
|
+
"reconciliation_required",
|
|
389
|
+
"deleted",
|
|
390
|
+
"suspended",
|
|
391
|
+
].includes(String(status)) ||
|
|
392
|
+
![
|
|
393
|
+
null,
|
|
394
|
+
"paid_plan_required",
|
|
395
|
+
"insufficient_organization_credits",
|
|
396
|
+
"project_budget_exceeded",
|
|
397
|
+
].includes(input["suspension_reason"] as null | string) ||
|
|
398
|
+
(status === "suspended") !== (input["suspension_reason"] !== null) ||
|
|
399
|
+
(status === "suspended" && input["url"] !== null) ||
|
|
400
|
+
input["environment"] !== "prod" ||
|
|
401
|
+
!isSaasCnameTarget(input["cname_target"]) ||
|
|
402
|
+
(hostname !== null && !isPaidHostname(hostname)) ||
|
|
403
|
+
(input["custom_hostname_status"] !== null &&
|
|
404
|
+
typeof input["custom_hostname_status"] !== "string") ||
|
|
405
|
+
(input["ssl_status"] !== null && typeof input["ssl_status"] !== "string") ||
|
|
406
|
+
!Array.isArray(records) ||
|
|
407
|
+
records.length > 16 ||
|
|
408
|
+
!records.every(isPaidValidationRecord) ||
|
|
409
|
+
(input["url"] !== null && input["url"] !== `https://${String(hostname)}`)
|
|
410
|
+
) {
|
|
411
|
+
throw new ResponseContractError();
|
|
412
|
+
}
|
|
413
|
+
return input as unknown as PublicPaidDomain;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
export const parseProjectDatabaseQuery = (value: unknown): PublicProjectDatabaseQuery => {
|
|
417
|
+
const input = exactRecord(value, ["environment", "row_count", "rows", "truncated"]);
|
|
418
|
+
const rows = input["rows"];
|
|
419
|
+
if (
|
|
420
|
+
(input["environment"] !== "dev" && input["environment"] !== "prod") ||
|
|
421
|
+
!Array.isArray(rows) ||
|
|
422
|
+
rows.length > 100 ||
|
|
423
|
+
!rows.every((row) => typeof row === "object" && row !== null && !Array.isArray(row)) ||
|
|
424
|
+
!Number.isSafeInteger(input["row_count"]) ||
|
|
425
|
+
input["row_count"] !== rows.length ||
|
|
426
|
+
typeof input["truncated"] !== "boolean"
|
|
427
|
+
) {
|
|
428
|
+
throw new ResponseContractError();
|
|
429
|
+
}
|
|
430
|
+
let encoded: string;
|
|
431
|
+
try {
|
|
432
|
+
encoded = JSON.stringify(input);
|
|
433
|
+
} catch {
|
|
434
|
+
throw new ResponseContractError();
|
|
435
|
+
}
|
|
436
|
+
if (new TextEncoder().encode(encoded).byteLength > 256 * 1_024) {
|
|
437
|
+
throw new ResponseContractError();
|
|
438
|
+
}
|
|
439
|
+
return input as unknown as PublicProjectDatabaseQuery;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export interface PublicCloudflareDnsAuthorization {
|
|
443
|
+
readonly authorization_id: string;
|
|
444
|
+
readonly authorization_url: string;
|
|
445
|
+
readonly expires_at: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface PublicCloudflareDnsAuthorizationStatus {
|
|
449
|
+
readonly status: "not_authorized" | "pending" | "authorized" | "expired" | "revoked";
|
|
450
|
+
readonly zone: string | null;
|
|
451
|
+
readonly scopes: readonly ("dns.write" | "zone.read")[];
|
|
452
|
+
readonly expires_at: string | null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export const parseCloudflareDnsAuthorization = (
|
|
456
|
+
value: unknown,
|
|
457
|
+
): PublicCloudflareDnsAuthorization => {
|
|
458
|
+
const input = exactRecord(value, ["authorization_id", "authorization_url", "expires_at"]);
|
|
459
|
+
const authorizationUrl = input["authorization_url"];
|
|
460
|
+
if (
|
|
461
|
+
!isUlid(input["authorization_id"]) ||
|
|
462
|
+
!boundedString(authorizationUrl, 20, 4096) ||
|
|
463
|
+
!isIsoInstant(input["expires_at"])
|
|
464
|
+
) {
|
|
465
|
+
throw new ResponseContractError();
|
|
466
|
+
}
|
|
467
|
+
let url: URL;
|
|
468
|
+
try {
|
|
469
|
+
url = new URL(authorizationUrl);
|
|
470
|
+
} catch {
|
|
471
|
+
throw new ResponseContractError();
|
|
472
|
+
}
|
|
473
|
+
if (
|
|
474
|
+
url.protocol !== "https:" ||
|
|
475
|
+
url.username !== "" ||
|
|
476
|
+
url.password !== "" ||
|
|
477
|
+
url.hostname !== "dash.cloudflare.com" ||
|
|
478
|
+
url.pathname !== "/oauth2/auth" ||
|
|
479
|
+
url.hash !== "" ||
|
|
480
|
+
!url.searchParams.has("client_id") ||
|
|
481
|
+
!url.searchParams.has("state")
|
|
482
|
+
) {
|
|
483
|
+
throw new ResponseContractError();
|
|
484
|
+
}
|
|
485
|
+
return input as unknown as PublicCloudflareDnsAuthorization;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const parseGithubOrganizationConnection = (value: unknown): GithubOrganizationConnection => {
|
|
489
|
+
const input = exactRecord(value, [
|
|
490
|
+
"connection_id",
|
|
491
|
+
"organization_id",
|
|
492
|
+
"installation_id",
|
|
493
|
+
"account_id",
|
|
494
|
+
"account_login",
|
|
495
|
+
"account_type",
|
|
496
|
+
"github_user_id",
|
|
497
|
+
"github_user_login",
|
|
498
|
+
"connected_at",
|
|
499
|
+
"revoked_at",
|
|
500
|
+
"settings_url",
|
|
501
|
+
]);
|
|
502
|
+
if (
|
|
503
|
+
!isUlid(input["connection_id"]) ||
|
|
504
|
+
!isUlid(input["organization_id"]) ||
|
|
505
|
+
!["installation_id", "account_id", "github_user_id"].every(
|
|
506
|
+
(key) => typeof input[key] === "string" && /^[1-9][0-9]{0,19}$/u.test(input[key]),
|
|
507
|
+
) ||
|
|
508
|
+
!["account_login", "github_user_login"].every(
|
|
509
|
+
(key) =>
|
|
510
|
+
typeof input[key] === "string" &&
|
|
511
|
+
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u.test(input[key]),
|
|
512
|
+
) ||
|
|
513
|
+
!["User", "Organization"].includes(String(input["account_type"])) ||
|
|
514
|
+
!isIsoInstant(input["connected_at"]) ||
|
|
515
|
+
(input["revoked_at"] !== null && !isIsoInstant(input["revoked_at"]))
|
|
516
|
+
)
|
|
517
|
+
throw new ResponseContractError();
|
|
518
|
+
const expected =
|
|
519
|
+
input["account_type"] === "User"
|
|
520
|
+
? `https://github.com/settings/installations/${String(input["installation_id"])}`
|
|
521
|
+
: `https://github.com/organizations/${String(input["account_login"])}/settings/installations/${String(input["installation_id"])}`;
|
|
522
|
+
if (input["settings_url"] !== expected) throw new ResponseContractError();
|
|
523
|
+
return input as unknown as GithubOrganizationConnection;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export const parseGithubOrganizationConnectionStatus = (
|
|
527
|
+
value: unknown,
|
|
528
|
+
): GithubOrganizationConnectionStatus => {
|
|
529
|
+
const input = exactRecord(value, ["status", "connection"]);
|
|
530
|
+
if (!["not_connected", "connected", "revoked"].includes(String(input["status"])))
|
|
531
|
+
throw new ResponseContractError();
|
|
532
|
+
if (input["status"] === "not_connected") {
|
|
533
|
+
if (input["connection"] !== null) throw new ResponseContractError();
|
|
534
|
+
} else {
|
|
535
|
+
const connection = parseGithubOrganizationConnection(input["connection"]);
|
|
536
|
+
if ((input["status"] === "connected") !== (connection.revoked_at === null))
|
|
537
|
+
throw new ResponseContractError();
|
|
538
|
+
}
|
|
539
|
+
return input as unknown as GithubOrganizationConnectionStatus;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
export const parseGithubConnectionAuthorization = (
|
|
543
|
+
value: unknown,
|
|
544
|
+
): GithubConnectionAuthorization => {
|
|
545
|
+
const input = exactRecord(value, [
|
|
546
|
+
"authorization_id",
|
|
547
|
+
"status",
|
|
548
|
+
"last_failure",
|
|
549
|
+
"authorization_url",
|
|
550
|
+
"expires_at",
|
|
551
|
+
"connection",
|
|
552
|
+
]);
|
|
553
|
+
if (
|
|
554
|
+
!["pending", "authorizing", "connected", "failed", "expired"].includes(
|
|
555
|
+
String(input["status"]),
|
|
556
|
+
) ||
|
|
557
|
+
!(
|
|
558
|
+
input["last_failure"] === null ||
|
|
559
|
+
[
|
|
560
|
+
"installation_access_required",
|
|
561
|
+
"account_admin_required",
|
|
562
|
+
"authorization_code_rejected",
|
|
563
|
+
"provider_unavailable",
|
|
564
|
+
"expired",
|
|
565
|
+
"session_inactive",
|
|
566
|
+
"denied",
|
|
567
|
+
"code_spent",
|
|
568
|
+
"state_unknown",
|
|
569
|
+
].includes(String(input["last_failure"]))
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
throw new ResponseContractError();
|
|
573
|
+
if (input["authorization_id"] === null || input["expires_at"] === null) {
|
|
574
|
+
if (
|
|
575
|
+
input["status"] !== "connected" ||
|
|
576
|
+
input["authorization_id"] !== null ||
|
|
577
|
+
input["expires_at"] !== null
|
|
578
|
+
)
|
|
579
|
+
throw new ResponseContractError();
|
|
580
|
+
} else if (!isUlid(input["authorization_id"]) || !isIsoInstant(input["expires_at"]))
|
|
581
|
+
throw new ResponseContractError();
|
|
582
|
+
if (input["status"] === "pending") {
|
|
583
|
+
let url: URL;
|
|
584
|
+
try {
|
|
585
|
+
url = new URL(String(input["authorization_url"]));
|
|
586
|
+
} catch {
|
|
587
|
+
throw new ResponseContractError();
|
|
588
|
+
}
|
|
589
|
+
const keys = [...url.searchParams.keys()];
|
|
590
|
+
const installation =
|
|
591
|
+
/^\/apps\/[a-z0-9-]+\/installations\/new$/u.test(url.pathname) &&
|
|
592
|
+
keys.length === 1 &&
|
|
593
|
+
keys[0] === "state";
|
|
594
|
+
const oauthKeys = [
|
|
595
|
+
"client_id",
|
|
596
|
+
"redirect_uri",
|
|
597
|
+
"state",
|
|
598
|
+
"code_challenge",
|
|
599
|
+
"code_challenge_method",
|
|
600
|
+
"prompt",
|
|
601
|
+
];
|
|
602
|
+
const oauth =
|
|
603
|
+
url.pathname === "/login/oauth/authorize" &&
|
|
604
|
+
keys.length === oauthKeys.length &&
|
|
605
|
+
new Set(keys).size === keys.length &&
|
|
606
|
+
keys.every((key) => oauthKeys.includes(key)) &&
|
|
607
|
+
/^[A-Za-z0-9_]{1,256}$/u.test(url.searchParams.get("client_id") ?? "") &&
|
|
608
|
+
[
|
|
609
|
+
"https://app.ohmyho.st/v1/github/oauth/callback",
|
|
610
|
+
"https://dev.app.ohmyho.st/v1/github/oauth/callback",
|
|
611
|
+
].includes(url.searchParams.get("redirect_uri") ?? "") &&
|
|
612
|
+
/^[A-Za-z0-9_-]{43}$/u.test(url.searchParams.get("code_challenge") ?? "") &&
|
|
613
|
+
url.searchParams.get("code_challenge_method") === "S256" &&
|
|
614
|
+
url.searchParams.get("prompt") === "select_account";
|
|
615
|
+
if (
|
|
616
|
+
url.origin !== "https://github.com" ||
|
|
617
|
+
url.username ||
|
|
618
|
+
url.password ||
|
|
619
|
+
url.hash ||
|
|
620
|
+
!boundedString(url.searchParams.get("state"), 1, 512) ||
|
|
621
|
+
!(installation || oauth)
|
|
622
|
+
)
|
|
623
|
+
throw new ResponseContractError();
|
|
624
|
+
} else if (input["authorization_url"] !== null) throw new ResponseContractError();
|
|
625
|
+
if (input["status"] === "connected") {
|
|
626
|
+
if (
|
|
627
|
+
parseGithubOrganizationConnection(input["connection"]).revoked_at !== null ||
|
|
628
|
+
input["last_failure"] !== null
|
|
629
|
+
)
|
|
630
|
+
throw new ResponseContractError();
|
|
631
|
+
} else if (input["connection"] !== null) throw new ResponseContractError();
|
|
632
|
+
return input as unknown as GithubConnectionAuthorization;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
export const parseCloudflareDnsAuthorizationStatus = (
|
|
636
|
+
value: unknown,
|
|
637
|
+
): PublicCloudflareDnsAuthorizationStatus => {
|
|
638
|
+
const input = exactRecord(value, ["status", "zone", "scopes", "expires_at"]);
|
|
639
|
+
const status = input["status"];
|
|
640
|
+
const scopes = input["scopes"];
|
|
641
|
+
if (
|
|
642
|
+
(status !== "not_authorized" &&
|
|
643
|
+
status !== "pending" &&
|
|
644
|
+
status !== "authorized" &&
|
|
645
|
+
status !== "expired" &&
|
|
646
|
+
status !== "revoked") ||
|
|
647
|
+
(status === "not_authorized"
|
|
648
|
+
? input["zone"] !== null
|
|
649
|
+
: typeof input["zone"] !== "string" || !isPaidHostname(input["zone"])) ||
|
|
650
|
+
!Array.isArray(scopes) ||
|
|
651
|
+
scopes.length > 2 ||
|
|
652
|
+
scopes.some((scope) => scope !== "dns.write" && scope !== "zone.read") ||
|
|
653
|
+
new Set(scopes).size !== scopes.length ||
|
|
654
|
+
(input["expires_at"] !== null && !isIsoInstant(input["expires_at"])) ||
|
|
655
|
+
(status === "authorized" && [...scopes].sort().join(",") !== "dns.write,zone.read") ||
|
|
656
|
+
(status === "not_authorized" && (scopes.length !== 0 || input["expires_at"] !== null))
|
|
657
|
+
) {
|
|
658
|
+
throw new ResponseContractError();
|
|
659
|
+
}
|
|
660
|
+
return input as unknown as PublicCloudflareDnsAuthorizationStatus;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
export const parseDevAccessTicket = (value: unknown): PublicDevAccessTicket => {
|
|
664
|
+
const input = exactRecord(value, [
|
|
665
|
+
"ticket_id",
|
|
666
|
+
"project_id",
|
|
667
|
+
"environment_id",
|
|
668
|
+
"origin",
|
|
669
|
+
"redeem_url",
|
|
670
|
+
"expires_at",
|
|
671
|
+
]);
|
|
672
|
+
const origin = input["origin"];
|
|
673
|
+
const redeemUrl = input["redeem_url"];
|
|
674
|
+
if (
|
|
675
|
+
!isUlid(input["ticket_id"]) ||
|
|
676
|
+
!isUlid(input["project_id"]) ||
|
|
677
|
+
!isUlid(input["environment_id"]) ||
|
|
678
|
+
typeof origin !== "string" ||
|
|
679
|
+
!/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$/u.test(origin) ||
|
|
680
|
+
typeof redeemUrl !== "string" ||
|
|
681
|
+
!/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(
|
|
682
|
+
redeemUrl,
|
|
683
|
+
) ||
|
|
684
|
+
new URL(redeemUrl).origin !== origin ||
|
|
685
|
+
!isIsoInstant(input["expires_at"])
|
|
686
|
+
) {
|
|
687
|
+
throw new ResponseContractError();
|
|
688
|
+
}
|
|
689
|
+
return input as unknown as PublicDevAccessTicket;
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
export const parseOrganization = (
|
|
693
|
+
value: unknown,
|
|
694
|
+
): { readonly id: string; readonly name: string } => {
|
|
695
|
+
const input = exactRecord(value, ["id", "name"]);
|
|
696
|
+
if (
|
|
697
|
+
!isUlid(input["id"]) ||
|
|
698
|
+
typeof input["name"] !== "string" ||
|
|
699
|
+
input["name"].length < 1 ||
|
|
700
|
+
input["name"].length > 128
|
|
701
|
+
)
|
|
702
|
+
throw new ResponseContractError();
|
|
703
|
+
return { id: input["id"], name: input["name"] };
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
export function parseOrganizationCredits(value: unknown): OrganizationCredits {
|
|
707
|
+
const input = exactRecord(value, [
|
|
708
|
+
"organization_id",
|
|
709
|
+
"unit",
|
|
710
|
+
"as_of",
|
|
711
|
+
"available_micros",
|
|
712
|
+
"reserved_micros",
|
|
713
|
+
"spent_micros",
|
|
714
|
+
"expired_micros",
|
|
715
|
+
"platform_overrun_micros",
|
|
716
|
+
"grace_started_at",
|
|
717
|
+
"grace_expires_at",
|
|
718
|
+
"active_meters",
|
|
719
|
+
"rate_cards",
|
|
720
|
+
]);
|
|
721
|
+
try {
|
|
722
|
+
assertPublishedCreditRateCards(input["rate_cards"]);
|
|
723
|
+
} catch {
|
|
724
|
+
throw new ResponseContractError();
|
|
725
|
+
}
|
|
726
|
+
const graceStart = input["grace_started_at"],
|
|
727
|
+
graceEnd = input["grace_expires_at"];
|
|
728
|
+
if (
|
|
729
|
+
!(graceStart === null && graceEnd === null) &&
|
|
730
|
+
!(
|
|
731
|
+
isIsoInstant(graceStart) &&
|
|
732
|
+
isIsoInstant(graceEnd) &&
|
|
733
|
+
Date.parse(graceEnd) - Date.parse(graceStart) === 604_800_000
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
throw new ResponseContractError();
|
|
737
|
+
if (
|
|
738
|
+
!isUlid(input["organization_id"]) ||
|
|
739
|
+
input["unit"] !== "microcredits" ||
|
|
740
|
+
!isIsoInstant(input["as_of"]) ||
|
|
741
|
+
!Array.isArray(input["active_meters"]) ||
|
|
742
|
+
input["active_meters"].some(
|
|
743
|
+
(m: unknown) => typeof m !== "string" || !/^[a-z0-9_.-]{1,128}$/u.test(m),
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
throw new ResponseContractError();
|
|
747
|
+
for (const field of [
|
|
748
|
+
"available_micros",
|
|
749
|
+
"reserved_micros",
|
|
750
|
+
"spent_micros",
|
|
751
|
+
"expired_micros",
|
|
752
|
+
"platform_overrun_micros",
|
|
753
|
+
]) {
|
|
754
|
+
const amount = input[field];
|
|
755
|
+
if (
|
|
756
|
+
typeof amount !== "string" ||
|
|
757
|
+
!/^(0|[1-9][0-9]{0,18})$/u.test(amount) ||
|
|
758
|
+
BigInt(amount) > 9223372036854775807n
|
|
759
|
+
)
|
|
760
|
+
throw new ResponseContractError();
|
|
761
|
+
}
|
|
762
|
+
return input as unknown as OrganizationCredits;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function parseProjectCreditBudget(value: unknown): ProjectCreditBudget {
|
|
766
|
+
const input = exactRecord(value, [
|
|
767
|
+
"project_id",
|
|
768
|
+
"amount_micros",
|
|
769
|
+
"mode",
|
|
770
|
+
"used_micros",
|
|
771
|
+
"reserved_micros",
|
|
772
|
+
"period_start",
|
|
773
|
+
"period_end",
|
|
774
|
+
"as_of",
|
|
775
|
+
]);
|
|
776
|
+
if (
|
|
777
|
+
!isUlid(input["project_id"]) ||
|
|
778
|
+
(input["mode"] !== "continue" && input["mode"] !== "stop") ||
|
|
779
|
+
(input["amount_micros"] === null && input["mode"] !== "continue")
|
|
780
|
+
)
|
|
781
|
+
throw new ResponseContractError();
|
|
782
|
+
for (const field of ["amount_micros", "used_micros", "reserved_micros"]) {
|
|
783
|
+
const amount = input[field];
|
|
784
|
+
if (field === "amount_micros" && amount === null) continue;
|
|
785
|
+
if (
|
|
786
|
+
typeof amount !== "string" ||
|
|
787
|
+
!/^(0|[1-9][0-9]{0,18})$/u.test(amount) ||
|
|
788
|
+
BigInt(amount) > 9223372036854775807n
|
|
789
|
+
)
|
|
790
|
+
throw new ResponseContractError();
|
|
791
|
+
}
|
|
792
|
+
for (const field of ["period_start", "period_end", "as_of"])
|
|
793
|
+
if (!isIsoInstant(input[field])) throw new ResponseContractError();
|
|
794
|
+
return input as unknown as ProjectCreditBudget;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export const parseCurrentIdentity = (value: unknown): PublicCurrentIdentity => {
|
|
798
|
+
const input = exactRecord(value, ["actor_id", "organization_ids"]);
|
|
799
|
+
const organizationIds = input["organization_ids"];
|
|
800
|
+
if (
|
|
801
|
+
typeof input["actor_id"] !== "string" ||
|
|
802
|
+
input["actor_id"].length === 0 ||
|
|
803
|
+
input["actor_id"].length > 256 ||
|
|
804
|
+
[...input["actor_id"]].some((character) => {
|
|
805
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
806
|
+
return codePoint <= 31 || codePoint === 127;
|
|
807
|
+
}) ||
|
|
808
|
+
!Array.isArray(organizationIds) ||
|
|
809
|
+
organizationIds.some((organizationId) => !isUlid(organizationId)) ||
|
|
810
|
+
new Set(organizationIds).size !== organizationIds.length
|
|
811
|
+
) {
|
|
812
|
+
throw new ResponseContractError();
|
|
813
|
+
}
|
|
814
|
+
return input as unknown as PublicCurrentIdentity;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
export const parseAccountProfile = (value: unknown): readonly PublicWorkspace[] => {
|
|
818
|
+
const input = exactRecord(value, [
|
|
819
|
+
"user_id",
|
|
820
|
+
"name",
|
|
821
|
+
"email",
|
|
822
|
+
"email_verified",
|
|
823
|
+
"organizations",
|
|
824
|
+
"organization_ids",
|
|
825
|
+
"signup_source",
|
|
826
|
+
"attributed_at",
|
|
827
|
+
"initial_workspace_id",
|
|
828
|
+
]);
|
|
829
|
+
const organizations = input["organizations"];
|
|
830
|
+
const organizationIds = input["organization_ids"];
|
|
831
|
+
if (
|
|
832
|
+
!boundedString(input["user_id"], 1, 128) ||
|
|
833
|
+
!Array.isArray(organizations) ||
|
|
834
|
+
!Array.isArray(organizationIds) ||
|
|
835
|
+
organizationIds.some((organizationId) => !isUlid(organizationId)) ||
|
|
836
|
+
new Set(organizationIds).size !== organizationIds.length
|
|
837
|
+
) {
|
|
838
|
+
throw new ResponseContractError();
|
|
839
|
+
}
|
|
840
|
+
const workspaces = organizations.map((organization: unknown) => {
|
|
841
|
+
const entry = exactRecord(organization, ["id", "workos_id", "name"]);
|
|
842
|
+
if (
|
|
843
|
+
!isUlid(entry["id"]) ||
|
|
844
|
+
!organizationIds.includes(entry["id"]) ||
|
|
845
|
+
!boundedString(entry["workos_id"], 1, 128) ||
|
|
846
|
+
!/^org_[A-Za-z0-9_]{1,120}$/u.test(entry["workos_id"]) ||
|
|
847
|
+
!boundedString(entry["name"], 1, 128)
|
|
848
|
+
) {
|
|
849
|
+
throw new ResponseContractError();
|
|
850
|
+
}
|
|
851
|
+
return { id: entry["id"], workos_id: entry["workos_id"], name: entry["name"] };
|
|
852
|
+
});
|
|
853
|
+
if (new Set(workspaces.map((workspace) => workspace.id)).size !== organizationIds.length)
|
|
854
|
+
throw new ResponseContractError();
|
|
855
|
+
return workspaces;
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
export interface PublicProject {
|
|
859
|
+
readonly id: string;
|
|
860
|
+
readonly organization_id: string;
|
|
861
|
+
readonly name: string;
|
|
862
|
+
readonly handle: string;
|
|
863
|
+
readonly data_mode: "shared" | "isolated";
|
|
864
|
+
readonly region: "us" | "eu";
|
|
865
|
+
readonly created_at: string;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const parseProject = (value: unknown): PublicProject => {
|
|
869
|
+
const input = exactRecord(value, [
|
|
870
|
+
"id",
|
|
871
|
+
"organization_id",
|
|
872
|
+
"name",
|
|
873
|
+
"handle",
|
|
874
|
+
"data_mode",
|
|
875
|
+
"region",
|
|
876
|
+
"created_at",
|
|
877
|
+
]);
|
|
878
|
+
if (
|
|
879
|
+
!isUlid(input["id"]) ||
|
|
880
|
+
!isUlid(input["organization_id"]) ||
|
|
881
|
+
!boundedString(input["name"], 1, 128) ||
|
|
882
|
+
input["name"] !== input["name"].trim() ||
|
|
883
|
+
!boundedString(input["handle"], 5, 59) ||
|
|
884
|
+
!/^[a-z]+-[a-z]+-[a-z]+$/u.test(input["handle"]) ||
|
|
885
|
+
(input["data_mode"] !== "shared" && input["data_mode"] !== "isolated") ||
|
|
886
|
+
(input["region"] !== "us" && input["region"] !== "eu") ||
|
|
887
|
+
!isIsoInstant(input["created_at"])
|
|
888
|
+
) {
|
|
889
|
+
throw new ResponseContractError();
|
|
890
|
+
}
|
|
891
|
+
return input as unknown as PublicProject;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
export const parseProjectPage = (
|
|
895
|
+
value: unknown,
|
|
896
|
+
): Readonly<{ items: readonly PublicProject[]; next_cursor?: string }> => {
|
|
897
|
+
const input = exactRecord(value, ["items", "next_cursor"]);
|
|
898
|
+
if (
|
|
899
|
+
!Array.isArray(input["items"]) ||
|
|
900
|
+
(input["next_cursor"] !== undefined && !isUlid(input["next_cursor"]))
|
|
901
|
+
) {
|
|
902
|
+
throw new ResponseContractError();
|
|
903
|
+
}
|
|
904
|
+
return Object.freeze({
|
|
905
|
+
items: input["items"].map(parseProject),
|
|
906
|
+
...(input["next_cursor"] === undefined ? {} : { next_cursor: input["next_cursor"] }),
|
|
907
|
+
});
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
export const parseProjectStatus = (value: unknown): Readonly<Record<string, unknown>> => {
|
|
911
|
+
const input = exactRecord(value, [
|
|
912
|
+
"project_id",
|
|
913
|
+
"handle",
|
|
914
|
+
"region",
|
|
915
|
+
"lifecycle",
|
|
916
|
+
"source",
|
|
917
|
+
"default_environment",
|
|
918
|
+
"environments",
|
|
919
|
+
"head_deployment",
|
|
920
|
+
"dev_url",
|
|
921
|
+
"prod_url",
|
|
922
|
+
"latest_operation",
|
|
923
|
+
"cleanup_state",
|
|
924
|
+
]);
|
|
925
|
+
const handle = input["handle"];
|
|
926
|
+
if (
|
|
927
|
+
!isUlid(input["project_id"]) ||
|
|
928
|
+
!boundedString(handle, 5, 59) ||
|
|
929
|
+
!/^[a-z]+-[a-z]+-[a-z]+$/u.test(handle) ||
|
|
930
|
+
(input["region"] !== "us" && input["region"] !== "eu") ||
|
|
931
|
+
(input["lifecycle"] !== "active" &&
|
|
932
|
+
input["lifecycle"] !== "deleting" &&
|
|
933
|
+
input["lifecycle"] !== "deleted") ||
|
|
934
|
+
(input["dev_url"] !== null &&
|
|
935
|
+
![`https://dev-${handle}.dev.check.omh.st`, `https://dev-${handle}.check.omh.st`].includes(
|
|
936
|
+
String(input["dev_url"]),
|
|
937
|
+
)) ||
|
|
938
|
+
(input["prod_url"] !== null &&
|
|
939
|
+
![`https://${handle}.dev.check.omh.st`, `https://${handle}.check.omh.st`].includes(
|
|
940
|
+
String(input["prod_url"]),
|
|
941
|
+
)) ||
|
|
942
|
+
(input["cleanup_state"] !== "not_started" &&
|
|
943
|
+
input["cleanup_state"] !== "pending" &&
|
|
944
|
+
input["cleanup_state"] !== "completed" &&
|
|
945
|
+
input["cleanup_state"] !== "reconciliation_required")
|
|
946
|
+
) {
|
|
947
|
+
throw new ResponseContractError();
|
|
948
|
+
}
|
|
949
|
+
if (input["source"] !== null) parseProjectSource(input["source"]);
|
|
950
|
+
if (input["default_environment"] !== null) {
|
|
951
|
+
const environment = exactRecord(input["default_environment"], ["id", "name"]);
|
|
952
|
+
if (!isUlid(environment["id"]) || !boundedString(environment["name"], 1, 128)) {
|
|
953
|
+
throw new ResponseContractError();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (input["head_deployment"] !== null) parseProjectDeployment(input["head_deployment"]);
|
|
957
|
+
if (input["environments"] !== undefined) {
|
|
958
|
+
const environments = input["environments"];
|
|
959
|
+
if (!Array.isArray(environments) || environments.length > 2) throw new ResponseContractError();
|
|
960
|
+
const ids = new Set<string>(),
|
|
961
|
+
names = new Set<string>();
|
|
962
|
+
for (const raw of environments) {
|
|
963
|
+
const environment = exactRecord(raw, ["id", "name"]);
|
|
964
|
+
const id = environment["id"],
|
|
965
|
+
name = environment["name"];
|
|
966
|
+
if (
|
|
967
|
+
!isUlid(id) ||
|
|
968
|
+
(name !== "dev" && name !== "prod") ||
|
|
969
|
+
ids.has(id as string) ||
|
|
970
|
+
names.has(name)
|
|
971
|
+
)
|
|
972
|
+
throw new ResponseContractError();
|
|
973
|
+
ids.add(id as string);
|
|
974
|
+
names.add(name);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (input["latest_operation"] !== null) parseOperation(input["latest_operation"]);
|
|
978
|
+
return input;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const parseProjectSource = (value: unknown): void => {
|
|
982
|
+
const input = exactRecord(value, [
|
|
983
|
+
"provider",
|
|
984
|
+
"installation_id",
|
|
985
|
+
"repository_full_name",
|
|
986
|
+
"status",
|
|
987
|
+
"linked_at",
|
|
988
|
+
"updated_at",
|
|
989
|
+
"failure_summary",
|
|
990
|
+
]);
|
|
991
|
+
if (
|
|
992
|
+
input["provider"] !== "github" ||
|
|
993
|
+
typeof input["installation_id"] !== "string" ||
|
|
994
|
+
!/^[1-9][0-9]{0,19}$/u.test(input["installation_id"]) ||
|
|
995
|
+
typeof input["repository_full_name"] !== "string" ||
|
|
996
|
+
!/^[^/]+\/[^/]+$/u.test(input["repository_full_name"]) ||
|
|
997
|
+
(input["status"] !== "pending" &&
|
|
998
|
+
input["status"] !== "ready" &&
|
|
999
|
+
input["status"] !== "failed" &&
|
|
1000
|
+
input["status"] !== "revoked") ||
|
|
1001
|
+
!isIsoInstant(input["linked_at"]) ||
|
|
1002
|
+
!isIsoInstant(input["updated_at"]) ||
|
|
1003
|
+
(input["failure_summary"] !== undefined && !boundedString(input["failure_summary"], 1, 512))
|
|
1004
|
+
) {
|
|
1005
|
+
throw new ResponseContractError();
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const parseProjectDeployment = (value: unknown): void => {
|
|
1010
|
+
const input = exactRecord(value, [
|
|
1011
|
+
"id",
|
|
1012
|
+
"project_id",
|
|
1013
|
+
"operation_id",
|
|
1014
|
+
"commit_sha",
|
|
1015
|
+
"source_digest",
|
|
1016
|
+
"build_plan_digest",
|
|
1017
|
+
"artifact_digest",
|
|
1018
|
+
"status",
|
|
1019
|
+
"url",
|
|
1020
|
+
"created_at",
|
|
1021
|
+
"updated_at",
|
|
1022
|
+
]);
|
|
1023
|
+
if (
|
|
1024
|
+
!isUlid(input["id"]) ||
|
|
1025
|
+
!isUlid(input["project_id"]) ||
|
|
1026
|
+
!isUlid(input["operation_id"]) ||
|
|
1027
|
+
!isCommitSha(input["commit_sha"]) ||
|
|
1028
|
+
!isDigest(input["source_digest"]) ||
|
|
1029
|
+
!isDigest(input["build_plan_digest"]) ||
|
|
1030
|
+
(input["artifact_digest"] !== undefined && !isDigest(input["artifact_digest"])) ||
|
|
1031
|
+
typeof input["status"] !== "string" ||
|
|
1032
|
+
![
|
|
1033
|
+
"queued",
|
|
1034
|
+
"building",
|
|
1035
|
+
"publishing",
|
|
1036
|
+
"active",
|
|
1037
|
+
"failed",
|
|
1038
|
+
"rolled_back",
|
|
1039
|
+
"deleting",
|
|
1040
|
+
"deleted",
|
|
1041
|
+
"reconciliation_required",
|
|
1042
|
+
].includes(input["status"]) ||
|
|
1043
|
+
(input["url"] !== undefined && !isHttpsUrlValue(input["url"])) ||
|
|
1044
|
+
!isIsoInstant(input["created_at"]) ||
|
|
1045
|
+
!isIsoInstant(input["updated_at"])
|
|
1046
|
+
) {
|
|
1047
|
+
throw new ResponseContractError();
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
export interface PublicOperation {
|
|
1052
|
+
readonly blocking_operation_id?: string;
|
|
1053
|
+
readonly progress?: OperationDeploymentProgress;
|
|
1054
|
+
readonly id: string;
|
|
1055
|
+
readonly state: "queued" | "running" | "succeeded" | "failed" | "cancelled";
|
|
1056
|
+
readonly created_at: string;
|
|
1057
|
+
readonly updated_at: string;
|
|
1058
|
+
readonly result?: Readonly<Record<string, unknown>>;
|
|
1059
|
+
readonly error?: PublicOperationFailure;
|
|
1060
|
+
readonly reconciliation?: PublicOperationReconciliation;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
export interface PublicProviderReconciliationAttempt {
|
|
1064
|
+
readonly reconciliation_id: string;
|
|
1065
|
+
readonly operation_id: string;
|
|
1066
|
+
readonly state: "completed" | "pending" | "uncertain";
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export const parseProviderReconciliationAttempt = (
|
|
1070
|
+
value: unknown,
|
|
1071
|
+
): PublicProviderReconciliationAttempt => {
|
|
1072
|
+
const input = exactRecord(value, ["reconciliation_id", "operation_id", "state"]);
|
|
1073
|
+
if (
|
|
1074
|
+
!isUlid(input["reconciliation_id"]) ||
|
|
1075
|
+
!isUlid(input["operation_id"]) ||
|
|
1076
|
+
(input["state"] !== "completed" &&
|
|
1077
|
+
input["state"] !== "pending" &&
|
|
1078
|
+
input["state"] !== "uncertain")
|
|
1079
|
+
) {
|
|
1080
|
+
throw new ResponseContractError();
|
|
1081
|
+
}
|
|
1082
|
+
return input as unknown as PublicProviderReconciliationAttempt;
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
export interface PublicEnvironmentSecret {
|
|
1086
|
+
readonly name: string;
|
|
1087
|
+
readonly revision: number;
|
|
1088
|
+
readonly key_version: number;
|
|
1089
|
+
readonly created_at: string;
|
|
1090
|
+
readonly updated_at: string;
|
|
1091
|
+
readonly desired_generation: number;
|
|
1092
|
+
readonly applied_generation: number;
|
|
1093
|
+
readonly delivery_state: "not_deployed" | "pending" | "ready" | "failed";
|
|
1094
|
+
readonly runtime_provider: "wfp" | null;
|
|
1095
|
+
readonly applied_secret_count: number;
|
|
1096
|
+
readonly last_error: string | null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export const parseEnvironmentSecret = (value: unknown): PublicEnvironmentSecret => {
|
|
1100
|
+
const input = exactRecord(value, [
|
|
1101
|
+
"applied_generation",
|
|
1102
|
+
"applied_secret_count",
|
|
1103
|
+
"created_at",
|
|
1104
|
+
"delivery_state",
|
|
1105
|
+
"desired_generation",
|
|
1106
|
+
"key_version",
|
|
1107
|
+
"last_error",
|
|
1108
|
+
"name",
|
|
1109
|
+
"revision",
|
|
1110
|
+
"runtime_provider",
|
|
1111
|
+
"updated_at",
|
|
1112
|
+
]);
|
|
1113
|
+
if (
|
|
1114
|
+
typeof input["name"] !== "string" ||
|
|
1115
|
+
!/^[A-Z][A-Z0-9_]{0,127}$/u.test(input["name"]) ||
|
|
1116
|
+
!Number.isSafeInteger(input["revision"]) ||
|
|
1117
|
+
Number(input["revision"]) < 1 ||
|
|
1118
|
+
!Number.isSafeInteger(input["key_version"]) ||
|
|
1119
|
+
Number(input["key_version"]) < 1 ||
|
|
1120
|
+
!isIsoInstant(input["created_at"]) ||
|
|
1121
|
+
!isIsoInstant(input["updated_at"]) ||
|
|
1122
|
+
!validDelivery(input)
|
|
1123
|
+
) {
|
|
1124
|
+
throw new ResponseContractError();
|
|
1125
|
+
}
|
|
1126
|
+
return input as unknown as PublicEnvironmentSecret;
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
export const parseEnvironmentSecretPage = (
|
|
1130
|
+
value: unknown,
|
|
1131
|
+
): Readonly<{ items: readonly PublicEnvironmentSecret[] } & RuntimeSecretDeliveryMetadata> => {
|
|
1132
|
+
const input = exactRecord(value, [
|
|
1133
|
+
"applied_generation",
|
|
1134
|
+
"applied_secret_count",
|
|
1135
|
+
"delivery_state",
|
|
1136
|
+
"desired_generation",
|
|
1137
|
+
"items",
|
|
1138
|
+
"last_error",
|
|
1139
|
+
"runtime_provider",
|
|
1140
|
+
]);
|
|
1141
|
+
if (!Array.isArray(input["items"]) || !validDelivery(input)) throw new ResponseContractError();
|
|
1142
|
+
return Object.freeze({
|
|
1143
|
+
items: input["items"].map(parseEnvironmentSecret),
|
|
1144
|
+
desired_generation: Number(input["desired_generation"]),
|
|
1145
|
+
applied_generation: Number(input["applied_generation"]),
|
|
1146
|
+
delivery_state: input["delivery_state"] as RuntimeSecretDeliveryMetadata["delivery_state"],
|
|
1147
|
+
runtime_provider: input[
|
|
1148
|
+
"runtime_provider"
|
|
1149
|
+
] as RuntimeSecretDeliveryMetadata["runtime_provider"],
|
|
1150
|
+
applied_secret_count: Number(input["applied_secret_count"]),
|
|
1151
|
+
last_error: input["last_error"] as string | null,
|
|
1152
|
+
});
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
export interface PublicFunctionRun {
|
|
1156
|
+
readonly id: string;
|
|
1157
|
+
readonly deployment_id: string;
|
|
1158
|
+
readonly cron: string;
|
|
1159
|
+
readonly scheduled_at: string;
|
|
1160
|
+
readonly state: "due" | "running" | "retry" | "succeeded" | "failed";
|
|
1161
|
+
readonly attempt: number;
|
|
1162
|
+
readonly status_code: number | null;
|
|
1163
|
+
readonly started_at: string | null;
|
|
1164
|
+
readonly finished_at: string | null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const FUNCTION_RUN_ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
|
|
1168
|
+
const isUlidValue = (value: unknown): value is string =>
|
|
1169
|
+
typeof value === "string" && FUNCTION_RUN_ULID.test(value);
|
|
1170
|
+
|
|
1171
|
+
const parseFunctionRun = (value: unknown): PublicFunctionRun => {
|
|
1172
|
+
const input = exactRecord(value, [
|
|
1173
|
+
"attempt",
|
|
1174
|
+
"cron",
|
|
1175
|
+
"deployment_id",
|
|
1176
|
+
"finished_at",
|
|
1177
|
+
"id",
|
|
1178
|
+
"scheduled_at",
|
|
1179
|
+
"started_at",
|
|
1180
|
+
"state",
|
|
1181
|
+
"status_code",
|
|
1182
|
+
]);
|
|
1183
|
+
const nullableInstant = (candidate: unknown) => candidate === null || isIsoInstant(candidate);
|
|
1184
|
+
if (
|
|
1185
|
+
!isUlidValue(input["id"]) ||
|
|
1186
|
+
!isUlidValue(input["deployment_id"]) ||
|
|
1187
|
+
typeof input["cron"] !== "string" ||
|
|
1188
|
+
input["cron"].length === 0 ||
|
|
1189
|
+
input["cron"].length > 64 ||
|
|
1190
|
+
!isIsoInstant(input["scheduled_at"]) ||
|
|
1191
|
+
!["due", "running", "retry", "succeeded", "failed"].includes(String(input["state"])) ||
|
|
1192
|
+
!Number.isSafeInteger(input["attempt"]) ||
|
|
1193
|
+
Number(input["attempt"]) < 0 ||
|
|
1194
|
+
Number(input["attempt"]) > 3 ||
|
|
1195
|
+
!(
|
|
1196
|
+
input["status_code"] === null ||
|
|
1197
|
+
(Number.isSafeInteger(input["status_code"]) &&
|
|
1198
|
+
Number(input["status_code"]) >= 100 &&
|
|
1199
|
+
Number(input["status_code"]) <= 599)
|
|
1200
|
+
) ||
|
|
1201
|
+
!nullableInstant(input["started_at"]) ||
|
|
1202
|
+
!nullableInstant(input["finished_at"])
|
|
1203
|
+
) {
|
|
1204
|
+
throw new ResponseContractError();
|
|
1205
|
+
}
|
|
1206
|
+
return input as unknown as PublicFunctionRun;
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
/** The newest scheduled runs of one environment; nothing but identity, timing and outcome. */
|
|
1210
|
+
export const parseFunctionRunPage = (
|
|
1211
|
+
value: unknown,
|
|
1212
|
+
): Readonly<{ items: readonly PublicFunctionRun[] }> => {
|
|
1213
|
+
const input = exactRecord(value, ["items"]);
|
|
1214
|
+
if (!Array.isArray(input["items"]) || input["items"].length > 100) {
|
|
1215
|
+
throw new ResponseContractError();
|
|
1216
|
+
}
|
|
1217
|
+
return Object.freeze({ items: Object.freeze(input["items"].map(parseFunctionRun)) });
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
export const parseEnvironmentSecretDeletion = (
|
|
1221
|
+
value: unknown,
|
|
1222
|
+
): Readonly<{ name: string; status: "deleted" | "absent" } & RuntimeSecretDeliveryMetadata> => {
|
|
1223
|
+
const input = exactRecord(value, [
|
|
1224
|
+
"applied_generation",
|
|
1225
|
+
"applied_secret_count",
|
|
1226
|
+
"delivery_state",
|
|
1227
|
+
"desired_generation",
|
|
1228
|
+
"last_error",
|
|
1229
|
+
"name",
|
|
1230
|
+
"runtime_provider",
|
|
1231
|
+
"status",
|
|
1232
|
+
]);
|
|
1233
|
+
if (
|
|
1234
|
+
typeof input["name"] !== "string" ||
|
|
1235
|
+
!/^[A-Z][A-Z0-9_]{0,127}$/u.test(input["name"]) ||
|
|
1236
|
+
(input["status"] !== "deleted" && input["status"] !== "absent") ||
|
|
1237
|
+
!validDelivery(input)
|
|
1238
|
+
) {
|
|
1239
|
+
throw new ResponseContractError();
|
|
1240
|
+
}
|
|
1241
|
+
return input as unknown as {
|
|
1242
|
+
name: string;
|
|
1243
|
+
status: "deleted" | "absent";
|
|
1244
|
+
} & RuntimeSecretDeliveryMetadata;
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
interface RuntimeSecretDeliveryMetadata {
|
|
1248
|
+
readonly desired_generation: number;
|
|
1249
|
+
readonly applied_generation: number;
|
|
1250
|
+
readonly delivery_state: "not_deployed" | "pending" | "ready" | "failed";
|
|
1251
|
+
readonly runtime_provider: "wfp" | null;
|
|
1252
|
+
readonly applied_secret_count: number;
|
|
1253
|
+
readonly last_error: string | null;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function validDelivery(input: Readonly<Record<string, unknown>>): boolean {
|
|
1257
|
+
return (
|
|
1258
|
+
Number.isSafeInteger(input["desired_generation"]) &&
|
|
1259
|
+
Number(input["desired_generation"]) >= 0 &&
|
|
1260
|
+
Number.isSafeInteger(input["applied_generation"]) &&
|
|
1261
|
+
Number(input["applied_generation"]) >= 0 &&
|
|
1262
|
+
Number(input["applied_generation"]) <= Number(input["desired_generation"]) &&
|
|
1263
|
+
["not_deployed", "pending", "ready", "failed"].includes(String(input["delivery_state"])) &&
|
|
1264
|
+
(input["runtime_provider"] === null || input["runtime_provider"] === "wfp") &&
|
|
1265
|
+
Number.isSafeInteger(input["applied_secret_count"]) &&
|
|
1266
|
+
Number(input["applied_secret_count"]) >= 0 &&
|
|
1267
|
+
Number(input["applied_secret_count"]) <= 100 &&
|
|
1268
|
+
(input["last_error"] === null ||
|
|
1269
|
+
(typeof input["last_error"] === "string" &&
|
|
1270
|
+
/^[A-Z][A-Z0-9_]{0,63}$/u.test(input["last_error"])))
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
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
|
+
export const parseDeploymentPlan = (value: unknown): Readonly<Record<string, unknown>> => {
|
|
1369
|
+
const input = exactRecord(value, [
|
|
1370
|
+
"id",
|
|
1371
|
+
"project_id",
|
|
1372
|
+
"commit_sha",
|
|
1373
|
+
"source_digest",
|
|
1374
|
+
"build_plan_digest",
|
|
1375
|
+
"application_root",
|
|
1376
|
+
"artifact_digest",
|
|
1377
|
+
"runtime",
|
|
1378
|
+
"route",
|
|
1379
|
+
"resource_effects",
|
|
1380
|
+
"limits",
|
|
1381
|
+
"estimated_cost",
|
|
1382
|
+
"risks",
|
|
1383
|
+
"destructive_effects",
|
|
1384
|
+
"required_confirmations",
|
|
1385
|
+
"created_at",
|
|
1386
|
+
"expires_at",
|
|
1387
|
+
]);
|
|
1388
|
+
const cost = exactRecord(input["estimated_cost"], [
|
|
1389
|
+
"amount_micros",
|
|
1390
|
+
"currency",
|
|
1391
|
+
"rate_card_version",
|
|
1392
|
+
"credit_micros",
|
|
1393
|
+
"provider_cost_micros",
|
|
1394
|
+
"scope",
|
|
1395
|
+
"reserved_seconds",
|
|
1396
|
+
]);
|
|
1397
|
+
if (
|
|
1398
|
+
!isUlid(input["id"]) ||
|
|
1399
|
+
!isUlid(input["project_id"]) ||
|
|
1400
|
+
!isCommitSha(input["commit_sha"]) ||
|
|
1401
|
+
!isDigest(input["source_digest"]) ||
|
|
1402
|
+
!isDigest(input["build_plan_digest"]) ||
|
|
1403
|
+
!isApplicationRoot(input["application_root"]) ||
|
|
1404
|
+
(input["artifact_digest"] !== undefined && !isDigest(input["artifact_digest"])) ||
|
|
1405
|
+
(input["runtime"] !== "cloudflare_workers_static_assets" &&
|
|
1406
|
+
input["runtime"] !== "cloudflare_workers_edge_ssr") ||
|
|
1407
|
+
!isHttpsUrlValue(input["route"]) ||
|
|
1408
|
+
!stringArray(input["resource_effects"]) ||
|
|
1409
|
+
!stringArray(input["limits"]) ||
|
|
1410
|
+
!stringArray(input["risks"]) ||
|
|
1411
|
+
!stringArray(input["destructive_effects"]) ||
|
|
1412
|
+
!stringArray(input["required_confirmations"]) ||
|
|
1413
|
+
!isIsoInstant(input["created_at"]) ||
|
|
1414
|
+
!isIsoInstant(input["expires_at"]) ||
|
|
1415
|
+
!decimal(cost["amount_micros"]) ||
|
|
1416
|
+
!boundedString(cost["currency"], 1, 16) ||
|
|
1417
|
+
!boundedString(cost["rate_card_version"], 1, 128) ||
|
|
1418
|
+
!decimal(cost["credit_micros"]) ||
|
|
1419
|
+
!decimal(cost["provider_cost_micros"]) ||
|
|
1420
|
+
cost["scope"] !== "build_compute" ||
|
|
1421
|
+
!decimal(cost["reserved_seconds"]) ||
|
|
1422
|
+
cost["reserved_seconds"] === "0"
|
|
1423
|
+
)
|
|
1424
|
+
throw new ResponseContractError();
|
|
1425
|
+
return input;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
const isApplicationRoot = (value: unknown): value is string => {
|
|
1429
|
+
if (
|
|
1430
|
+
typeof value !== "string" ||
|
|
1431
|
+
!/^(?:\.|[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*)$/u.test(value) ||
|
|
1432
|
+
new TextEncoder().encode(value).byteLength > 64
|
|
1433
|
+
) {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
const segments = value === "." ? [] : value.split("/");
|
|
1437
|
+
return (
|
|
1438
|
+
segments.length <= 8 &&
|
|
1439
|
+
segments.every((segment) => new TextEncoder().encode(segment).byteLength <= 63)
|
|
1440
|
+
);
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
export const parseGuardedActionPlan = (value: unknown): Readonly<Record<string, unknown>> => {
|
|
1444
|
+
const input = exactRecord(value, [
|
|
1445
|
+
"action",
|
|
1446
|
+
"project_id",
|
|
1447
|
+
"target_deployment_id",
|
|
1448
|
+
"target_artifact_digest",
|
|
1449
|
+
"source_deployment_id",
|
|
1450
|
+
"source_artifact_digest",
|
|
1451
|
+
"source_environment",
|
|
1452
|
+
"target_environment",
|
|
1453
|
+
"resource_etag",
|
|
1454
|
+
"effects",
|
|
1455
|
+
"risks",
|
|
1456
|
+
"confirmation_token",
|
|
1457
|
+
"created_at",
|
|
1458
|
+
"expires_at",
|
|
1459
|
+
]);
|
|
1460
|
+
const rollback = input["action"] === "rollback";
|
|
1461
|
+
const promote = input["action"] === "promote";
|
|
1462
|
+
if (
|
|
1463
|
+
(!rollback && !promote && input["action"] !== "delete") ||
|
|
1464
|
+
!isUlid(input["project_id"]) ||
|
|
1465
|
+
(rollback
|
|
1466
|
+
? !isUlid(input["target_deployment_id"])
|
|
1467
|
+
: input["target_deployment_id"] !== undefined) ||
|
|
1468
|
+
(rollback
|
|
1469
|
+
? !isDigest(input["target_artifact_digest"])
|
|
1470
|
+
: input["target_artifact_digest"] !== undefined) ||
|
|
1471
|
+
(promote
|
|
1472
|
+
? !isUlid(input["source_deployment_id"]) ||
|
|
1473
|
+
!isDigest(input["source_artifact_digest"]) ||
|
|
1474
|
+
input["source_environment"] !== "dev" ||
|
|
1475
|
+
input["target_environment"] !== "prod"
|
|
1476
|
+
: input["source_deployment_id"] !== undefined ||
|
|
1477
|
+
input["source_artifact_digest"] !== undefined ||
|
|
1478
|
+
input["source_environment"] !== undefined ||
|
|
1479
|
+
input["target_environment"] !== undefined) ||
|
|
1480
|
+
!boundedString(input["resource_etag"], 1, 512) ||
|
|
1481
|
+
!stringArray(input["effects"]) ||
|
|
1482
|
+
!stringArray(input["risks"]) ||
|
|
1483
|
+
!boundedString(input["confirmation_token"], 1, 4096) ||
|
|
1484
|
+
!isIsoInstant(input["created_at"]) ||
|
|
1485
|
+
!isIsoInstant(input["expires_at"])
|
|
1486
|
+
)
|
|
1487
|
+
throw new ResponseContractError();
|
|
1488
|
+
return input;
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
export const parseOperationEvent = (value: unknown): Readonly<Record<string, unknown>> => {
|
|
1492
|
+
const input = exactRecord(value, [
|
|
1493
|
+
"event_id",
|
|
1494
|
+
"operation_id",
|
|
1495
|
+
"project_id",
|
|
1496
|
+
"type",
|
|
1497
|
+
"occurred_at",
|
|
1498
|
+
"failure",
|
|
1499
|
+
]);
|
|
1500
|
+
const types = [
|
|
1501
|
+
"OperationQueued",
|
|
1502
|
+
"OperationStarted",
|
|
1503
|
+
"OperationSucceeded",
|
|
1504
|
+
"OperationFailed",
|
|
1505
|
+
"OperationCancelled",
|
|
1506
|
+
];
|
|
1507
|
+
if (
|
|
1508
|
+
!isUlid(input["event_id"]) ||
|
|
1509
|
+
!isUlid(input["operation_id"]) ||
|
|
1510
|
+
!isUlid(input["project_id"]) ||
|
|
1511
|
+
typeof input["type"] !== "string" ||
|
|
1512
|
+
!types.includes(input["type"]) ||
|
|
1513
|
+
!isIsoInstant(input["occurred_at"])
|
|
1514
|
+
)
|
|
1515
|
+
throw new ResponseContractError();
|
|
1516
|
+
if (input["failure"] === undefined) return input;
|
|
1517
|
+
if (input["type"] !== "OperationFailed") throw new ResponseContractError();
|
|
1518
|
+
return { ...input, failure: parseOperationFailure(input["failure"]) };
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
export const parseSafeProblem = (value: unknown): SafeProblem => {
|
|
1522
|
+
const input = exactRecord(value, [
|
|
1523
|
+
"type",
|
|
1524
|
+
"title",
|
|
1525
|
+
"status",
|
|
1526
|
+
"detail",
|
|
1527
|
+
"instance",
|
|
1528
|
+
"code",
|
|
1529
|
+
"request_id",
|
|
1530
|
+
"retryable",
|
|
1531
|
+
"suggested_action",
|
|
1532
|
+
"retry_after_seconds",
|
|
1533
|
+
]);
|
|
1534
|
+
const code = input["code"];
|
|
1535
|
+
if (typeof code !== "string" || !(code in PROBLEM_POLICY)) throw new ResponseContractError();
|
|
1536
|
+
const policy = PROBLEM_POLICY[code as ProblemCode];
|
|
1537
|
+
if (
|
|
1538
|
+
input["status"] !== policy.status ||
|
|
1539
|
+
!boundedString(input["type"], 1, 512) ||
|
|
1540
|
+
!isHttpsUrl(input["type"]) ||
|
|
1541
|
+
!boundedString(input["title"], 1, 160) ||
|
|
1542
|
+
!boundedString(input["request_id"], 1, 128) ||
|
|
1543
|
+
typeof input["retryable"] !== "boolean" ||
|
|
1544
|
+
!boundedString(input["suggested_action"], 1, 500) ||
|
|
1545
|
+
(input["detail"] !== undefined && !boundedString(input["detail"], 1, 2_000)) ||
|
|
1546
|
+
(input["instance"] !== undefined && !boundedString(input["instance"], 1, 512))
|
|
1547
|
+
) {
|
|
1548
|
+
throw new ResponseContractError();
|
|
1549
|
+
}
|
|
1550
|
+
const retryAfter = input["retry_after_seconds"];
|
|
1551
|
+
if (
|
|
1552
|
+
retryAfter !== undefined &&
|
|
1553
|
+
(code !== "rate_limited" ||
|
|
1554
|
+
typeof retryAfter !== "number" ||
|
|
1555
|
+
!Number.isSafeInteger(retryAfter) ||
|
|
1556
|
+
retryAfter < 1 ||
|
|
1557
|
+
retryAfter > 86400)
|
|
1558
|
+
)
|
|
1559
|
+
throw new ResponseContractError();
|
|
1560
|
+
return {
|
|
1561
|
+
code: code as ProblemCode,
|
|
1562
|
+
retryable: policy.retryable,
|
|
1563
|
+
suggestedAction:
|
|
1564
|
+
(code === "framework_conversion_required"
|
|
1565
|
+
? (parseFrameworkConversionDetail(input["detail"]) ?? policy.action)
|
|
1566
|
+
: policy.action) +
|
|
1567
|
+
(retryAfter === undefined ? "" : ` Retry after ${String(retryAfter)} seconds.`),
|
|
1568
|
+
...(retryAfter === undefined ? {} : { retryAfterSeconds: retryAfter as number }),
|
|
1569
|
+
};
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
export const parseOperation = (value: unknown): PublicOperation => {
|
|
1573
|
+
const input = exactRecord(value, [
|
|
1574
|
+
"blocking_operation_id",
|
|
1575
|
+
"id",
|
|
1576
|
+
"state",
|
|
1577
|
+
"created_at",
|
|
1578
|
+
"updated_at",
|
|
1579
|
+
"result",
|
|
1580
|
+
"error",
|
|
1581
|
+
"reconciliation",
|
|
1582
|
+
"progress",
|
|
1583
|
+
]);
|
|
1584
|
+
const id = input["id"];
|
|
1585
|
+
const state = input["state"];
|
|
1586
|
+
const createdAt = input["created_at"];
|
|
1587
|
+
const updatedAt = input["updated_at"];
|
|
1588
|
+
const result = input["result"];
|
|
1589
|
+
const blockingOperationId = input["blocking_operation_id"];
|
|
1590
|
+
if (
|
|
1591
|
+
blockingOperationId !== undefined &&
|
|
1592
|
+
(state !== "queued" || !isUlid(blockingOperationId) || blockingOperationId === id)
|
|
1593
|
+
)
|
|
1594
|
+
throw new ResponseContractError();
|
|
1595
|
+
if (
|
|
1596
|
+
!isUlid(id) ||
|
|
1597
|
+
!isOperationState(state) ||
|
|
1598
|
+
!isIsoInstant(createdAt) ||
|
|
1599
|
+
!isIsoInstant(updatedAt) ||
|
|
1600
|
+
(result !== undefined && !isRecord(result))
|
|
1601
|
+
) {
|
|
1602
|
+
throw new ResponseContractError();
|
|
1603
|
+
}
|
|
1604
|
+
let progress: OperationDeploymentProgress | undefined;
|
|
1605
|
+
if (input["progress"] !== undefined) {
|
|
1606
|
+
if (state !== "queued" && state !== "running") throw new ResponseContractError();
|
|
1607
|
+
try {
|
|
1608
|
+
progress = parseOperationDeploymentProgress(input["progress"]);
|
|
1609
|
+
} catch {
|
|
1610
|
+
throw new ResponseContractError();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
let error: PublicOperationFailure | undefined;
|
|
1614
|
+
if (input["error"] !== undefined) {
|
|
1615
|
+
if (state !== "failed") throw new ResponseContractError();
|
|
1616
|
+
error = parseOperationFailure(input["error"]);
|
|
1617
|
+
} else if (state === "failed") {
|
|
1618
|
+
throw new ResponseContractError();
|
|
1619
|
+
}
|
|
1620
|
+
let reconciliation: PublicOperationReconciliation | undefined;
|
|
1621
|
+
if (input["reconciliation"] !== undefined) {
|
|
1622
|
+
const observed = exactRecord(input["reconciliation"], [
|
|
1623
|
+
"state",
|
|
1624
|
+
"attempt_id",
|
|
1625
|
+
"observed_at",
|
|
1626
|
+
"suggested_action",
|
|
1627
|
+
]);
|
|
1628
|
+
if (
|
|
1629
|
+
(state !== "queued" && state !== "running") ||
|
|
1630
|
+
(observed["state"] !== "required" && observed["state"] !== "pending") ||
|
|
1631
|
+
!isIsoInstant(observed["observed_at"]) ||
|
|
1632
|
+
!boundedString(observed["suggested_action"], 1, 500) ||
|
|
1633
|
+
(observed["state"] === "required"
|
|
1634
|
+
? observed["attempt_id"] !== null
|
|
1635
|
+
: !isUlid(observed["attempt_id"]))
|
|
1636
|
+
)
|
|
1637
|
+
throw new ResponseContractError();
|
|
1638
|
+
reconciliation = publicOperationReconciliation({
|
|
1639
|
+
state: observed["state"],
|
|
1640
|
+
attempt_id: observed["attempt_id"] as string | null,
|
|
1641
|
+
observed_at: observed["observed_at"] as string,
|
|
1642
|
+
});
|
|
1643
|
+
if (reconciliation.suggested_action !== observed["suggested_action"])
|
|
1644
|
+
throw new ResponseContractError();
|
|
1645
|
+
}
|
|
1646
|
+
return {
|
|
1647
|
+
id,
|
|
1648
|
+
state,
|
|
1649
|
+
created_at: createdAt,
|
|
1650
|
+
updated_at: updatedAt,
|
|
1651
|
+
...(blockingOperationId === undefined
|
|
1652
|
+
? {}
|
|
1653
|
+
: { blocking_operation_id: blockingOperationId as string }),
|
|
1654
|
+
...(progress === undefined ? {} : { progress }),
|
|
1655
|
+
...(result === undefined ? {} : { result }),
|
|
1656
|
+
...(error === undefined ? {} : { error }),
|
|
1657
|
+
...(reconciliation === undefined ? {} : { reconciliation }),
|
|
1658
|
+
};
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
function parseOperationFailure(value: unknown): PublicOperationFailure {
|
|
1662
|
+
const input = exactRecord(value, ["code", "message", "retryable", "suggested_action"]);
|
|
1663
|
+
if (
|
|
1664
|
+
typeof input["code"] !== "string" ||
|
|
1665
|
+
!boundedString(input["message"], 1, 512) ||
|
|
1666
|
+
input["retryable"] !== false ||
|
|
1667
|
+
!boundedString(input["suggested_action"], 1, 500)
|
|
1668
|
+
)
|
|
1669
|
+
throw new ResponseContractError();
|
|
1670
|
+
const safe = publicOperationFailure(input["code"]);
|
|
1671
|
+
if (safe.code !== input["code"]) throw new ResponseContractError();
|
|
1672
|
+
return safe;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const exactRecord = (value: unknown, allowed: readonly string[]): Record<string, unknown> => {
|
|
1676
|
+
if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) {
|
|
1677
|
+
throw new ResponseContractError();
|
|
1678
|
+
}
|
|
1679
|
+
return value;
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
1683
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1684
|
+
|
|
1685
|
+
const boundedString = (value: unknown, minimum: number, maximum: number): value is string =>
|
|
1686
|
+
typeof value === "string" && value.length >= minimum && value.length <= maximum;
|
|
1687
|
+
|
|
1688
|
+
const isUlid = (value: unknown): value is string =>
|
|
1689
|
+
typeof value === "string" && /^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value);
|
|
1690
|
+
|
|
1691
|
+
const isIsoInstant = (value: unknown): value is string =>
|
|
1692
|
+
typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value));
|
|
1693
|
+
|
|
1694
|
+
const isOperationState = (value: unknown): value is PublicOperation["state"] =>
|
|
1695
|
+
value === "queued" ||
|
|
1696
|
+
value === "running" ||
|
|
1697
|
+
value === "succeeded" ||
|
|
1698
|
+
value === "failed" ||
|
|
1699
|
+
value === "cancelled";
|
|
1700
|
+
|
|
1701
|
+
const isHttpsUrl = (value: string): boolean => {
|
|
1702
|
+
try {
|
|
1703
|
+
const url = new URL(value);
|
|
1704
|
+
return url.protocol === "https:" && url.username === "" && url.password === "";
|
|
1705
|
+
} catch {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1710
|
+
const isCommitSha = (value: unknown): value is string =>
|
|
1711
|
+
typeof value === "string" && /^[a-f0-9]{40}$/u.test(value);
|
|
1712
|
+
const isDigest = (value: unknown): value is string =>
|
|
1713
|
+
typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value);
|
|
1714
|
+
const decimal = (value: unknown): value is string =>
|
|
1715
|
+
typeof value === "string" && /^(?:0|[1-9][0-9]*)$/u.test(value);
|
|
1716
|
+
const stringArray = (value: unknown): value is string[] =>
|
|
1717
|
+
Array.isArray(value) &&
|
|
1718
|
+
value.length <= 100 &&
|
|
1719
|
+
value.every((item) => boundedString(item, 1, 1_000));
|
|
1720
|
+
const shortStringArray = (value: unknown, maximumItems: number, maximumLength: number) =>
|
|
1721
|
+
Array.isArray(value) &&
|
|
1722
|
+
value.length >= 1 &&
|
|
1723
|
+
value.length <= maximumItems &&
|
|
1724
|
+
value.every((item) => boundedString(item, 1, maximumLength));
|
|
1725
|
+
const isPaidHostname = (value: unknown): value is string =>
|
|
1726
|
+
typeof value === "string" &&
|
|
1727
|
+
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(value) &&
|
|
1728
|
+
!value.endsWith(".poc.aixyte.com");
|
|
1729
|
+
const isPaidValidationRecord = (value: unknown) => {
|
|
1730
|
+
if (
|
|
1731
|
+
typeof value !== "object" ||
|
|
1732
|
+
value === null ||
|
|
1733
|
+
Array.isArray(value) ||
|
|
1734
|
+
Object.keys(value).sort().join(",") !== "content,name,purpose,type"
|
|
1735
|
+
) {
|
|
1736
|
+
return false;
|
|
1737
|
+
}
|
|
1738
|
+
const record = value as Record<string, unknown>;
|
|
1739
|
+
return (
|
|
1740
|
+
(record["type"] === "CNAME" || record["type"] === "TXT") &&
|
|
1741
|
+
boundedString(record["name"], 1, 253) &&
|
|
1742
|
+
boundedString(record["content"], 1, 2_048) &&
|
|
1743
|
+
["traffic", "ownership", "ssl", "dcv_delegation"].includes(String(record["purpose"]))
|
|
1744
|
+
);
|
|
1745
|
+
};
|
|
1746
|
+
const isHttpsUrlValue = (value: unknown): value is string =>
|
|
1747
|
+
typeof value === "string" && isHttpsUrl(value);
|