@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/dist/egress.js CHANGED
@@ -46,11 +46,46 @@
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 { shellQuote } from './utilities.js';
52
76
  /** Default name of the egress policy this provider owns on each sandbox app. */
53
77
  export const DEFAULT_EGRESS_POLICY_NAME = 'molecule-sandbox-egress';
78
+ /**
79
+ * UDP port name resolution runs on.
80
+ *
81
+ * Excluded from {@link describePublicReach}'s "reachable on" list because DNS is
82
+ * not a TCP channel and is a residual of its own (a resolver forwards queries
83
+ * upstream on the platform's behalf, which is why DNS tunnelling is tracked
84
+ * separately in `docs/sandbox-egress-enforcement.md` rather than as an open
85
+ * port). It is never excluded from {@link unexpectedPolicyPorts}: a policy that
86
+ * allows DNS this provider did not configure is still drift.
87
+ */
88
+ const DNS_PORT = 53;
54
89
  /**
55
90
  * Probe exit status meaning at least one raw connection to a public IP
56
91
  * SUCCEEDED. Deliberately not 1, so an interpreter error (which exits 1) can
@@ -175,6 +210,14 @@ export function buildEgressProbeCommand(targets, timeoutMs) {
175
210
  `s.on('error',()=>done())}`;
176
211
  return `node -e ${shellQuote(script)}`;
177
212
  }
213
+ /**
214
+ * Renders a port list the way an operator reads it back.
215
+ * @param ports - The ports.
216
+ * @returns `tcp/443, udp/53`.
217
+ */
218
+ function formatPorts(ports) {
219
+ return ports.map((port) => `${port.protocol}/${port.port}`).join(', ');
220
+ }
178
221
  /**
179
222
  * Describes the policy this provider applied, for the verdict message.
180
223
  * @param policyPorts - Allowed ports, or `undefined` when no policy is applied.
@@ -183,8 +226,55 @@ export function buildEgressProbeCommand(targets, timeoutMs) {
183
226
  function describePolicy(policyPorts) {
184
227
  if (!policyPorts)
185
228
  return 'no Fly network policy applied by this provider';
186
- const ports = policyPorts.map((port) => `${port.protocol}/${port.port}`).join(', ');
187
- return `a Fly network policy allowing egress on ${ports}`;
229
+ return `a Fly network policy allowing egress on ${formatPorts(policyPorts)}`;
230
+ }
231
+ /**
232
+ * States, in the verdict itself, what an allowed port actually permits.
233
+ *
234
+ * A Fly network policy has no destination (see the module description — measured
235
+ * against the live API, not inferred), so every allowed port is open to every
236
+ * host on the internet, not only to the private service it was derived for. That
237
+ * is the single most consequential thing about this mechanism and it was invisible
238
+ * everywhere it mattered: the probe attempts one port the policy denies, reports
239
+ * `filtered`, and an operator reasonably reads that as "nothing gets out".
240
+ *
241
+ * Naming it here means the residual is restated on every re-probe (the consumer
242
+ * re-verifies every 15 minutes) instead of living only in a document.
243
+ * @param policyPorts - Allowed ports, or `undefined` when no policy is applied.
244
+ * @returns A sentence naming the public reach, or `''` when there is nothing to
245
+ * say (no policy, or nothing but UDP/53 — DNS is a documented residual of its
246
+ * own and not a TCP channel).
247
+ */
248
+ export function describePublicReach(policyPorts) {
249
+ const reachable = (policyPorts ?? []).filter((port) => port.protocol !== 'udp' || port.port !== DNS_PORT);
250
+ if (reachable.length === 0)
251
+ return '';
252
+ return (`Every allowed port is open to EVERY host, not only to the private service it exists for — ` +
253
+ `a Fly network policy matches protocol and port with no destination field of any kind — so ` +
254
+ `code in this app can still reach an arbitrary public host on ${formatPorts(reachable)}. ` +
255
+ 'Route what must be filtered by host through an egress proxy on one of those ports.');
256
+ }
257
+ /**
258
+ * Finds ports the policy Fly holds allows that this provider never configured.
259
+ *
260
+ * The raw-connect probe can only speak for the targets it was handed, so a
261
+ * policy widened outside this provider — edited by hand to unblock something, or
262
+ * left behind by an older build — is invisible to it: the probe's own port stays
263
+ * denied and the verdict stays `filtered` while a port nothing here asked for is
264
+ * open to the whole internet. Comparing what Fly holds against what this
265
+ * provider intended is the one check that can catch that, and it is why the
266
+ * verdict reads the policy back rather than trusting the write.
267
+ * @param applied - Ports in the policy Fly actually holds, or `undefined` when
268
+ * the readback found nothing (in which case there is nothing to compare).
269
+ * @param intended - Ports this provider configured, or `undefined` when it
270
+ * applies no policy.
271
+ * @returns The applied ports with no counterpart in `intended`, in order.
272
+ */
273
+ export function unexpectedPolicyPorts(applied, intended) {
274
+ if (!applied)
275
+ return [];
276
+ const wanted = intended ?? [];
277
+ return applied.filter((port) => !wanted.some((own) => own.protocol === port.protocol && own.port === port.port));
188
278
  }
