@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/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);