@molecule/api-code-sandbox-flyio 1.3.8 → 1.4.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 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-11-01T09:52:42.927Z
6
+ Generated: 2026-11-04T14:19:55.149Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-code-sandbox-flyio
@@ -484,169 +484,95 @@ Every option is also readable from an environment variable (see
484
484
 
485
485
  ```typescript
486
486
  interface FlyioConfig {
487
- /**
488
- * Fly API token (sent as `Authorization: Bearer <token>`). Falls back to
489
- * `FLY_API_TOKEN`, then `FLY_ACCESS_TOKEN`. Required — the provider throws a
490
- * named error on the first API call if none is resolvable.
491
- */
487
+ /** Fly API token (sent as `Authorization: Bearer <token>`). */
492
488
  apiToken?: string
493
- /**
494
- * Machines API base URL, INCLUDING the `/v1` path segment. Defaults to
495
- * `FLY_API_HOSTNAME` (with `/v1` appended when the value has no path) or
496
- * `https://api.machines.dev/v1`. From inside a Fly private network the
497
- * documented internal endpoint is `http://_api.internal:4280/v1`.
498
- */
489
+ /** Machines API base URL, INCLUDING the `/v1` path segment. */
499
490
  apiUrl?: string
500
- /**
501
- * Fly organization slug that owns the sandbox apps. Falls back to
502
- * `FLY_ORG_SLUG`, then `personal`. Required for app creation and for
503
- * {@link FlyioConfig.appPerProject} listing.
504
- */
491
+ /** Fly organization slug that owns the sandbox apps. */
505
492
  orgSlug?: string
506
- /**
507
- * Shared Fly app that holds every sandbox Machine. Only used when
508
- * `appPerProject` is `false`. Falls back to `FLY_SANDBOX_APP`.
509
- *
510
- * A shared app puts every tenant on ONE 6PN private network, where each
511
- * sandbox can reach every other sandbox's dev-server ports by private IPv6.
512
- * That is the same cross-tenant exposure the Docker bond's `bridge` network
513
- * has, so this mode is REFUSED in production.
514
- */
493
+ /** Shared Fly app that holds every sandbox Machine. */
515
494
  appName?: string
516
- /**
517
- * Name prefix for the per-project Fly app when `appPerProject` is `true`
518
- * (default). The app is `<appPrefix>-<sanitized projectId>`. Falls back to
519
- * `FLY_SANDBOX_APP_PREFIX`, then `mol-sandbox`.
520
- */
495
+ /** Name prefix for the per-project Fly app when `appPerProject` is `true` (default). */
521
496
  appPrefix?: string
522
- /**
523
- * Create one Fly app — on its own custom 6PN private network — per project,
524
- * so tenants are network-isolated from each other. Defaults to `true`.
525
- * Set `false` (or `FLY_SANDBOX_APP_PER_PROJECT=false`) only for a
526
- * single-tenant deployment; it is refused in production.
527
- */
497
+ /** Create one Fly app — on its own custom 6PN private network — per project, so tenants are network-isolated from each other. */
528
498
  appPerProject?: boolean
529
- /**
530
- * Custom 6PN network name for a per-project app. Defaults to the app name, so
531
- * every project lands on its own private network. Ignored when
532
- * `appPerProject` is `false`. A Fly app's network CANNOT be changed after
533
- * creation.
534
- */
499
+ /** Custom 6PN network name for a per-project app. */
535
500
  network?: string
536
- /**
537
- * Default Fly region for Machines and volumes (e.g. `iad`). Defaults to
538
- * `iad`; `SandboxConfig.region` overrides it for one recorded project create.
539
- * The API Machine's own `FLY_REGION` is never a tenant-placement fallback.
540
- */
501
+ /** Default Fly region for Machines and volumes (e.g. `iad`). */
541
502
  region?: string
542
- /**
543
- * OCI image every sandbox Machine runs. Must be pullable by Fly — a tag in
544
- * the org's `registry.fly.io` repository or a public registry. A LOCAL
545
- * `molecule-sandbox:latest` is not reachable by Fly; push it first. Falls
546
- * back to `FLY_SANDBOX_IMAGE`, then `registry.fly.io/molecule-sandbox:latest`.
547
- */
503
+ /** OCI image every sandbox Machine runs. */
548
504
  baseImage?: string
549
- /** Default vCPU count (`guest.cpus`). Defaults to 1. */
505
+ /** Default vCPU count (`guest.cpus`). */
550
506
  defaultCpu?: number
551
- /** Default `guest.cpu_kind` — `shared` or `performance`. Defaults to `shared`. */
507
+ /** Default `guest.cpu_kind` — `shared` or `performance`. */
552
508
  defaultCpuKind?: string
553
- /** Default memory in MB (`guest.memory_mb`). Defaults to 1024. */
509
+ /** Default memory in MB (`guest.memory_mb`). */
554
510
  defaultMemoryMB?: number
555
- /**
556
- * Size in GB of the volume created for a sandbox's `/workspace` when
557
- * `SandboxConfig.volumeName` is set. Fly volumes are sized in whole GB, so the
558
- * core's `resources.diskMB` is rounded UP to the next GB. Defaults to 10.
559
- */
511
+ /** Size in GB of the volume created for a sandbox's `/workspace` when `SandboxConfig.volumeName` is set. */
560
512
  defaultVolumeGB?: number
561
- /** Internal port the preview service forwards to (the Vite dev server). Defaults to 5173. */
513
+ /** Internal port the preview service forwards to (the Vite dev server). */
562
514
  previewPort?: number
563
- /**
564
- * Preview URL template. Placeholders `{app}`, `{machineId}` and `{port}` are
565
- * all replaced globally. Defaults to `https://{app}.fly.dev`.
566
- *
567
- * For a control plane that runs on the SAME Fly 6PN, the private form
568
- * `http://{machineId}.vm.{app}.internal:{port}` reaches the Machine with no
569
- * public exposure at all — but it does NOT work across custom 6PNs, which is
570
- * exactly what `appPerProject` creates. See the module `@remarks`.
571
- */
515
+ /** Preview URL template. */
572
516
  previewUrlTemplate?: string
573
- /**
574
- * Attach a public Fly Proxy service (`80` → http, `443` → tls+http) forwarding
575
- * to {@link FlyioConfig.previewPort}. Defaults to `true`. Set `false` for a
576
- * fully private sandbox reached only over 6PN/Flycast.
577
- */
517
+ /** Attach a public Fly Proxy service (`80` → http, `443` → tls+http) forwarding to {@link FlyioConfig.previewPort}. */
578
518
  publicService?: boolean
579
- /**
580
- * Fly Proxy idle behaviour for the preview service. Defaults to `'off'` —
581
- * never idle the Machine — because the autostop timer counts the PROXY
582
- * service's idle time, not the workload's, so a sandbox with nobody looking at
583
- * its preview is suspended out from under a build that is still running.
584
- * `suspend` is the scale-to-zero mapping (resume from a memory snapshot on the
585
- * next request) and `stop` is a full stop with a cold boot on wake; opt into
586
- * either deliberately.
587
- *
588
- * This is the provider's STEADY-STATE policy, reported as
589
- * `SandboxProvider.defaultIdleAction`. A caller can override it for one sandbox
590
- * with `SandboxConfig.idleAction` and restore this value afterwards with
591
- * `Sandbox.setIdleAction` — which is what a production deploy does so its
592
- * destination cannot suspend mid-copy.
593
- */
519
+ /** Fly Proxy idle behaviour for the preview service. */
594
520
  autostop?: 'off' | 'stop' | 'suspend'
595
- /**
596
- * Allocate a shared Anycast IPv4 for a newly-created app so `<app>.fly.dev`
597
- * serves traffic. Defaults to `false` (opt-in). See the module `@remarks` —
598
- * the accepted `type` values for the IP-assignment endpoint are NOT enumerated
599
- * in Fly's OpenAPI specification.
600
- */
521
+ /** Allocate a shared Anycast IPv4 for a newly-created app so `<app>.fly.dev` serves traffic. */
601
522
  assignSharedIpv4?: boolean
602
- /** IP type requested when {@link FlyioConfig.assignSharedIpv4} is on. Defaults to `shared_v4`. */
523
+ /** IP type requested when {@link FlyioConfig.assignSharedIpv4} is on. */
603
524
  ipAssignmentType?: string
604
- /** Prefix for the Machine metadata keys this provider owns. Defaults to `molecule-sandbox`. */
525
+ /** Prefix for the Machine metadata keys this provider owns. */
605
526
  metadataPrefix?: string
606
- /** Durable create-attempt ownership. Required for automatic failed-create reconciliation. */
527
+ /** Durable create-attempt ownership. */
607
528
  createAttemptJournal?: FlyCreateAttemptJournal
608
- /** Timeout for a single Machines API request, in ms. Defaults to 30000. */
529
+ /** Timeout for a single Machines API request, in ms. */
609
530
  requestTimeoutMs?: number
610
- /**
611
- * TOTAL budget, in seconds, that `create()`/`start()`/`wake()` block waiting
612
- * for the Machine to actually reach `started`. Fly's `GET .../wait` blocks
613
- * for at most 60 seconds per call, so a larger budget is spent as consecutive
614
- * wait rounds. Defaults to 180 — a Machine whose image is not yet cached on
615
- * its host (every first boot after an image push) pulls it before starting,
616
- * and that alone can exceed a single 60 s round.
617
- *
618
- * Without this wait, `start()` resolves the moment Fly ACCEPTS the request,
619
- * and the caller's very next `exec` hits a Machine that is not running yet.
620
- */
531
+ /** TOTAL budget, in seconds, that `create()`/`start()`/`wake()` block waiting for the Machine to actually reach `started`. */
621
532
  startTimeoutSeconds?: number
622
- /**
623
- * Ports a sandbox Machine may open outbound connections on. Setting this makes
624
- * the provider apply a Fly **network policy** to every app it provisions;
625
- * leaving it unset applies no policy at all, and `verifyEgress()` will then
626
- * observe (correctly) that egress is `open`. Falls back to
627
- * `FLY_SANDBOX_EGRESS_ALLOWED_PORTS` (`tcp:3128,udp:53`).
628
- *
629
- * Read {@link https://fly.io/docs/machines/guides-examples/network-policies/}
630
- * before choosing a value, because the mechanism is narrower than it looks:
631
- * rules match protocol and port ONLY — no host, no CIDR, no ranges — so this
632
- * can never be a host allowlist. `tcp:443` lets a sandbox reach EVERY host on
633
- * the internet that listens on 443. To get a host allowlist, allow only the
634
- * port of an egress proxy you control and route sandbox traffic through it.
635
- *
636
- * An empty array is rejected rather than treated as "deny all": Fly documents
637
- * the deny default as a consequence of an `allow` rule existing, and says
638
- * nothing about a rule with no ports.
639
- */
533
+ /** Ports a sandbox Machine may open outbound connections on. */
640
534
  egressAllowedPorts?: FlyNetworkPolicyPort[]
641
- /** Name of the egress policy this provider owns on each app. Defaults to `molecule-sandbox-egress`. */
535
+ /** Name of the egress policy this provider owns on each app. */
642
536
  egressPolicyName?: string
643
- /**
644
- * Apps every sandbox must be able to reach ACROSS its per-project 6PN — the
645
- * tenant Postgres cluster and the control plane's egress proxy. For each one,
646
- * this provider allocates a **Flycast** private address into the project's own
647
- // … truncated — the full declaration is in node_modules/@molecule/api-code-sandbox-flyio/dist/types.d.ts
537
+ /** Apps every sandbox must be able to reach ACROSS its per-project 6PN — the tenant Postgres cluster and the control plane's egress proxy. */
538
+ privateServices?: FlyPrivateService[]
539
+ /** Per-create regional overrides for {@link FlyioConfig.privateServices}. */
540
+ privateServicesByRegion?: Record<string, FlyPrivateService[]>
541
+ /** `type` sent to `POST /apps/{app}/ip_assignments` when allocating a Flycast address. */
542
+ privateIpAssignmentType?: string
543
+ /** Literal `ip:port` targets `verifyEgress()` attempts raw TCP connections to. */
544
+ egressProbeTargets?: string[]
545
+ /** Per-connection timeout for the egress probe, in ms. */
546
+ egressProbeTimeoutMs?: number
547
+ /** Image the throwaway `verifyEgress()` probe Machine runs. */
548
+ egressProbeImage?: string
549
+ /** Bucket holding sandbox templates. */
550
+ templateBucket?: string
551
+ /** S3 endpoint for {@link FlyioConfig.templateBucket}, e.g. Tigris's `https://t3.storage.dev`. */
552
+ templateEndpoint?: string
553
+ /** Region used to sign template-store requests. */
554
+ templateRegion?: string
555
+ /** Access key for the template store. */
556
+ templateAccessKeyId?: string
557
+ /** Secret key for the template store. */
558
+ templateSecretAccessKey?: string
559
+ /** Session token for temporary template-store credentials. */
560
+ templateSessionToken?: string
561
+ /** Key prefix every template object lives under. */
562
+ templatePrefix?: string
563
+ /** Address the template bucket path-style (`https://endpoint/bucket/key`) instead of virtual-host style. */
564
+ templateForcePathStyle?: boolean
565
+ /** Lifetime of the presigned capture/restore URL handed to a sandbox, in seconds. */
566
+ templateUrlExpirySeconds?: number
567
+ /** Wall-clock budget for one capture or restore transfer, in ms. */
568
+ templateTransferTimeoutMs?: number
569
+ /** Largest template archive this provider will store, in bytes. */
570
+ templateMaxArchiveBytes?: number
571
+ }
648
572
  ```
649
573
 
574
+ _(Member comments are shortened to their first sentence to fit; the full declaration is in `node_modules/@molecule/api-code-sandbox-flyio/dist/types.d.ts`.)_
575
+
650
576
  #### `ProcessEnv`
651
577
 
652
578
  Environment variables the Fly.io sandbox provider reads. Every one is
@@ -1605,8 +1531,15 @@ its own 6PN: under a policy allowing only `udp:53`, `molecule-pg-tenant.flycast`
1605
1531
  and `molecule-api.flycast` both RESOLVED and every TCP connect to them was
1606
1532
  dropped; re-applying the policy with `tcp:443` opened 443 and nothing else.
1607
1533
  Fly's sentence is about INGRESS — a Machine's egress TOWARD a Flycast address
1608
-
1609
- _(description truncated — read the full text in `node_modules/@molecule/api-code-sandbox-flyio/dist/flycast.d.ts`.)_
1534
+ is filtered like any other. Drop a declared service's port and every database
1535
+ connection in the fleet goes with it.
1536
+
1537
+ The cost of each derived port, stated plainly because a Fly policy has no
1538
+ destination field of any kind (also measured — see the `egress.ts` module
1539
+ description): the port is opened to EVERY host on the internet, not only to
1540
+ the private service it was derived for. That residual cannot be closed at this
1541
+ layer; it is named in `verifyEgress()`'s verdict and in
1542
+ `docs/sandbox-egress-enforcement.md`.
1610
1543
 
1611
1544
  ```typescript
