@runonflux/flux-cloud-mcp 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 +21 -0
- package/README.md +178 -0
- package/dist/chain.d.ts +71 -0
- package/dist/chain.js +144 -0
- package/dist/chain.js.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/deploy.d.ts +111 -0
- package/dist/deploy.js +216 -0
- package/dist/deploy.js.map +1 -0
- package/dist/docs.d.ts +5 -0
- package/dist/docs.js +164 -0
- package/dist/docs.js.map +1 -0
- package/dist/enterprise.d.ts +29 -0
- package/dist/enterprise.js +45 -0
- package/dist/enterprise.js.map +1 -0
- package/dist/fluxapi.d.ts +71 -0
- package/dist/fluxapi.js +165 -0
- package/dist/fluxapi.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +44 -0
- package/dist/keys.js +79 -0
- package/dist/keys.js.map +1 -0
- package/dist/pricing.d.ts +93 -0
- package/dist/pricing.js +192 -0
- package/dist/pricing.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +877 -0
- package/dist/server.js.map +1 -0
- package/dist/spec.d.ts +144 -0
- package/dist/spec.js +312 -0
- package/dist/spec.js.map +1 -0
- package/package.json +69 -0
package/dist/deploy.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploying an app is two halves that must agree:
|
|
3
|
+
*
|
|
4
|
+
* 1. A signed specification message, handed to one FluxOS node and gossiped
|
|
5
|
+
* to the network. The node answers with a 64-character hash and keeps the
|
|
6
|
+
* message in a temporary store for one hour.
|
|
7
|
+
* 2. An on-chain payment to the network deployment address carrying that
|
|
8
|
+
* hash in an OP_RETURN. When nodes index the transaction they pair it with
|
|
9
|
+
* the stored message, check the amount, and publish the app globally.
|
|
10
|
+
*
|
|
11
|
+
* If the payment never lands the message just expires and nothing is spent.
|
|
12
|
+
* The amount paid is the Flux Cloud USD quote converted to FLUX. Consensus
|
|
13
|
+
* would accept far less, but that minimum is checked here only as a guard:
|
|
14
|
+
* underpaying it burns the FLUX with no refund, since the message is dropped
|
|
15
|
+
* silently and the same hash cannot be paid twice.
|
|
16
|
+
*/
|
|
17
|
+
import { Explorer, buildPayment, selectSpendable, toFlux, SATOSHIS } from './chain.js';
|
|
18
|
+
import { FluxClient, findHealthyNodes } from './fluxapi.js';
|
|
19
|
+
import { currentSession, identityFromWif, signMessage } from './keys.js';
|
|
20
|
+
import { encryptEnterprise } from './enterprise.js';
|
|
21
|
+
import { consensusMinimumFlux, quoteFromNetwork, } from './pricing.js';
|
|
22
|
+
import { REGISTER_TYPE, UPDATE_TYPE, durabilityWarnings, formatSpecification, signablePayload, validateSpecification, } from './spec.js';
|
|
23
|
+
const TEMP_MESSAGE_TTL_MS = 3600 * 1000;
|
|
24
|
+
const FEE_ALLOWANCE_SAT = 100000;
|
|
25
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
export function walletFromConfig(config) {
|
|
27
|
+
if (!config.ownerWif) {
|
|
28
|
+
throw new Error('FLUX_ID_PRIVATE_KEY is not configured. Run flux_generate_keys, or set it to the WIF of your Flux ID.');
|
|
29
|
+
}
|
|
30
|
+
if (!config.payerWif) {
|
|
31
|
+
throw new Error('FLUX_PAYMENT_PRIVATE_KEY is not configured. Run flux_generate_keys, or set it to the WIF of a funded Flux address.');
|
|
32
|
+
}
|
|
33
|
+
return { owner: identityFromWif(config.ownerWif), payer: identityFromWif(config.payerWif) };
|
|
34
|
+
}
|
|
35
|
+
export function api(config) {
|
|
36
|
+
return new FluxClient(config.apiUrl, config.requestTimeoutMs);
|
|
37
|
+
}
|
|
38
|
+
export function explorer(config) {
|
|
39
|
+
return new Explorer(config.explorerUrls);
|
|
40
|
+
}
|
|
41
|
+
export async function publishedSpec(client, name) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = await client.raw('GET', `/apps/appspecifications/${name}`, { timeoutMs: 30000 });
|
|
44
|
+
const payload = raw;
|
|
45
|
+
return payload.status === 'success' && payload.data?.name === name ? payload.data : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function appLocations(client, name) {
|
|
52
|
+
const raw = (await client.raw('GET', `/apps/location/${name}`, { timeoutMs: 30000 }));
|
|
53
|
+
return raw.status === 'success' && Array.isArray(raw.data) ? raw.data : [];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pick a node that can take the registration: enough peers, ArcaneOS when
|
|
57
|
+
* the spec is enterprise, and honouring a pinned FLUX_NODE_URL.
|
|
58
|
+
*/
|
|
59
|
+
export async function selectNode(config, { arcane = false, log = () => { } } = {}) {
|
|
60
|
+
if (config.nodeUrl)
|
|
61
|
+
return new FluxClient(config.nodeUrl, config.requestTimeoutMs);
|
|
62
|
+
const [node] = await findHealthyNodes(api(config), 1, { arcane, log });
|
|
63
|
+
return node.client;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Everything needed to decide whether a deployment can proceed, without
|
|
67
|
+
* signing, spending or broadcasting anything.
|
|
68
|
+
*/
|
|
69
|
+
export async function plan(config, input, options = {}) {
|
|
70
|
+
const { log = () => { } } = options;
|
|
71
|
+
const wallet = walletFromConfig(config);
|
|
72
|
+
const lb = api(config);
|
|
73
|
+
const spec = formatSpecification({ ...input, owner: wallet.owner.zelid });
|
|
74
|
+
const isEnterprise = Boolean(options.enterprise) || Boolean(spec.enterprise);
|
|
75
|
+
const errors = validateSpecification(options.enterprise ? { ...spec, compose: options.enterprise.compose } : spec);
|
|
76
|
+
if (errors.length)
|
|
77
|
+
throw new Error(`Specification is not valid:\n- ${errors.join('\n- ')}`);
|
|
78
|
+
const [previous, node] = await Promise.all([
|
|
79
|
+
publishedSpec(lb, spec.name),
|
|
80
|
+
selectNode(config, { arcane: isEnterprise, log }),
|
|
81
|
+
]);
|
|
82
|
+
const action = previous ? 'update' : 'register';
|
|
83
|
+
if (previous && previous.owner !== wallet.owner.zelid) {
|
|
84
|
+
throw new Error(`${spec.name} is already registered on the network by ${previous.owner}. Pick another name.`);
|
|
85
|
+
}
|
|
86
|
+
let candidate = spec;
|
|
87
|
+
if (options.enterprise) {
|
|
88
|
+
const session = currentSession(wallet.owner.wif);
|
|
89
|
+
candidate = await encryptEnterprise(node, session, spec, options.enterprise);
|
|
90
|
+
log('enterprise components encrypted for the network');
|
|
91
|
+
}
|
|
92
|
+
const verifyPath = action === 'update'
|
|
93
|
+
? '/apps/verifyappupdatespecifications'
|
|
94
|
+
: '/apps/verifyappregistrationspecifications';
|
|
95
|
+
const formatted = await node.post(verifyPath, candidate, { timeoutMs: 120000 });
|
|
96
|
+
log(`node validated the specification (${action})`);
|
|
97
|
+
const [quote, deployment, info, balance] = await Promise.all([
|
|
98
|
+
quoteFromNetwork(lb, candidate),
|
|
99
|
+
lb.get('/apps/deploymentinformation', { timeoutMs: 30000 }),
|
|
100
|
+
lb.get('/daemon/getinfo', { timeoutMs: 20000 }),
|
|
101
|
+
explorer(config).balance(wallet.payer.fluxAddress),
|
|
102
|
+
]);
|
|
103
|
+
const height = info.blocks;
|
|
104
|
+
const minimum = consensusMinimumFlux(candidate, deployment.price, height);
|
|
105
|
+
const requiredSat = Math.round(quote.flux * SATOSHIS) + FEE_ALLOWANCE_SAT;
|
|
106
|
+
const warnings = durabilityWarnings(options.enterprise ? { ...spec, compose: options.enterprise.compose } : spec);
|
|
107
|
+
if (quote.flux < minimum) {
|
|
108
|
+
warnings.push(`The network quoted ${quote.flux} FLUX but consensus needs at least ${minimum} FLUX; the payment will be raised to ${minimum} FLUX.`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
action,
|
|
112
|
+
spec: candidate,
|
|
113
|
+
formatted,
|
|
114
|
+
previous,
|
|
115
|
+
quote,
|
|
116
|
+
consensusMinimumFlux: minimum,
|
|
117
|
+
deployment,
|
|
118
|
+
height,
|
|
119
|
+
payer: wallet.payer.fluxAddress,
|
|
120
|
+
balanceFlux: toFlux(balance.spendable),
|
|
121
|
+
requiredFlux: toFlux(requiredSat),
|
|
122
|
+
funded: balance.spendable >= requiredSat,
|
|
123
|
+
warnings,
|
|
124
|
+
node,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Sign, broadcast the message, then pay for it. Spends FLUX. */
|
|
128
|
+
export async function execute(config, prepared, log = () => { }) {
|
|
129
|
+
const wallet = walletFromConfig(config);
|
|
130
|
+
if (!prepared.funded) {
|
|
131
|
+
throw new Error(`Payer ${prepared.payer} holds ${prepared.balanceFlux} FLUX spendable but needs about ${prepared.requiredFlux} FLUX. Fund the address and try again.`);
|
|
132
|
+
}
|
|
133
|
+
const type = prepared.action === 'update' ? UPDATE_TYPE : REGISTER_TYPE;
|
|
134
|
+
const timestamp = Date.now();
|
|
135
|
+
const signature = signMessage(signablePayload(prepared.formatted, timestamp, type), wallet.owner.wif);
|
|
136
|
+
const session = currentSession(wallet.owner.wif);
|
|
137
|
+
const message = { type, version: 1, appSpecification: prepared.formatted, timestamp, signature };
|
|
138
|
+
const hash = await prepared.node.post(prepared.action === 'update' ? '/apps/appupdate' : '/apps/appregister', message, { session, timeoutMs: 180000 });
|
|
139
|
+
if (typeof hash !== 'string' || hash.length !== 64) {
|
|
140
|
+
throw new Error(`Node returned an unexpected message hash: ${JSON.stringify(hash)}`);
|
|
141
|
+
}
|
|
142
|
+
log(`message broadcast, hash ${hash}`);
|
|
143
|
+
// Re-check against a fresh price table and height right before spending:
|
|
144
|
+
// consensus enforces the interval in force where the payment CONFIRMS.
|
|
145
|
+
const lb = api(config);
|
|
146
|
+
const [freshDeployment, info] = await Promise.all([
|
|
147
|
+
lb.get('/apps/deploymentinformation', { timeoutMs: 30000 }),
|
|
148
|
+
lb.get('/daemon/getinfo', { timeoutMs: 20000 }),
|
|
149
|
+
]);
|
|
150
|
+
if (freshDeployment.address !== prepared.deployment.address) {
|
|
151
|
+
throw new Error('Deployment address changed between quote and payment; nothing was paid.');
|
|
152
|
+
}
|
|
153
|
+
const minimum = consensusMinimumFlux(prepared.spec, freshDeployment.price, info.blocks + 5);
|
|
154
|
+
const payFlux = Math.max(prepared.quote.flux, minimum);
|
|
155
|
+
const amountSat = Math.round(payFlux * SATOSHIS);
|
|
156
|
+
const chain = explorer(config);
|
|
157
|
+
const utxos = selectSpendable(await chain.utxos(wallet.payer.fluxAddress));
|
|
158
|
+
const payment = buildPayment({
|
|
159
|
+
wif: wallet.payer.wif,
|
|
160
|
+
to: freshDeployment.address,
|
|
161
|
+
amountSat,
|
|
162
|
+
message: hash,
|
|
163
|
+
utxos,
|
|
164
|
+
height: info.blocks,
|
|
165
|
+
});
|
|
166
|
+
const txid = await chain.broadcast(payment.hex);
|
|
167
|
+
log(`paid ${payFlux} FLUX, txid ${txid}`);
|
|
168
|
+
return {
|
|
169
|
+
action: prepared.action,
|
|
170
|
+
name: prepared.formatted.name,
|
|
171
|
+
messageHash: hash,
|
|
172
|
+
messageExpiresAt: new Date(timestamp + TEMP_MESSAGE_TTL_MS).toISOString(),
|
|
173
|
+
txid,
|
|
174
|
+
paidFlux: payFlux,
|
|
175
|
+
paidUsd: prepared.quote.usd,
|
|
176
|
+
feeFlux: toFlux(payment.fee),
|
|
177
|
+
deploymentAddress: freshDeployment.address,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Poll until the message is accepted and instances run, within `timeoutMs`.
|
|
182
|
+
* Returns partial progress on timeout so a caller can simply call again.
|
|
183
|
+
*/
|
|
184
|
+
export async function waitForApp(config, name, options = {}) {
|
|
185
|
+
const { txid, previousHash, timeoutMs = 5 * 60 * 1000, pollMs = 20000 } = options;
|
|
186
|
+
const lb = api(config);
|
|
187
|
+
const chain = explorer(config);
|
|
188
|
+
const deadline = Date.now() + timeoutMs;
|
|
189
|
+
let confirmations = 0;
|
|
190
|
+
let accepted = null;
|
|
191
|
+
let instances = [];
|
|
192
|
+
for (;;) {
|
|
193
|
+
if (txid && confirmations < 1) {
|
|
194
|
+
confirmations = Number((await chain.transaction(txid).catch(() => ({ confirmations: 0 }))).confirmations ?? 0);
|
|
195
|
+
}
|
|
196
|
+
const current = await publishedSpec(lb, name);
|
|
197
|
+
if (current && (!previousHash || current.hash !== previousHash))
|
|
198
|
+
accepted = current;
|
|
199
|
+
if (accepted)
|
|
200
|
+
instances = await appLocations(lb, name);
|
|
201
|
+
const wanted = accepted?.instances ?? 0;
|
|
202
|
+
const done = Boolean(accepted) && instances.length >= wanted;
|
|
203
|
+
if (done || Date.now() >= deadline) {
|
|
204
|
+
return {
|
|
205
|
+
accepted,
|
|
206
|
+
paymentConfirmations: confirmations,
|
|
207
|
+
instances,
|
|
208
|
+
wantedInstances: wanted,
|
|
209
|
+
done,
|
|
210
|
+
timedOut: !done,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
await sleep(pollMs);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.js","sourceRoot":"","sources":["../src/deploy.ts"],"names":[],"mappings":"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"}
|
package/dist/docs.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Guide texts served as MCP resources so a host can load them into context. */
|
|
2
|
+
export declare const OVERVIEW = "# Flux Cloud, for agents\n\nFlux Cloud is a decentralized cloud: thousands of independently operated\nnodes run Docker containers for a monthly price paid in FLUX. There is no\naccount to create. Two secp256k1 keys are all the identity there is:\n\n- **Flux ID** (a \"1...\" address, also called ZelID): owns apps. It signs the\n app specification and API sessions. It never holds funds.\n- **Payment address** (a \"t1...\" address): holds FLUX and pays deployment\n fees on-chain. It can be rotated without changing app ownership.\n\nBoth come from `FLUX_ID_PRIVATE_KEY` and `FLUX_PAYMENT_PRIVATE_KEY` in this\nserver's environment. `flux_generate_keys` creates a fresh pair.\n\n## How a deployment works\n\n1. Describe the app: name, Docker image(s), ports, cpu/ram/hdd per component,\n instance count, term. `flux_build_spec` turns that into a v8 spec.\n2. `flux_quote_app` returns the price in USD and the FLUX to pay.\n3. `flux_deploy_app` with `confirm: true` validates the spec on a node, signs it\n with the Flux ID, broadcasts it, and pays the quoted FLUX from the payment\n address. The transaction carries the message hash in an OP_RETURN.\n4. `flux_wait_for_app` (or `flux_get_app`) reports acceptance and the IPs\n where instances run. Acceptance takes a few minutes; images then pull.\n\nAn existing app of the same owner is **updated** by the same call: the new\nspec replaces the old, and the network credits the unused part of the old term.\nTo stop an app early, update it with a very short term (`flux_cancel_app`).\n\n## Reaching an app\n\nEach instance is reachable at `http://<node-ip>:<port>` for every public port\nin the spec. The network also serves `https://<appname>.app.runonflux.io`\n(an alias for the first public port) and `<appname>_<port>.app.runonflux.io`\nfor every public port, load balanced across instances. Custom domains go in the component's\n`domains` list, one per port, and need a CNAME to `<appname>.app.runonflux.io`.\n\n## Rules of thumb\n\n- Names: 1-63 chars, letters/digits/inner hyphens, must not start with \"flux\"\n or \"zel\", unique network-wide.\n- Public ports: pick from 31000-39999 unless you need a specific one; ports\n 0-1023, 8080, 8081, 8443 and 6667 are \"enterprise ports\" billed extra.\n- Persisted paths must be prefixed `r:` (replicated) to survive instance moves.\n- Images need an explicit tag and must be public unless `repoauth` is set.\n- One network month is 88000 blocks (about 30.5 days). Terms run 1 block to\n 12 months; 3+ months earn 3 to 12 percent off.\n- Flux sets only the container's Cmd, never Entrypoint.\n";
|
|
3
|
+
export declare const SPEC_FORMAT = "# Flux v8 application specification\n\n```json\n{\n \"version\": 8,\n \"name\": \"myapp\",\n \"description\": \"What it is\",\n \"owner\": \"1YourFluxIdAddress\",\n \"compose\": [\n {\n \"name\": \"web\",\n \"description\": \"Web front end\",\n \"repotag\": \"nginx:1.27-alpine\",\n \"ports\": [31080],\n \"domains\": [\"\"],\n \"environmentParameters\": [\"KEY=value\"],\n \"commands\": [],\n \"containerPorts\": [80],\n \"containerData\": \"r:/usr/share/nginx/html\",\n \"cpu\": 0.5,\n \"ram\": 500,\n \"hdd\": 5,\n \"repoauth\": \"\"\n }\n ],\n \"instances\": 3,\n \"contacts\": [],\n \"geolocation\": [],\n \"expire\": 88000,\n \"nodes\": [],\n \"staticip\": false,\n \"enterprise\": \"\"\n}\n```\n\nField notes:\n\n- `compose`: 1-10 components. Each is one container. Components of one app\n share a network and reach each other by component name.\n- `ports` / `containerPorts` / `domains`: parallel arrays, one entry per\n exposed port. `domains` entries are \"\" or a custom domain.\n- `environmentParameters`: up to 20 \"KEY=value\" strings.\n- `commands`: up to 20 strings, passed as the container Cmd.\n- `containerData`: the path inside the container that persists. Prefixes:\n `r:` replicate across instances (Syncthing), `g:` primary/standby\n replication, `s:` sync only. Multiple paths: \"r:/data|/config\".\n- `cpu`: 0.1 to 15 in 0.1 steps. `ram`: 100 to 59000 MB in 100 MB steps.\n `hdd`: 1 to 820 GB whole numbers.\n- `instances`: 1-100 copies, each on a different node.\n- `expire`: term in blocks. 88000 = 1 month. Max 1056000 (12 months).\n- `geolocation`: rules like \"acEU\" (allow continent EU), \"acNA_US\" (allow a\n country), \"a!cAS\" (deny a continent), \"acALL\". Empty means anywhere.\n- `nodes`: pin to specific node IPs (adds the scope surcharge).\n- `staticip`: only nodes with a static IP (surcharge).\n- `enterprise`: \"\" for a normal app. For a private app the components are\n encrypted into this field and `compose` is published empty; only the node\n running it can decrypt. Use `enterprise` in `flux_deploy_app`.\n";
|
|
4
|
+
export declare const PRICING = "# Flux Cloud pricing\n\nPrices are set in US dollars and paid in FLUX. This server always quotes the\nFlux Cloud USD price, converted at the live FLUX market rate.\n\nPer month, per instance-third of the app's resource total (an app's resources\nare priced as a single node's share; the per-instance figure is total / 3):\n\n| Resource | USD / month |\n|---|---|\n| 0.1 CPU core | $0.15 |\n| 100 MB RAM | $0.05 |\n| 1 GB SSD | $0.02 |\n| enterprise port (0-1023, 8080, 8081, 8443, 6667) | $2.00 each |\n| node pinning or private (enterprise) app | $4.00 |\n| static IP nodes | $2.00 |\n\nThen: multiplied by the instance count; minimum $0.99 per app per month;\nsmall apps (under 3 cores, 6 GB RAM, 150 GB and fewer than 4 instances) get\n20 percent off, medium apps 10 percent off; primary/standby (\"g:\") storage 20\npercent off; terms of 3, 6 and 9+ months get 3, 6 and 12 percent off.\n\nPaying in FLUX earns a 5 percent discount, so\n`FLUX to pay = USD / market rate x 0.95`.\n\n`flux_quote_app` returns the authoritative figure from the network; the same\nendpoint prices updates, crediting the unused part of the previous term.\n\nWorked example: one component with 0.5 CPU, 500 MB, 5 GB, 3 instances, 1 month\n= (0.75 + 0.25 + 0.10) / 3 = $0.37 per instance, x3 = $1.11, small-app\ndiscount = $0.89, floored to $0.99 per month.\n";
|
|
5
|
+
export declare const GOTCHAS = "# Things that are easy to get wrong on Flux\n\n- **Underpaying burns FLUX.** Nodes drop an underpaid message silently and the\n payment is not refunded. This server pays the quoted amount and re-checks\n the network's minimum immediately before broadcasting.\n- **The message lives one hour.** Payment has to be broadcast within an hour of\n registration; this server does both in one step.\n- **Acceptance is not instant.** The payment needs a confirmation (about 30\n seconds per block), then nodes pair it with the message. Expect 2-10\n minutes, then image pull time.\n- **Node-local data disappears.** Instances move when nodes churn. Persist\n with `r:` and run 3+ instances if the data matters.\n- **Entrypoint images.** Flux only sets Cmd, so an image whose ENTRYPOINT is a\n binary cannot be given a shell command through `commands`.\n- **Ports must be unique per node.** Two apps using the same public port\n cannot share a node; pick unusual ports in 31000-39999.\n- **Non-streamed HTTP through the shared domain times out at 25 s.** Long\n requests must stream or use the direct `ip:port`.\n- **Updates cost the difference.** The network credits the unused part of the\n previous term; a mid-term update with the same resources is nearly free.\n- **Enterprise apps need ArcaneOS nodes.** Only they can decrypt and validate\n a private spec; this server selects one automatically.\n";
|
package/dist/docs.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** Guide texts served as MCP resources so a host can load them into context. */
|
|
2
|
+
export const OVERVIEW = `# Flux Cloud, for agents
|
|
3
|
+
|
|
4
|
+
Flux Cloud is a decentralized cloud: thousands of independently operated
|
|
5
|
+
nodes run Docker containers for a monthly price paid in FLUX. There is no
|
|
6
|
+
account to create. Two secp256k1 keys are all the identity there is:
|
|
7
|
+
|
|
8
|
+
- **Flux ID** (a "1..." address, also called ZelID): owns apps. It signs the
|
|
9
|
+
app specification and API sessions. It never holds funds.
|
|
10
|
+
- **Payment address** (a "t1..." address): holds FLUX and pays deployment
|
|
11
|
+
fees on-chain. It can be rotated without changing app ownership.
|
|
12
|
+
|
|
13
|
+
Both come from \`FLUX_ID_PRIVATE_KEY\` and \`FLUX_PAYMENT_PRIVATE_KEY\` in this
|
|
14
|
+
server's environment. \`flux_generate_keys\` creates a fresh pair.
|
|
15
|
+
|
|
16
|
+
## How a deployment works
|
|
17
|
+
|
|
18
|
+
1. Describe the app: name, Docker image(s), ports, cpu/ram/hdd per component,
|
|
19
|
+
instance count, term. \`flux_build_spec\` turns that into a v8 spec.
|
|
20
|
+
2. \`flux_quote_app\` returns the price in USD and the FLUX to pay.
|
|
21
|
+
3. \`flux_deploy_app\` with \`confirm: true\` validates the spec on a node, signs it
|
|
22
|
+
with the Flux ID, broadcasts it, and pays the quoted FLUX from the payment
|
|
23
|
+
address. The transaction carries the message hash in an OP_RETURN.
|
|
24
|
+
4. \`flux_wait_for_app\` (or \`flux_get_app\`) reports acceptance and the IPs
|
|
25
|
+
where instances run. Acceptance takes a few minutes; images then pull.
|
|
26
|
+
|
|
27
|
+
An existing app of the same owner is **updated** by the same call: the new
|
|
28
|
+
spec replaces the old, and the network credits the unused part of the old term.
|
|
29
|
+
To stop an app early, update it with a very short term (\`flux_cancel_app\`).
|
|
30
|
+
|
|
31
|
+
## Reaching an app
|
|
32
|
+
|
|
33
|
+
Each instance is reachable at \`http://<node-ip>:<port>\` for every public port
|
|
34
|
+
in the spec. The network also serves \`https://<appname>.app.runonflux.io\`
|
|
35
|
+
(an alias for the first public port) and \`<appname>_<port>.app.runonflux.io\`
|
|
36
|
+
for every public port, load balanced across instances. Custom domains go in the component's
|
|
37
|
+
\`domains\` list, one per port, and need a CNAME to \`<appname>.app.runonflux.io\`.
|
|
38
|
+
|
|
39
|
+
## Rules of thumb
|
|
40
|
+
|
|
41
|
+
- Names: 1-63 chars, letters/digits/inner hyphens, must not start with "flux"
|
|
42
|
+
or "zel", unique network-wide.
|
|
43
|
+
- Public ports: pick from 31000-39999 unless you need a specific one; ports
|
|
44
|
+
0-1023, 8080, 8081, 8443 and 6667 are "enterprise ports" billed extra.
|
|
45
|
+
- Persisted paths must be prefixed \`r:\` (replicated) to survive instance moves.
|
|
46
|
+
- Images need an explicit tag and must be public unless \`repoauth\` is set.
|
|
47
|
+
- One network month is 88000 blocks (about 30.5 days). Terms run 1 block to
|
|
48
|
+
12 months; 3+ months earn 3 to 12 percent off.
|
|
49
|
+
- Flux sets only the container's Cmd, never Entrypoint.
|
|
50
|
+
`;
|
|
51
|
+
export const SPEC_FORMAT = `# Flux v8 application specification
|
|
52
|
+
|
|
53
|
+
\`\`\`json
|
|
54
|
+
{
|
|
55
|
+
"version": 8,
|
|
56
|
+
"name": "myapp",
|
|
57
|
+
"description": "What it is",
|
|
58
|
+
"owner": "1YourFluxIdAddress",
|
|
59
|
+
"compose": [
|
|
60
|
+
{
|
|
61
|
+
"name": "web",
|
|
62
|
+
"description": "Web front end",
|
|
63
|
+
"repotag": "nginx:1.27-alpine",
|
|
64
|
+
"ports": [31080],
|
|
65
|
+
"domains": [""],
|
|
66
|
+
"environmentParameters": ["KEY=value"],
|
|
67
|
+
"commands": [],
|
|
68
|
+
"containerPorts": [80],
|
|
69
|
+
"containerData": "r:/usr/share/nginx/html",
|
|
70
|
+
"cpu": 0.5,
|
|
71
|
+
"ram": 500,
|
|
72
|
+
"hdd": 5,
|
|
73
|
+
"repoauth": ""
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"instances": 3,
|
|
77
|
+
"contacts": [],
|
|
78
|
+
"geolocation": [],
|
|
79
|
+
"expire": 88000,
|
|
80
|
+
"nodes": [],
|
|
81
|
+
"staticip": false,
|
|
82
|
+
"enterprise": ""
|
|
83
|
+
}
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
Field notes:
|
|
87
|
+
|
|
88
|
+
- \`compose\`: 1-10 components. Each is one container. Components of one app
|
|
89
|
+
share a network and reach each other by component name.
|
|
90
|
+
- \`ports\` / \`containerPorts\` / \`domains\`: parallel arrays, one entry per
|
|
91
|
+
exposed port. \`domains\` entries are "" or a custom domain.
|
|
92
|
+
- \`environmentParameters\`: up to 20 "KEY=value" strings.
|
|
93
|
+
- \`commands\`: up to 20 strings, passed as the container Cmd.
|
|
94
|
+
- \`containerData\`: the path inside the container that persists. Prefixes:
|
|
95
|
+
\`r:\` replicate across instances (Syncthing), \`g:\` primary/standby
|
|
96
|
+
replication, \`s:\` sync only. Multiple paths: "r:/data|/config".
|
|
97
|
+
- \`cpu\`: 0.1 to 15 in 0.1 steps. \`ram\`: 100 to 59000 MB in 100 MB steps.
|
|
98
|
+
\`hdd\`: 1 to 820 GB whole numbers.
|
|
99
|
+
- \`instances\`: 1-100 copies, each on a different node.
|
|
100
|
+
- \`expire\`: term in blocks. 88000 = 1 month. Max 1056000 (12 months).
|
|
101
|
+
- \`geolocation\`: rules like "acEU" (allow continent EU), "acNA_US" (allow a
|
|
102
|
+
country), "a!cAS" (deny a continent), "acALL". Empty means anywhere.
|
|
103
|
+
- \`nodes\`: pin to specific node IPs (adds the scope surcharge).
|
|
104
|
+
- \`staticip\`: only nodes with a static IP (surcharge).
|
|
105
|
+
- \`enterprise\`: "" for a normal app. For a private app the components are
|
|
106
|
+
encrypted into this field and \`compose\` is published empty; only the node
|
|
107
|
+
running it can decrypt. Use \`enterprise\` in \`flux_deploy_app\`.
|
|
108
|
+
`;
|
|
109
|
+
export const PRICING = `# Flux Cloud pricing
|
|
110
|
+
|
|
111
|
+
Prices are set in US dollars and paid in FLUX. This server always quotes the
|
|
112
|
+
Flux Cloud USD price, converted at the live FLUX market rate.
|
|
113
|
+
|
|
114
|
+
Per month, per instance-third of the app's resource total (an app's resources
|
|
115
|
+
are priced as a single node's share; the per-instance figure is total / 3):
|
|
116
|
+
|
|
117
|
+
| Resource | USD / month |
|
|
118
|
+
|---|---|
|
|
119
|
+
| 0.1 CPU core | $0.15 |
|
|
120
|
+
| 100 MB RAM | $0.05 |
|
|
121
|
+
| 1 GB SSD | $0.02 |
|
|
122
|
+
| enterprise port (0-1023, 8080, 8081, 8443, 6667) | $2.00 each |
|
|
123
|
+
| node pinning or private (enterprise) app | $4.00 |
|
|
124
|
+
| static IP nodes | $2.00 |
|
|
125
|
+
|
|
126
|
+
Then: multiplied by the instance count; minimum $0.99 per app per month;
|
|
127
|
+
small apps (under 3 cores, 6 GB RAM, 150 GB and fewer than 4 instances) get
|
|
128
|
+
20 percent off, medium apps 10 percent off; primary/standby ("g:") storage 20
|
|
129
|
+
percent off; terms of 3, 6 and 9+ months get 3, 6 and 12 percent off.
|
|
130
|
+
|
|
131
|
+
Paying in FLUX earns a 5 percent discount, so
|
|
132
|
+
\`FLUX to pay = USD / market rate x 0.95\`.
|
|
133
|
+
|
|
134
|
+
\`flux_quote_app\` returns the authoritative figure from the network; the same
|
|
135
|
+
endpoint prices updates, crediting the unused part of the previous term.
|
|
136
|
+
|
|
137
|
+
Worked example: one component with 0.5 CPU, 500 MB, 5 GB, 3 instances, 1 month
|
|
138
|
+
= (0.75 + 0.25 + 0.10) / 3 = $0.37 per instance, x3 = $1.11, small-app
|
|
139
|
+
discount = $0.89, floored to $0.99 per month.
|
|
140
|
+
`;
|
|
141
|
+
export const GOTCHAS = `# Things that are easy to get wrong on Flux
|
|
142
|
+
|
|
143
|
+
- **Underpaying burns FLUX.** Nodes drop an underpaid message silently and the
|
|
144
|
+
payment is not refunded. This server pays the quoted amount and re-checks
|
|
145
|
+
the network's minimum immediately before broadcasting.
|
|
146
|
+
- **The message lives one hour.** Payment has to be broadcast within an hour of
|
|
147
|
+
registration; this server does both in one step.
|
|
148
|
+
- **Acceptance is not instant.** The payment needs a confirmation (about 30
|
|
149
|
+
seconds per block), then nodes pair it with the message. Expect 2-10
|
|
150
|
+
minutes, then image pull time.
|
|
151
|
+
- **Node-local data disappears.** Instances move when nodes churn. Persist
|
|
152
|
+
with \`r:\` and run 3+ instances if the data matters.
|
|
153
|
+
- **Entrypoint images.** Flux only sets Cmd, so an image whose ENTRYPOINT is a
|
|
154
|
+
binary cannot be given a shell command through \`commands\`.
|
|
155
|
+
- **Ports must be unique per node.** Two apps using the same public port
|
|
156
|
+
cannot share a node; pick unusual ports in 31000-39999.
|
|
157
|
+
- **Non-streamed HTTP through the shared domain times out at 25 s.** Long
|
|
158
|
+
requests must stream or use the direct \`ip:port\`.
|
|
159
|
+
- **Updates cost the difference.** The network credits the unused part of the
|
|
160
|
+
previous term; a mid-term update with the same resources is nearly free.
|
|
161
|
+
- **Enterprise apps need ArcaneOS nodes.** Only they can decrypt and validate
|
|
162
|
+
a private spec; this server selects one automatically.
|
|
163
|
+
`;
|
|
164
|
+
//# sourceMappingURL=docs.js.map
|
package/dist/docs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../src/docs.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDvB,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyD1B,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BtB,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBtB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise (private) specifications.
|
|
3
|
+
*
|
|
4
|
+
* A v8 enterprise app publishes an empty `compose` and carries the real
|
|
5
|
+
* components and contacts encrypted in the `enterprise` field. Layout, from
|
|
6
|
+
* enterpriseHelper.js decryptEnterpriseFromSession:
|
|
7
|
+
*
|
|
8
|
+
* base64( RSA-OAEP-SHA256(256 bytes)( base64 text of AES-256 key )
|
|
9
|
+
* || nonce(12) || AES-256-GCM ciphertext || tag(16) )
|
|
10
|
+
*
|
|
11
|
+
* The RSA block wraps the BASE64 TEXT of the AES key, not the raw bytes; raw
|
|
12
|
+
* bytes decrypt fine and then fail one step later as "Invalid key length".
|
|
13
|
+
*
|
|
14
|
+
* Only ArcaneOS nodes hold the private half, so only they can validate or
|
|
15
|
+
* accept an enterprise registration.
|
|
16
|
+
*/
|
|
17
|
+
import type { KeyObject } from 'node:crypto';
|
|
18
|
+
import type { FluxClient } from './fluxapi.js';
|
|
19
|
+
import type { Session } from './keys.js';
|
|
20
|
+
import type { AppComponent, AppSpec } from './spec.js';
|
|
21
|
+
export declare function buildEnterpriseBlob(publicKey: KeyObject, plaintextJson: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Encrypt `compose` and `contacts` into the envelope. The node's app public
|
|
24
|
+
* key is fetched through an owner session (a 'user' privileged endpoint).
|
|
25
|
+
*/
|
|
26
|
+
export declare function encryptEnterprise(node: FluxClient, session: Session, spec: AppSpec, plaintext: {
|
|
27
|
+
compose: AppComponent[];
|
|
28
|
+
contacts: string[];
|
|
29
|
+
}): Promise<AppSpec>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise (private) specifications.
|
|
3
|
+
*
|
|
4
|
+
* A v8 enterprise app publishes an empty `compose` and carries the real
|
|
5
|
+
* components and contacts encrypted in the `enterprise` field. Layout, from
|
|
6
|
+
* enterpriseHelper.js decryptEnterpriseFromSession:
|
|
7
|
+
*
|
|
8
|
+
* base64( RSA-OAEP-SHA256(256 bytes)( base64 text of AES-256 key )
|
|
9
|
+
* || nonce(12) || AES-256-GCM ciphertext || tag(16) )
|
|
10
|
+
*
|
|
11
|
+
* The RSA block wraps the BASE64 TEXT of the AES key, not the raw bytes; raw
|
|
12
|
+
* bytes decrypt fine and then fail one step later as "Invalid key length".
|
|
13
|
+
*
|
|
14
|
+
* Only ArcaneOS nodes hold the private half, so only they can validate or
|
|
15
|
+
* accept an enterprise registration.
|
|
16
|
+
*/
|
|
17
|
+
import { constants, createCipheriv, createPublicKey, publicEncrypt, randomBytes, } from 'node:crypto';
|
|
18
|
+
function toPublicKey(raw) {
|
|
19
|
+
const s = raw.trim();
|
|
20
|
+
if (s.includes('-----BEGIN'))
|
|
21
|
+
return createPublicKey(s);
|
|
22
|
+
return createPublicKey({ key: Buffer.from(s, 'base64'), format: 'der', type: 'spki' });
|
|
23
|
+
}
|
|
24
|
+
export function buildEnterpriseBlob(publicKey, plaintextJson) {
|
|
25
|
+
const aesKey = randomBytes(32);
|
|
26
|
+
const encryptedKey = publicEncrypt({ key: publicKey, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' }, Buffer.from(aesKey.toString('base64')));
|
|
27
|
+
if (encryptedKey.length !== 256) {
|
|
28
|
+
throw new Error(`Expected a 256-byte RSA block (RSA-2048), got ${encryptedKey.length}`);
|
|
29
|
+
}
|
|
30
|
+
const nonce = randomBytes(12);
|
|
31
|
+
const cipher = createCipheriv('aes-256-gcm', aesKey, nonce);
|
|
32
|
+
const body = Buffer.concat([cipher.update(plaintextJson, 'utf8'), cipher.final()]);
|
|
33
|
+
return Buffer.concat([encryptedKey, nonce, body, cipher.getAuthTag()]).toString('base64');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Encrypt `compose` and `contacts` into the envelope. The node's app public
|
|
37
|
+
* key is fetched through an owner session (a 'user' privileged endpoint).
|
|
38
|
+
*/
|
|
39
|
+
export async function encryptEnterprise(node, session, spec, plaintext) {
|
|
40
|
+
const raw = await node.post('/apps/getpublickey', { owner: spec.owner, name: spec.name }, { session, timeoutMs: 60000 });
|
|
41
|
+
const publicKey = toPublicKey(raw);
|
|
42
|
+
const blob = buildEnterpriseBlob(publicKey, JSON.stringify({ contacts: plaintext.contacts, compose: plaintext.compose }));
|
|
43
|
+
return { ...spec, contacts: [], compose: [], enterprise: blob };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=enterprise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enterprise.js","sourceRoot":"","sources":["../src/enterprise.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,SAAS,EACT,cAAc,EACd,eAAe,EACf,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAC;AAMrB,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACrB,IAAI,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AACzF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,SAAoB,EAAE,aAAqB;IAC7E,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC/B,MAAM,YAAY,GAAG,aAAa,CAChC,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,sBAAsB,EAAE,QAAQ,EAAE,QAAQ,EAAE,EACjF,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CACvC,CAAC;IACF,IAAI,YAAY,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,iDAAiD,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACnF,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5F,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAgB,EAChB,OAAgB,EAChB,IAAa,EACb,SAA0D;IAE1D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CACzB,oBAAoB,EACpB,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EACtC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAC9B,CAAC;IACF,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,mBAAmB,CAC9B,SAAS,EACT,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,CAAC,CAC7E,CAAC;IACF,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluxOS HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Every FluxOS response is `{ status: 'success' | 'error', data }`; `unwrap`
|
|
5
|
+
* turns the error shape into a thrown Error with the node's message.
|
|
6
|
+
*
|
|
7
|
+
* POST bodies are sent as text/plain, never application/json. FluxOS installs
|
|
8
|
+
* `express.json()` globally, but the handlers this server needs
|
|
9
|
+
* (/apps/appregister, /apps/appupdate, /apps/calculatefiatandfluxprice,
|
|
10
|
+
* /apps/verifyapp*specifications, /apps/getpublickey) read the raw request
|
|
11
|
+
* stream themselves. With a JSON content type the middleware has already
|
|
12
|
+
* consumed the stream, the handler's 'end' listener never fires and the
|
|
13
|
+
* request hangs until a gateway 504. The payload is still JSON text.
|
|
14
|
+
*/
|
|
15
|
+
import type { Session } from './keys.js';
|
|
16
|
+
export interface FluxResponse<T> {
|
|
17
|
+
status: 'success' | 'error';
|
|
18
|
+
data: T;
|
|
19
|
+
}
|
|
20
|
+
export declare class FluxApiError extends Error {
|
|
21
|
+
readonly endpoint: string;
|
|
22
|
+
constructor(message: string, endpoint: string);
|
|
23
|
+
}
|
|
24
|
+
export declare function unwrap<T>(payload: unknown, endpoint: string): T;
|
|
25
|
+
export declare function sessionHeader(session: Session): string;
|
|
26
|
+
export interface RequestOptions {
|
|
27
|
+
body?: unknown;
|
|
28
|
+
session?: Session | undefined;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class FluxClient {
|
|
32
|
+
readonly baseUrl: string;
|
|
33
|
+
private readonly defaultTimeoutMs;
|
|
34
|
+
constructor(baseUrl: string, defaultTimeoutMs?: number);
|
|
35
|
+
raw(method: 'GET' | 'POST', pathname: string, options?: RequestOptions): Promise<unknown>;
|
|
36
|
+
get<T>(pathname: string, options?: RequestOptions): Promise<T>;
|
|
37
|
+
post<T>(pathname: string, body: unknown, options?: RequestOptions): Promise<T>;
|
|
38
|
+
}
|
|
39
|
+
export interface NodeCandidate {
|
|
40
|
+
endpoint: string;
|
|
41
|
+
ip: string;
|
|
42
|
+
tier: string;
|
|
43
|
+
}
|
|
44
|
+
export interface NodeHealth {
|
|
45
|
+
outgoing: number;
|
|
46
|
+
incoming: number;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
arcaneVersion: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
/** FluxOS refuses registrations on nodes with fewer peers than this. */
|
|
51
|
+
export declare const MIN_OUTGOING_PEERS = 8;
|
|
52
|
+
export declare const MIN_INCOMING_PEERS = 4;
|
|
53
|
+
export declare function listNodeEndpoints(api: FluxClient): Promise<NodeCandidate[]>;
|
|
54
|
+
export declare function nodeHealth(node: FluxClient): Promise<NodeHealth>;
|
|
55
|
+
export interface HealthyNode extends NodeCandidate {
|
|
56
|
+
client: FluxClient;
|
|
57
|
+
health: NodeHealth;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Probe random nodes until `count` of them meet the peer thresholds. With
|
|
61
|
+
* `arcane` only ArcaneOS nodes qualify: they alone hold the key that
|
|
62
|
+
* decrypts enterprise specifications, so only they can validate or accept one.
|
|
63
|
+
*/
|
|
64
|
+
export declare function findHealthyNodes(api: FluxClient, count: number, options?: {
|
|
65
|
+
arcane?: boolean;
|
|
66
|
+
batchSize?: number;
|
|
67
|
+
maxProbes?: number;
|
|
68
|
+
log?: (m: string) => void;
|
|
69
|
+
}): Promise<HealthyNode[]>;
|
|
70
|
+
/** `http://ip:port` for an app instance reported by /apps/location. */
|
|
71
|
+
export declare function instanceEndpoint(ip: string): string;
|