189
279
  /**
190
280
  * Maps a probe's exit status onto an {@link EgressVerdict}.
@@ -211,6 +301,24 @@ function describePolicy(policyPorts) {
211
301
  export function verdictForProbeExit(exitCode, context) {
212
302
  const targets = context.targets.map(formatEgressProbeTarget).join(', ');
213
303
  const policy = describePolicy(context.policyPorts);
304
+ const reach = describePublicReach(context.appliedPolicyPorts ?? context.policyPorts);
305
+ // Drift, and it outranks a blocked probe: the probe attempts the targets it
306
+ // was handed, so a port widened outside this provider is open with nothing
307
+ // observing it. `open` is the honest state — something out there permits
308
+ // traffic this provider never configured.
309
+ const unexpected = unexpectedPolicyPorts(context.appliedPolicyPorts, context.policyPorts);
310
+ if (unexpected.length > 0) {
311
+ return {
312
+ state: 'open',
313
+ detail: (`The Fly network policy in force on app "${context.app}" allows ` +
314
+ `${formatPorts(unexpected)}, which this provider never configured (it applies ` +
315
+ `${policy}). ${describePublicReach(context.appliedPolicyPorts)}`).trim(),
316
+ remediation: 'Something outside this provider widened the policy. Inspect it with ' +
317
+ `\`GET /v1/apps/${context.app}/network_policies\`, remove the rule that does not belong, ` +
318
+ 'and restart the Machines — Fly applies a policy at boot, so an already-running Machine ' +
319
+ 'keeps the wider one until it is recreated.',
320
+ };
321
+ }
214
322
  if (exitCode === EGRESS_PROBE_EXIT_REACHED) {
215
323
  return {
216
324
  state: 'open',
@@ -231,7 +339,7 @@ export function verdictForProbeExit(exitCode, context) {
231
339
  return {
232
340
  state: 'filtered',
233
341
  detail: `Raw TCP connects to ${targets} from a Machine in Fly app "${context.app}" ` +
234
- `(${policy}) were refused or timed out.`,
342
+ `(${policy}) were refused or timed out.${reach ? ` ${reach}` : ''}`,
235
343
  };
236
344
  }
237
345
  return {
@@ -267,6 +375,59 @@ function isRecord(value) {
267
375
  * @returns The matching policy's id, or `undefined`.
268
376
  */