1612
1545
  function mergeEgressPorts(
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAaV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EACL,YAAY,EAKb,MAAM,UAAU,CAAA;AA8BjB,OAAO,KAAK,EAEV,wBAAwB,EAEzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;AAyBnB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAIjG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AAgyID;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,GAAG,wBAAwB,CAE5C"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAaV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EACL,YAAY,EAKb,MAAM,UAAU,CAAA;AA8BjB,OAAO,KAAK,EAEV,wBAAwB,EAEzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;AAyBnB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAIjG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AA4zID;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,GAAG,wBAAwB,CAE5C"}
package/dist/provider.js CHANGED
@@ -3336,6 +3336,19 @@ class FlyioSandboxProvider {
3336
3336
  `${totalSeconds}s (last observed state: ${lastState ?? 'unknown'}).`,
3337
3337
  }), { cause: lastError });
3338
3338
  }
3339
+ /**
3340
+ * Activate an already populated Machine with one full-config update: startup command and idle
3341
+ * policy change together. The former command keeps the workload stopped, so the update's reboot
3342
+ * is the workload's first boot. Fly preserves parked Machines with skip_launch; startMachine
3343
+ * explicitly wakes those and waits, and is idempotent after a running Machine's update.
3344
+ */
3345
+ async activate(id, options) {
3346
+ if (!options.command.length)
3347
+ throw new Error('A sandbox activation needs a startup command');
3348
+ const { app, machineId } = parseSandboxId(id);
3349
+ await this.applyIdleAction(app, machineId, options.idleAction, options.command);
3350
+ await this.startMachine(app, machineId);
3351
+ }
3339
3352
  /**
3340
3353
  * Applies an idle policy to a running Machine's public service, in place.
3341
3354
  *
@@ -3356,10 +3369,11 @@ class FlyioSandboxProvider {
3356
3369
  * @param app - The Fly app name.
3357
3370
  * @param machineId - The Fly Machine id.
3358
3371
  * @param action - The idle action to apply to every service.
3372
+ * @param command - An activation command installed in the SAME update as the policy.
3359
3373
  * @throws {Error} When the Machine or its config cannot be read, the update is
3360
3374
  * rejected, or a rebooted Machine does not come back to `started`.
3361
3375
  */
