@volter/twin-fly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +125 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +78 -0
- package/dist/src/fly-budget.d.ts +41 -0
- package/dist/src/fly-budget.js +94 -0
- package/dist/src/fly-capabilities.d.ts +4 -0
- package/dist/src/fly-capabilities.js +1212 -0
- package/dist/src/fly-conformance.d.ts +8 -0
- package/dist/src/fly-conformance.js +142 -0
- package/dist/src/fly-connector.d.ts +75 -0
- package/dist/src/fly-connector.js +277 -0
- package/dist/src/fly-docker.d.ts +121 -0
- package/dist/src/fly-docker.js +509 -0
- package/dist/src/fly-machines.d.ts +152 -0
- package/dist/src/fly-machines.js +293 -0
- package/dist/src/fly-runtime-choice.d.ts +12 -0
- package/dist/src/fly-runtime-choice.js +21 -0
- package/dist/src/fly-server.d.ts +31 -0
- package/dist/src/fly-server.js +116 -0
- package/dist/src/fly-twin.d.ts +25 -0
- package/dist/src/fly-twin.js +1272 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +117 -0
- package/package.json +51 -0
- package/src/cli.ts +77 -0
- package/src/fly-budget.ts +121 -0
- package/src/fly-capabilities.ts +1261 -0
- package/src/fly-conformance.ts +168 -0
- package/src/fly-connector.ts +278 -0
- package/src/fly-docker.ts +539 -0
- package/src/fly-machines.ts +383 -0
- package/src/fly-runtime-choice.ts +38 -0
- package/src/fly-server.ts +116 -0
- package/src/fly-twin.ts +1218 -0
- package/src/index.ts +171 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// @volter/twin-fly — the Fly.io MACHINES API twin (api.machines.dev/v1), built on the shared
|
|
2
|
+
// @volter/world-core kernel. The first REAL-EXECUTION-PLANE compute twin in the catalog: the control
|
|
3
|
+
// plane (apps / machines / volumes / secrets, machine lifecycle states + events, leases, wait)
|
|
4
|
+
// is a faithful, stateful local surface over kernel event-sourced state — and when a machine is
|
|
5
|
+
// created or started the twin ACTUALLY runs `config.image` as a real local Docker container
|
|
6
|
+
// through the injected `FlyContainerRuntime` seam (fly-docker.ts). The supabase doctrine at the
|
|
7
|
+
// compute layer: twin the control plane, run the real engine. Offline by default: the pure-ledger
|
|
8
|
+
// VIRTUAL runtime means every verify, test and gate runs with no Docker daemon anywhere.
|
|
9
|
+
//
|
|
10
|
+
// Bearer auth is faked-but-enforced. No real Fly is ever contacted (the connector's injected
|
|
11
|
+
// executor is the one live boundary, budget-guarded).
|
|
12
|
+
//
|
|
13
|
+
// (Conformance tooling lives in @volter/world-tooling, a dev dependency — not shipped here, and
|
|
14
|
+
// `./fly-conformance.ts` is NOT re-exported: cli.ts imports it lazily.)
|
|
15
|
+
export { handleFlyTwinRequest, flyTwinSnapshot, FLY_API_HOST, FLY_INTERNAL_API_HOST, FLY_RESOURCE_TYPES } from './fly-twin.ts';
|
|
16
|
+
export type { FlyRequest, FlyResponse } from './fly-twin.ts';
|
|
17
|
+
|
|
18
|
+
export { createFlyTwinFetch, createFlyTwinServer } from './fly-server.ts';
|
|
19
|
+
export type { FlyTwinFetchOptions } from './fly-server.ts';
|
|
20
|
+
export { resolveFlyRuntime } from './fly-runtime-choice.ts';
|
|
21
|
+
export type { FlyServerRuntimeChoice, FlyLocalExecutionOptions } from './fly-runtime-choice.ts';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
containerSpecOf,
|
|
25
|
+
FLY_MACHINE_STATES,
|
|
26
|
+
flyRuntimeEnv,
|
|
27
|
+
imageRefOf,
|
|
28
|
+
machinePrivateIp,
|
|
29
|
+
newInstanceId,
|
|
30
|
+
newMachineId,
|
|
31
|
+
newMachineName,
|
|
32
|
+
newVolumeId,
|
|
33
|
+
VIRTUAL_FLY_RUNTIME,
|
|
34
|
+
} from './fly-machines.ts';
|
|
35
|
+
export type {
|
|
36
|
+
FlyContainerRuntime,
|
|
37
|
+
FlyContainerSpec,
|
|
38
|
+
FlyExecResult,
|
|
39
|
+
FlyMachineEvent,
|
|
40
|
+
FlyMachineState,
|
|
41
|
+
FlyPortSpec,
|
|
42
|
+
} from './fly-machines.ts';
|
|
43
|
+
|
|
44
|
+
// NB: `./fly-docker.ts` (the real execution plane) is deliberately NOT re-exported here: the
|
|
45
|
+
// host boundary resolves it lazily (`resolveFlyRuntime('docker')` in fly-runtime-choice.ts, dev plane), so `node:child_process` and the
|
|
46
|
+
// docker CLI wrapper never enter the runtime graph of a consumer that stays virtual. Import it
|
|
47
|
+
// by direct path if you genuinely want the docker runtime in-process.
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
liveFlyExecute,
|
|
51
|
+
mapApp,
|
|
52
|
+
mapMachine,
|
|
53
|
+
mapVolume,
|
|
54
|
+
pullFlyApps,
|
|
55
|
+
pullFlyMachines,
|
|
56
|
+
pullFlyVolumes,
|
|
57
|
+
syncFlyFromReal,
|
|
58
|
+
flyExecuteOver,
|
|
59
|
+
syncFlyFromRemote,
|
|
60
|
+
performFlyAction,
|
|
61
|
+
} from './fly-connector.ts';
|
|
62
|
+
export type { FlyExecute, LiveFlyOptions } from './fly-connector.ts';
|
|
63
|
+
|
|
64
|
+
// The client-side rate budget — the fail-closed backstop `liveFlyExecute` routes every live
|
|
65
|
+
// request through. Exported so an operator can inspect spend; there is deliberately no export
|
|
66
|
+
// that disables the guard.
|
|
67
|
+
export {
|
|
68
|
+
FLY_BUDGET_CEILING,
|
|
69
|
+
FLY_BUDGET_MAX_RETRY_AFTER_S,
|
|
70
|
+
FLY_BUDGET_WINDOW_MS,
|
|
71
|
+
FLY_CALL_WEIGHTS,
|
|
72
|
+
FLY_RATE_BUDGET,
|
|
73
|
+
FlyBudget,
|
|
74
|
+
FlyBudgetError,
|
|
75
|
+
flyBudgetPath,
|
|
76
|
+
flyCallWeight,
|
|
77
|
+
} from './fly-budget.ts';
|
|
78
|
+
export type { FlyBudgetErrorKind, FlyBudgetOptions, FlyBudgetReservation, FlyBudgetSnapshot } from './fly-budget.ts';
|
|
79
|
+
|
|
80
|
+
// Registry descriptor: the pack self-describes so tooling can discover it.
|
|
81
|
+
import { registerPack, referenceField, type TwinPack } from '@volter/world-core';
|
|
82
|
+
import { FLY_RATE_BUDGET as RATE_BUDGET } from './fly-budget.ts';
|
|
83
|
+
import { performFlyAction as perform, syncFlyFromRemote as refresh } from './fly-connector.ts';
|
|
84
|
+
|
|
85
|
+
export const pack: TwinPack = {
|
|
86
|
+
vendor: 'fly',
|
|
87
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of the real
|
|
88
|
+
// state system: perform one entry against Fly, refresh the root from it. Moved 2026-09-08. A performed
|
|
89
|
+
// machine-create provisions real, billable compute: the head performs only when an operator bound it to
|
|
90
|
+
// a real state system.
|
|
91
|
+
protocol: '2',
|
|
92
|
+
refresh: { every: '5m', onDemand: { atMost: '30s' } }, // the Machines API has no webhooks: polled, or on demand
|
|
93
|
+
stateSystem: { perform, refresh },
|
|
94
|
+
// the round trip: an app (its name is Fly's id — the first write may answer 422 name-taken on a branch that
|
|
95
|
+
// already holds it, and the round trip reads the LAST write's answer), then a volume in it
|
|
96
|
+
roundTrip: [
|
|
97
|
+
{ method: 'POST', path: '/v1/apps', body: { app_name: 'round-trip', org_slug: 'personal' }, headers: { authorization: 'Bearer round-trip' } },
|
|
98
|
+
{ method: 'POST', path: '/v1/apps/round-trip/volumes', body: { name: 'data', region: 'sjc', size_gb: 1 }, headers: { authorization: 'Bearer round-trip' } },
|
|
99
|
+
],
|
|
100
|
+
// rule 5: a machine, a volume and a secret name their app; a machine's mount names its volume, and a volume
|
|
101
|
+
// names the machine it is attached to. When the head adopts Fly's id for any of them, the kernel resolves the
|
|
102
|
+
// referencing side first. The reference trip is a machine in the round trip's app (a mount would bind the one
|
|
103
|
+
// volume to the first machine — Fly attaches a volume once — so the trip that repeats names only its app).
|
|
104
|
+
referenceTrip: { method: 'POST', path: '/v1/apps/{{id}}/machines', body: { region: 'sjc', config: { image: 'nginx:1.27' } }, headers: { authorization: 'Bearer round-trip' } },
|
|
105
|
+
references: [
|
|
106
|
+
referenceField('machine', 'app_name', 'app'),
|
|
107
|
+
referenceField('volume', 'app_name', 'app'),
|
|
108
|
+
referenceField('secret', 'app_name', 'app'),
|
|
109
|
+
{
|
|
110
|
+
type: 'machine', to: 'volume',
|
|
111
|
+
key: (f: Record<string, unknown>) => { const mounts = (f.config as { mounts?: Array<{ volume?: unknown }> } | undefined)?.mounts; const v = mounts?.[0]?.volume; return typeof v === 'string' && v ? v : undefined; },
|
|
112
|
+
adopt: (f: Record<string, unknown>, vendorId: string) => { const config = (f.config ?? {}) as { mounts?: Array<Record<string, unknown>> }; const mounts = [...(config.mounts ?? [])]; if (mounts[0]) mounts[0] = { ...mounts[0], volume: vendorId }; return { config: { ...config, mounts } }; },
|
|
113
|
+
},
|
|
114
|
+
referenceField('volume', 'attached_machine_id', 'machine'),
|
|
115
|
+
],
|
|
116
|
+
parityOrigin: 'http://twin',
|
|
117
|
+
shapeParity: 'held',
|
|
118
|
+
// the execution plane: real local Docker containers live outside the world store, by declaration
|
|
119
|
+
engine: { module: 'fly-docker', note: 'real local Docker containers: a started machine runs config.image; the tree references the container by ref' },
|
|
120
|
+
// The SAME object fly-budget.ts declares at module load — one source of truth, so registering
|
|
121
|
+
// the pack and importing the connector can never arm two different ceilings.
|
|
122
|
+
rateBudget: RATE_BUDGET,
|
|
123
|
+
transport: 'rest',
|
|
124
|
+
archetype: 'engine-control',
|
|
125
|
+
bin: 'world-fly',
|
|
126
|
+
resources: ['app', 'machine', 'volume', 'secret'],
|
|
127
|
+
specSource:
|
|
128
|
+
"Fly.io Machines API — first-party OpenAPI (docs.machines.dev/swagger/doc.json, 'Machines API 1.0') "
|
|
129
|
+
+ 'cross-checked against superfly/fly-go (flyctl\'s own client); fly.io/docs/machines/api for hosts, '
|
|
130
|
+
+ 'response codes and rate limits',
|
|
131
|
+
description:
|
|
132
|
+
'Fly.io Machines API twin — apps/machines/volumes/secrets CRUD, the machine lifecycle '
|
|
133
|
+
+ '(states + ledgered events, wait, leases, metadata, exec), connector pull. The execution '
|
|
134
|
+
+ 'plane is REAL local Docker via the injected FlyContainerRuntime seam: created/started '
|
|
135
|
+
+ 'machines genuinely run config.image as local containers (virtual pure-ledger runtime by '
|
|
136
|
+
+ 'default, so everything works offline).',
|
|
137
|
+
// Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
138
|
+
// 2026-08-31).
|
|
139
|
+
//
|
|
140
|
+
// Fly publishes NO first-party npm client (flyctl embeds the Go client superfly/fly-go; the
|
|
141
|
+
// docs' contract is "point FLY_API_HOSTNAME at the base URL and speak REST"), so the canonical
|
|
142
|
+
// integration is a hand-written fetch wrapper and the credential env var is the PRIMARY scan
|
|
143
|
+
// signal: FLY_API_TOKEN — the exact var Fly's own docs export next to FLY_API_HOSTNAME — stems
|
|
144
|
+
// to `fly` under _API_TOKEN. FLY_API_HOSTNAME itself deliberately does NOT stem: _HOSTNAME is
|
|
145
|
+
// no credential/endpoint suffix, so it never reaches the lookup. `fly-admin` is the one real
|
|
146
|
+
// npm client of the modeled surface worth claiming (Supabase's maintained TS client for
|
|
147
|
+
// api.machines.dev; its base URL is a constructor option, so pointing it at the twin is
|
|
148
|
+
// configuration). No scope to claim — Fly owns no npm scope for API clients.
|
|
149
|
+
adoption: {
|
|
150
|
+
// No official Python SDK - flyctl and `fly-admin` (npm) are the vendor's clients. The community
|
|
151
|
+
// `fly-python-sdk` is a single-author wrapper with negligible adoption, deliberately unclaimed.
|
|
152
|
+
pypi: [],
|
|
153
|
+
sdks: ['fly-admin'], envStems: ['FLY'],
|
|
154
|
+
},
|
|
155
|
+
// TWO base hosts, both from Fly's own docs (fly.io/docs/machines/api/working-with-machines-api
|
|
156
|
+
// "API addresses"):
|
|
157
|
+
// • api.machines.dev — the public base URL every out-of-mesh client uses (flyctl's flaps
|
|
158
|
+
// transport, fly-admin, plain fetch with FLY_API_HOSTNAME).
|
|
159
|
+
// • _api.internal — the internal base URL (port 4280) reachable only inside a Fly
|
|
160
|
+
// WireGuard mesh; claimed so code written for in-mesh deployment redirects to the twin
|
|
161
|
+
// instead of failing DNS on a laptop.
|
|
162
|
+
// fly.io itself (the dashboard/website) and api.fly.io (the legacy GraphQL API this pack does
|
|
163
|
+
// not model) are deliberately unclaimed: strict egress should fail closed on a surface the twin
|
|
164
|
+
// does not serve rather than let a half-modeled flow through.
|
|
165
|
+
hosts: [{ host: 'api.machines.dev' }, { host: '_api.internal' }],
|
|
166
|
+
// Machines API clients hit https://api.machines.dev/v1/... (or http://_api.internal:4280 from
|
|
167
|
+
// inside a Fly private network); the dev proxy forwards the /v1/ prefix to the twin.
|
|
168
|
+
browserRouting: { apiPathPrefix: '/v1/', loaderHost: 'https://api.machines.dev' },
|
|
169
|
+
};
|
|
170
|
+
// registered at import: the kernel learns the pack's state system and its references (protocol 2)
|
|
171
|
+
registerPack(pack);
|