@molecule/api-code-sandbox-flyio 1.0.18 → 1.2.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.
- package/README.md +205 -32
- package/dist/api.d.ts +5 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +11 -0
- package/dist/egress.d.ts +97 -2
- package/dist/egress.d.ts.map +1 -1
- package/dist/egress.js +166 -6
- package/dist/flycast.d.ts +18 -7
- package/dist/flycast.d.ts.map +1 -1
- package/dist/flycast.js +18 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +280 -11
- package/dist/storage.d.ts +8 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +17 -0
- package/dist/types.d.ts +52 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-08-
|
|
6
|
+
Generated: 2026-08-16T14:04:35.624Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-code-sandbox-flyio
|
|
@@ -75,7 +75,7 @@ const warm = await provider.create({
|
|
|
75
75
|
## Installation
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
npm install @molecule/api-code-sandbox-flyio @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n
|
|
78
|
+
npm install @molecule/api-code-sandbox-flyio @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n @molecule/api-proxy-agent
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
## API
|
|
@@ -94,10 +94,24 @@ interface EgressProbeContext {
|
|
|
94
94
|
targets: EgressProbeTarget[]
|
|
95
95
|
/**
|
|
96
96
|
* Ports this provider's own egress policy allows, or `undefined` when it
|
|
97
|
-
* applies no policy at all. Used
|
|
98
|
-
*
|
|
97
|
+
* applies no policy at all. Used for the message, the remediation, and as the
|
|
98
|
+
* INTENT half of the drift check against {@link appliedPolicyPorts} — never to
|
|
99
|
+
* decide `open` vs `filtered` from a probe that reached a target, which comes
|
|
100
|
+
* from the observation alone.
|
|
99
101
|
*/
|
|
100
102
|
policyPorts?: FlyNetworkPolicyPort[]
|
|
103
|
+
/**
|
|
104
|
+
* Ports the policy Fly ACTUALLY holds for this app allows, read back from the
|
|
105
|
+
* API after the probe ran, or `undefined` when the readback failed or found no
|
|
106
|
+
* policy of this provider's name.
|
|
107
|
+
*
|
|
108
|
+
* This is the observation half of the only drift this module can catch: a
|
|
109
|
+
* policy widened outside the provider (by hand, by another tool, by an older
|
|
110
|
+
* build) allows ports nothing here configured, and the raw-connect probe
|
|
111
|
+
* cannot see it because it only attempts the targets it was given. See
|
|
112
|
+
* {@link unexpectedPolicyPorts}.
|
|
113
|
+
*/
|
|
114
|
+
appliedPolicyPorts?: FlyNetworkPolicyPort[]
|
|
101
115
|
}
|
|
102
116
|
```
|
|
103
117
|
|
|
@@ -133,6 +147,30 @@ interface FlyApiClientOptions {
|
|
|
133
147
|
}
|
|
134
148
|
```
|
|
135
149
|
|
|
150
|
+
#### `FlyApp`
|
|
151
|
+
|
|
152
|
+
An app as returned by `GET /v1/apps?org_slug=…` (the fields this provider reads).
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface FlyApp {
|
|
156
|
+
id?: string
|
|
157
|
+
name: string
|
|
158
|
+
machine_count?: number
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `FlyAppList`
|
|
163
|
+
|
|
164
|
+
Response body of `GET /v1/apps?org_slug=…`.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
interface FlyAppList {
|
|
168
|
+
/** Total apps in the org, which the returned page may not cover. */
|
|
169
|
+
total_apps?: number
|
|
170
|
+
apps?: FlyApp[]
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
136
174
|
#### `FlyExecResponse`
|
|
137
175
|
|
|
138
176
|
Response body of `POST /v1/apps/{app}/machines/{id}/exec` (`flydv1.ExecResponse`).
|
|
@@ -244,10 +282,13 @@ interface FlyioConfig {
|
|
|
244
282
|
*/
|
|
245
283
|
publicService?: boolean
|
|
246
284
|
/**
|
|
247
|
-
* Fly Proxy idle behaviour for the preview service. `
|
|
248
|
-
*
|
|
249
|
-
* idle
|
|
250
|
-
*
|
|
285
|
+
* Fly Proxy idle behaviour for the preview service. Defaults to `'off'` —
|
|
286
|
+
* never idle the Machine — because the autostop timer counts the PROXY
|
|
287
|
+
* service's idle time, not the workload's, so a sandbox with nobody looking at
|
|
288
|
+
* its preview is suspended out from under a build that is still running.
|
|
289
|
+
* `suspend` is the scale-to-zero mapping (resume from a memory snapshot on the
|
|
290
|
+
* next request) and `stop` is a full stop with a cold boot on wake; opt into
|
|
291
|
+
* either deliberately.
|
|
251
292
|
*/
|
|
252
293
|
autostop?: 'off' | 'stop' | 'suspend'
|
|
253
294
|
/**
|
|
@@ -264,9 +305,12 @@ interface FlyioConfig {
|
|
|
264
305
|
/** Timeout for a single Machines API request, in ms. Defaults to 30000. */
|
|
265
306
|
requestTimeoutMs?: number
|
|
266
307
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
308
|
+
* TOTAL budget, in seconds, that `create()`/`start()`/`wake()` block waiting
|
|
309
|
+
* for the Machine to actually reach `started`. Fly's `GET .../wait` blocks
|
|
310
|
+
* for at most 60 seconds per call, so a larger budget is spent as consecutive
|
|
311
|
+
* wait rounds. Defaults to 180 — a Machine whose image is not yet cached on
|
|
312
|
+
* its host (every first boot after an image push) pulls it before starting,
|
|
313
|
+
* and that alone can exceed a single 60 s round.
|
|
270
314
|
*
|
|
271
315
|
* Without this wait, `start()` resolves the moment Fly ACCEPTS the request,
|
|
272
316
|
* and the caller's very next `exec` hits a Machine that is not running yet.
|
|
@@ -449,6 +493,14 @@ interface FlyMachine {
|
|
|
449
493
|
region?: string
|
|
450
494
|
private_ip?: string
|
|
451
495
|
config?: FlyMachineConfig
|
|
496
|
+
/** RFC 3339 creation time, as Fly reports it. */
|
|
497
|
+
created_at?: string
|
|
498
|
+
/**
|
|
499
|
+
* Lifecycle events, newest first. The ONLY place Fly records when a Machine
|
|
500
|
+
* was first started — there is no `started_at` field — which is what
|
|
501
|
+
* distinguishes a stopped Machine from one that was created and never ran.
|
|
502
|
+
*/
|
|
503
|
+
events?: FlyMachineEvent[]
|
|
452
504
|
}
|
|
453
505
|
```
|
|
454
506
|
|
|
@@ -470,6 +522,20 @@ interface FlyMachineConfig {
|
|
|
470
522
|
}
|
|
471
523
|
```
|
|
472
524
|
|
|
525
|
+
#### `FlyMachineEvent`
|
|
526
|
+
|
|
527
|
+
One entry of {@link FlyMachine.events}.
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
interface FlyMachineEvent {
|
|
531
|
+
/** `start`, `launch`, `exit`, … */
|
|
532
|
+
type?: string
|
|
533
|
+
status?: string
|
|
534
|
+
/** Epoch milliseconds. */
|
|
535
|
+
timestamp?: number
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
473
539
|
#### `FlyMachineService`
|
|
474
540
|
|
|
475
541
|
A Fly Proxy service attached to a Machine (`fly.MachineService`).
|
|
@@ -528,6 +594,13 @@ One rule in a Fly network policy. `allow` is the only documented action:
|
|
|
528
594
|
"Once you create a rule for a given direction, the default for that direction
|
|
529
595
|
becomes drop."
|
|
530
596
|
|
|
597
|
+
There is no destination field, and that is measured rather than assumed
|
|
598
|
+
(2026-08-16, live API): `ipv6_cidrs`, `ipv4_cidrs`, `cidrs`, `destinations`,
|
|
599
|
+
`apps`, `app`, `to` and `dst` were each POSTed on a rule and each came back
|
|
600
|
+
absent — the stored rule is `{action, direction, ports}` and nothing more. An
|
|
601
|
+
`allow tcp/5432` rule therefore permits the Machine to reach port 5432 on
|
|
602
|
+
every host on the internet.
|
|
603
|
+
|
|
531
604
|
```typescript
|
|
532
605
|
interface FlyNetworkPolicyRule {
|
|
533
606
|
/** Only `allow` is supported by Fly. */
|
|
@@ -541,8 +614,16 @@ interface FlyNetworkPolicyRule {
|
|
|
541
614
|
|
|
542
615
|
#### `FlyNetworkPolicySelector`
|
|
543
616
|
|
|
544
|
-
Which Machines in the app a policy applies to.
|
|
545
|
-
|
|
617
|
+
Which Machines in the app a policy applies to — the SOURCE side only. There is
|
|
618
|
+
no destination selector: an `apps` array is the one candidate the API answers
|
|
619
|
+
about, and it answers `400 {"error":"apps array not currently supported in
|
|
620
|
+
selectors"}` (measured 2026-08-16); every other candidate field is silently
|
|
621
|
+
dropped. Documented criteria combine with AND, so this provider uses
|
|
622
|
+
`{ all: true }` alone.
|
|
623
|
+
|
|
624
|
+
The API's LIST response returns this object under the key `netpolSelector`
|
|
625
|
+
while accepting it as `selector` on write — which is why nothing here reads it
|
|
626
|
+
back by name.
|
|
546
627
|
|
|
547
628
|
```typescript
|
|
548
629
|
interface FlyNetworkPolicySelector {
|
|
@@ -855,11 +936,9 @@ type RawExec = (command: string[], timeoutSeconds: number) => Promise<FlyExecRes
|
|
|
855
936
|
|
|
856
937
|
#### `FlyApiClient`
|
|
857
938
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
generated client would drag in the whole Machines + Postgres + tokens surface
|
|
862
|
-
for no benefit.
|
|
939
|
+
Minimal Fly Machines API client: bearer auth, JSON bodies, per-request
|
|
940
|
+
timeout, bounded retries with backoff, and account-wide request pacing via
|
|
941
|
+
{@link paceFlyRequest}.
|
|
863
942
|
|
|
864
943
|
#### `FlyApiError`
|
|
865
944
|
|
|
@@ -1115,6 +1194,28 @@ function decodePrivateRoutes(raw: string | undefined): Record<string, string>
|
|
|
1115
1194
|
|
|
1116
1195
|
**Returns:** Target app name → allocated private address. Unparseable entries are skipped: this value only drives cleanup and self-healing, and a malformed entry must not stop either.
|
|
1117
1196
|
|
|
1197
|
+
#### `describePublicReach(policyPorts)`
|
|
1198
|
+
|
|
1199
|
+
States, in the verdict itself, what an allowed port actually permits.
|
|
1200
|
+
|
|
1201
|
+
A Fly network policy has no destination (see the module description — measured
|
|
1202
|
+
against the live API, not inferred), so every allowed port is open to every
|
|
1203
|
+
host on the internet, not only to the private service it was derived for. That
|
|
1204
|
+
is the single most consequential thing about this mechanism and it was invisible
|
|
1205
|
+
everywhere it mattered: the probe attempts one port the policy denies, reports
|
|
1206
|
+
`filtered`, and an operator reasonably reads that as "nothing gets out".
|
|
1207
|
+
|
|
1208
|
+
Naming it here means the residual is restated on every re-probe (the consumer
|
|
1209
|
+
re-verifies every 15 minutes) instead of living only in a document.
|
|
1210
|
+
|
|
1211
|
+
```typescript
|
|
1212
|
+
function describePublicReach(policyPorts: FlyNetworkPolicyPort[] | undefined): string
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
- `policyPorts` — Allowed ports, or `undefined` when no policy is applied.
|
|
1216
|
+
|
|
1217
|
+
**Returns:** A sentence naming the public reach, or `''` when there is nothing to say (no policy, or nothing but UDP/53 — DNS is a documented residual of its own and not a TCP channel).
|
|
1218
|
+
|
|
1118
1219
|
#### `encodePrivateRoutes(routes)`
|
|
1119
1220
|
|
|
1120
1221
|
Encodes allocated addresses for storage in Machine metadata.
|
|
@@ -1173,6 +1274,32 @@ function extractPolicyId(response: unknown, name: string): string | undefined
|
|
|
1173
1274
|
|
|
1174
1275
|
**Returns:** The matching policy's id, or `undefined`.
|
|
1175
1276
|
|
|
1277
|
+
#### `extractPolicyPorts(response, name)`
|
|
1278
|
+
|
|
1279
|
+
Reads back the egress ports the policy Fly ACTUALLY holds allows.
|
|
1280
|
+
|
|
1281
|
+
The write is not the observation: this provider POSTs a policy, but what
|
|
1282
|
+
governs a Machine is whatever Fly has when that Machine boots — which may
|
|
1283
|
+
differ because a person, another tool or an older build changed it. Reading it
|
|
1284
|
+
back is what lets {@link unexpectedPolicyPorts} catch a policy wider than the
|
|
1285
|
+
one this provider configured, so `verifyEgress()` can fail on drift its raw
|
|
1286
|
+
connects were never going to attempt.
|
|
1287
|
+
|
|
1288
|
+
Parsed as defensively as {@link extractPolicyId}, and for the same reason:
|
|
1289
|
+
neither Fly document specifies the LIST response shape. The live API answers
|
|
1290
|
+
with a bare array whose selector key is `netpolSelector` rather than the
|
|
1291
|
+
`selector` it accepts (verified 2026-08-16), which is exactly why nothing here
|
|
1292
|
+
depends on any key but `name` and `rules`.
|
|
1293
|
+
|
|
1294
|
+
```typescript
|
|
1295
|
+
function extractPolicyPorts(response: unknown, name: string): FlyNetworkPolicyPort[] | undefined
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
- `response` — The raw parsed LIST response.
|
|
1299
|
+
- `name` — The policy name to match.
|
|
1300
|
+
|
|
1301
|
+
**Returns:** The allowed egress ports, or `undefined` when no policy of that name is present or the shape is unrecognized. `undefined` means "nothing to compare", never "nothing is allowed".
|
|
1302
|
+
|
|
1176
1303
|
#### `flycastHost(app)`
|
|
1177
1304
|
|
|
1178
1305
|
Renders the DNS name a sandbox dials to reach an app over its Flycast address.
|
|
@@ -1338,14 +1465,25 @@ It is deliberately a UNION and never a replacement, so an operator's list is
|
|
|
1338
1465
|
still exactly what they wrote plus the ports their own declarations require,
|
|
1339
1466
|
and the applied policy is logged.
|
|
1340
1467
|
|
|
1341
|
-
|
|
1342
|
-
policies only apply to traffic directly to and from Machines. They do
|
|
1343
|
-
affect traffic routed through the Fly Proxy"
|
|
1468
|
+
The addition is load-bearing — VERIFIED 2026-08-16, not inferred. Fly states
|
|
1469
|
+
"Network policies only apply to traffic directly to and from Machines. They do
|
|
1470
|
+
not affect traffic routed through the Fly Proxy"
|
|
1344
1471
|
(https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
|
|
1345
|
-
traffic IS routed through Fly Proxy
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1472
|
+
traffic IS routed through Fly Proxy, so it was an open question whether these
|
|
1473
|
+
ports did anything. Measured on a throwaway app with a Flycast address into
|
|
1474
|
+
its own 6PN: under a policy allowing only `udp:53`, `molecule-pg-tenant.flycast`
|
|
1475
|
+
and `molecule-api.flycast` both RESOLVED and every TCP connect to them was
|
|
1476
|
+
dropped; re-applying the policy with `tcp:443` opened 443 and nothing else.
|
|
1477
|
+
Fly's sentence is about INGRESS — a Machine's egress TOWARD a Flycast address
|
|
1478
|
+
is filtered like any other. Drop a declared service's port and every database
|
|
1479
|
+
connection in the fleet goes with it.
|
|
1480
|
+
|
|
1481
|
+
The cost of each derived port, stated plainly because a Fly policy has no
|
|
1482
|
+
destination field of any kind (also measured — see the `egress.ts` module
|
|
1483
|
+
description): the port is opened to EVERY host on the internet, not only to
|
|
1484
|
+
the private service it was derived for. That residual cannot be closed at this
|
|
1485
|
+
layer; it is named in `verifyEgress()`'s verdict and in
|
|
1486
|
+
`docs/sandbox-egress-enforcement.md`.
|
|
1349
1487
|
|
|
1350
1488
|
```typescript
|
|
1351
1489
|
function mergeEgressPorts(
|
|
@@ -1520,6 +1658,14 @@ function requireStore(ctx: TemplateContext): ObjectStore
|
|
|
1520
1658
|
|
|
1521
1659
|
**Returns:** The configured store.
|
|
1522
1660
|
|
|
1661
|
+
#### `resetFlyPacerForTests()`
|
|
1662
|
+
|
|
1663
|
+
Test-only: reset the shared pacer so timing tests start from a clean slate.
|
|
1664
|
+
|
|
1665
|
+
```typescript
|
|
1666
|
+
function resetFlyPacerForTests(): void
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1523
1669
|
#### `resolveTemplateStorage(config)`
|
|
1524
1670
|
|
|
1525
1671
|
Resolves template-storage settings from config and the environment.
|
|
@@ -1614,6 +1760,30 @@ function toSandboxId(app: string, machineId: string): string
|
|
|
1614
1760
|
|
|
1615
1761
|
**Returns:** The composite sandbox id.
|
|
1616
1762
|
|
|
1763
|
+
#### `unexpectedPolicyPorts(applied, intended)`
|
|
1764
|
+
|
|
1765
|
+
Finds ports the policy Fly holds allows that this provider never configured.
|
|
1766
|
+
|
|
1767
|
+
The raw-connect probe can only speak for the targets it was handed, so a
|
|
1768
|
+
policy widened outside this provider — edited by hand to unblock something, or
|
|
1769
|
+
left behind by an older build — is invisible to it: the probe's own port stays
|
|
1770
|
+
denied and the verdict stays `filtered` while a port nothing here asked for is
|
|
1771
|
+
open to the whole internet. Comparing what Fly holds against what this
|
|
1772
|
+
provider intended is the one check that can catch that, and it is why the
|
|
1773
|
+
verdict reads the policy back rather than trusting the write.
|
|
1774
|
+
|
|
1775
|
+
```typescript
|
|
1776
|
+
function unexpectedPolicyPorts(
|
|
1777
|
+
applied: FlyNetworkPolicyPort[] | undefined,
|
|
1778
|
+
intended: FlyNetworkPolicyPort[] | undefined,
|
|
1779
|
+
): FlyNetworkPolicyPort[]
|
|
1780
|
+
```
|
|
1781
|
+
|
|
1782
|
+
- `applied` — Ports in the policy Fly actually holds, or `undefined` when the readback found nothing (in which case there is nothing to compare).
|
|
1783
|
+
- `intended` — Ports this provider configured, or `undefined` when it applies no policy.
|
|
1784
|
+
|
|
1785
|
+
**Returns:** The applied ports with no counterpart in `intended`, in order.
|
|
1786
|
+
|
|
1617
1787
|
#### `verdictForProbeExit(exitCode, context)`
|
|
1618
1788
|
|
|
1619
1789
|
Maps a probe's exit status onto an {@link EgressVerdict}.
|
|
@@ -1903,7 +2073,7 @@ export function setupCodeSandboxFlyio(): void {
|
|
|
1903
2073
|
Peer dependencies:
|
|
1904
2074
|
|
|
1905
2075
|
- `@molecule/api-bond` ^1.0.1
|
|
1906
|
-
- `@molecule/api-code-sandbox` ^1.0
|
|
2076
|
+
- `@molecule/api-code-sandbox` ^1.2.0
|
|
1907
2077
|
- `@molecule/api-i18n` ^1.0.1
|
|
1908
2078
|
|
|
1909
2079
|
### Environment Variables
|
|
@@ -1942,6 +2112,7 @@ Peer dependencies:
|
|
|
1942
2112
|
- `@molecule/api-bond`
|
|
1943
2113
|
- `@molecule/api-code-sandbox`
|
|
1944
2114
|
- `@molecule/api-i18n`
|
|
2115
|
+
- `@molecule/api-proxy-agent`
|
|
1945
2116
|
|
|
1946
2117
|
**Sources.** Every endpoint, payload field and documented behaviour used here
|
|
1947
2118
|
was checked against Fly's own material, not recalled:
|
|
@@ -2169,12 +2340,14 @@ live, and any failure to LIST or read the store throws rather than reporting
|
|
|
2169
2340
|
exist for providers whose templates are host-local; object storage is already
|
|
2170
2341
|
the shared store, so there is nothing to publish or fetch.
|
|
2171
2342
|
|
|
2172
|
-
**`
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2343
|
+
**`importFiles` IS implemented; `exportFiles` is not.** Import takes the same
|
|
2344
|
+
route the template restore does — a presigned object-store URL the Machine
|
|
2345
|
+
pulls itself, falling back to chunked base64 over `exec` when no store is
|
|
2346
|
+
configured — so an app tree can be delivered into a fresh Machine. There is no
|
|
2347
|
+
inverse: Fly's only read channel is the `exec` endpoint's JSON response, and a
|
|
2348
|
+
base64 emulation of a whole-tree export would look supported and route every
|
|
2349
|
+
byte through the control plane. Pair a Fly destination with a source that CAN
|
|
2350
|
+
export (the core's rooting contract makes them interchangeable).
|
|
2178
2351
|
|
|
2179
2352
|
**Sandbox ids are composite: `"<app>:<machineId>"`.** A Fly Machine id is only
|
|
2180
2353
|
unique within its app and every endpoint is `/v1/apps/{app}/machines/{id}`, so
|
package/dist/api.d.ts
CHANGED
|
@@ -115,6 +115,11 @@ export interface FlyApiClientOptions {
|
|
|
115
115
|
}
|
|
116
116
|
/** Test-only: reset the shared pacer so timing tests start from a clean slate. */
|
|
117
117
|
export declare function resetFlyPacerForTests(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Minimal Fly Machines API client: bearer auth, JSON bodies, per-request
|
|
120
|
+
* timeout, bounded retries with backoff, and account-wide request pacing via
|
|
121
|
+
* {@link paceFlyRequest}.
|
|
122
|
+
*/
|
|
118
123
|
export declare class FlyApiClient {
|
|
119
124
|
private readonly token;
|
|
120
125
|
private readonly baseUrl;
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;gBAED,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAgBhC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBjE;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;gBAED,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAgBhC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBjE;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAgDD,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IAErD;;;OAGG;gBACS,OAAO,EAAE,mBAAmB;IASxC;;;;;;;;;;OAUG;IACG,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAiFnF"}
|
package/dist/api.js
CHANGED
|
@@ -145,6 +145,12 @@ function minRequestGapMs() {
|
|
|
145
145
|
*/
|
|
146
146
|
let flyPacerTail = Promise.resolve();
|
|
147
147
|
let flyLastRequestAt = 0;
|
|
148
|
+
/**
|
|
149
|
+
* Queues the caller behind the shared pacer chain and resolves once its slot
|
|
150
|
+
* arrives (at least {@link MIN_REQUEST_GAP_MS} after the previous request).
|
|
151
|
+
* @param sleep - Injected sleep, so tests can collapse the waits.
|
|
152
|
+
* @returns Resolves when the caller may fire its request.
|
|
153
|
+
*/
|
|
148
154
|
function paceFlyRequest(sleep) {
|
|
149
155
|
const mine = flyPacerTail.then(async () => {
|
|
150
156
|
const wait = Math.max(0, flyLastRequestAt + minRequestGapMs() - Date.now());
|
|
@@ -160,6 +166,11 @@ export function resetFlyPacerForTests() {
|
|
|
160
166
|
flyPacerTail = Promise.resolve();
|
|
161
167
|
flyLastRequestAt = 0;
|
|
162
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Minimal Fly Machines API client: bearer auth, JSON bodies, per-request
|
|
171
|
+
* timeout, bounded retries with backoff, and account-wide request pacing via
|
|
172
|
+
* {@link paceFlyRequest}.
|
|
173
|
+
*/
|
|
163
174
|
export class FlyApiClient {
|
|
164
175
|
token;
|
|
165
176
|
baseUrl;
|
package/dist/egress.d.ts
CHANGED
|
@@ -46,6 +46,30 @@
|
|
|
46
46
|
* and that `name` is mandatory — which is why {@link DEFAULT_EGRESS_POLICY_NAME}
|
|
47
47
|
* is always sent.
|
|
48
48
|
*
|
|
49
|
+
* **THE PORT LIST IS THE WHOLE POLICY — there is no destination, and this was
|
|
50
|
+
* measured rather than inferred (2026-08-16, live API, throwaway app).** Every
|
|
51
|
+
* plausible destination field was POSTed and read back: on a RULE,
|
|
52
|
+
* `ipv6_cidrs` / `ipv4_cidrs` / `cidrs` / `destinations` / `apps` / `app` / `to`
|
|
53
|
+
* / `dst` are all accepted by the decoder and DROPPED — the stored rule comes
|
|
54
|
+
* back as `{action, direction, ports}` and nothing else. On the SELECTOR (which
|
|
55
|
+
* chooses which Machines a policy covers, i.e. the SOURCE, never a destination)
|
|
56
|
+
* only `all`, `machines` and `metadata` persist; `apps` is the one field the API
|
|
57
|
+
* answers about, with `400 {"error":"apps array not currently supported in
|
|
58
|
+
* selectors"}`. So an `allow tcp/5432` rule permits that Machine to reach port
|
|
59
|
+
* 5432 on EVERY host on the internet, and no arrangement of this object can say
|
|
60
|
+
* otherwise. {@link describePublicReach} states that in the verdict rather than
|
|
61
|
+
* leaving an operator to deduce it.
|
|
62
|
+
*
|
|
63
|
+
* **The derived private-service ports ARE load-bearing** (the open question this
|
|
64
|
+
* module and FLY-OPERATOR-SETUP §9 both carried since 2026-08-05). Measured the
|
|
65
|
+
* same day: with a policy allowing only `udp:53`, a Machine on a custom 6PN
|
|
66
|
+
* could resolve `molecule-pg-tenant.flycast` and `molecule-api.flycast` but
|
|
67
|
+
* every TCP connect to them was dropped, and re-applying `tcp:443` re-opened
|
|
68
|
+
* only 443. Fly's "policies do not affect traffic routed through the Fly Proxy"
|
|
69
|
+
* is about INGRESS; a Machine's egress TOWARD a Flycast address is filtered like
|
|
70
|
+
* any other. Dropping a declared service's port would therefore take out every
|
|
71
|
+
* database connection in the fleet.
|
|
72
|
+
*
|
|
49
73
|
* @module
|
|
50
74
|
*/
|
|
51
75
|
import type { EgressVerdict } from '@molecule/api-code-sandbox';
|
|
@@ -83,10 +107,24 @@ export interface EgressProbeContext {
|
|
|
83
107
|
targets: EgressProbeTarget[];
|
|
84
108
|
/**
|
|
85
109
|
* Ports this provider's own egress policy allows, or `undefined` when it
|
|
86
|
-
* applies no policy at all. Used
|
|
87
|
-
*
|
|
110
|
+
* applies no policy at all. Used for the message, the remediation, and as the
|
|
111
|
+
* INTENT half of the drift check against {@link appliedPolicyPorts} — never to
|
|
112
|
+
* decide `open` vs `filtered` from a probe that reached a target, which comes
|
|
113
|
+
* from the observation alone.
|
|
88
114
|
*/
|
|
89
115
|
policyPorts?: FlyNetworkPolicyPort[];
|
|
116
|
+
/**
|
|
117
|
+
* Ports the policy Fly ACTUALLY holds for this app allows, read back from the
|
|
118
|
+
* API after the probe ran, or `undefined` when the readback failed or found no
|
|
119
|
+
* policy of this provider's name.
|
|
120
|
+
*
|
|
121
|
+
* This is the observation half of the only drift this module can catch: a
|
|
122
|
+
* policy widened outside the provider (by hand, by another tool, by an older
|
|
123
|
+
* build) allows ports nothing here configured, and the raw-connect probe
|
|
124
|
+
* cannot see it because it only attempts the targets it was given. See
|
|
125
|
+
* {@link unexpectedPolicyPorts}.
|
|
126
|
+
*/
|
|
127
|
+
appliedPolicyPorts?: FlyNetworkPolicyPort[];
|
|
90
128
|
}
|
|
91
129
|
/**
|
|
92
130
|
* Renders a target the way it is written in configuration, so an operator can
|
|
@@ -139,6 +177,41 @@ export declare function parseEgressAllowedPorts(raw: string | undefined): FlyNet
|
|
|
139
177
|
* @returns A `sh`-safe command string.
|
|
140
178
|
*/
|
|
141
179
|
export declare function buildEgressProbeCommand(targets: EgressProbeTarget[], timeoutMs: number): string;
|
|
180
|
+
/**
|
|
181
|
+
* States, in the verdict itself, what an allowed port actually permits.
|
|
182
|
+
*
|
|
183
|
+
* A Fly network policy has no destination (see the module description — measured
|
|
184
|
+
* against the live API, not inferred), so every allowed port is open to every
|
|
185
|
+
* host on the internet, not only to the private service it was derived for. That
|
|
186
|
+
* is the single most consequential thing about this mechanism and it was invisible
|
|
187
|
+
* everywhere it mattered: the probe attempts one port the policy denies, reports
|
|
188
|
+
* `filtered`, and an operator reasonably reads that as "nothing gets out".
|
|
189
|
+
*
|
|
190
|
+
* Naming it here means the residual is restated on every re-probe (the consumer
|
|
191
|
+
* re-verifies every 15 minutes) instead of living only in a document.
|
|
192
|
+
* @param policyPorts - Allowed ports, or `undefined` when no policy is applied.
|
|
193
|
+
* @returns A sentence naming the public reach, or `''` when there is nothing to
|
|
194
|
+
* say (no policy, or nothing but UDP/53 — DNS is a documented residual of its
|
|
195
|
+
* own and not a TCP channel).
|
|
196
|
+
*/
|
|
197
|
+
export declare function describePublicReach(policyPorts: FlyNetworkPolicyPort[] | undefined): string;
|
|
198
|
+
/**
|
|
199
|
+
* Finds ports the policy Fly holds allows that this provider never configured.
|
|
200
|
+
*
|
|
201
|
+
* The raw-connect probe can only speak for the targets it was handed, so a
|
|
202
|
+
* policy widened outside this provider — edited by hand to unblock something, or
|
|
203
|
+
* left behind by an older build — is invisible to it: the probe's own port stays
|
|
204
|
+
* denied and the verdict stays `filtered` while a port nothing here asked for is
|
|
205
|
+
* open to the whole internet. Comparing what Fly holds against what this
|
|
206
|
+
* provider intended is the one check that can catch that, and it is why the
|
|
207
|
+
* verdict reads the policy back rather than trusting the write.
|
|
208
|
+
* @param applied - Ports in the policy Fly actually holds, or `undefined` when
|
|
209
|
+
* the readback found nothing (in which case there is nothing to compare).
|
|
210
|
+
* @param intended - Ports this provider configured, or `undefined` when it
|
|
211
|
+
* applies no policy.
|
|
212
|
+
* @returns The applied ports with no counterpart in `intended`, in order.
|
|
213
|
+
*/
|
|
214
|
+
export declare function unexpectedPolicyPorts(applied: FlyNetworkPolicyPort[] | undefined, intended: FlyNetworkPolicyPort[] | undefined): FlyNetworkPolicyPort[];
|
|
142
215
|
/**
|
|
143
216
|
* Maps a probe's exit status onto an {@link EgressVerdict}.
|
|
144
217
|
*
|
|
@@ -177,4 +250,26 @@ export declare function verdictForProbeExit(exitCode: number, context: EgressPro
|
|
|
177
250
|
* @returns The matching policy's id, or `undefined`.
|
|
178
251
|
*/
|
|
179
252
|
export declare function extractPolicyId(response: unknown, name: string): string | undefined;
|
|
253
|
+
/**
|
|
254
|
+
* Reads back the egress ports the policy Fly ACTUALLY holds allows.
|
|
255
|
+
*
|
|
256
|
+
* The write is not the observation: this provider POSTs a policy, but what
|
|
257
|
+
* governs a Machine is whatever Fly has when that Machine boots — which may
|
|
258
|
+
* differ because a person, another tool or an older build changed it. Reading it
|
|
259
|
+
* back is what lets {@link unexpectedPolicyPorts} catch a policy wider than the
|
|
260
|
+
* one this provider configured, so `verifyEgress()` can fail on drift its raw
|
|
261
|
+
* connects were never going to attempt.
|
|
262
|
+
*
|
|
263
|
+
* Parsed as defensively as {@link extractPolicyId}, and for the same reason:
|
|
264
|
+
* neither Fly document specifies the LIST response shape. The live API answers
|
|
265
|
+
* with a bare array whose selector key is `netpolSelector` rather than the
|
|
266
|
+
* `selector` it accepts (verified 2026-08-16), which is exactly why nothing here
|
|
267
|
+
* depends on any key but `name` and `rules`.
|
|
268
|
+
* @param response - The raw parsed LIST response.
|
|
269
|
+
* @param name - The policy name to match.
|
|
270
|
+
* @returns The allowed egress ports, or `undefined` when no policy of that name
|
|
271
|
+
* is present or the shape is unrecognized. `undefined` means "nothing to
|
|
272
|
+
* compare", never "nothing is allowed".
|
|
273
|
+
*/
|
|
274
|
+
export declare function extractPolicyPorts(response: unknown, name: string): FlyNetworkPolicyPort[] | undefined;
|
|
180
275
|
//# sourceMappingURL=egress.d.ts.map
|
package/dist/egress.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"egress.d.ts","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"egress.d.ts","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAE/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAGtD,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,4BAA4B,CAAA;AAcnE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C,uFAAuF;AACvF,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,2CAA2C,CAAA;AAEpF,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,8FAA8F;IAC9F,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,mCAAmC;IACnC,OAAO,EAAE,iBAAiB,EAAE,CAAA;IAC5B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAA;IACpC;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,oBAAoB,EAAE,CAAA;CAC5C;AAQD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAGzE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAc/F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,oBAAoB,EAAE,GAAG,SAAS,CAyBpC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAY/F;AAqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,oBAAoB,EAAE,GAAG,SAAS,GAAG,MAAM,CAW3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,EAAE,GAAG,SAAS,EAC3C,QAAQ,EAAE,oBAAoB,EAAE,GAAG,SAAS,GAC3C,oBAAoB,EAAE,CAMxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,aAAa,CAgEhG;AAWD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE,MAAM,GACX,oBAAoB,EAAE,GAAG,SAAS,CAiBpC"}
|