@fly.io/sdk 0.1.0

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +181 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +212 -0
  5. package/dist/client.d.ts +42 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +200 -0
  8. package/dist/errors.d.ts +48 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +85 -0
  11. package/dist/index.d.ts +28 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +18 -0
  14. package/dist/machine.d.ts +319 -0
  15. package/dist/machine.d.ts.map +1 -0
  16. package/dist/machine.js +249 -0
  17. package/dist/network.d.ts +45 -0
  18. package/dist/network.d.ts.map +1 -0
  19. package/dist/network.js +44 -0
  20. package/dist/organization.d.ts +20 -0
  21. package/dist/organization.d.ts.map +1 -0
  22. package/dist/organization.js +22 -0
  23. package/dist/regions.d.ts +35 -0
  24. package/dist/regions.d.ts.map +1 -0
  25. package/dist/regions.js +53 -0
  26. package/dist/secret.d.ts +55 -0
  27. package/dist/secret.d.ts.map +1 -0
  28. package/dist/secret.js +53 -0
  29. package/dist/token.d.ts +14 -0
  30. package/dist/token.d.ts.map +1 -0
  31. package/dist/token.js +16 -0
  32. package/dist/types.d.ts +937 -0
  33. package/dist/types.d.ts.map +1 -0
  34. package/dist/types.js +40 -0
  35. package/dist/volume.d.ts +72 -0
  36. package/dist/volume.d.ts.map +1 -0
  37. package/dist/volume.js +45 -0
  38. package/package.json +54 -0
  39. package/src/app.ts +462 -0
  40. package/src/client.ts +262 -0
  41. package/src/errors.ts +135 -0
  42. package/src/index.ts +141 -0
  43. package/src/machine.ts +644 -0
  44. package/src/network.ts +87 -0
  45. package/src/organization.ts +43 -0
  46. package/src/regions.ts +94 -0
  47. package/src/secret.ts +101 -0
  48. package/src/token.ts +29 -0
  49. package/src/types.ts +1072 -0
  50. package/src/volume.ts +124 -0