3362
- async applyIdleAction(app, machineId, action) {
3376
+ async applyIdleAction(app, machineId, action, command) {
3363
3377
  const machine = await this.client.request(`/apps/${app}/machines/${machineId}`, {
3364
3378
  nullOn: [404],
3365
3379
  });
@@ -3370,14 +3384,21 @@ class FlyioSandboxProvider {
3370
3384
  }));
3371
3385
  }
3372
3386
  const services = machine.config.services;
3373
- if (!services?.length)
3374
- return;
3375
- if (services.every((service) => (service.autostop ?? 'off') === action))
3376
- return;
3387
+ const samePolicy = !services?.length || services.every((service) => (service.autostop ?? 'off') === action);
3388
+ const sameCommand = !command || JSON.stringify(machine.config.init?.exec) === JSON.stringify(command);
3389
+ if (samePolicy && sameCommand)
3390
+ return mapMachineState(machine.state ?? 'created');
3377
3391
  const config = {
3378
3392
  ...machine.config,
3379
- services: services.map((service) => ({ ...service, autostop: action })),
3393
+ ...(services
3394
+ ? { services: services.map((service) => ({ ...service, autostop: action })) }
3395
+ : {}),
3380
3396
  };
3397
+ if (command) {
3398
+ config.init = { ...machine.config.init, exec: [...command] };
3399
+ delete config.init.entrypoint;
3400
+ delete config.init.cmd;
3401
+ }
3381
3402
  // A running Machine reboots to apply the config, so wait for it; a parked
