@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InFlux Technologies Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # Flux Cloud MCP server
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that lets any AI agent use
4
+ [Flux Cloud](https://runonflux.com), the decentralized cloud: quote an app in
5
+ US dollars, deploy it, pay for it, watch it come up, read its logs, update it,
6
+ and cancel it. Works with Claude Code, Claude Desktop, Cursor, Windsurf,
7
+ OpenCode and every other MCP host.
8
+
9
+ There is no account. Two keys are the whole identity:
10
+
11
+ | Key | Address | Role |
12
+ | -------------------------- | --------------------- | -------------------------------------------------------------------- |
13
+ | `FLUX_ID_PRIVATE_KEY` | Flux ID, `1...` | Owns apps. Signs specifications and API sessions. Never holds funds. |
14
+ | `FLUX_PAYMENT_PRIVATE_KEY` | Flux address, `t1...` | Holds FLUX and pays deployment fees on-chain. |
15
+
16
+ The server generates a pair for you (`flux_generate_keys`); fund the payment
17
+ address with FLUX and deploy.
18
+
19
+ ## Pricing policy
20
+
21
+ Every price this server shows is the **Flux Cloud USD price**, the same one
22
+ [home.runonflux.io](https://home.runonflux.io) charges, converted to FLUX at the
23
+ live market rate with the 5 percent pay-in-FLUX discount. Quotes come from the
24
+ network itself (`/apps/calculatefiatandfluxprice`), so they include the $0.99
25
+ minimum, hardware and term discounts, and update credits.
26
+
27
+ The blockchain's own acceptance threshold is several times lower. It is never
28
+ shown as a price; it is only checked as a guard before a payment is broadcast.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install -g @runonflux/flux-cloud-mcp # or: yarn global add @runonflux/flux-cloud-mcp
34
+ ```
35
+
36
+ Or run it without installing: `npx @runonflux/flux-cloud-mcp`.
37
+
38
+ ### Claude Code
39
+
40
+ ```bash
41
+ claude mcp add flux-cloud -s user \
42
+ -e FLUX_ID_PRIVATE_KEY=<wif> -e FLUX_PAYMENT_PRIVATE_KEY=<wif> \
43
+ -- npx -y @runonflux/flux-cloud-mcp
44
+ ```
45
+
46
+ ### Claude Desktop, Cursor, Windsurf (JSON config)
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "flux-cloud": {
52
+ "command": "npx",
53
+ "args": ["-y", "@runonflux/flux-cloud-mcp"],
54
+ "env": {
55
+ "FLUX_ID_PRIVATE_KEY": "<wif of the Flux ID>",
56
+ "FLUX_PAYMENT_PRIVATE_KEY": "<wif of the payment address>"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Keys are optional. Without them every read-only tool works, and
64
+ `flux_generate_keys` will create a pair to put into the config.
65
+
66
+ ### OpenCode
67
+
68
+ ```json
69
+ {
70
+ "mcp": {
71
+ "flux-cloud": {
72
+ "type": "local",
73
+ "command": ["npx", "-y", "@runonflux/flux-cloud-mcp"],
74
+ "environment": {
75
+ "FLUX_ID_PRIVATE_KEY": "<wif>",
76
+ "FLUX_PAYMENT_PRIVATE_KEY": "<wif>"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Tools
84
+
85
+ | Tool | What it does | Spends FLUX |
86
+ | ----------------------- | ------------------------------------------------------------------------ | -------------- |
87
+ | `flux_get_identity` | Flux ID, payment address, balance in FLUX and USD | no |
88
+ | `flux_generate_keys` | New Flux ID + payment key pair with setup instructions | no |
89
+ | `flux_get_pricing` | USD rate card, FLUX/USD rate, discounts, reference sizes | no |
90
+ | `flux_build_spec` | Simple description (image, ports, cpu/ram/hdd, months) to a full v8 spec | no |
91
+ | `flux_validate_spec` | Local rules plus verification on a FluxOS node | no |
92
+ | `flux_quote_app` | USD price and FLUX to pay, for a registration or an update | no |
93
+ | `flux_deploy_app` | Plan (default) or, with `confirm=true`, sign, broadcast and pay | with `confirm` |
94
+ | `flux_wait_for_app` | Poll until accepted and instances run; returns URLs | no |
95
+ | `flux_get_app` | Any app's spec, expiry, instances, URLs | no |
96
+ | `flux_list_my_apps` | Apps owned by the Flux ID, with instance counts and days left | no |
97
+ | `flux_get_app_logs` | Container logs from a running instance (owner only) | no |
98
+ | `flux_get_app_stats` | Live CPU/memory/network of an instance (owner only) | no |
99
+ | `flux_control_app` | Restart, redeploy or remove instances, per node or globally | no |
100
+ | `flux_cancel_app` | End an app early by shortening its term | with `confirm` |
101
+ | `flux_get_network_info` | Node counts, height, FLUX/USD, deployment address | no |
102
+
103
+ Resources `flux://guide/overview`, `flux://guide/spec-format`,
104
+ `flux://guide/pricing` and `flux://guide/gotchas` give the agent the domain
105
+ knowledge; the `deploy_on_flux` prompt walks it through a deployment.
106
+
107
+ ## A typical session
108
+
109
+ ```
110
+ > deploy nginx on flux, 3 instances, one month
111
+
112
+ flux_get_identity -> Flux ID 1Ab..., payer t1Cd..., 996.98 FLUX ($48.74)
113
+ flux_build_spec -> v8 spec, port 39978 -> 80, r:/data, valid
114
+ flux_quote_app -> $0.99 for 1 month, pay 19.24 FLUX (5% FLUX discount)
115
+ user agrees
116
+ flux_deploy_app confirm=true
117
+ -> message hash 8d2f..., paid 19.24 FLUX, txid 0c41...
118
+ flux_wait_for_app -> accepted at height 2941560, 3/3 instances running
119
+ https://myapp.app.runonflux.io
120
+ ```
121
+
122
+ ## How a deployment works
123
+
124
+ 1. The spec is verified by a FluxOS node exactly as it will be at
125
+ registration (image reachable, architecture, ports, name free).
126
+ 2. The network quotes the USD price; the server checks the payer's balance.
127
+ 3. With `confirm=true` the Flux ID signs the spec, one node broadcasts it to
128
+ the network and returns a 64-character message hash.
129
+ 4. The payment address sends the quoted FLUX to the network deployment
130
+ address with the hash in an OP_RETURN output. Right before signing, the
131
+ amount is re-checked against a freshly fetched price table, because an
132
+ underpaid message is dropped silently and the FLUX is not refunded.
133
+ 5. Nodes pair the confirmed payment with the message and publish the app.
134
+ Instances then spawn and pull the image.
135
+
136
+ Updates use the same tool: an existing app of the same owner gets a
137
+ `fluxappupdate` message and the network credits the unused part of the old
138
+ term. Private (enterprise) apps are supported through the `enterprise`
139
+ argument: components are encrypted for the network and only ArcaneOS nodes
140
+ can run them.
141
+
142
+ ## Configuration
143
+
144
+ | Variable | Default | Purpose |
145
+ | -------------------------- | -------------------------------------------------- | --------------------------------------------- |
146
+ | `FLUX_ID_PRIVATE_KEY` | | WIF of the Flux ID (owner) |
147
+ | `FLUX_PAYMENT_PRIVATE_KEY` | | WIF of the paying address |
148
+ | `FLUX_API_URL` | `https://api.runonflux.io` | Load-balanced FluxOS API for reads and quotes |
149
+ | `FLUX_NODE_URL` | auto | Pin one FluxOS node for registrations |
150
+ | `FLUX_EXPLORER_URLS` | `explorer.runonflux.io,flux-explorer.sspwallet.io` | Insight explorers for UTXOs and broadcast |
151
+ | `FLUX_STATS_URL` | `https://stats.runonflux.io` | USD rate card |
152
+ | `FLUX_RATES_URL` | `https://viprates.runonflux.io` | FLUX market rate |
153
+
154
+ ## Security notes
155
+
156
+ - Private keys stay in the server process. No tool ever returns a configured
157
+ key; `flux_generate_keys` returns only the keys it just created.
158
+ - Nothing is spent unless a tool is called with `confirm=true`. Hosts that
159
+ ask before tool calls will show the USD amount in the arguments' context.
160
+ - FluxOS sessions are self-issued signed phrases valid for a few hours; no
161
+ password or token is stored anywhere.
162
+ - Use a dedicated payment address holding only what you intend to spend.
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ yarn install
168
+ yarn type-check && yarn lint && yarn format:check && yarn build && yarn test
169
+ yarn test:live # differential test of the USD estimator against the network
170
+ ```
171
+
172
+ The USD estimator, the specification formatter and the consensus guard mirror
173
+ the FluxOS code (`appSpecHelpers.js`, `appUtilities.js`, `messageVerifier.js`)
174
+ and the live test checks the estimator against the network on every run.
175
+
176
+ ## License
177
+
178
+ MIT
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Flux blockchain access over the public Insight explorer API, plus
3
+ * construction of the deployment payment.
4
+ *
5
+ * A registration payment is an ordinary transparent transaction with two
6
+ * outputs that matter: `amount` FLUX to the network deployment address, and an
7
+ * OP_RETURN carrying the 64-character message hash. Nodes index exactly that
8
+ * pair; nothing else about the payer is consulted, which is why the payment
9
+ * key and the owner Flux ID are independent.
10
+ */
11
+ export declare const DUST_LIMIT = 546;
12
+ export declare const SATOSHIS = 100000000;
13
+ export interface Utxo {
14
+ txid: string;
15
+ vout: number;
16
+ satoshis: number;
17
+ confirmations: number;
18
+ coinbase: boolean;
19
+ }
20
+ export interface Balance {
21
+ total: number;
22
+ spendable: number;
23
+ utxos: Utxo[];
24
+ spendableUtxos: Utxo[];
25
+ }
26
+ export declare function toFlux(satoshis: number): number;
27
+ export declare class Explorer {
28
+ private readonly urls;
29
+ private readonly timeoutMs;
30
+ constructor(urls: string[], timeoutMs?: number);
31
+ private request;
32
+ height(): Promise<number>;
33
+ utxos(address: string): Promise<Utxo[]>;
34
+ balance(address: string): Promise<Balance>;
35
+ broadcast(rawtx: string): Promise<string>;
36
+ transaction(txid: string): Promise<{
37
+ confirmations?: number;
38
+ blockheight?: number;
39
+ }>;
40
+ }
41
+ /** Coinbase outputs are unspendable until mature; unconfirmed inputs are avoided. */
42
+ export declare function selectSpendable(utxos: Utxo[]): Utxo[];
43
+ /** 148 bytes per p2pkh input, 34 per standard output, 10 overhead, plus the OP_RETURN. */
44
+ export declare function estimateSize(inputCount: number, outputCount: number, opReturnBytes: number): number;
45
+ export interface InputSelection {
46
+ inputs: Utxo[];
47
+ total: number;
48
+ fee: number;
49
+ change: number;
50
+ }
51
+ /** Greedily pick the fewest inputs covering amount + fee, recomputing the fee as inputs are added. */
52
+ export declare function selectInputs(utxos: Utxo[], amountSat: number, opReturnBytes: number): InputSelection;
53
+ export interface PaymentParams {
54
+ wif: string;
55
+ to: string;
56
+ amountSat: number;
57
+ message: string;
58
+ utxos: Utxo[];
59
+ height: number;
60
+ maxFeeSat?: number;
61
+ }
62
+ export interface SignedPayment {
63
+ hex: string;
64
+ txid: string;
65
+ fee: number;
66
+ change: number;
67
+ inputs: number;
68
+ sizeBytes: number;
69
+ }
70
+ /** Build and sign the deployment payment. */
71
+ export declare function buildPayment({ wif, to, amountSat, message, utxos, height, maxFeeSat, }: PaymentParams): SignedPayment;
package/dist/chain.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Flux blockchain access over the public Insight explorer API, plus
3
+ * construction of the deployment payment.
4
+ *
5
+ * A registration payment is an ordinary transparent transaction with two
6
+ * outputs that matter: `amount` FLUX to the network deployment address, and an
7
+ * OP_RETURN carrying the 64-character message hash. Nodes index exactly that
8
+ * pair; nothing else about the payer is consulted, which is why the payment
9
+ * key and the owner Flux ID are independent.
10
+ */
11
+ import utxolib from '@runonflux/utxo-lib';
12
+ const NETWORK = utxolib.networks.flux;
13
+ const TX_VERSION = 4; // sapling
14
+ const TX_VERSION_GROUP_ID = 0x892f2085;
15
+ const TX_EXPIRY_DELTA = 30; // blocks
16
+ export const DUST_LIMIT = 546;
17
+ const FEE_PER_BYTE = 1;
18
+ const COINBASE_MATURITY = 100;
19
+ export const SATOSHIS = 1e8;
20
+ export function toFlux(satoshis) {
21
+ return Number((satoshis / SATOSHIS).toFixed(8));
22
+ }
23
+ export class Explorer {
24
+ urls;
25
+ timeoutMs;
26
+ constructor(urls, timeoutMs = 30000) {
27
+ this.urls = urls;
28
+ this.timeoutMs = timeoutMs;
29
+ }
30
+ async request(method, pathname, body) {
31
+ let lastError;
32
+ for (const base of this.urls) {
33
+ try {
34
+ const response = await fetch(`${base}${pathname}`, {
35
+ method,
36
+ headers: body === undefined ? {} : { 'Content-Type': 'application/json' },
37
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
38
+ signal: AbortSignal.timeout(this.timeoutMs),
39
+ });
40
+ const text = await response.text();
41
+ if (!response.ok)
42
+ throw new Error(`HTTP ${response.status} ${text.slice(0, 200)}`);
43
+ return JSON.parse(text);
44
+ }
45
+ catch (error) {
46
+ lastError = new Error(`${base}${pathname}: ${error.message}`);
47
+ }
48
+ }
49
+ throw lastError ?? new Error('No explorer configured');
50
+ }
51
+ async height() {
52
+ const status = (await this.request('GET', '/api/sync'));
53
+ const height = Number(status.blockChainHeight ?? status.height);
54
+ if (!Number.isFinite(height))
55
+ throw new Error('Explorer returned no block height');
56
+ return height;
57
+ }
58
+ async utxos(address) {
59
+ const list = (await this.request('GET', `/api/addr/${address}/utxo`));
60
+ return (Array.isArray(list) ? list : []).map((u) => ({
61
+ txid: String(u.txid),
62
+ vout: Number(u.vout),
63
+ satoshis: Number(u.satoshis),
64
+ confirmations: Number(u.confirmations ?? 0),
65
+ coinbase: Boolean(u.coinbase),
66
+ }));
67
+ }
68
+ async balance(address) {
69
+ const utxos = await this.utxos(address);
70
+ const spendable = selectSpendable(utxos);
71
+ const sum = (list) => list.reduce((acc, u) => acc + u.satoshis, 0);
72
+ return { total: sum(utxos), spendable: sum(spendable), utxos, spendableUtxos: spendable };
73
+ }
74
+ async broadcast(rawtx) {
75
+ const result = (await this.request('POST', '/api/tx/send', { rawtx }));
76
+ const txid = typeof result?.txid === 'object' ? result.txid?.result : (result?.txid ?? result?.result);
77
+ if (!txid)
78
+ throw new Error(`Broadcast returned no txid: ${JSON.stringify(result)}`);
79
+ return String(txid);
80
+ }
81
+ async transaction(txid) {
82
+ return (await this.request('GET', `/api/tx/${txid}`));
83
+ }
84
+ }
85
+ /** Coinbase outputs are unspendable until mature; unconfirmed inputs are avoided. */
86
+ export function selectSpendable(utxos) {
87
+ return utxos.filter((u) => {
88
+ if (u.coinbase && u.confirmations < COINBASE_MATURITY)
89
+ return false;
90
+ return u.confirmations >= 1;
91
+ });
92
+ }
93
+ /** 148 bytes per p2pkh input, 34 per standard output, 10 overhead, plus the OP_RETURN. */
94
+ export function estimateSize(inputCount, outputCount, opReturnBytes) {
95
+ const opReturn = opReturnBytes ? 9 + opReturnBytes + 2 : 0;
96
+ return inputCount * 148 + outputCount * 34 + 10 + opReturn;
97
+ }
98
+ /** Greedily pick the fewest inputs covering amount + fee, recomputing the fee as inputs are added. */
99
+ export function selectInputs(utxos, amountSat, opReturnBytes) {
100
+ const sorted = [...utxos].sort((a, b) => b.satoshis - a.satoshis);
101
+ const chosen = [];
102
+ let total = 0;
103
+ for (const utxo of sorted) {
104
+ chosen.push(utxo);
105
+ total += utxo.satoshis;
106
+ const withChange = estimateSize(chosen.length, 2, opReturnBytes) * FEE_PER_BYTE;
107
+ const withoutChange = estimateSize(chosen.length, 1, opReturnBytes) * FEE_PER_BYTE;
108
+ if (total >= amountSat + withChange) {
109
+ return { inputs: chosen, total, fee: withChange, change: total - amountSat - withChange };
110
+ }
111
+ if (total >= amountSat + withoutChange && total - amountSat - withoutChange < DUST_LIMIT) {
112
+ return { inputs: chosen, total, fee: total - amountSat, change: 0 };
113
+ }
114
+ }
115
+ const needed = amountSat + estimateSize(Math.max(chosen.length, 1), 2, opReturnBytes) * FEE_PER_BYTE;
116
+ throw new Error(`Insufficient funds: have ${toFlux(total)} FLUX spendable, need about ${toFlux(needed)} FLUX`);
117
+ }
118
+ /** Build and sign the deployment payment. */
119
+ export function buildPayment({ wif, to, amountSat, message, utxos, height, maxFeeSat = 1000000, }) {
120
+ const keyPair = utxolib.ECPair.fromWIF(wif, NETWORK);
121
+ const changeAddress = keyPair.getAddress();
122
+ const opReturnBytes = Buffer.byteLength(message, 'utf8');
123
+ if (opReturnBytes > 80)
124
+ throw new Error('OP_RETURN payload exceeds 80 bytes');
125
+ const { inputs, fee, change } = selectInputs(utxos, amountSat, opReturnBytes);
126
+ if (fee > maxFeeSat)
127
+ throw new Error(`Refusing to sign: fee ${fee} sat exceeds cap ${maxFeeSat} sat`);
128
+ const builder = new utxolib.TransactionBuilder(NETWORK, fee);
129
+ builder.setVersion(TX_VERSION);
130
+ builder.setVersionGroupId(TX_VERSION_GROUP_ID);
131
+ builder.setExpiryHeight(height + TX_EXPIRY_DELTA);
132
+ inputs.forEach((utxo) => builder.addInput(utxo.txid, utxo.vout));
133
+ builder.addOutput(to, amountSat);
134
+ if (change >= DUST_LIMIT)
135
+ builder.addOutput(changeAddress, change);
136
+ builder.addOutput(utxolib.script.nullData.output.encode(Buffer.from(message, 'utf8')), 0);
137
+ inputs.forEach((utxo, index) => {
138
+ builder.sign(index, keyPair, undefined, utxolib.Transaction.SIGHASH_ALL, utxo.satoshis);
139
+ });
140
+ const tx = builder.build();
141
+ const hex = tx.toHex();
142
+ return { hex, txid: tx.getId(), fee, change, inputs: inputs.length, sizeBytes: hex.length / 2 };
143
+ }
144
+ //# sourceMappingURL=chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.js","sourceRoot":"","sources":["../src/chain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Runtime configuration, read from the environment the MCP host passes in.
3
+ *
4
+ * Two independent keys are used, exactly as the Flux network models them:
5
+ *
6
+ * FLUX_ID_PRIVATE_KEY WIF of the Flux ID (ZelID). It is the app owner:
7
+ * it signs registration messages and API sessions.
8
+ * It never holds funds.
9
+ * FLUX_PAYMENT_PRIVATE_KEY WIF of a Flux transparent address (t1...). It
10
+ * holds FLUX and pays deployment fees on-chain.
11
+ *
12
+ * Both are optional: without them every read-only tool still works, and the
13
+ * server can generate a fresh pair for the user.
14
+ */
15
+ export interface Config {
16
+ ownerWif: string | undefined;
17
+ payerWif: string | undefined;
18
+ /** Load-balanced FluxOS API, fine for reads and price quotes. */
19
+ apiUrl: string;
20
+ /** A specific FluxOS node to use for sessions and registrations, if pinned. */
21
+ nodeUrl: string | undefined;
22
+ explorerUrls: string[];
23
+ statsUrl: string;
24
+ ratesUrl: string;
25
+ /** Deployment address override, only for testing. */
26
+ requestTimeoutMs: number;
27
+ }
28
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
package/dist/config.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Runtime configuration, read from the environment the MCP host passes in.
3
+ *
4
+ * Two independent keys are used, exactly as the Flux network models them:
5
+ *
6
+ * FLUX_ID_PRIVATE_KEY WIF of the Flux ID (ZelID). It is the app owner:
7
+ * it signs registration messages and API sessions.
8
+ * It never holds funds.
9
+ * FLUX_PAYMENT_PRIVATE_KEY WIF of a Flux transparent address (t1...). It
10
+ * holds FLUX and pays deployment fees on-chain.
11
+ *
12
+ * Both are optional: without them every read-only tool still works, and the
13
+ * server can generate a fresh pair for the user.
14
+ */
15
+ const DEFAULT_EXPLORERS = ['https://explorer.runonflux.io', 'https://flux-explorer.sspwallet.io'];
16
+ function optional(value) {
17
+ const trimmed = value?.trim();
18
+ return trimmed ? trimmed : undefined;
19
+ }
20
+ export function loadConfig(env = process.env) {
21
+ const explorers = optional(env.FLUX_EXPLORER_URLS)
22
+ ?.split(',')
23
+ .map((url) => url.trim().replace(/\/$/, ''))
24
+ .filter(Boolean);
25
+ return {
26
+ ownerWif: optional(env.FLUX_ID_PRIVATE_KEY),
27
+ payerWif: optional(env.FLUX_PAYMENT_PRIVATE_KEY),
28
+ apiUrl: (optional(env.FLUX_API_URL) ?? 'https://api.runonflux.io').replace(/\/$/, ''),
29
+ nodeUrl: optional(env.FLUX_NODE_URL)?.replace(/\/$/, ''),
30
+ explorerUrls: explorers && explorers.length ? explorers : DEFAULT_EXPLORERS,
31
+ statsUrl: (optional(env.FLUX_STATS_URL) ?? 'https://stats.runonflux.io').replace(/\/$/, ''),
32
+ ratesUrl: (optional(env.FLUX_RATES_URL) ?? 'https://viprates.runonflux.io').replace(/\/$/, ''),
33
+ requestTimeoutMs: Number(optional(env.FLUX_REQUEST_TIMEOUT_MS) ?? 60000),
34
+ };
35
+ }
36
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAgBH,MAAM,iBAAiB,GAAG,CAAC,+BAA+B,EAAE,oCAAoC,CAAC,CAAC;AAElG,SAAS,QAAQ,CAAC,KAAyB;IACzC,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAChD,EAAE,KAAK,CAAC,GAAG,CAAC;SACX,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;SAC3C,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,mBAAmB,CAAC;QAC3C,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,wBAAwB,CAAC;QAChD,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,0BAA0B,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QACrF,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QACxD,YAAY,EAAE,SAAS,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB;QAC3E,QAAQ,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,4BAA4B,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3F,QAAQ,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,+BAA+B,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC9F,gBAAgB,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,uBAAuB,CAAC,IAAI,KAAK,CAAC;KACzE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,111 @@
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 type { Config } from './config.js';
18
+ import { Explorer } from './chain.js';
19
+ import { FluxClient } from './fluxapi.js';
20
+ import { type Identity } from './keys.js';
21
+ import { type DeploymentInformation, type Quote } from './pricing.js';
22
+ import { type AppComponent, type AppSpec, type PublishedAppSpec, type SpecInput } from './spec.js';
23
+ export interface Wallet {
24
+ owner: Identity;
25
+ payer: Identity;
26
+ }
27
+ export declare function walletFromConfig(config: Config): Wallet;
28
+ export declare function api(config: Config): FluxClient;
29
+ export declare function explorer(config: Config): Explorer;
30
+ export declare function publishedSpec(client: FluxClient, name: string): Promise<PublishedAppSpec | null>;
31
+ export interface Location {
32
+ ip: string;
33
+ name: string;
34
+ broadcastedAt?: string;
35
+ expireAt?: string;
36
+ hash?: string;
37
+ runningSince?: string;
38
+ osUptime?: number;
39
+ staticIp?: boolean;
40
+ }
41
+ export declare function appLocations(client: FluxClient, name: string): Promise<Location[]>;
42
+ /**
43
+ * Pick a node that can take the registration: enough peers, ArcaneOS when
44
+ * the spec is enterprise, and honouring a pinned FLUX_NODE_URL.
45
+ */
46
+ export declare function selectNode(config: Config, { arcane, log }?: {
47
+ arcane?: boolean;
48
+ log?: (m: string) => void;
49
+ }): Promise<FluxClient>;
50
+ export interface EnterpriseInput {
51
+ compose: AppComponent[];
52
+ contacts: string[];
53
+ }
54
+ export interface Plan {
55
+ action: 'register' | 'update';
56
+ spec: AppSpec;
57
+ /** The node's own formatting of the spec: this is what gets signed. */
58
+ formatted: AppSpec;
59
+ previous: PublishedAppSpec | null;
60
+ quote: Quote;
61
+ consensusMinimumFlux: number;
62
+ deployment: DeploymentInformation;
63
+ height: number;
64
+ payer: string;
65
+ balanceFlux: number;
66
+ requiredFlux: number;
67
+ funded: boolean;
68
+ warnings: string[];
69
+ node: FluxClient;
70
+ }
71
+ /**
72
+ * Everything needed to decide whether a deployment can proceed, without
73
+ * signing, spending or broadcasting anything.
74
+ */
75
+ export declare function plan(config: Config, input: SpecInput & {
76
+ name: string;
77
+ }, options?: {
78
+ enterprise?: EnterpriseInput | undefined;
79
+ log?: (m: string) => void;
80
+ }): Promise<Plan>;
81
+ export interface DeployResult {
82
+ action: 'register' | 'update';
83
+ name: string;
84
+ messageHash: string;
85
+ messageExpiresAt: string;
86
+ txid: string;
87
+ paidFlux: number;
88
+ paidUsd: number;
89
+ feeFlux: number;
90
+ deploymentAddress: string;
91
+ }
92
+ /** Sign, broadcast the message, then pay for it. Spends FLUX. */
93
+ export declare function execute(config: Config, prepared: Plan, log?: (m: string) => void): Promise<DeployResult>;
94
+ export interface WaitResult {
95
+ accepted: PublishedAppSpec | null;
96
+ paymentConfirmations: number;
97
+ instances: Location[];
98
+ wantedInstances: number;
99
+ done: boolean;
100
+ timedOut: boolean;
101
+ }
102
+ /**
103
+ * Poll until the message is accepted and instances run, within `timeoutMs`.
104
+ * Returns partial progress on timeout so a caller can simply call again.
105
+ */
106
+ export declare function waitForApp(config: Config, name: string, options?: {
107
+ txid?: string | undefined;
108
+ previousHash?: string | undefined;
109
+ timeoutMs?: number;
110
+ pollMs?: number;
111
+ }): Promise<WaitResult>;