269
377
  export function extractPolicyId(response, name) {
378
+ const entry = findPolicy(response, name);
379
+ return entry && typeof entry.id === 'string' && entry.id ? entry.id : undefined;
380
+ }
381
+ /**
382
+ * Reads back the egress ports the policy Fly ACTUALLY holds allows.
383
+ *
384
+ * The write is not the observation: this provider POSTs a policy, but what
385
+ * governs a Machine is whatever Fly has when that Machine boots — which may
386
+ * differ because a person, another tool or an older build changed it. Reading it
387
+ * back is what lets {@link unexpectedPolicyPorts} catch a policy wider than the
388
+ * one this provider configured, so `verifyEgress()` can fail on drift its raw
389
+ * connects were never going to attempt.
390
+ *
391
+ * Parsed as defensively as {@link extractPolicyId}, and for the same reason:
392
+ * neither Fly document specifies the LIST response shape. The live API answers
393
+ * with a bare array whose selector key is `netpolSelector` rather than the
394
+ * `selector` it accepts (verified 2026-08-16), which is exactly why nothing here
395
+ * depends on any key but `name` and `rules`.
396
+ * @param response - The raw parsed LIST response.
397
+ * @param name - The policy name to match.
398
+ * @returns The allowed egress ports, or `undefined` when no policy of that name
399
+ * is present or the shape is unrecognized. `undefined` means "nothing to
400
+ * compare", never "nothing is allowed".
401
+ */
402
+ export function extractPolicyPorts(response, name) {
403
+ const entry = findPolicy(response, name);
404
+ if (!entry || !Array.isArray(entry.rules))
405
+ return undefined;
406
+ const ports = [];
407
+ for (const rule of entry.rules) {
408
+ if (!isRecord(rule) || rule.direction !== 'egress' || !Array.isArray(rule.ports))
409
+ continue;
410
+ for (const port of rule.ports) {
411
+ if (!isRecord(port))
412
+ continue;
413
+ const protocol = port.protocol;
414
+ const value = port.port;
415
+ if ((protocol !== 'tcp' && protocol !== 'udp') || typeof value !== 'number')
416
+ continue;
417
+ if (!ports.some((seen) => seen.protocol === protocol && seen.port === value)) {
418
+ ports.push({ protocol, port: value });
419
+ }
420
+ }
421
+ }
422
+ return ports;
423
+ }
424
+ /**
425
+ * Finds the named policy in a LIST response.
426
+ * @param response - The raw parsed LIST response.
427
+ * @param name - The policy name to match.
428
+ * @returns The matching entry, or `undefined`.
429
+ */
430
+ function findPolicy(response, name) {
270
431
  const list = Array.isArray(response)
271
432
  ? response
272
433
  : isRecord(response)
@@ -275,9 +436,8 @@ export function extractPolicyId(response, name) {
275
436
  if (!Array.isArray(list))
276
437
  return undefined;
277
438
  for (const entry of list) {
278
- if (isRecord(entry) && entry.name === name && typeof entry.id === 'string' && entry.id) {
279
- return entry.id;
280
- }
439
+ if (isRecord(entry) && entry.name === name)
440
+ return entry;
281
441
  }
282
442
  return undefined;
283
443
  }
package/dist/flycast.d.ts CHANGED
@@ -115,14 +115,25 @@ export declare function parsePrivateServices(raw: string | undefined): FlyPrivat
115
115
  * still exactly what they wrote plus the ports their own declarations require,
116
116
  * and the applied policy is logged.
117
117
  *
118
- * Whether the addition is even load-bearing is UNVERIFIED: Fly states "Network
119
- * policies only apply to traffic directly to and from Machines. They do not
120
- * affect traffic routed through the Fly Proxy"
118
+ * The addition is load-bearing — VERIFIED 2026-08-16, not inferred. Fly states
119
+ * "Network policies only apply to traffic directly to and from Machines. They do
120
+ * not affect traffic routed through the Fly Proxy"
121
121
  * (https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
122
- * traffic IS routed through Fly Proxy — but Fly says that about ingress and
123
- * never addresses a Machine's egress toward a Flycast address. An inert entry
124
- * costs a port on the allow list; a missing one costs every database in the
125
- * fleet, so the port is added.
122
+ * traffic IS routed through Fly Proxy, so it was an open question whether these
123
+ * ports did anything. Measured on a throwaway app with a Flycast address into
124
+ * its own 6PN: under a policy allowing only `udp:53`, `molecule-pg-tenant.flycast`
125
+ * and `molecule-api.flycast` both RESOLVED and every TCP connect to them was
126
+ * dropped; re-applying the policy with `tcp:443` opened 443 and nothing else.
127
+ * Fly's sentence is about INGRESS — a Machine's egress TOWARD a Flycast address
128
+ * is filtered like any other. Drop a declared service's port and every database
129
+ * connection in the fleet goes with it.
130
+ *
131
+ * The cost of each derived port, stated plainly because a Fly policy has no
132
+ * destination field of any kind (also measured — see the `egress.ts` module
133
+ * description): the port is opened to EVERY host on the internet, not only to
134
+ * the private service it was derived for. That residual cannot be closed at this
135
+ * layer; it is named in `verifyEgress()`'s verdict and in
136
+ * `docs/sandbox-egress-enforcement.md`.
126
137
  * @param ports - The operator's configured ports, or `undefined` when no policy
127
138
  * is being applied at all.
128
139
  * @param services - The declared private services, or `undefined`.
@@ -1 +1 @@
1
- {"version":3,"file":"flycast.d.ts","sourceRoot":"","sources":["../src/flycast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEzE;;;;GAIG;AACH,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,kBAAkB,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,eAAe,CAAA;AAKnD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,GAAG,SAAS,CAqC7F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,oBAAoB,EAAE,GAAG,SAAS,EACzC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACxC,oBAAoB,EAAE,GAAG,SAAS,CAUpC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAI1E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUnF;AA2BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACvC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACxC,IAAI,CAwCN"}
1
+ {"version":3,"file":"flycast.d.ts","sourceRoot":"","sources":["../src/flycast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEzE;;;;GAIG;AACH,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,kBAAkB,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,eAAe,CAAA;AAKnD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,GAAG,SAAS,CAqC7F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,oBAAoB,EAAE,GAAG,SAAS,EACzC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACxC,oBAAoB,EAAE,GAAG,SAAS,CAUpC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAI1E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUnF;AA2BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACvC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACxC,IAAI,CAwCN"}
package/dist/flycast.js CHANGED
@@ -147,14 +147,25 @@ export function parsePrivateServices(raw) {
147
147
  * still exactly what they wrote plus the ports their own declarations require,
148
148
  * and the applied policy is logged.
149
149
  *
150
- * Whether the addition is even load-bearing is UNVERIFIED: Fly states "Network
151
- * policies only apply to traffic directly to and from Machines. They do not
152
- * affect traffic routed through the Fly Proxy"
150
+ * The addition is load-bearing — VERIFIED 2026-08-16, not inferred. Fly states
151
+ * "Network policies only apply to traffic directly to and from Machines. They do
152
+ * not affect traffic routed through the Fly Proxy"
153
153
  * (https://fly.io/docs/machines/guides-examples/network-policies/), and Flycast
154
- * traffic IS routed through Fly Proxy — but Fly says that about ingress and
155
- * never addresses a Machine's egress toward a Flycast address. An inert entry
156
- * costs a port on the allow list; a missing one costs every database in the
157
- * fleet, so the port is added.
154
+ * traffic IS routed through Fly Proxy, so it was an open question whether these
155
+ * ports did anything. Measured on a throwaway app with a Flycast address into
156
+ * its own 6PN: under a policy allowing only `udp:53`, `molecule-pg-tenant.flycast`
157
+ * and `molecule-api.flycast` both RESOLVED and every TCP connect to them was
158
+ * dropped; re-applying the policy with `tcp:443` opened 443 and nothing else.
159
+ * Fly's sentence is about INGRESS — a Machine's egress TOWARD a Flycast address
160
+ * is filtered like any other. Drop a declared service's port and every database
161
+ * connection in the fleet goes with it.
162
+ *
163
+ * The cost of each derived port, stated plainly because a Fly policy has no
164
+ * destination field of any kind (also measured — see the `egress.ts` module
165
+ * description): the port is opened to EVERY host on the internet, not only to
166
+ * the private service it was derived for. That residual cannot be closed at this
167
+ * layer; it is named in `verifyEgress()`'s verdict and in
168
+ * `docs/sandbox-egress-enforcement.md`.
158
169
  * @param ports - The operator's configured ports, or `undefined` when no policy
159
170
  * is being applied at all.
160
171
  * @param services - The declared private services, or `undefined`.
package/dist/index.d.ts CHANGED
@@ -281,12 +281,14 @@
281
281
  * exist for providers whose templates are host-local; object storage is already
282
282
  * the shared store, so there is nothing to publish or fetch.
283
283
  *
284
- * **`exportFiles`/`importFiles` are NOT implemented.** The core says a provider
285
- * with no bulk transfer should leave them unimplemented rather than emulate one,
286
- * and Fly's only channel is the `exec` endpoint's JSON response — a file-by-file
287
- * or base64 emulation would look supported and take hours. The template path
288
- * moves bulk data without them precisely because it never routes the bytes
289
- * through the control plane.
284
+ * **`importFiles` IS implemented; `exportFiles` is not.** Import takes the same
285
+ * route the template restore does — a presigned object-store URL the Machine
286
+ * pulls itself, falling back to chunked base64 over `exec` when no store is
287
+ * configured — so an app tree can be delivered into a fresh Machine. There is no
288
+ * inverse: Fly's only read channel is the `exec` endpoint's JSON response, and a
289
+ * base64 emulation of a whole-tree export would look supported and route every
290
+ * byte through the control plane. Pair a Fly destination with a source that CAN
291
+ * export (the core's rooting contract makes them interchangeable).
290
292
  *
291
293
  * **Sandbox ids are composite: `"<app>:<machineId>"`.** A Fly Machine id is only
292
294
  * unique within its app and every endpoint is `/v1/apps/{app}/machines/{id}`, so
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwUG;AAEH,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAE9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,eAUrB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0UG;AAEH,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAE9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,eAUrB,CAAA"}
package/dist/index.js CHANGED
@@ -281,12 +281,14 @@
281
281
  * exist for providers whose templates are host-local; object storage is already
282
282
  * the shared store, so there is nothing to publish or fetch.
283
283
  *
284
- * **`exportFiles`/`importFiles` are NOT implemented.** The core says a provider
285
- * with no bulk transfer should leave them unimplemented rather than emulate one,
286
- * and Fly's only channel is the `exec` endpoint's JSON response — a file-by-file
287
- * or base64 emulation would look supported and take hours. The template path
288
- * moves bulk data without them precisely because it never routes the bytes
289
- * through the control plane.
284
+ * **`importFiles` IS implemented; `exportFiles` is not.** Import takes the same
285
+ * route the template restore does — a presigned object-store URL the Machine
286
+ * pulls itself, falling back to chunked base64 over `exec` when no store is
287
+ * configured — so an app tree can be delivered into a fresh Machine. There is no
288
+ * inverse: Fly's only read channel is the `exec` endpoint's JSON response, and a
289
+ * base64 emulation of a whole-tree export would look supported and route every
290
+ * byte through the control plane. Pair a Fly destination with a source that CAN
291
+ * export (the core's rooting contract makes them interchangeable).
290
292
  *
291
293
  * **Sandbox ids are composite: `"<app>:<machineId>"`.** A Fly Machine id is only
292
294
  * unique within its app and every endpoint is `/v1/apps/{app}/machines/{id}`, so
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;AAw6DnB;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAWV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAwBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;AA0tEnB;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}