3382
3403
  // one is updated without being started (there is nothing to wait for).
3383
3404
  const wasRunning = machine.state === 'started' || machine.state === 'starting';
@@ -3394,6 +3415,7 @@ class FlyioSandboxProvider {
3394
3415
  });
3395
3416
  if (wasRunning)
3396
3417
  await this.waitForStarted(app, machineId);
3418
+ return wasRunning ? 'running' : mapMachineState(machine.state ?? 'created');
3397
3419
  }
3398
3420
  /**
3399
3421
  * Builds the `Sandbox` facade for one Machine.
@@ -3442,11 +3464,10 @@ class FlyioSandboxProvider {
3442
3464
  /**
3443
3465
  * Applies an idle policy to this Machine's public service (see
3444
3466
  * {@link FlyioSandboxProvider.applyIdleAction} for the endpoint and the
3445
- * reboot it costs). Resolves with the Machine running again.
3467
+ * reboot it costs). A parked Machine stays parked; a rebooted Machine is running again.
3446
3468
  */
3447
3469
  async setIdleAction(action) {
3448
- await provider.applyIdleAction(app, machineId, action);
3449
- this.status = 'running';
3470
+ this.status = await provider.applyIdleAction(app, machineId, action);
3450
3471
  },
3451
3472
  async exec(command, opts) {
3452
3473
  return await execCommand(exec, command, opts, WORKSPACE_PATH);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-code-sandbox-flyio",
3
- "version": "1.3.8",
3
+ "version": "1.4.0",
4
4
  "description": "Fly.io Machines (Firecracker microVM) code sandbox provider",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "license": "Apache-2.0",
32
32
  "devDependencies": {
33
33
  "@molecule/api-bond": "1.0.4",
34
- "@molecule/api-code-sandbox": "1.5.1",
34
+ "@molecule/api-code-sandbox": "1.7.0",
35
35
  "@types/node": "26.6.5",
36
36
  "typescript": "6.0.3",
37
37
  "vitest": "4.1.12"