@paydlux/agent 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/agent.d.ts +8 -0
- package/dist/agent.js +1 -1
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +165 -8
- package/dist/desk.d.ts +50 -0
- package/dist/desk.js +215 -56
- package/dist/gateway.d.ts +10 -2
- package/dist/gateway.js +20 -1
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +2 -2
- package/package.json +10 -2
- package/phone-app/app.js +653 -224
- package/phone-app/icon-192.png +0 -0
- package/phone-app/icon-512.png +0 -0
- package/phone-app/index.html +195 -53
- package/phone-app/manifest.webmanifest +19 -0
- package/phone-app/xdr.js +45 -5
package/README.md
CHANGED
|
@@ -84,11 +84,17 @@ one key. So the approval half is split off:
|
|
|
84
84
|
npx tsx stellar/phone-check.ts # 17 checks: seller alone cannot pay; phone alone cannot pay
|
|
85
85
|
npx tsx stellar/e2e-phone.ts # desk with no approver key still settles
|
|
86
86
|
npx tsx phone-app/xdr-check.ts # 32 checks: the phone's own decoder is byte-exact with the SDK
|
|
87
|
-
npx tsx phone-app/desk-app-check.ts #
|
|
87
|
+
npx tsx phone-app/desk-app-check.ts # 57 checks: the real app over real HTTP, end to end
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
The seller builds the payment and signs it with their own half. The human opens
|
|
91
|
-
one link — `/phone/?
|
|
91
|
+
one link — `/phone/?k=...` — and the approver app in `phone-app/` shows them the
|
|
92
|
+
`https://desk.paydlux.com/phone/` link opens an installable app: it has a manifest
|
|
93
|
+
and icons, so a phone can put it on the home screen and it runs full-screen. The
|
|
94
|
+
shell (html, js, icons) is public because it holds no secret and no payment data
|
|
95
|
+
and must load before the key does; every route that returns data or moves money
|
|
96
|
+
(`/queue`, `/challenge`, `/approve`, `/signed`, `/deny`) still answers 403 without
|
|
97
|
+
the portal key.
|
|
92
98
|
amount, the token, the payee and the deadline, then signs on the device.
|
|
93
99
|
|
|
94
100
|
The app is three static files with no dependencies and no build step. It carries
|
package/dist/agent.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export type PayResponse<T = string> = {
|
|
|
19
19
|
tx?: string;
|
|
20
20
|
/** set when a human has to approve */
|
|
21
21
|
handle?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Why a payment was refused, as the seller named it: "mandate_refused" (the
|
|
24
|
+
* seller's spending ceiling — retrying the same request can never work) or
|
|
25
|
+
* "payment_denied" (a human said no to this one). Without the distinction an
|
|
26
|
+
* agent retries a refused payment forever, which is exactly what a polite
|
|
27
|
+
* buyer does.
|
|
28
|
+
*/
|
|
29
|
+
error?: string;
|
|
22
30
|
/** where a human approves (the seller's desk) */
|
|
23
31
|
desk?: string;
|
|
24
32
|
/** where THIS buyer polls for the answer (the gateway, no key needed) */
|
package/dist/agent.js
CHANGED
|
@@ -46,7 +46,7 @@ export class PaydluxAgent {
|
|
|
46
46
|
if (parked.status === 200) {
|
|
47
47
|
return { ok: true, status: 200, data: (j.data ?? j), tx: j.tx };
|
|
48
48
|
}
|
|
49
|
-
return { ok: false, status: parked.status, handle: j.handle, desk: j.desk,
|
|
49
|
+
return { ok: false, status: parked.status, handle: j.handle, error: j.error, desk: j.desk,
|
|
50
50
|
statusUrl: j.statusUrl, requirements: reqs,
|
|
51
51
|
reason: j.reason ?? "a human has to approve this payment" };
|
|
52
52
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { StellarVault } from "./vault.ts";
|
|
2
3
|
export declare const VAULT_FILE_DEFAULT = "paydlux-vault.json";
|
|
3
4
|
/** The one file that holds everything. Refuses to read a file that is world-readable. */
|
|
4
5
|
export declare function loadVault(path?: string): {
|
|
5
6
|
path: string;
|
|
6
7
|
data: any;
|
|
7
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Persist the portal key so the phone link survives a restart. A new key every
|
|
11
|
+
* boot means the human re-scans a QR every time and any link already opened on
|
|
12
|
+
* a phone stops working mid-payment.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deskKeys(path: string, data: any): string;
|
|
15
|
+
/**
|
|
16
|
+
* Where the money lands.
|
|
17
|
+
*
|
|
18
|
+
* `setup` creates a payee account for you. Two cases break that: a vault made
|
|
19
|
+
* before the payee existed, and a seller who wants the money in their own
|
|
20
|
+
* account (a Binance deposit address, an exchange wallet). Both are handled
|
|
21
|
+
* here, because a payment to an account that does not exist — or that does not
|
|
22
|
+
* trust the token — fails on the ledger with a code the seller never sees.
|
|
23
|
+
*
|
|
24
|
+
* Adding a trustline needs the payee's own key, so for an outside account we can
|
|
25
|
+
* only check and explain. For a payee we generated, we do it ourselves.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ensurePayee(path: string, data: any, vault: StellarVault): Promise<any>;
|
|
8
28
|
export declare function main(): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -17,8 +17,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, chmodSync, realpathSync } from "node:fs";
|
|
18
18
|
import { pathToFileURL } from "node:url";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
|
-
import { Keypair } from "@stellar/stellar-sdk";
|
|
21
|
-
import { StellarVault, USDC_ISSUER } from "./vault.js";
|
|
20
|
+
import { Account, Keypair, Operation, TransactionBuilder } from "@stellar/stellar-sdk";
|
|
21
|
+
import { StellarVault, USDC_ISSUER, passphrase } from "./vault.js";
|
|
22
22
|
import { Gateway } from "./gateway.js";
|
|
23
23
|
import { newPortalKey } from "./desk.js";
|
|
24
24
|
import { PolicyEngine } from "./policy.js";
|
|
@@ -49,7 +49,7 @@ function pub(seed) { return Keypair.fromSecret(seed).publicKey(); }
|
|
|
49
49
|
* boot means the human re-scans a QR every time and any link already opened on
|
|
50
50
|
* a phone stops working mid-payment.
|
|
51
51
|
*/
|
|
52
|
-
function deskKeys(path, data) {
|
|
52
|
+
export function deskKeys(path, data) {
|
|
53
53
|
if (!data.desk)
|
|
54
54
|
data.desk = {};
|
|
55
55
|
if (!data.desk.portalKey) {
|
|
@@ -60,14 +60,104 @@ function deskKeys(path, data) {
|
|
|
60
60
|
}
|
|
61
61
|
return data.desk.portalKey;
|
|
62
62
|
}
|
|
63
|
+
/** Does this account already hold the token the vault pays in? */
|
|
64
|
+
async function trusts(vault, address) {
|
|
65
|
+
const acc = await vault.horizon.loadAccount(address).catch(() => null);
|
|
66
|
+
if (!acc)
|
|
67
|
+
return false;
|
|
68
|
+
const code = vault.token.getCode(), issuer = vault.token.getIssuer();
|
|
69
|
+
return acc.balances.some((b) => b.asset_type !== "native"
|
|
70
|
+
&& b.asset_code === code && b.asset_issuer === issuer);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Where the money lands.
|
|
74
|
+
*
|
|
75
|
+
* `setup` creates a payee account for you. Two cases break that: a vault made
|
|
76
|
+
* before the payee existed, and a seller who wants the money in their own
|
|
77
|
+
* account (a Binance deposit address, an exchange wallet). Both are handled
|
|
78
|
+
* here, because a payment to an account that does not exist — or that does not
|
|
79
|
+
* trust the token — fails on the ledger with a code the seller never sees.
|
|
80
|
+
*
|
|
81
|
+
* Adding a trustline needs the payee's own key, so for an outside account we can
|
|
82
|
+
* only check and explain. For a payee we generated, we do it ourselves.
|
|
83
|
+
*/
|
|
84
|
+
export async function ensurePayee(path, data, vault) {
|
|
85
|
+
const specified = arg("payee");
|
|
86
|
+
if (specified) {
|
|
87
|
+
if (!/^G[A-Z2-7]{55}$/.test(specified))
|
|
88
|
+
bad(`--payee must be a Stellar account (G…56 chars), got "${specified}"`);
|
|
89
|
+
if (specified === vault.address)
|
|
90
|
+
bad("--payee cannot be the vault itself");
|
|
91
|
+
if (!(await trusts(vault, specified))) {
|
|
92
|
+
bad(`${specified} does not hold ${vault.token.getCode()} yet, so a payment to it would be\n rejected by the ledger (OP_NO_TRUST).\n\n add a trustline for ${vault.token.getCode()} from ${vault.token.getIssuer()} to that account\n (the exchange or wallet that owns it does this), or drop --payee and Paydlux will\n create and manage a payee account for you.`);
|
|
93
|
+
}
|
|
94
|
+
data.payee = { address: specified, external: true };
|
|
95
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
96
|
+
chmodSync(path, 0o600);
|
|
97
|
+
log(`payments land in your account ${specified}`);
|
|
98
|
+
return specified;
|
|
99
|
+
}
|
|
100
|
+
if (data.payee?.address && !data.payee.seed)
|
|
101
|
+
return data.payee.address;
|
|
102
|
+
if (data.payee?.address && (await trusts(vault, data.payee.address)))
|
|
103
|
+
return data.payee.address;
|
|
104
|
+
const keys = data.keys;
|
|
105
|
+
if (!keys.approver) {
|
|
106
|
+
bad(`this vault has no payee account yet, and creating one needs both human keys —\n this machine holds only one.\n\n point the payments at an account you already own that holds ${vault.token.getCode()}:\n\n paydlux sell --payee G…`);
|
|
107
|
+
}
|
|
108
|
+
const kp = Keypair.fromSecret(data.payee?.seed ?? Keypair.random().secret());
|
|
109
|
+
if (!data.payee?.address) {
|
|
110
|
+
log(`creating the payee account (2 XLM of ledger rent)…`);
|
|
111
|
+
const acc = await vault.horizon.loadAccount(vault.address);
|
|
112
|
+
const tx = new TransactionBuilder(new Account(vault.address, acc.sequence), {
|
|
113
|
+
fee: "100", networkPassphrase: passphrase((data.network ?? "testnet")),
|
|
114
|
+
})
|
|
115
|
+
.addOperation(Operation.createAccount({ destination: kp.publicKey(), startingBalance: "2" }))
|
|
116
|
+
.setTimeout(120).build();
|
|
117
|
+
tx.sign(Keypair.fromSecret(keys.principal));
|
|
118
|
+
tx.sign(Keypair.fromSecret(keys.approver));
|
|
119
|
+
await vault.horizon.submitTransaction(tx);
|
|
120
|
+
}
|
|
121
|
+
if (!(await trusts(vault, kp.publicKey())))
|
|
122
|
+
await vault.trustToken(kp.publicKey(), "1000000", kp);
|
|
123
|
+
data.payee = { address: kp.publicKey(), seed: kp.secret() };
|
|
124
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
125
|
+
chmodSync(path, 0o600);
|
|
126
|
+
log(`payments land in ${kp.publicKey()}`);
|
|
127
|
+
return kp.publicKey();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Change what the vault settles in. USDC only: a custom token would need an
|
|
131
|
+
* issuer key, and holding one quietly is not a decision to make in a flag.
|
|
132
|
+
* The trustline needs both human keys, so this has to run before
|
|
133
|
+
* `approver --forget` takes one away.
|
|
134
|
+
*/
|
|
135
|
+
async function switchToken(path, data, code) {
|
|
136
|
+
if (code !== "USDC") {
|
|
137
|
+
bad(`switching to a custom token means holding an issuer key. Only USDC can be\n switched to here. Create a token vault with: paydlux setup --token ${code}`);
|
|
138
|
+
}
|
|
139
|
+
const network = (data.network ?? "testnet");
|
|
140
|
+
if (!data.keys.approver)
|
|
141
|
+
bad(`adding the USDC trustline needs both human keys and this machine holds one. Re-run on the machine with both, then use approver --forget.`);
|
|
142
|
+
const vault = await StellarVault.load(data.vault, data.token.code, data.token.issuer, { principal: pub(data.keys.principal), approver: pub(data.keys.approver), agent: pub(data.keys.agent) }, network);
|
|
143
|
+
log(`adding a USDC trustline to the vault…`);
|
|
144
|
+
await vault.trustUsdc(Keypair.fromSecret(data.keys.principal), Keypair.fromSecret(data.keys.approver));
|
|
145
|
+
data.token = { code: "USDC", issuer: USDC_ISSUER[network] };
|
|
146
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
147
|
+
chmodSync(path, 0o600);
|
|
148
|
+
log("the vault now settles in Circle USDC");
|
|
149
|
+
}
|
|
63
150
|
// ------------------------------------------------------------------------ sell
|
|
64
151
|
async function sell() {
|
|
65
152
|
const { path, data } = loadVault();
|
|
66
153
|
const network = (data.network ?? "testnet");
|
|
67
154
|
const keys = data.keys;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
155
|
+
// Settle in real money instead of the token this vault issued. Needs a
|
|
156
|
+
// trustline, which needs both human keys, so it happens here — before
|
|
157
|
+
// `approver --forget` takes one of them away.
|
|
158
|
+
const wantToken = arg("token")?.toUpperCase();
|
|
159
|
+
if (wantToken && wantToken !== data.token.code)
|
|
160
|
+
await switchToken(path, data, wantToken);
|
|
71
161
|
const price = arg("price") ?? data.policy?.defaultPrice ?? "1";
|
|
72
162
|
if (!(Number(price) > 0))
|
|
73
163
|
bad("--price must be a positive number");
|
|
@@ -82,9 +172,26 @@ async function sell() {
|
|
|
82
172
|
const maxPer = Number(arg("max") ?? data.policy?.maxPerPayment ?? 2);
|
|
83
173
|
const daily = Number(arg("daily") ?? data.policy?.dailyBudget ?? 10);
|
|
84
174
|
const policy = new PolicyEngine({ maxPerPayment: maxPer, dailyBudget: daily });
|
|
85
|
-
|
|
175
|
+
// A ceiling below the price blocks every payment, and the failure looks like
|
|
176
|
+
// a bug in the desk: the buyer waits, the human never sees an approvable
|
|
177
|
+
// item, and the log says "mandate refused". Say it now, before anything is
|
|
178
|
+
// started, because this is a mistake in the flags and not in the product.
|
|
179
|
+
const priceNum = Number(price);
|
|
180
|
+
if (!(priceNum > 0))
|
|
181
|
+
bad(`--price ${price} is not a number greater than zero.`);
|
|
182
|
+
if (priceNum > maxPer) {
|
|
183
|
+
bad(`--price ${price} is above --max ${maxPer}, so every payment would be refused by your own mandate.\n raise --max to at least ${price} (for example --max ${price}) or lower --price.`);
|
|
184
|
+
}
|
|
185
|
+
if (priceNum > daily) {
|
|
186
|
+
bad(`--price ${price} is above --daily ${daily}: one payment would spend the whole day's budget and the second would be refused.\n set --daily to at least ${price}.`);
|
|
187
|
+
}
|
|
188
|
+
const vault = await StellarVault.load(data.vault, data.token.code, data.token.issuer, { principal: pub(keys.principal), approver: keys.approver ? pub(keys.approver) : undefined, agent: pub(keys.agent) }, network);
|
|
86
189
|
if (data.token.code === "USDC")
|
|
87
190
|
vault.useUsdc();
|
|
191
|
+
// Resolve where the money lands before anything starts listening. A parked
|
|
192
|
+
// payment with no valid payee is a payment that fails an hour later, after
|
|
193
|
+
// the human already said yes.
|
|
194
|
+
const payee = await ensurePayee(path, data, vault);
|
|
88
195
|
if (network === "public") {
|
|
89
196
|
console.log(`
|
|
90
197
|
MAINNET — real money.
|
|
@@ -123,6 +230,8 @@ async function sell() {
|
|
|
123
230
|
publicHost,
|
|
124
231
|
agentKeys: [pub(keys.agent)],
|
|
125
232
|
routes: [{ uri, price, mode, holdSeconds: Number(arg("hold") ?? 3600) }],
|
|
233
|
+
// The banner prints these numbers. This line is what makes them true.
|
|
234
|
+
policy,
|
|
126
235
|
});
|
|
127
236
|
let tunnel = null;
|
|
128
237
|
if (tls) {
|
|
@@ -186,7 +295,7 @@ async function vaultRent(vault) {
|
|
|
186
295
|
async function status() {
|
|
187
296
|
const { data } = loadVault();
|
|
188
297
|
const network = (data.network ?? "testnet");
|
|
189
|
-
const vault = await StellarVault.load(data.vault, data.token.code, data.token.issuer, { principal: pub(data.keys.principal), approver: pub(data.keys.approver), agent: pub(data.keys.agent) }, network);
|
|
298
|
+
const vault = await StellarVault.load(data.vault, data.token.code, data.token.issuer, { principal: pub(data.keys.principal), approver: data.keys.approver ? pub(data.keys.approver) : undefined, agent: pub(data.keys.agent) }, network);
|
|
190
299
|
if (data.token.code === "USDC")
|
|
191
300
|
vault.useUsdc();
|
|
192
301
|
const rent = await vaultRent(vault);
|
|
@@ -407,6 +516,49 @@ removed from ${path}. this machine can no longer approve anything by itself.
|
|
|
407
516
|
sell with: paydlux sell --split --tls
|
|
408
517
|
`);
|
|
409
518
|
}
|
|
519
|
+
// --------------------------------------------------------------------- withdraw
|
|
520
|
+
/**
|
|
521
|
+
* Take your money back out of the vault. This is a normal 2-of-2 payment:
|
|
522
|
+
* principal (this laptop) + approver (your phone, or the laptop until you run
|
|
523
|
+
* `approver --forget`). The agent key cannot do this — it has no path to an
|
|
524
|
+
* arbitrary destination, which is the whole point of the vault.
|
|
525
|
+
*
|
|
526
|
+
* The XLM the vault pays in ledger rent stays locked while the account exists.
|
|
527
|
+
* Closing the vault (not done here) returns that too.
|
|
528
|
+
*/
|
|
529
|
+
async function withdraw() {
|
|
530
|
+
const { data } = loadVault();
|
|
531
|
+
const to = arg("to");
|
|
532
|
+
const amount = arg("amount");
|
|
533
|
+
const all = flag("all");
|
|
534
|
+
if (!to)
|
|
535
|
+
bad(`where should it go?\n\n paydlux withdraw --to G… [--all | --amount 8]\n\n --to can be any Stellar account, including your Binance deposit address.`);
|
|
536
|
+
const vault = await StellarVault.load(data.vault, data.token.code, data.token.issuer, {
|
|
537
|
+
principal: pub(data.keys.principal),
|
|
538
|
+
approver: data.keys.approver ? pub(data.keys.approver) : undefined,
|
|
539
|
+
agent: pub(data.keys.agent),
|
|
540
|
+
}, (data.network ?? "testnet"));
|
|
541
|
+
if (data.token.code === "USDC")
|
|
542
|
+
vault.useUsdc();
|
|
543
|
+
const bal = await vault.balance();
|
|
544
|
+
const want = all ? bal : amount;
|
|
545
|
+
if (!want)
|
|
546
|
+
bad(`say how much: --all, or --amount 8 (vault holds ${bal} ${data.token.code})`);
|
|
547
|
+
if (Number(want) > Number(bal))
|
|
548
|
+
bad(`the vault holds ${bal} ${data.token.code}, not ${want}.`);
|
|
549
|
+
const keys = data.keys ?? {};
|
|
550
|
+
const principal = Keypair.fromSecret(keys.principal);
|
|
551
|
+
const approver = keys.approver ? Keypair.fromSecret(keys.approver) : null;
|
|
552
|
+
if (!approver) {
|
|
553
|
+
console.log(`this laptop holds only one of the two keys, so the withdrawal needs your phone.
|
|
554
|
+
open: ${flag("tls") ? `https://${PUBLIC_HOST}` : "http://127.0.0.1:4022"}/?k=${data.desk?.portalKey ?? "…"}`);
|
|
555
|
+
bad("then run this again — approvals from the phone land automatically.");
|
|
556
|
+
}
|
|
557
|
+
console.log(`sending ${want} ${data.token.code} from the vault to ${to} …`);
|
|
558
|
+
const res = await vault.release(to, String(want), principal, approver, "paydlux withdraw");
|
|
559
|
+
console.log(`done. tx ${res?.hash}`);
|
|
560
|
+
console.log(`vault now holds ${await vault.balance()} ${data.token.code}`);
|
|
561
|
+
}
|
|
410
562
|
// ------------------------------------------------------------------------- main
|
|
411
563
|
const HELP = `paydlux — get paid by AI agents, with a human in the loop.
|
|
412
564
|
|
|
@@ -419,6 +571,9 @@ const HELP = `paydlux — get paid by AI agents, with a human in the loop.
|
|
|
419
571
|
paydlux approver [--forget]
|
|
420
572
|
print the human half of the vault, then optionally delete it from this
|
|
421
573
|
machine so only the phone can approve.
|
|
574
|
+
paydlux withdraw --to G… [--all | --amount N]
|
|
575
|
+
take your money back out of the vault. Needs both human keys; the agent
|
|
576
|
+
key can never do this.
|
|
422
577
|
|
|
423
578
|
one file holds it all: ./paydlux-vault.json (0600, never commit it)
|
|
424
579
|
`;
|
|
@@ -436,6 +591,8 @@ export async function main() {
|
|
|
436
591
|
return status();
|
|
437
592
|
if (cmd === "approver")
|
|
438
593
|
return approver();
|
|
594
|
+
if (cmd === "withdraw")
|
|
595
|
+
return withdraw();
|
|
439
596
|
bad(`unknown command "${cmd}".\n\n${HELP}`);
|
|
440
597
|
}
|
|
441
598
|
// Run only when this file is the entry point, so importing it in a test is safe.
|
package/dist/desk.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import type { StellarVault } from "./vault.ts";
|
|
3
|
+
import type { PolicyEngine } from "./policy.ts";
|
|
3
4
|
import { type ApprovalChallenge } from "./phone.ts";
|
|
4
5
|
/** A portal link is a capability: whoever holds it can approve. So it is long,
|
|
5
6
|
* random, and compared without leaking timing. */
|
|
@@ -20,6 +21,13 @@ export type DeskRecord = {
|
|
|
20
21
|
* twice and the buyer never gets the content they already bought.
|
|
21
22
|
*/
|
|
22
23
|
key?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The failure came from the seller's spending mandate, not from the ledger.
|
|
26
|
+
* The gateway needs this distinction: a ledger failure is worth retrying, a
|
|
27
|
+
* mandate refusal never changes, so re-parking the same request would loop
|
|
28
|
+
* forever — refuse, and make the buyer change the request to ask again.
|
|
29
|
+
*/
|
|
30
|
+
mandateBlocked?: boolean;
|
|
23
31
|
/** split mode: what the phone is asked to sign, and what it sent back */
|
|
24
32
|
challenge?: ApprovalChallenge;
|
|
25
33
|
qr?: string;
|
|
@@ -53,6 +61,18 @@ export type DeskConfig = {
|
|
|
53
61
|
* gives you. Pinning the Host means the only way in is through the tunnel.
|
|
54
62
|
*/
|
|
55
63
|
publicHost?: string;
|
|
64
|
+
/**
|
|
65
|
+
* The spending mandate. Optional only so existing tests keep compiling; every
|
|
66
|
+
* real seller passes one.
|
|
67
|
+
*
|
|
68
|
+
* Enforced HERE, at the moment money moves, not in the gateway. The gateway is
|
|
69
|
+
* the buyer's surface: it can be restarted, bypassed, or run by someone who
|
|
70
|
+
* forgot to configure it. The desk is the one place every payment passes
|
|
71
|
+
* through on its way to the ledger — dev mode signs and submits here, split
|
|
72
|
+
* mode submits the phone's signature here — so this is the only place a
|
|
73
|
+
* ceiling can actually hold.
|
|
74
|
+
*/
|
|
75
|
+
policy?: PolicyEngine;
|
|
56
76
|
};
|
|
57
77
|
export declare class ApprovalDesk {
|
|
58
78
|
private records;
|
|
@@ -75,6 +95,20 @@ export declare class ApprovalDesk {
|
|
|
75
95
|
* principal only, and hand it to the phone. The money moves when
|
|
76
96
|
* /signed comes back with the phone's signature.
|
|
77
97
|
*/
|
|
98
|
+
/**
|
|
99
|
+
* The mandate check, in one place, called before money moves.
|
|
100
|
+
*
|
|
101
|
+
* Returns null when the payment is allowed, or the reason it is not. The
|
|
102
|
+
* record is marked failed so the buyer's poll gets a real answer instead of a
|
|
103
|
+
* timeout, and the reason is surfaced to the human rather than logged only.
|
|
104
|
+
*
|
|
105
|
+
* Why this exists at all: `--max` and `--daily` were printed in the seller's
|
|
106
|
+
* banner and never applied to anything. A ceiling nobody enforces is worse
|
|
107
|
+
* than no ceiling, because the seller believes they have one.
|
|
108
|
+
*/
|
|
109
|
+
private mandateBlock;
|
|
110
|
+
/** The mandate has been satisfied: count the money as spent. */
|
|
111
|
+
private recordSpend;
|
|
78
112
|
approve(id: string): Promise<DeskRecord>;
|
|
79
113
|
/** Split mode step 1: ask the phone to sign. Nothing is submitted yet. */
|
|
80
114
|
private requestPhoneSignature;
|
|
@@ -85,7 +119,23 @@ export declare class ApprovalDesk {
|
|
|
85
119
|
* ledger rejects it if the signature is not over this exact transaction.
|
|
86
120
|
*/
|
|
87
121
|
complete(id: string, signedXdr: string): Promise<DeskRecord>;
|
|
122
|
+
/**
|
|
123
|
+
* The human said no.
|
|
124
|
+
*
|
|
125
|
+
* `awaiting_phone` is a valid state to deny from. Opening the phone link
|
|
126
|
+
* builds the challenge on the spot (so a human needs one tap, not two), which
|
|
127
|
+
* means the first thing a person sees on their phone is already
|
|
128
|
+
* `awaiting_phone` — refusing to deny from that state left the Reject button
|
|
129
|
+
* dead, which was verified on live testnet. Denying is always allowed up to
|
|
130
|
+
* the point the transaction is submitted; after that it is too late.
|
|
131
|
+
*/
|
|
88
132
|
deny(id: string, reason?: string): DeskRecord;
|
|
133
|
+
/**
|
|
134
|
+
* What the phone app is allowed to see. Never the paid `content` — that is
|
|
135
|
+
* what the buyer has not paid for yet — and never the raw challenge XDR,
|
|
136
|
+
* which the app fetches per item so the desk can re-check the binding.
|
|
137
|
+
*/
|
|
138
|
+
queue(): Record<string, unknown>[];
|
|
89
139
|
/** Serve the pending list as JSON (the portal polls this). */
|
|
90
140
|
api(handler: (req: IncomingMessage, res: ServerResponse) => void): (req: IncomingMessage, res: ServerResponse) => void;
|
|
91
141
|
listen(port?: number): Promise<void>;
|