package/src/app.ts ADDED
@@ -0,0 +1,462 @@
1
+ // App management for Fly Machines REST + GraphQL API.
2
+ // Types aligned with OpenAPI spec at https://docs.machines.dev/spec/openapi3.json
3
+
4
+ import { Client } from './client.ts'
5
+ import type { FlyResult } from './errors.ts'
6
+ import type {
7
+ App as ApiApp,
8
+ AppOrganizationInfo as ApiAppOrganizationInfo,
9
+ AppSecret,
10
+ AppSecrets,
11
+ AppSecretsUpdateRequest,
12
+ AppSecretsUpdateResp,
13
+ CertificateCheckResponse,
14
+ CertificateDetail,
15
+ CreateAcmeCertificateRequest,
16
+ CreateAppDeployTokenRequest,
17
+ CreateAppResponse,
18
+ CreateCustomCertificateRequest,
19
+ DeleteAppSecretResponse,
20
+ DeleteSecretkeyResponse,
21
+ DecryptSecretkeyRequest,
22
+ DecryptSecretkeyResponse,
23
+ DestroyCustomCertificateResponse,
24
+ EncryptSecretkeyRequest,
25
+ EncryptSecretkeyResponse,
26
+ IPAssignment,
27
+ ListAppsResponse as ApiListAppsResponse,
28
+ ListCertificatesResponse,
29
+ ListIPAssignmentsResponse,
30
+ SecretKey,
31
+ SecretKeys,
32
+ SetAppSecretRequest,
33
+ SetAppSecretResponse,
34
+ SetSecretkeyRequest,
35
+ SetSecretkeyResponse,
36
+ SignSecretkeyRequest,
37
+ SignSecretkeyResponse,
38
+ VerifySecretkeyRequest,
39
+ } from './types.ts'
40
+
41
+ export type ListAppRequest = string
42
+
43
+ /** Matches OpenAPI ListAppsResponse schema. */
44
+ export type ListAppResponse = ApiListAppsResponse
45
+
46
+ /** Query params for GET /apps. org_slug is required, app_role is optional. */
47
+ export interface ListAppsParams {
48
+ org_slug: string
49
+ app_role?: string
50
+ }
51
+
52
+ export type GetAppRequest = string
53
+
54
+ const getAppQuery = `query($name: String!) {
55
+ app(name: $name) {
56
+ name
57
+ status
58
+ organization {
59
+ name
60
+ slug
61
+ }
62
+ ipAddresses {
63
+ nodes {
64
+ type
65
+ region
66
+ address
67
+ }
68
+ }
69
+ }
70
+ }`
71
+
72
+ export enum AppStatus {
73
+ deployed = 'deployed',
74
+ pending = 'pending',
75
+ suspended = 'suspended',
76
+ }
77
+
78
+ /** Matches OpenAPI AppOrganizationInfo schema. */
79
+ export type AppOrganizationInfo = ApiAppOrganizationInfo
80
+
81
+ /** Matches OpenAPI App schema — used in both GET /apps/{app_name} and ListAppsResponse. */
82
+ export type AppInfo = ApiApp
83
+
84
+ /**
85
+ * Full app response from GraphQL getAppDetailed.
86
+ * Extends REST AppInfo with ipAddresses from the GraphQL query.
87
+ */
88
+ export interface AppResponse {
89
+ name: string
90
+ status: AppStatus
91
+ organization: {
92
+ name: string
93
+ slug: string
94
+ }
95
+ ipAddresses: IPAddress[]
96
+ }
97
+
98
+ export interface IPAddress {
99
+ type: string
100
+ region?: string
101
+ address: string
102
+ }
103
+
104
+ /**
105
+ * Matches OpenAPI CreateAppRequest schema.
106
+ * Note: the spec uses `name` (not `app_name`) for the app name field.
107
+ */
108
+ export interface CreateAppRequest {
109
+ org_slug: string
110
+ name: string
111
+ network?: string
112
+ enable_subdomains?: boolean
113
+ }
114
+
115
+ export type DeleteAppRequest = string
116
+
117
+ export interface ListCertificatesRequest {
118
+ app_name: string
119
+ filter?: string
120
+ cursor?: string
121
+ limit?: number
122
+ }
123
+
124
+ export interface RequestAcmeCertificateRequest {
125
+ app_name: string
126
+ request: CreateAcmeCertificateRequest
127
+ }
128
+
129
+ export interface RequestCustomCertificateRequest {
130
+ app_name: string
131
+ request: CreateCustomCertificateRequest
132
+ }
133
+
134
+ export interface CertificateRequest {
135
+ app_name: string
136
+ hostname: string
137
+ }
138
+
139
+ export interface CreateDeployTokenRequest {
140
+ app_name: string
141
+ request: CreateAppDeployTokenRequest
142
+ }
143
+
144
+ export interface ListSecretKeysRequest {
145
+ app_name: string
146
+ min_version?: string
147
+ types?: string
148
+ }
149
+
150
+ export interface SecretKeyRequest {
151
+ app_name: string
152
+ secret_name: string
153
+ min_version?: string
154
+ }
155
+
156
+ export interface SetSecretKeyRequest {
157
+ app_name: string
158
+ secret_name: string
159
+ request: SetSecretkeyRequest
160
+ }
161
+
162
+ export interface SecretKeyDecryptRequest {
163
+ app_name: string
164
+ secret_name: string
165
+ request: DecryptSecretkeyRequest
166
+ min_version?: string
167
+ }
168
+
169
+ export interface SecretKeyEncryptRequest {
170
+ app_name: string
171
+ secret_name: string
172
+ request: EncryptSecretkeyRequest
173
+ min_version?: string
174
+ }
175
+
176
+ export interface SecretKeySignRequest {
177
+ app_name: string
178
+ secret_name: string
179
+ request: SignSecretkeyRequest
180
+ min_version?: string
181
+ }
182
+
183
+ export interface SecretKeyVerifyRequest {
184
+ app_name: string
185
+ secret_name: string
186
+ request: VerifySecretkeyRequest
187
+ min_version?: string
188
+ }
189
+
190
+ export interface ListSecretsRequest {
191
+ app_name: string
192
+ min_version?: string
193
+ show_secrets?: boolean
194
+ }
195
+
196
+ export interface UpdateSecretsRequest {
197
+ app_name: string
198
+ request: AppSecretsUpdateRequest
199
+ }
200
+
201
+ export interface SecretRequest {
202
+ app_name: string
203
+ secret_name: string
204
+ min_version?: string
205
+ show_secrets?: boolean
206
+ }
207
+
208
+ export interface SetSecretRequest {
209
+ app_name: string
210
+ secret_name: string
211
+ request: SetAppSecretRequest
212
+ }
213
+
214
+ export interface AssignIPAddressRequest {
215
+ app_name: string
216
+ request: {
217
+ region?: string
218
+ service_name?: string
219
+ type?: string
220
+ }
221
+ }
222
+
223
+ export interface DeleteIPAddressRequest {
224
+ app_name: string
225
+ ip: string
226
+ }
227
+
228
+ export class App {
229
+ private client: Client
230
+
231
+ constructor(client: Client) {
232
+ this.client = client
233
+ }
234
+
235
+ async listApps(org_slug: ListAppRequest): Promise<FlyResult<ListAppResponse>> {
236
+ return await this.client.restOrThrow(`apps?org_slug=${org_slug}`)
237
+ }
238
+
239
+ /** List apps with full query params (org_slug + optional app_role filter). */
240
+ async listAppsWithParams(params: ListAppsParams): Promise<FlyResult<ListAppResponse>> {
241
+ const query = new URLSearchParams({ org_slug: params.org_slug })
242
+ if (params.app_role) {
243
+ query.set('app_role', params.app_role)
244
+ }
245
+ return await this.client.restOrThrow(`apps?${query.toString()}`)
246
+ }
247
+
248
+ async getApp(app_name: GetAppRequest): Promise<FlyResult<AppInfo>> {
249
+ return await this.client.restOrThrow(`apps/${app_name}`)
250
+ }
251
+
252
+ async getAppDetailed(app_name: GetAppRequest): Promise<FlyResult<AppResponse>> {
253
+ const result = await this.client.gqlPostOrThrow<string, { app: AppResponse & { ipAddresses: { nodes: IPAddress[] } } }>({
254
+ query: getAppQuery,
255
+ variables: { name: app_name },
256
+ })
257
+
258
+ if (result instanceof Error) {
259
+ return result
260
+ }
261
+
262
+ const { app } = result
263
+
264
+ return {
265
+ ...app,
266
+ ipAddresses: app.ipAddresses.nodes,
267
+ }
268
+ }
269
+
270
+ async createApp(payload: CreateAppRequest): Promise<FlyResult<void>> {
271
+ return await this.client.restOrThrow('apps', 'POST', payload)
272
+ }
273
+
274
+ async deleteApp(app_name: DeleteAppRequest): Promise<FlyResult<void>> {
275
+ return await this.client.restOrThrow(`apps/${app_name}`, 'DELETE')
276
+ }
277
+
278
+ async listCertificates(payload: ListCertificatesRequest): Promise<FlyResult<ListCertificatesResponse>> {
279
+ const { app_name, filter, cursor, limit } = payload
280
+ const params = new URLSearchParams()
281
+ if (filter) {
282
+ params.set('filter', filter)
283
+ }
284
+ if (cursor) {
285
+ params.set('cursor', cursor)
286
+ }
287
+ if (limit !== undefined) {
288
+ params.set('limit', String(limit))
289
+ }
290
+ const query = params.toString()
291
+ const path = `apps/${app_name}/certificates${query ? `?${query}` : ''}`
292
+ return await this.client.restOrThrow(path)
293
+ }
294
+
295
+ async requestAcmeCertificate(payload: RequestAcmeCertificateRequest): Promise<FlyResult<CertificateDetail>> {
296
+ const { app_name, request } = payload
297
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/acme`, 'POST', request)
298
+ }
299
+
300
+ async requestCustomCertificate(payload: RequestCustomCertificateRequest): Promise<FlyResult<CertificateDetail>> {
301
+ const { app_name, request } = payload
302
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/custom`, 'POST', request)
303
+ }
304
+
305
+ async getCertificate(payload: CertificateRequest): Promise<FlyResult<CertificateDetail>> {
306
+ const { app_name, hostname } = payload
307
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/${hostname}`)
308
+ }
309
+
310
+ async deleteCertificate(payload: CertificateRequest): Promise<FlyResult<void>> {
311
+ const { app_name, hostname } = payload
312
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/${hostname}`, 'DELETE')
313
+ }
314
+
315
+ async deleteAcmeCertificates(payload: CertificateRequest): Promise<FlyResult<CertificateDetail>> {
316
+ const { app_name, hostname } = payload
317
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/${hostname}/acme`, 'DELETE')
318
+ }
319
+
320
+ async checkCertificate(payload: CertificateRequest): Promise<FlyResult<CertificateCheckResponse>> {
321
+ const { app_name, hostname } = payload
322
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/${hostname}/check`, 'POST')
323
+ }
324
+
325
+ async deleteCustomCertificate(payload: CertificateRequest): Promise<FlyResult<DestroyCustomCertificateResponse>> {
326
+ const { app_name, hostname } = payload
327
+ return await this.client.restOrThrow(`apps/${app_name}/certificates/${hostname}/custom`, 'DELETE')
328
+ }
329
+
330
+ async createDeployToken(payload: CreateDeployTokenRequest): Promise<FlyResult<CreateAppResponse>> {
331
+ const { app_name, request } = payload
332
+ return await this.client.restOrThrow(`apps/${app_name}/deploy_token`, 'POST', request)
333
+ }
334
+
335
+ async listIpAssignments(app_name: string): Promise<FlyResult<ListIPAssignmentsResponse>> {
336
+ return await this.client.restOrThrow(`apps/${app_name}/ip_assignments`)
337
+ }
338
+
339
+ async assignIpAddress(payload: AssignIPAddressRequest): Promise<FlyResult<IPAssignment>> {
340
+ const { app_name, request } = payload
341
+ return await this.client.restOrThrow(`apps/${app_name}/ip_assignments`, 'POST', request)
342
+ }
343
+
344
+ async deleteIpAssignment(payload: DeleteIPAddressRequest): Promise<FlyResult<void>> {
345
+ const { app_name, ip } = payload
346
+ return await this.client.restOrThrow(`apps/${app_name}/ip_assignments/${ip}`, 'DELETE')
347
+ }
348
+
349
+ async listSecretKeys(payload: ListSecretKeysRequest): Promise<FlyResult<SecretKeys>> {
350
+ const { app_name, min_version, types } = payload
351
+ const params = new URLSearchParams()
352
+ if (min_version) {
353
+ params.set('min_version', min_version)
354
+ }
355
+ if (types) {
356
+ params.set('types', types)
357
+ }
358
+ const query = params.toString()
359
+ const path = `apps/${app_name}/secretkeys${query ? `?${query}` : ''}`
360
+ return await this.client.restOrThrow(path)
361
+ }
362
+
363
+ async getSecretKey(payload: SecretKeyRequest): Promise<FlyResult<SecretKey>> {
364
+ const { app_name, secret_name, min_version } = payload
365
+ const query = min_version ? `?min_version=${encodeURIComponent(min_version)}` : ''
366
+ return await this.client.restOrThrow(`apps/${app_name}/secretkeys/${secret_name}${query}`)
367
+ }
368
+
369
+ async setSecretKey(payload: SetSecretKeyRequest): Promise<FlyResult<SetSecretkeyResponse>> {
370
+ const { app_name, secret_name, request } = payload
371
+ return await this.client.restOrThrow(`apps/${app_name}/secretkeys/${secret_name}`, 'POST', request)
372
+ }
373
+
374
+ async deleteSecretKey(payload: SecretKeyRequest): Promise<FlyResult<DeleteSecretkeyResponse>> {
375
+ const { app_name, secret_name } = payload
376
+ return await this.client.restOrThrow(`apps/${app_name}/secretkeys/${secret_name}`, 'DELETE')
377
+ }
378
+
379
+ async decryptSecretKey(payload: SecretKeyDecryptRequest): Promise<FlyResult<DecryptSecretkeyResponse>> {
380
+ const { app_name, secret_name, request, min_version } = payload
381
+ const query = min_version ? `?min_version=${encodeURIComponent(min_version)}` : ''
382
+ return await this.client.restOrThrow(
383
+ `apps/${app_name}/secretkeys/${secret_name}/decrypt${query}`,
384
+ 'POST',
385
+ request,
386
+ )
387
+ }
388
+
389
+ async encryptSecretKey(payload: SecretKeyEncryptRequest): Promise<FlyResult<EncryptSecretkeyResponse>> {
390
+ const { app_name, secret_name, request, min_version } = payload
391
+ const query = min_version ? `?min_version=${encodeURIComponent(min_version)}` : ''
392
+ return await this.client.restOrThrow(
393
+ `apps/${app_name}/secretkeys/${secret_name}/encrypt${query}`,
394
+ 'POST',
395
+ request,
396
+ )
397
+ }
398
+
399
+ async generateSecretKey(payload: SetSecretKeyRequest): Promise<FlyResult<SetSecretkeyResponse>> {
400
+ const { app_name, secret_name, request } = payload
401
+ return await this.client.restOrThrow(
402
+ `apps/${app_name}/secretkeys/${secret_name}/generate`,
403
+ 'POST',
404
+ request,
405
+ )
406
+ }
407
+
408
+ async signSecretKey(payload: SecretKeySignRequest): Promise<FlyResult<SignSecretkeyResponse>> {
409
+ const { app_name, secret_name, request, min_version } = payload
410
+ const query = min_version ? `?min_version=${encodeURIComponent(min_version)}` : ''
411
+ return await this.client.restOrThrow(`apps/${app_name}/secretkeys/${secret_name}/sign${query}`, 'POST', request)
412
+ }
413
+
414
+ async verifySecretKey(payload: SecretKeyVerifyRequest): Promise<FlyResult<void>> {
415
+ const { app_name, secret_name, request, min_version } = payload
416
+ const query = min_version ? `?min_version=${encodeURIComponent(min_version)}` : ''
417
+ return await this.client.restOrThrow(`apps/${app_name}/secretkeys/${secret_name}/verify${query}`, 'POST', request)
418
+ }
419
+
420
+ async listSecrets(payload: ListSecretsRequest): Promise<FlyResult<AppSecrets>> {
421
+ const { app_name, min_version, show_secrets } = payload
422
+ const params = new URLSearchParams()
423
+ if (min_version) {
424
+ params.set('min_version', min_version)
425
+ }
426
+ if (show_secrets !== undefined) {
427
+ params.set('show_secrets', String(show_secrets))
428
+ }
429
+ const query = params.toString()
430
+ const path = `apps/${app_name}/secrets${query ? `?${query}` : ''}`
431
+ return await this.client.restOrThrow(path)
432
+ }
433
+
434
+ async updateSecrets(payload: UpdateSecretsRequest): Promise<FlyResult<AppSecretsUpdateResp>> {
435
+ const { app_name, request } = payload
436
+ return await this.client.restOrThrow(`apps/${app_name}/secrets`, 'POST', request)
437
+ }
438
+
439
+ async getSecret(payload: SecretRequest): Promise<FlyResult<AppSecret>> {
440
+ const { app_name, secret_name, min_version, show_secrets } = payload
441
+ const params = new URLSearchParams()
442
+ if (min_version) {
443
+ params.set('min_version', min_version)
444
+ }
445
+ if (show_secrets !== undefined) {
446
+ params.set('show_secrets', String(show_secrets))
447
+ }
448
+ const query = params.toString()
449
+ const path = `apps/${app_name}/secrets/${secret_name}${query ? `?${query}` : ''}`
450
+ return await this.client.restOrThrow(path)
451
+ }
452
+
453
+ async setSecret(payload: SetSecretRequest): Promise<FlyResult<SetAppSecretResponse>> {
454
+ const { app_name, secret_name, request } = payload
455
+ return await this.client.restOrThrow(`apps/${app_name}/secrets/${secret_name}`, 'POST', request)
456
+ }
457
+
458
+ async deleteSecret(payload: SecretRequest): Promise<FlyResult<DeleteAppSecretResponse>> {
459
+ const { app_name, secret_name } = payload
460
+ return await this.client.restOrThrow(`apps/${app_name}/secrets/${secret_name}`, 'DELETE')
461
+ }
462
+ }
package/src/client.ts ADDED
@@ -0,0 +1,262 @@
1
+ // HTTP client for Fly.io Machines REST API and GraphQL API.
2
+ // Uses native fetch (no cross-fetch dependency).
3
+ // Vendored from supabase/fly-admin with modifications.
4
+
5
+ import * as errore from 'errore'
6
+
7
+ import { App } from './app.ts'
8
+ import {
9
+ createFlyGraphQLError,
10
+ createFlyHttpError,
11
+ FlyApiError,
12
+ type FlyClientError,
13
+ type FlyResult,
14
+ } from './errors.ts'
15
+ import { Machine } from './machine.ts'
16
+ import { Network } from './network.ts'
17
+ import { Organization } from './organization.ts'
18
+ import { Regions } from './regions.ts'
19
+ import { Secret } from './secret.ts'
20
+ import { Token } from './token.ts'
21
+ import { Volume } from './volume.ts'
22
+
23
+ export const FLY_API_GRAPHQL = 'https://api.fly.io'
24
+ export const FLY_API_HOSTNAME = 'https://api.machines.dev'
25
+
26
+ interface GraphQLRequest<T> {
27
+ query: string
28
+ variables?: Record<string, T>
29
+ }
30
+
31
+ interface GraphQLResponse<T> {
32
+ data: T
33
+ errors?: {
34
+ message: string
35
+ locations: { line: number; column: number }[]
36
+ }[]
37
+ }
38
+
39
+ export interface ClientConfig {
40
+ graphqlUrl?: string
41
+ apiUrl?: string
42
+ }
43
+
44
+ export class Client {
45
+ private graphqlUrl: string
46
+ private apiUrl: string
47
+ private apiKey: string
48
+ App: App
49
+ Machine: Machine
50
+ Regions: Regions
51
+ Network: Network
52
+ Organization: Organization
53
+ Secret: Secret
54
+ Volume: Volume
55
+ Token: Token
56
+
57
+ constructor(apiKey: string, { graphqlUrl, apiUrl }: ClientConfig = {}) {
58
+ if (!apiKey) {
59
+ throw new Error('Fly API Key is required')
60
+ }
61
+ this.graphqlUrl = graphqlUrl || FLY_API_GRAPHQL
62
+ this.apiUrl = apiUrl || FLY_API_HOSTNAME
63
+ this.apiKey = apiKey
64
+ this.App = new App(this)
65
+ this.Machine = new Machine(this)
66
+ this.Network = new Network(this)
67
+ this.Regions = new Regions(this)
68
+ this.Organization = new Organization(this)
69
+ this.Secret = new Secret(this)
70
+ this.Volume = new Volume(this)
71
+ this.Token = new Token(this)
72
+ }
73
+
74
+ getApiKey(): string {
75
+ return this.apiKey
76
+ }
77
+
78
+ getApiUrl(): string {
79
+ return this.apiUrl
80
+ }
81
+
82
+ getGraphqlUrl(): string {
83
+ return this.graphqlUrl
84
+ }
85
+
86
+ async gqlPost<U, V>(payload: GraphQLRequest<U>): Promise<FlyClientError | V> {
87
+ const path = 'graphql'
88
+ const response = await fetch(`${this.graphqlUrl}/${path}`, {
89
+ method: 'POST',
90
+ headers: {
91
+ Authorization: `Bearer ${this.apiKey}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify(payload),
95
+ }).catch((cause: unknown) => {
96
+ return new FlyApiError({
97
+ method: 'POST',
98
+ path,
99
+ httpStatus: 0,
100
+ cause,
101
+ })
102
+ })
103
+
104
+ if (response instanceof Error) {
105
+ return response
106
+ }
107
+
108
+ const responseText = await response.text().catch((cause: unknown) => {
109
+ return new FlyApiError({
110
+ method: 'POST',
111
+ path,
112
+ httpStatus: response.status,
113
+ cause,
114
+ })
115
+ })
116
+
117
+ if (responseText instanceof Error) {
118
+ return responseText
119
+ }
120
+
121
+ if (!response.ok) {
122
+ const payloadOrError = parseJson({ text: responseText })
123
+ if (payloadOrError instanceof Error) {
124
+ return new FlyApiError({
125
+ method: 'POST',
126
+ path,
127
+ httpStatus: response.status,
128
+ cause: payloadOrError,
129
+ })
130
+ }
131
+ return createFlyHttpError({
132
+ method: 'POST',
133
+ path,
134
+ httpStatus: response.status,
135
+ payload: payloadOrError,
136
+ })
137
+ }
138
+
139
+ const payloadOrError = parseJson({ text: responseText })
140
+ if (payloadOrError instanceof Error) {
141
+ return new FlyApiError({
142
+ method: 'POST',
143
+ path,
144
+ httpStatus: response.status,
145
+ cause: payloadOrError,
146
+ })
147
+ }
148
+
149
+ const parsed = payloadOrError as GraphQLResponse<V>
150
+ const { data, errors } = parsed
151
+ if (errors) {
152
+ return createFlyGraphQLError({
153
+ path,
154
+ messages: errors.map((error) => {
155
+ return error.message
156
+ }),
157
+ })
158
+ }
159
+
160
+ return data
161
+ }
162
+
163
+ async gqlPostOrThrow<U, V>(payload: GraphQLRequest<U>): Promise<FlyResult<V>> {
164
+ return await this.gqlPost<U, V>(payload)
165
+ }
166
+
167
+ async rest<V>(
168
+ path: string,
169
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
170
+ body?: unknown,
171
+ headers?: Record<string, string>,
172
+ ): Promise<FlyClientError | V> {
173
+ const response = await fetch(`${this.apiUrl}/v1/${path}`, {
174
+ method,
175
+ headers: {
176
+ Authorization: `Bearer ${this.apiKey}`,
177
+ 'Content-Type': 'application/json',
178
+ ...headers,
179
+ },
180
+ body: body !== undefined ? JSON.stringify(body) : undefined,
181
+ }).catch((cause: unknown) => {
182
+ return new FlyApiError({
183
+ method,
184
+ path,
185
+ httpStatus: 0,
186
+ cause,
187
+ })
188
+ })
189
+
190
+ if (response instanceof Error) {
191
+ return response
192
+ }
193
+
194
+ const responseText = await response.text().catch((cause: unknown) => {
195
+ return new FlyApiError({
196
+ method,
197
+ path,
198
+ httpStatus: response.status,
199
+ cause,
200
+ })
201
+ })
202
+
203
+ if (responseText instanceof Error) {
204
+ return responseText
205
+ }
206
+
207
+ if (!response.ok) {
208
+ const payloadOrError = parseJson({ text: responseText })
209
+ if (payloadOrError instanceof Error) {
210
+ return new FlyApiError({
211
+ method,
212
+ path,
213
+ httpStatus: response.status,
214
+ cause: payloadOrError,
215
+ })
216
+ }
217
+
218
+ return createFlyHttpError({
219
+ method,
220
+ path,
221
+ httpStatus: response.status,
222
+ payload: payloadOrError,
223
+ })
224
+ }
225
+
226
+ if (!responseText) {
227
+ return undefined as V
228
+ }
229
+
230
+ const payloadOrError = parseJson({ text: responseText })
231
+ if (payloadOrError instanceof Error) {
232
+ return new FlyApiError({
233
+ method,
234
+ path,
235
+ httpStatus: response.status,
236
+ cause: payloadOrError,
237
+ })
238
+ }
239
+
240
+ return payloadOrError as V
241
+ }
242
+
243
+ async restOrThrow<V>(
244
+ path: string,
245
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
246
+ body?: unknown,
247
+ headers?: Record<string, string>,
248
+ ): Promise<FlyResult<V>> {
249
+ return await this.rest(path, method, body, headers)
250
+ }
251
+ }
252
+
253
+ function parseJson({ text }: { text: string }): Error | unknown {
254
+ return errore.try({
255
+ try: () => {
256
+ return JSON.parse(text) as unknown
257
+ },
258
+ catch: (cause) => {
259
+ return new Error('Failed to parse JSON response', { cause })
260
+ },
261
+ })
262
+ }