@distilled.cloud/fly-io 1.0.0-rc.15 → 1.0.0-rc.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/.lazy.json +1 -0
- package/lib/_bundle/lazy.js +4 -0
- package/lib/_bundle/sdk.js +2 -0
- package/lib/_bundle/sdk.js.map +1 -0
- package/lib/credentials.d.ts +3 -3
- package/lib/credentials.d.ts.map +1 -1
- package/lib/credentials.js +1 -65
- package/lib/errors.d.ts +39 -24
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -62
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -33
- package/lib/index.lazy.js +7 -0
- package/lib/pagination.d.ts +1 -1
- package/lib/pagination.js +1 -5
- package/lib/protocol.d.ts +2 -2
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +1 -541
- package/lib/retry.d.ts +1 -1
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +1 -33
- package/lib/services/addons.d.ts +0 -65
- package/lib/services/addons.d.ts.map +1 -1
- package/lib/services/addons.js +22 -567
- package/lib/services/addons.js.map +1 -1
- package/lib/services/builders.d.ts +31 -0
- package/lib/services/builders.d.ts.map +1 -0
- package/lib/services/builders.js +2 -0
- package/lib/services/builders.js.map +1 -0
- package/lib/services/index.d.ts +1 -0
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +1 -2
- package/lib/services/machines.d.ts +25 -444
- package/lib/services/machines.d.ts.map +1 -1
- package/lib/services/machines.js +137 -2736
- package/lib/services/machines.js.map +1 -1
- package/lib/services/mpg.d.ts +1 -67
- package/lib/services/mpg.d.ts.map +1 -1
- package/lib/services/mpg.js +31 -453
- package/lib/services/mpg.js.map +1 -1
- package/lib/services/sprites.d.ts +1 -125
- package/lib/services/sprites.d.ts.map +1 -1
- package/lib/services/sprites.js +62 -890
- package/lib/services/sprites.js.map +1 -1
- package/lib/services.lazy/addons.js +3 -0
- package/lib/services.lazy/builders.js +1 -0
- package/lib/services.lazy/index.js +3 -0
- package/lib/services.lazy/machines.js +4 -0
- package/lib/services.lazy/mpg.js +3 -0
- package/lib/services.lazy/sprites.js +3 -0
- package/lib/traits.d.ts.map +1 -1
- package/lib/traits.js +1 -21
- package/package.json +20 -7
- package/src/credentials.ts +18 -15
- package/src/errors.ts +40 -28
- package/src/index.ts +2 -2
- package/src/pagination.ts +1 -1
- package/src/protocol.ts +67 -36
- package/src/retry.ts +6 -4
- package/src/services/addons.ts +593 -692
- package/src/services/builders.ts +93 -0
- package/src/services/index.ts +1 -0
- package/src/services/machines.ts +3332 -3364
- package/src/services/mpg.ts +582 -519
- package/src/services/sprites.ts +1008 -948
- package/src/traits.ts +8 -4
- package/lib/credentials.js.map +0 -1
- package/lib/errors.js.map +0 -1
- package/lib/pagination.js.map +0 -1
- package/lib/protocol.js.map +0 -1
- package/lib/protocol.test.d.ts +0 -2
- package/lib/protocol.test.d.ts.map +0 -1
- package/lib/protocol.test.js +0 -126
- package/lib/protocol.test.js.map +0 -1
- package/lib/retry.js.map +0 -1
- package/lib/services/index.js.map +0 -1
- package/lib/sprites-mint.test.d.ts +0 -2
- package/lib/sprites-mint.test.d.ts.map +0 -1
- package/lib/sprites-mint.test.js +0 -169
- package/lib/sprites-mint.test.js.map +0 -1
- package/lib/traits.js.map +0 -1
- package/src/protocol.test.ts +0 -214
- package/src/sprites-mint.test.ts +0 -228
- /package/lib/{index.js.map → index.lazy.js.map} +0 -0
package/lib/protocol.js
CHANGED
|
@@ -1,541 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { HTTP_STATUS_MAP, InternalServerError, } from "@distilled.cloud/core/errors";
|
|
3
|
-
import { buildRequest, getAnn, mapKeys, matchTypedError, } from "@distilled.cloud/core/protocol-http";
|
|
4
|
-
import { makeRestProtocol, unwrapRedactedDeep, wrapSensitive, } from "@distilled.cloud/core/protocol-rest";
|
|
5
|
-
import { failIfStrict, validateResponse } from "@distilled.cloud/core/response-validation";
|
|
6
|
-
import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
|
|
7
|
-
/**
|
|
8
|
-
* Fly.io protocols — Machines REST, UI-EX REST (MPG), GraphQL add-ons, and
|
|
9
|
-
* Sprites REST.
|
|
10
|
-
*
|
|
11
|
-
* Machines (`FlyIoProtocol`) is a plain bearer-token JSON API at
|
|
12
|
-
* `api.machines.dev` (routes carry the `/v1` prefix). MPG (`FlyApiProtocol`) is the same token against
|
|
13
|
-
* `https://api.fly.io`. GraphQL add-ons (`FlyGraphqlProtocol`) POST
|
|
14
|
-
* `{ query, operationName, variables }` to `https://api.fly.io/graphql`.
|
|
15
|
-
*
|
|
16
|
-
* Sprites (`SpritesProtocol`) also resolve `FLY_API_TOKEN` (same
|
|
17
|
-
* {@link Credentials} as Machines). Sprite CRUD rejects a Fly macaroon as
|
|
18
|
-
* Bearer, so the protocol mints a Sprites bearer once:
|
|
19
|
-
*
|
|
20
|
-
* POST https://api.sprites.dev/v1/organizations/{org}/tokens
|
|
21
|
-
* Authorization: FlyV1 {FLY_API_TOKEN}
|
|
22
|
-
*
|
|
23
|
-
* Org slug comes from Machines `GET /tokens/current` (GraphQL fallback).
|
|
24
|
-
* The minted token is cached process-wide, then sent as
|
|
25
|
-
* `Authorization: Bearer` on sprite ops. There is no `SPRITES_TOKEN`.
|
|
26
|
-
*/
|
|
27
|
-
import * as Context from "effect/Context";
|
|
28
|
-
import * as Deferred from "effect/Deferred";
|
|
29
|
-
import * as Effect from "effect/Effect";
|
|
30
|
-
import * as Exit from "effect/Exit";
|
|
31
|
-
import * as HttpClient from "effect/http/HttpClient";
|
|
32
|
-
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
33
|
-
import * as Layer from "effect/Layer";
|
|
34
|
-
import * as MutableHashMap from "effect/MutableHashMap";
|
|
35
|
-
import * as Option from "effect/Option";
|
|
36
|
-
import * as Redacted from "effect/Redacted";
|
|
37
|
-
import * as Schema from "effect/Schema";
|
|
38
|
-
import { Credentials, DEFAULT_FLY_API_BASE_URL, DEFAULT_SPRITES_API_BASE_URL, } from "./credentials.js";
|
|
39
|
-
import { ConfigError as FlyConfigError, FlyIoParseError, CreateExtensionTosAgreementNotAuthorized, SpritesNotEnabled, UnknownFlyIoError, } from "./errors.js";
|
|
40
|
-
import { graphqlOpSymbol, httpSymbol, responsePathSymbol, } from "./traits.js";
|
|
41
|
-
const flyErrorEnvelope = (body) => {
|
|
42
|
-
if (body === null || typeof body !== "object")
|
|
43
|
-
return undefined;
|
|
44
|
-
const b = body;
|
|
45
|
-
const errors = b.errors;
|
|
46
|
-
const detail = errors && typeof errors.detail === "string" ? errors.detail : undefined;
|
|
47
|
-
return {
|
|
48
|
-
message: typeof b.error === "string" ? b.error : typeof b.message === "string" ? b.message : detail,
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
/** Sprites prefers `message` when the body carries both `error` (code) and `message`. */
|
|
52
|
-
const spritesErrorEnvelope = (body) => {
|
|
53
|
-
if (body === null || typeof body !== "object")
|
|
54
|
-
return undefined;
|
|
55
|
-
const b = body;
|
|
56
|
-
const message = typeof b.message === "string" ? b.message : typeof b.error === "string" ? b.error : undefined;
|
|
57
|
-
const code = typeof b.error === "string" && typeof b.message === "string" ? b.error : undefined;
|
|
58
|
-
return { message, code };
|
|
59
|
-
};
|
|
60
|
-
const resolveFlyCreds = Effect.gen(function* () {
|
|
61
|
-
const resolve = yield* Credentials;
|
|
62
|
-
return yield* resolve;
|
|
63
|
-
});
|
|
64
|
-
/**
|
|
65
|
-
* Per-call signal that this Machines request is the Fly Machine itself
|
|
66
|
-
* (unix socket `/.fly/api`). Org tokens stay on {@link Credentials} for
|
|
67
|
-
* GetSecret in the same process; this service only changes headers:
|
|
68
|
-
* omit `Authorization`, send `Connection: close`.
|
|
69
|
-
*/
|
|
70
|
-
export class MachineIdentity extends Context.Service()("FlyIoMachineIdentity") {
|
|
71
|
-
}
|
|
72
|
-
const FlyIoProtocolRest = makeRestProtocol({
|
|
73
|
-
credentials: resolveFlyCreds,
|
|
74
|
-
baseUrl: (creds) => creds.apiBaseUrl,
|
|
75
|
-
headers: (creds) => ({
|
|
76
|
-
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
|
|
77
|
-
}),
|
|
78
|
-
errorEnvelope: flyErrorEnvelope,
|
|
79
|
-
unknownError: ({ message, body }) => new UnknownFlyIoError({ message, body }),
|
|
80
|
-
parseError: ({ body, cause }) => new FlyIoParseError({ body, cause }),
|
|
81
|
-
});
|
|
82
|
-
export const FlyIoProtocol = Layer.effect(API.Protocol, Effect.gen(function* () {
|
|
83
|
-
const rest = yield* API.Protocol;
|
|
84
|
-
return API.Protocol.of({
|
|
85
|
-
encode: (args) => Effect.gen(function* () {
|
|
86
|
-
const request = yield* rest.encode(args);
|
|
87
|
-
const machine = yield* Effect.serviceOption(MachineIdentity);
|
|
88
|
-
if (Option.isNone(machine))
|
|
89
|
-
return request;
|
|
90
|
-
return request.pipe(HttpClientRequest.removeHeader("authorization"), HttpClientRequest.setHeader("connection", "close"));
|
|
91
|
-
}),
|
|
92
|
-
decode: rest.decode,
|
|
93
|
-
});
|
|
94
|
-
})).pipe(Layer.provide(FlyIoProtocolRest));
|
|
95
|
-
/** MPG UI-EX REST at `https://api.fly.io` with the Machines bearer token. */
|
|
96
|
-
export const FlyApiProtocol = makeRestProtocol({
|
|
97
|
-
credentials: resolveFlyCreds,
|
|
98
|
-
baseUrl: () => DEFAULT_FLY_API_BASE_URL,
|
|
99
|
-
headers: (creds) => ({
|
|
100
|
-
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
|
|
101
|
-
}),
|
|
102
|
-
errorEnvelope: flyErrorEnvelope,
|
|
103
|
-
unknownError: ({ message, body }) => new UnknownFlyIoError({ message, body }),
|
|
104
|
-
parseError: ({ body, cause }) => new FlyIoParseError({ body, cause }),
|
|
105
|
-
});
|
|
106
|
-
const parseMaybeNdjson = (body) => {
|
|
107
|
-
if (typeof body !== "string")
|
|
108
|
-
return body;
|
|
109
|
-
const lines = body
|
|
110
|
-
.split(/\r?\n/)
|
|
111
|
-
.map((l) => l.trim())
|
|
112
|
-
.filter((l) => l.length > 0);
|
|
113
|
-
if (lines.length === 0)
|
|
114
|
-
return body;
|
|
115
|
-
try {
|
|
116
|
-
return lines.map((l) => JSON.parse(l));
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return body;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
/**
|
|
123
|
-
* HTTP exec (`POST /sprites/{name}/exec`) returns `application/octet-stream`
|
|
124
|
-
* frames: 1-byte stream id + payload. Ids: 1 stdout, 2 stderr, 3 exit
|
|
125
|
-
* (payload is a single exit-code byte). HTTP may coalesce chunks; for a
|
|
126
|
-
* complete body the exit frame is the last two bytes when present.
|
|
127
|
-
*/
|
|
128
|
-
const parseSpritesExecFrames = (bytes) => {
|
|
129
|
-
if (bytes.length === 0)
|
|
130
|
-
return undefined;
|
|
131
|
-
const first = bytes[0];
|
|
132
|
-
if (first > 3)
|
|
133
|
-
return undefined;
|
|
134
|
-
let end = bytes.length;
|
|
135
|
-
let exit_code = 0;
|
|
136
|
-
if (end >= 2 && bytes[end - 2] === 3) {
|
|
137
|
-
exit_code = bytes[end - 1];
|
|
138
|
-
end -= 2;
|
|
139
|
-
}
|
|
140
|
-
let stdout = "";
|
|
141
|
-
let stderr = "";
|
|
142
|
-
if (end > 0) {
|
|
143
|
-
const decoder = new TextDecoder();
|
|
144
|
-
const id = bytes[0];
|
|
145
|
-
const payload = decoder.decode(bytes.subarray(1, end));
|
|
146
|
-
if (id === 2)
|
|
147
|
-
stderr = payload;
|
|
148
|
-
else
|
|
149
|
-
stdout = payload;
|
|
150
|
-
}
|
|
151
|
-
return { stdout, stderr, exit_code };
|
|
152
|
-
};
|
|
153
|
-
const fail = (e) => Effect.fail(e);
|
|
154
|
-
/** Strip a leading `FlyV1` / `Bearer` scheme so we can re-prefix `FlyV1`. */
|
|
155
|
-
const stripFlyAuthScheme = (token) => {
|
|
156
|
-
const trimmed = token.trim();
|
|
157
|
-
const space = trimmed.indexOf(" ");
|
|
158
|
-
if (space === -1)
|
|
159
|
-
return trimmed;
|
|
160
|
-
const scheme = trimmed.slice(0, space).toLowerCase();
|
|
161
|
-
if (scheme === "flyv1" || scheme === "bearer") {
|
|
162
|
-
return trimmed.slice(space + 1).trim();
|
|
163
|
-
}
|
|
164
|
-
return trimmed;
|
|
165
|
-
};
|
|
166
|
-
const isMintOp = (http) => http !== undefined &&
|
|
167
|
-
http.method === "POST" &&
|
|
168
|
-
/\/organizations\/\{[^}]+}\/tokens$/.test(http.uri);
|
|
169
|
-
const SPRITES_NOT_ENABLED = /sprites not enabled/i;
|
|
170
|
-
const readResponseText = (response) => response.text.pipe(Effect.orDie, Effect.map((text) => text ?? ""));
|
|
171
|
-
const parseJson = (text) => {
|
|
172
|
-
if (text.trim().length === 0)
|
|
173
|
-
return undefined;
|
|
174
|
-
try {
|
|
175
|
-
return JSON.parse(text);
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
const haystackOf = (body, text) => {
|
|
182
|
-
const parts = [text];
|
|
183
|
-
if (body !== null && typeof body === "object") {
|
|
184
|
-
const b = body;
|
|
185
|
-
if (typeof b.message === "string")
|
|
186
|
-
parts.push(b.message);
|
|
187
|
-
if (typeof b.error === "string")
|
|
188
|
-
parts.push(b.error);
|
|
189
|
-
}
|
|
190
|
-
return parts.join(" ");
|
|
191
|
-
};
|
|
192
|
-
const errorEnv = (body) => spritesErrorEnvelope(body) ?? flyErrorEnvelope(body) ?? {};
|
|
193
|
-
const failHttp = (status, body, text, headers) => {
|
|
194
|
-
const env = errorEnv(body);
|
|
195
|
-
const message = env.message ?? (text.trim().length > 0 ? text.trim() : `HTTP ${status}`);
|
|
196
|
-
if (status === 401 && SPRITES_NOT_ENABLED.test(haystackOf(body, message))) {
|
|
197
|
-
return fail(new SpritesNotEnabled({ message }));
|
|
198
|
-
}
|
|
199
|
-
const StatusClass = HTTP_STATUS_MAP[status];
|
|
200
|
-
if (StatusClass && status >= 400) {
|
|
201
|
-
return fail(new StatusClass({
|
|
202
|
-
message,
|
|
203
|
-
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
204
|
-
}));
|
|
205
|
-
}
|
|
206
|
-
return fail(new UnknownFlyIoError({ message, body: body ?? text }));
|
|
207
|
-
};
|
|
208
|
-
const orgFromTokensCurrent = (body) => {
|
|
209
|
-
if (body === null || typeof body !== "object")
|
|
210
|
-
return undefined;
|
|
211
|
-
const tokens = body.tokens;
|
|
212
|
-
if (!Array.isArray(tokens) || tokens.length === 0)
|
|
213
|
-
return undefined;
|
|
214
|
-
const first = tokens[0];
|
|
215
|
-
if (typeof first.org_slug === "string" && first.org_slug.length > 0) {
|
|
216
|
-
return first.org_slug;
|
|
217
|
-
}
|
|
218
|
-
if (typeof first.organization === "string" && first.organization.length > 0) {
|
|
219
|
-
return first.organization;
|
|
220
|
-
}
|
|
221
|
-
return undefined;
|
|
222
|
-
};
|
|
223
|
-
const orgFromGraphql = (body) => {
|
|
224
|
-
if (body === null || typeof body !== "object")
|
|
225
|
-
return undefined;
|
|
226
|
-
const data = body.data;
|
|
227
|
-
if (data === null || typeof data !== "object")
|
|
228
|
-
return undefined;
|
|
229
|
-
const viewer = data.viewer;
|
|
230
|
-
const root = viewer !== null && typeof viewer === "object" ? viewer : data;
|
|
231
|
-
const personal = root.personalOrganization;
|
|
232
|
-
if (typeof personal?.slug === "string" && personal.slug.length > 0) {
|
|
233
|
-
return personal.slug;
|
|
234
|
-
}
|
|
235
|
-
const nodes = root.organizations?.nodes;
|
|
236
|
-
if (Array.isArray(nodes)) {
|
|
237
|
-
for (const node of nodes) {
|
|
238
|
-
if (node !== null &&
|
|
239
|
-
typeof node === "object" &&
|
|
240
|
-
typeof node.slug === "string" &&
|
|
241
|
-
node.slug.length > 0) {
|
|
242
|
-
return node.slug;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return undefined;
|
|
247
|
-
};
|
|
248
|
-
const discoverOrgSlug = (fly) => Effect.gen(function* () {
|
|
249
|
-
const http = yield* HttpClient.HttpClient;
|
|
250
|
-
const machinesRoot = fly.apiBaseUrl.replace(/\/+$/, "");
|
|
251
|
-
const currentReq = HttpClientRequest.make("GET")(`${machinesRoot}/v1/tokens/current`).pipe(HttpClientRequest.setHeaders({
|
|
252
|
-
Authorization: `Bearer ${Redacted.value(fly.apiKey)}`,
|
|
253
|
-
Accept: "application/json",
|
|
254
|
-
}));
|
|
255
|
-
const currentRes = yield* http.execute(currentReq);
|
|
256
|
-
const currentText = yield* readResponseText(currentRes);
|
|
257
|
-
const currentJson = parseJson(currentText);
|
|
258
|
-
if (currentRes.status < 400) {
|
|
259
|
-
const slug = orgFromTokensCurrent(currentJson);
|
|
260
|
-
if (slug !== undefined)
|
|
261
|
-
return slug;
|
|
262
|
-
}
|
|
263
|
-
const gqlReq = HttpClientRequest.make("POST")(`${DEFAULT_FLY_API_BASE_URL}/graphql`).pipe(HttpClientRequest.setHeaders({
|
|
264
|
-
Authorization: `Bearer ${Redacted.value(fly.apiKey)}`,
|
|
265
|
-
Accept: "application/json",
|
|
266
|
-
}), HttpClientRequest.bodyJsonUnsafe({
|
|
267
|
-
query: "{ viewer { personalOrganization { slug } organizations { nodes { slug } } } }",
|
|
268
|
-
}));
|
|
269
|
-
const gqlRes = yield* http.execute(gqlReq);
|
|
270
|
-
const gqlText = yield* readResponseText(gqlRes);
|
|
271
|
-
const gqlJson = parseJson(gqlText);
|
|
272
|
-
if (gqlRes.status < 400) {
|
|
273
|
-
const slug = orgFromGraphql(gqlJson);
|
|
274
|
-
if (slug !== undefined)
|
|
275
|
-
return slug;
|
|
276
|
-
}
|
|
277
|
-
return yield* new FlyConfigError({
|
|
278
|
-
message: "Could not discover a Fly organization slug from /tokens/current or GraphQL",
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
const mintOnce = (fly) => Effect.gen(function* () {
|
|
282
|
-
const http = yield* HttpClient.HttpClient;
|
|
283
|
-
const orgSlug = yield* discoverOrgSlug(fly);
|
|
284
|
-
const macaroon = stripFlyAuthScheme(Redacted.value(fly.apiKey));
|
|
285
|
-
const request = HttpClientRequest.make("POST")(`${DEFAULT_SPRITES_API_BASE_URL}/organizations/${encodeURIComponent(orgSlug)}/tokens`).pipe(HttpClientRequest.setHeaders({
|
|
286
|
-
Authorization: `FlyV1 ${macaroon}`,
|
|
287
|
-
Accept: "application/json",
|
|
288
|
-
"Content-Type": "application/json",
|
|
289
|
-
}), HttpClientRequest.bodyJsonUnsafe({
|
|
290
|
-
description: "Alchemy Sprites token",
|
|
291
|
-
}));
|
|
292
|
-
const response = yield* http.execute(request);
|
|
293
|
-
const text = yield* readResponseText(response);
|
|
294
|
-
const json = parseJson(text);
|
|
295
|
-
const headers = response.headers;
|
|
296
|
-
if (response.status >= 400) {
|
|
297
|
-
return yield* failHttp(response.status, json, text, headers);
|
|
298
|
-
}
|
|
299
|
-
const token = json !== null && typeof json === "object" ? json.token : undefined;
|
|
300
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
301
|
-
return yield* fail(new FlyIoParseError({
|
|
302
|
-
body: json ?? text,
|
|
303
|
-
cause: "mint response missing token",
|
|
304
|
-
}));
|
|
305
|
-
}
|
|
306
|
-
return Redacted.make(token);
|
|
307
|
-
});
|
|
308
|
-
// Keyed by the Fly token itself: `Redacted` hashes and compares by value.
|
|
309
|
-
const mintedTokens = MutableHashMap.empty();
|
|
310
|
-
const mintInFlight = MutableHashMap.empty();
|
|
311
|
-
/** Mint (and process-wide cache) a Sprites bearer from `FLY_API_TOKEN`. */
|
|
312
|
-
const mintSpritesToken = (fly) => Effect.uninterruptibleMask((restore) => Effect.suspend(() => {
|
|
313
|
-
const key = fly.apiKey;
|
|
314
|
-
const hit = MutableHashMap.get(mintedTokens, key);
|
|
315
|
-
if (Option.isSome(hit))
|
|
316
|
-
return Effect.succeed(hit.value);
|
|
317
|
-
const pending = MutableHashMap.get(mintInFlight, key);
|
|
318
|
-
if (Option.isSome(pending)) {
|
|
319
|
-
return restore(Deferred.await(pending.value));
|
|
320
|
-
}
|
|
321
|
-
const result = Deferred.makeUnsafe();
|
|
322
|
-
MutableHashMap.set(mintInFlight, key, result);
|
|
323
|
-
return restore(mintOnce(fly)).pipe(Effect.onExit((exit) => Effect.gen(function* () {
|
|
324
|
-
if (Exit.isSuccess(exit)) {
|
|
325
|
-
MutableHashMap.set(mintedTokens, key, exit.value);
|
|
326
|
-
}
|
|
327
|
-
// Release all waiters, including on defects and owner interruption.
|
|
328
|
-
MutableHashMap.remove(mintInFlight, key);
|
|
329
|
-
yield* Deferred.done(result, exit);
|
|
330
|
-
})));
|
|
331
|
-
}));
|
|
332
|
-
const encodeSpritesRequest = ({ input, inputAst, }) => Effect.gen(function* () {
|
|
333
|
-
const fly = yield* resolveFlyCreds;
|
|
334
|
-
const http = getAnn(inputAst, httpSymbol);
|
|
335
|
-
const mint = isMintOp(http);
|
|
336
|
-
let authorization;
|
|
337
|
-
if (mint) {
|
|
338
|
-
authorization = `FlyV1 ${stripFlyAuthScheme(Redacted.value(fly.apiKey))}`;
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
const token = yield* mintSpritesToken(fly);
|
|
342
|
-
authorization = `Bearer ${Redacted.value(token)}`;
|
|
343
|
-
}
|
|
344
|
-
return buildRequest({
|
|
345
|
-
input: unwrapRedactedDeep(input),
|
|
346
|
-
inputAst,
|
|
347
|
-
baseUrl: DEFAULT_SPRITES_API_BASE_URL,
|
|
348
|
-
headers: {
|
|
349
|
-
Authorization: authorization,
|
|
350
|
-
Accept: "application/json",
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
const decodeSpritesResponse = ({ response, outputAst, errors: errorClasses, }) => Effect.gen(function* () {
|
|
355
|
-
const buffer = yield* response.arrayBuffer.pipe(Effect.map((bytes) => new Uint8Array(bytes)));
|
|
356
|
-
const text = new TextDecoder().decode(buffer);
|
|
357
|
-
const execFrames = parseSpritesExecFrames(buffer);
|
|
358
|
-
let json;
|
|
359
|
-
let nonJson = false;
|
|
360
|
-
if (execFrames === undefined && text.trim().length > 0) {
|
|
361
|
-
try {
|
|
362
|
-
json = JSON.parse(text);
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
nonJson = true;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const status = response.status;
|
|
369
|
-
const headers = response.headers;
|
|
370
|
-
if (status >= 400) {
|
|
371
|
-
const env = nonJson ? {} : errorEnv(json);
|
|
372
|
-
const message = env.message ?? (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
|
|
373
|
-
if (SPRITES_NOT_ENABLED.test(haystackOf(nonJson ? text : json, message))) {
|
|
374
|
-
return yield* fail(new SpritesNotEnabled({ message }));
|
|
375
|
-
}
|
|
376
|
-
const typed = matchTypedError(errorClasses, status, [
|
|
377
|
-
{
|
|
378
|
-
code: typeof env.code === "number" ? env.code : undefined,
|
|
379
|
-
message,
|
|
380
|
-
},
|
|
381
|
-
], { body: nonJson ? text : json, headers });
|
|
382
|
-
if (typed !== undefined)
|
|
383
|
-
return yield* fail(typed);
|
|
384
|
-
const StatusErrorClass = HTTP_STATUS_MAP[status];
|
|
385
|
-
if (StatusErrorClass) {
|
|
386
|
-
return yield* fail(new StatusErrorClass({
|
|
387
|
-
message,
|
|
388
|
-
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
if (status >= 500) {
|
|
392
|
-
return yield* fail(new InternalServerError({
|
|
393
|
-
message,
|
|
394
|
-
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
395
|
-
}));
|
|
396
|
-
}
|
|
397
|
-
return yield* fail(new UnknownFlyIoError({
|
|
398
|
-
message,
|
|
399
|
-
body: nonJson ? text : json,
|
|
400
|
-
}));
|
|
401
|
-
}
|
|
402
|
-
// Strict mode checks the mapped body (or parsed exec frames) against the
|
|
403
|
-
// output schema.
|
|
404
|
-
const body = execFrames ?? parseMaybeNdjson(nonJson ? text : (json ?? {}));
|
|
405
|
-
const mapped = yield* validateResponse(outputAst, mapKeys(outputAst, body, "decode"), (cause) => new FlyIoParseError({
|
|
406
|
-
body: execFrames ?? (nonJson ? text : json),
|
|
407
|
-
cause,
|
|
408
|
-
})).pipe(Effect.catch(fail));
|
|
409
|
-
return wrapSensitive(outputAst, mapped);
|
|
410
|
-
});
|
|
411
|
-
/** Sprites REST at `https://api.sprites.dev/v1`, minted from `FLY_API_TOKEN`. */
|
|
412
|
-
export const SpritesProtocol = Layer.succeed(API.Protocol, API.Protocol.of({
|
|
413
|
-
encode: (args) => encodeSpritesRequest(args),
|
|
414
|
-
decode: (args) => decodeSpritesResponse(args),
|
|
415
|
-
}));
|
|
416
|
-
// =============================================================================
|
|
417
|
-
// GraphQL add-ons (Tigris, Redis) — POST https://api.fly.io/graphql
|
|
418
|
-
// =============================================================================
|
|
419
|
-
const GraphQLError = Schema.Struct({
|
|
420
|
-
message: Schema.String,
|
|
421
|
-
path: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
422
|
-
extensions: Schema.optional(Schema.Struct({
|
|
423
|
-
code: Schema.optional(Schema.String),
|
|
424
|
-
})),
|
|
425
|
-
});
|
|
426
|
-
const GraphQLEnvelope = Schema.Struct({
|
|
427
|
-
data: Schema.optional(Schema.NullOr(Schema.Unknown)),
|
|
428
|
-
errors: Schema.optional(Schema.Array(GraphQLError)),
|
|
429
|
-
});
|
|
430
|
-
const decodeEnvelope = Schema.decodeUnknownOption(GraphQLEnvelope);
|
|
431
|
-
const matchGraphqlError = (status, errorBody, headers, errorClasses) => {
|
|
432
|
-
const envelope = decodeEnvelope(errorBody);
|
|
433
|
-
if (envelope._tag === "Some" && envelope.value.errors?.length) {
|
|
434
|
-
const first = envelope.value.errors[0];
|
|
435
|
-
const message = first.message;
|
|
436
|
-
const typed = matchTypedError(errorClasses, status, [{ message }], {
|
|
437
|
-
body: errorBody,
|
|
438
|
-
headers,
|
|
439
|
-
});
|
|
440
|
-
if (typed !== undefined)
|
|
441
|
-
return fail(typed);
|
|
442
|
-
if (/not authorized to access this createextensiontosagreement/i.test(message)) {
|
|
443
|
-
return fail(new CreateExtensionTosAgreementNotAuthorized({ message }));
|
|
444
|
-
}
|
|
445
|
-
const StatusClass = HTTP_STATUS_MAP[status];
|
|
446
|
-
if (StatusClass && status >= 400) {
|
|
447
|
-
return fail(new StatusClass({
|
|
448
|
-
message,
|
|
449
|
-
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
450
|
-
}));
|
|
451
|
-
}
|
|
452
|
-
return fail(new UnknownFlyIoError({
|
|
453
|
-
code: first.extensions?.code,
|
|
454
|
-
message,
|
|
455
|
-
body: errorBody,
|
|
456
|
-
}));
|
|
457
|
-
}
|
|
458
|
-
const StatusClass = HTTP_STATUS_MAP[status];
|
|
459
|
-
if (StatusClass && status >= 400) {
|
|
460
|
-
return fail(new StatusClass({
|
|
461
|
-
message: `HTTP ${status}`,
|
|
462
|
-
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
463
|
-
}));
|
|
464
|
-
}
|
|
465
|
-
return fail(new UnknownFlyIoError({ body: errorBody }));
|
|
466
|
-
};
|
|
467
|
-
const graphqlEncode = ({ input, inputAst, }) => Effect.gen(function* () {
|
|
468
|
-
const resolveCredentials = yield* Credentials;
|
|
469
|
-
const creds = yield* resolveCredentials;
|
|
470
|
-
const op = getAnn(inputAst, graphqlOpSymbol);
|
|
471
|
-
if (!op) {
|
|
472
|
-
throw new Error("operation input is missing the GraphQLOp() trait");
|
|
473
|
-
}
|
|
474
|
-
const http = getAnn(inputAst, httpSymbol);
|
|
475
|
-
const variables = {};
|
|
476
|
-
for (const [k, v] of Object.entries((input ?? {}))) {
|
|
477
|
-
if (v !== undefined)
|
|
478
|
-
variables[k] = v;
|
|
479
|
-
}
|
|
480
|
-
const url = `${DEFAULT_FLY_API_BASE_URL}${http?.uri ?? "/graphql"}`;
|
|
481
|
-
return HttpClientRequest.make("POST")(url).pipe(HttpClientRequest.setHeaders({
|
|
482
|
-
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
|
|
483
|
-
Accept: "application/json",
|
|
484
|
-
}), HttpClientRequest.bodyJsonUnsafe({
|
|
485
|
-
query: op.query,
|
|
486
|
-
operationName: op.operationName,
|
|
487
|
-
variables,
|
|
488
|
-
}));
|
|
489
|
-
});
|
|
490
|
-
const graphqlDecode = ({ response, outputAst, errors, }) => Effect.gen(function* () {
|
|
491
|
-
const text = (yield* response.text.pipe(Effect.orDie)) ?? "";
|
|
492
|
-
const status = response.status;
|
|
493
|
-
const headers = response.headers;
|
|
494
|
-
let json;
|
|
495
|
-
let parsed = false;
|
|
496
|
-
if (text.trim().length > 0) {
|
|
497
|
-
try {
|
|
498
|
-
json = JSON.parse(text);
|
|
499
|
-
parsed = true;
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
parsed = false;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (!parsed) {
|
|
506
|
-
if (status >= 400) {
|
|
507
|
-
return yield* matchGraphqlError(status, text, headers, errors);
|
|
508
|
-
}
|
|
509
|
-
// Lenient mode returns the body as read.
|
|
510
|
-
return yield* failIfStrict(new FlyIoParseError({
|
|
511
|
-
body: text,
|
|
512
|
-
cause: "response body is not valid JSON",
|
|
513
|
-
}), text).pipe(Effect.catch(fail));
|
|
514
|
-
}
|
|
515
|
-
const envelope = json;
|
|
516
|
-
const hasGraphQLErrors = envelope !== null &&
|
|
517
|
-
typeof envelope === "object" &&
|
|
518
|
-
Array.isArray(envelope.errors) &&
|
|
519
|
-
envelope.errors.length > 0;
|
|
520
|
-
if (hasGraphQLErrors || status >= 400) {
|
|
521
|
-
return yield* matchGraphqlError(status, json, headers, errors);
|
|
522
|
-
}
|
|
523
|
-
const path = getAnn(outputAst, responsePathSymbol);
|
|
524
|
-
let payload = envelope !== null && typeof envelope === "object" ? envelope.data : undefined;
|
|
525
|
-
if (path !== undefined) {
|
|
526
|
-
for (const seg of path.split(".")) {
|
|
527
|
-
payload =
|
|
528
|
-
payload !== null && typeof payload === "object"
|
|
529
|
-
? payload[seg]
|
|
530
|
-
: undefined;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// Strict mode checks the payload at the response path against the
|
|
534
|
-
// output schema.
|
|
535
|
-
return yield* validateResponse(outputAst, payload === undefined ? null : payload, (cause) => new FlyIoParseError({ body: json, cause })).pipe(Effect.catch(fail));
|
|
536
|
-
});
|
|
537
|
-
export const FlyGraphqlProtocol = Layer.succeed(API.Protocol, API.Protocol.of({
|
|
538
|
-
encode: (args) => graphqlEncode(args),
|
|
539
|
-
decode: graphqlDecode,
|
|
540
|
-
}));
|
|
541
|
-
//# sourceMappingURL=protocol.js.map
|
|
1
|
+
export { protocol$FlyApiProtocol as FlyApiProtocol, protocol$FlyGraphqlProtocol as FlyGraphqlProtocol, protocol$FlyIoProtocol as FlyIoProtocol, protocol$MachineIdentity as MachineIdentity, protocol$SpritesProtocol as SpritesProtocol } from "./_bundle/sdk.js";
|
package/lib/retry.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import * as Effect from "effect/Effect";
|
|
|
20
20
|
export { type Options, type Factory, type Policy, makeDefault, jittered, capped, throttlingOptions, transientOptions, throttlingFactory, transientFactory, } from "@distilled.cloud/core/retry";
|
|
21
21
|
declare const Retry_base: Context.ServiceClass<Retry, "FlyIoRetry", Policy>;
|
|
22
22
|
/** Context tag for configuring retry behavior of FlyIo API calls. */
|
|
23
|
-
export declare class Retry extends Retry_base {
|
|
23
|
+
export declare class Retry extends /*@__PURE__*/ Retry_base {
|
|
24
24
|
}
|
|
25
25
|
/** Provides a custom retry policy to every FlyIo API call below it. */
|
|
26
26
|
export declare const policy: (optionsOrFactory: Policy) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Retry>>;
|
package/lib/retry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAuC,MAAM,6BAA6B,CAAC;AAC/F;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,MAAM,EACX,WAAW,EACX,QAAQ,EACR,MAAM,EACN,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;;AAErC,qEAAqE;AACrE,qBAAa,KAAM,SAAQ,UAA8C;CAAG;
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAuC,MAAM,6BAA6B,CAAC;AAC/F;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,MAAM,EACX,WAAW,EACX,QAAQ,EACR,MAAM,EACN,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;;AAErC,qEAAqE;AACrE,qBAAa,KAAM,SAAQ,aAAa,CAAC,UAA8C;CAAG;AAE1F,uEAAuE;AACvE,eAAO,MAAM,MAAM,qBAAsB,MAAM,sFACS,CAAC;AAEzD,sCAAsC;AACtC,eAAO,MAAM,IAAI,mFAEhB,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,UAAU,mFAA0C,CAAC;AAElE,oFAAoF;AACpF,eAAO,MAAM,SAAS,mFAAyC,CAAC"}
|
package/lib/retry.js
CHANGED
|
@@ -1,33 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* FlyIo retry configuration.
|
|
4
|
-
*
|
|
5
|
-
* Defines the per-SDK `Retry` Context.Service tag that the generated
|
|
6
|
-
* operations wire into `API.make`. Callers can install a blanket retry
|
|
7
|
-
* policy at the layer level and have every FlyIo API call below it pick
|
|
8
|
-
* it up:
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* import * as FlyIo from "@distilled.cloud/fly-io";
|
|
13
|
-
*
|
|
14
|
-
* myEffect.pipe(FlyIo.Retry.transient);
|
|
15
|
-
* Effect.provide(myEffect, Layer.succeed(FlyIo.Retry.Retry, customPolicy));
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import * as Context from "effect/Context";
|
|
19
|
-
import * as Effect from "effect/Effect";
|
|
20
|
-
import * as Layer from "effect/Layer";
|
|
21
|
-
export { makeDefault, jittered, capped, throttlingOptions, transientOptions, throttlingFactory, transientFactory, } from "@distilled.cloud/core/retry";
|
|
22
|
-
/** Context tag for configuring retry behavior of FlyIo API calls. */
|
|
23
|
-
export class Retry extends Context.Service()("FlyIoRetry") {
|
|
24
|
-
}
|
|
25
|
-
/** Provides a custom retry policy to every FlyIo API call below it. */
|
|
26
|
-
export const policy = (optionsOrFactory) => Effect.provide(Layer.succeed(Retry, optionsOrFactory));
|
|
27
|
-
/** Disables all automatic retries. */
|
|
28
|
-
export const none = Effect.provide(Layer.succeed(Retry, { while: () => false }));
|
|
29
|
-
/** Apply the throttling retry policy (retries throttling errors indefinitely). */
|
|
30
|
-
export const throttling = policy(throttlingFactory);
|
|
31
|
-
/** Apply the transient retry policy (retries all transient errors indefinitely). */
|
|
32
|
-
export const transient = policy(transientFactory);
|
|
33
|
-
//# sourceMappingURL=retry.js.map
|
|
1
|
+
export { retry$Retry as Retry, retry$capped as capped, retry$jittered as jittered, retry$makeDefault as makeDefault, retry$none as none, retry$policy as policy, retry$throttling as throttling, retry$throttlingFactory as throttlingFactory, retry$throttlingOptions as throttlingOptions, retry$transient as transient, retry$transientFactory as transientFactory, retry$transientOptions as transientOptions } from "./_bundle/sdk.js";
|