@paydlux/agent 0.0.0-stage → 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.
@@ -0,0 +1,252 @@
1
+ // Paydlux gateway on Stellar — an HTTP resource server that answers 402 and
2
+ // only serves the body once payment has actually landed on the ledger.
3
+ //
4
+ // Two modes per route:
5
+ //
6
+ // preauth the human already registered a mandate envelope for this seller
7
+ // and amount. The gateway hands it over, the agent signs, submits,
8
+ // and the ledger enforces the cap. No human needed at buy time.
9
+ //
10
+ // approval nothing is pre-authorised. The gateway verifies the request,
11
+ // holds it at the approval desk, and a person approves or denies.
12
+ // On approval the vault releases with principal + approver.
13
+ //
14
+ // Either way the agent's key alone can never move money: threshold is 2.
15
+ import { createServer } from "node:http";
16
+ import { Keypair, StrKey, Transaction } from "stellar-sdk";
17
+ import { StellarTxError, passphrase } from "./vault.js";
18
+ import { ApprovalDesk } from "./desk.js";
19
+ export const PAY_HEADER = "x-payment";
20
+ export class Gateway {
21
+ routes = new Map();
22
+ mandates = new Map();
23
+ /** envelope hashes already redeemed, so one mandate = one delivery */
24
+ settled = new Set();
25
+ desk;
26
+ cfg;
27
+ constructor(cfg) {
28
+ this.cfg = cfg;
29
+ for (const r of cfg.routes)
30
+ this.routes.set(r.uri, r);
31
+ this.desk = new ApprovalDesk({
32
+ vault: cfg.vault,
33
+ payee: cfg.payee,
34
+ principal: cfg.principal,
35
+ approver: cfg.approver,
36
+ port: cfg.deskPort ?? 4022,
37
+ portalKey: cfg.portalKey,
38
+ });
39
+ }
40
+ /** Mandates the vault currently holds, so the gateway can hand them out. */
41
+ async syncMandates() {
42
+ const s = await this.cfg.vault.signerSummary();
43
+ return s.signers.filter((x) => x.type === "preauth-mandate");
44
+ }
45
+ /** Register a pre-authorised mandate for a route (needs both human keys). */
46
+ async authorise(uri, spec, principal, approver) {
47
+ const r = this.routes.get(uri);
48
+ if (!r)
49
+ throw new Error(`no route ${uri}`);
50
+ if (spec.payee !== this.cfg.payee)
51
+ throw new Error("mandate payee does not match this gateway's payee");
52
+ const m = await this.cfg.vault.createMandate(spec, principal, approver);
53
+ this.mandates.set(m.hash, m);
54
+ return m;
55
+ }
56
+ /** The intent must be signed by a real agent key on the vault. */
57
+ verifyIntent(intent) {
58
+ if (!intent?.payload || !intent?.signature)
59
+ return false;
60
+ const [url, amount, vault] = intent.payload.split("|");
61
+ if (vault !== this.cfg.vault.address)
62
+ return false;
63
+ if (Number(amount) <= 0)
64
+ return false;
65
+ const age = Date.now() - Number(intent.payload.split("|").at(-1));
66
+ if (!Number.isFinite(age) || age > 5 * 60 * 1000)
67
+ return false;
68
+ const msg = Buffer.from(intent.payload);
69
+ const sig = Buffer.from(intent.signature, "base64");
70
+ for (const s of this.agentKeys) {
71
+ try {
72
+ if (Keypair.fromPublicKey(s).verify(msg, sig))
73
+ return true;
74
+ }
75
+ catch { /* keep going */ }
76
+ }
77
+ return false;
78
+ }
79
+ /** Keys allowed to place approval requests. */
80
+ agentKeys = [];
81
+ requirements(uri) {
82
+ const r = this.routes.get(uri);
83
+ return {
84
+ scheme: "stellar-exact",
85
+ network: `stellar:${this.cfg.network}`,
86
+ maxAmountRequired: r.price,
87
+ resource: uri,
88
+ payTo: this.cfg.payee,
89
+ token: { code: this.cfg.vault.token.getCode(), issuer: this.cfg.vault.token.getIssuer() },
90
+ vault: this.cfg.vault.address,
91
+ mode: r.mode,
92
+ maxTimeoutSeconds: r.holdSeconds ?? 3600,
93
+ version: "1",
94
+ };
95
+ }
96
+ /**
97
+ * Check a proof against the live request before spending anything.
98
+ * The envelope must name this gateway's payee and this exact price, and it
99
+ * must be a hash the vault actually recognises.
100
+ */
101
+ async verify(uri, proof) {
102
+ const r = this.routes.get(uri);
103
+ if (!r)
104
+ throw new Error("unpriced resource " + uri);
105
+ if (r.mode !== "preauth")
106
+ throw new Error("route requires human approval, not a preauth envelope");
107
+ if (!proof.envelope)
108
+ throw new Error("preauth mode needs an envelope");
109
+ const tx = new Transaction(proof.envelope, passphrase(this.cfg.vault.network));
110
+ if (tx.source !== this.cfg.vault.address)
111
+ throw new Error("envelope is not drawn on this vault");
112
+ const ops = tx.operations;
113
+ if (ops.length !== 1 || ops[0].type !== "payment")
114
+ throw new Error("envelope must contain exactly one payment");
115
+ const op = ops[0];
116
+ // Stellar normalises amounts to 7 decimals, so compare as numbers.
117
+ if (Number(op.amount) !== Number(r.price)) {
118
+ throw new Error(`price mismatch: want ${r.price} got ${op.amount}`);
119
+ }
120
+ if (op.destination !== this.cfg.payee)
121
+ throw new Error("envelope pays a different seller");
122
+ if (op.asset?.getCode?.() !== this.cfg.vault.token.getCode()
123
+ || op.asset?.getIssuer?.() !== this.cfg.vault.token.getIssuer()) {
124
+ throw new Error("envelope pays in a different token");
125
+ }
126
+ const hash = tx.hash().toString("hex");
127
+ if (hash !== proof.mandateHash)
128
+ throw new Error("envelope hash does not match the declared mandate");
129
+ // The vault stores the mandate as a strkey "T…" signer (base32 of the hash),
130
+ // so decode it back to hex before comparing.
131
+ const live = await this.cfg.vault.signerSummary();
132
+ const registered = live.signers
133
+ .filter((s) => s.type === "preauth-mandate")
134
+ .map((s) => StrKey.decodePreAuthTx(s.key).toString("hex"));
135
+ if (!registered.includes(hash)) {
136
+ throw new Error("mandate is not registered on the vault (expired or revoked)");
137
+ }
138
+ const tb = tx.timebounds;
139
+ if (tb?.maxTime && Number(tb.maxTime) * 1000 < Date.now())
140
+ throw new Error("mandate window closed");
141
+ return { route: r, tx, hash };
142
+ }
143
+ handler(getContent) {
144
+ return async (req, res) => {
145
+ const uri = (req.url ?? "/").split("?")[0];
146
+ if (uri === "/pay/status") {
147
+ const handle = new URL(req.url, "http://x").searchParams.get("handle");
148
+ const rec = handle ? this.desk.get(handle) : null;
149
+ res.writeHead(rec ? 200 : 404, { "content-type": "application/json" });
150
+ res.end(JSON.stringify(rec ?? { error: "unknown handle" }));
151
+ return;
152
+ }
153
+ const r = this.routes.get(uri);
154
+ if (!r) {
155
+ res.writeHead(404).end("not found");
156
+ return;
157
+ }
158
+ const body = await readBody(req);
159
+ const raw = req.headers[PAY_HEADER];
160
+ if (!raw) {
161
+ const reqs = this.requirements(uri);
162
+ res.writeHead(402, { [PAY_HEADER]: JSON.stringify(reqs) });
163
+ res.end(JSON.stringify({ error: "payment_required", accepts: reqs }));
164
+ return;
165
+ }
166
+ let proof;
167
+ try {
168
+ proof = JSON.parse(String(raw));
169
+ }
170
+ catch {
171
+ res.writeHead(400).end("bad payment header");
172
+ return;
173
+ }
174
+ if (!proof.intent && !proof.envelope) {
175
+ res.writeHead(400).end("payment header needs an envelope or an intent");
176
+ return;
177
+ }
178
+ // approval mode: the agent sends a signed intent, a human decides
179
+ if (r.mode === "approval") {
180
+ if (!proof.intent) {
181
+ res.writeHead(402, { [PAY_HEADER]: JSON.stringify(this.requirements(uri)) });
182
+ res.end(JSON.stringify({ error: "payment_invalid", reason: "approval mode needs a signed intent" }));
183
+ return;
184
+ }
185
+ if (!this.verifyIntent(proof.intent)) {
186
+ res.writeHead(402, { [PAY_HEADER]: JSON.stringify(this.requirements(uri)) });
187
+ res.end(JSON.stringify({ error: "payment_invalid", reason: "intent signature is not from the agent key" }));
188
+ return;
189
+ }
190
+ const handle = this.desk.enqueue(uri, r.price, body, getContent(uri));
191
+ res.writeHead(202, { "content-type": "application/json", "x-payment-hold": handle });
192
+ res.end(JSON.stringify({ status: "awaiting_approval", handle, desk: `http://127.0.0.1:${this.cfg.deskPort ?? 4022}` }));
193
+ return;
194
+ }
195
+ try {
196
+ await this.verify(uri, proof);
197
+ }
198
+ catch (e) {
199
+ res.writeHead(402, { [PAY_HEADER]: JSON.stringify(this.requirements(uri)) });
200
+ res.end(JSON.stringify({ error: "payment_invalid", reason: e.message }));
201
+ return;
202
+ }
203
+ // Submit the envelope. The agent signed it; the preauth hash is the
204
+ // second signature. If the ledger refuses, the body does not ship.
205
+ //
206
+ // Replay: Stellar de-duplicates an identical resubmission and hands back
207
+ // the ORIGINAL result instead of an error. So the money cannot move
208
+ // twice, but a naive gateway would happily serve the body twice. Check
209
+ // the ledger for an already-applied transaction before serving.
210
+ try {
211
+ const tx = new Transaction(proof.envelope, passphrase(this.cfg.vault.network));
212
+ const agentSig = tx.signatures?.[0];
213
+ if (!agentSig)
214
+ throw new Error("envelope carries no agent signature");
215
+ const envHash = tx.hash().toString("hex");
216
+ if (this.settled.has(envHash))
217
+ throw new Error("mandate already redeemed");
218
+ const applied = await this.cfg.vault.txExists(envHash);
219
+ if (applied) {
220
+ this.settled.add(envHash);
221
+ throw new Error("mandate already redeemed");
222
+ }
223
+ const out = await this.cfg.vault.horizon.submitTransaction(tx).catch(() => null);
224
+ if (!out)
225
+ throw new Error("ledger rejected the envelope");
226
+ this.settled.add(envHash);
227
+ const content = getContent(uri);
228
+ res.writeHead(200, {
229
+ "content-type": "application/json",
230
+ "x-payment-response": JSON.stringify({ tx: out.hash, ledger: out.ledger }),
231
+ });
232
+ res.end(JSON.stringify({ data: content, tx: out.hash }));
233
+ }
234
+ catch (e) {
235
+ res.writeHead(402, { [PAY_HEADER]: JSON.stringify(this.requirements(uri)) });
236
+ res.end(JSON.stringify({ error: "payment_invalid", reason: e.message }));
237
+ }
238
+ };
239
+ }
240
+ listen(port, getContent = (u) => `payload for ${u} at ${new Date().toISOString()}`) {
241
+ const srv = createServer(this.handler(getContent));
242
+ return new Promise((resolve) => srv.listen(port, () => resolve()));
243
+ }
244
+ }
245
+ function readBody(req) {
246
+ return new Promise((resolve) => {
247
+ let acc = "";
248
+ req.on("data", (c) => (acc += c.toString()));
249
+ req.on("end", () => resolve(acc));
250
+ });
251
+ }
252
+ export { StellarTxError };
@@ -0,0 +1,11 @@
1
+ export { StellarVault, StellarTxError, passphrase, USDC_ISSUER } from "./vault.ts";
2
+ export type { Network, Amount, Mandate, MandateSpec, VaultSigners } from "./vault.ts";
3
+ export { Gateway } from "./gateway.ts";
4
+ export type { GatewayConfig, Route, PaymentProof } from "./gateway.ts";
5
+ export { PaydluxAgent } from "./agent.ts";
6
+ export type { AgentConfig, PayResponse } from "./agent.ts";
7
+ export { ApprovalDesk, newPortalKey } from "./desk.ts";
8
+ export type { DeskConfig, DeskRecord } from "./desk.ts";
9
+ export { portalPage } from "./portal.ts";
10
+ export { PolicyEngine, DEFAULT_POLICY } from "./policy.ts";
11
+ export type { Policy, SpendEvent, Verdict } from "./policy.ts";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Public API of the Paydlux agent-payment SDK.
2
+ //
3
+ // import { StellarVault, Gateway, PaydluxAgent, ApprovalDesk } from "@paydlux/agent";
4
+ export { StellarVault, StellarTxError, passphrase, USDC_ISSUER } from "./vault.js";
5
+ export { Gateway } from "./gateway.js";
6
+ export { PaydluxAgent } from "./agent.js";
7
+ export { ApprovalDesk, newPortalKey } from "./desk.js";
8
+ export { portalPage } from "./portal.js";
9
+ export { PolicyEngine, DEFAULT_POLICY } from "./policy.js";
@@ -0,0 +1,52 @@
1
+ export type Policy = {
2
+ /** hard ceiling on any single payment */
3
+ maxPerPayment: number;
4
+ /** total across a calendar day */
5
+ dailyBudget: number;
6
+ /** sellers that may be paid; empty = any */
7
+ allowlist?: string[];
8
+ /** sellers that may never be paid, even if allowlisted */
9
+ denylist?: string[];
10
+ /** longest a mandate may stay live, seconds */
11
+ maxMandateTtl: number;
12
+ /**
13
+ * Mandates live at once. Stellar binds a pre-auth transaction to a sequence
14
+ * number, so only the newest can ever run — keep this at 1 and the state
15
+ * matches what the ledger will actually honour.
16
+ */
17
+ maxLiveMandates: number;
18
+ };
19
+ export declare const DEFAULT_POLICY: Policy;
20
+ export type SpendEvent = {
21
+ at: number;
22
+ amount: number;
23
+ payee: string;
24
+ uri?: string;
25
+ mandateHash?: string;
26
+ };
27
+ export type Verdict = {
28
+ ok: true;
29
+ } | {
30
+ ok: false;
31
+ rule: string;
32
+ detail: string;
33
+ };
34
+ export declare class PolicyEngine {
35
+ private events;
36
+ policy: Policy;
37
+ constructor(policy?: Partial<Policy>);
38
+ record(e: SpendEvent): void;
39
+ /** Everything spent in the current UTC day. */
40
+ spentToday(): number;
41
+ today(): SpendEvent[];
42
+ /**
43
+ * Check a request BEFORE any transaction is built. Returns the reason a
44
+ * payment is refused; the caller must not proceed on ok:false.
45
+ */
46
+ check(req: {
47
+ amount: number;
48
+ payee: string;
49
+ ttl?: number;
50
+ liveMandates?: number;
51
+ }): Verdict;
52
+ }
package/dist/policy.js ADDED
@@ -0,0 +1,58 @@
1
+ export const DEFAULT_POLICY = {
2
+ maxPerPayment: 50,
3
+ dailyBudget: 200,
4
+ allowlist: [],
5
+ denylist: [],
6
+ maxMandateTtl: 3600,
7
+ maxLiveMandates: 1,
8
+ };
9
+ export class PolicyEngine {
10
+ events = [];
11
+ policy;
12
+ constructor(policy = {}) {
13
+ this.policy = { ...DEFAULT_POLICY, ...policy };
14
+ }
15
+ record(e) { this.events.push(e); }
16
+ /** Everything spent in the current UTC day. */
17
+ spentToday() {
18
+ const start = new Date();
19
+ start.setUTCHours(0, 0, 0, 0);
20
+ const t = start.getTime();
21
+ return this.events.filter((e) => e.at >= t).reduce((a, b) => a + b.amount, 0);
22
+ }
23
+ today() {
24
+ const start = new Date();
25
+ start.setUTCHours(0, 0, 0, 0);
26
+ return this.events.filter((e) => e.at >= start.getTime());
27
+ }
28
+ /**
29
+ * Check a request BEFORE any transaction is built. Returns the reason a
30
+ * payment is refused; the caller must not proceed on ok:false.
31
+ */
32
+ check(req) {
33
+ const p = this.policy;
34
+ if (!(req.amount > 0))
35
+ return no("amount", "amount must be greater than zero");
36
+ if (req.amount > p.maxPerPayment) {
37
+ return no("max_per_payment", `${req.amount} exceeds the per-payment ceiling of ${p.maxPerPayment}`);
38
+ }
39
+ const spent = this.spentToday();
40
+ if (spent + req.amount > p.dailyBudget) {
41
+ return no("daily_budget", `${req.amount} would take today to ${(spent + req.amount).toFixed(2)}, over the ${p.dailyBudget} budget (spent ${spent.toFixed(2)})`);
42
+ }
43
+ if (p.denylist?.includes(req.payee))
44
+ return no("denylist", `${short(req.payee)} is blocked`);
45
+ if (p.allowlist?.length && !p.allowlist.includes(req.payee)) {
46
+ return no("allowlist", `${short(req.payee)} is not on the approved seller list`);
47
+ }
48
+ if (req.ttl !== undefined && req.ttl > p.maxMandateTtl) {
49
+ return no("max_ttl", `mandate would live ${req.ttl}s, longer than the ${p.maxMandateTtl}s limit`);
50
+ }
51
+ if (req.liveMandates !== undefined && req.liveMandates >= p.maxLiveMandates) {
52
+ return no("max_live", `${req.liveMandates} mandates already live (limit ${p.maxLiveMandates})`);
53
+ }
54
+ return { ok: true };
55
+ }
56
+ }
57
+ const no = (rule, detail) => ({ ok: false, rule, detail });
58
+ const short = (k) => k.slice(0, 6) + "…" + k.slice(-4);
@@ -0,0 +1,4 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { DeskRecord } from "./desk.ts";
3
+ export declare function portalPage(records: DeskRecord[], vault: string, network: string, qs?: string): string;
4
+ export declare function servePortal(req: IncomingMessage, res: ServerResponse, html: string): void;
package/dist/portal.js ADDED
@@ -0,0 +1,62 @@
1
+ export function portalPage(records, vault, network, qs = "") {
2
+ const rows = records.length
3
+ ? records.map(render).join("\n")
4
+ : `<tr><td colspan="5" class="empty">nothing waiting</td></tr>`;
5
+ return `<!doctype html><html><head>
6
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <title>Paydlux — approvals</title>
8
+ <style>
9
+ :root{--ink:#0d0f0e;--line:#262a28;--text:#eceae4;--muted:#9aa39d;--gold:#e8b04b;--green:#5fbf8b}
10
+ *{box-sizing:border-box}
11
+ body{margin:0;background:var(--ink);color:var(--text);font:16px/1.5 -apple-system,"Avenir Next",system-ui,sans-serif;padding:24px}
12
+ h1{font-size:20px;margin:0 0 4px}
13
+ .sub{color:var(--muted);font-size:13px;margin-bottom:24px;word-break:break-all}
14
+ table{width:100%;border-collapse:collapse}
15
+ th{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em;text-align:left;padding:8px;border-bottom:1px solid var(--line)}
16
+ td{padding:14px 8px;border-bottom:1px solid var(--line);vertical-align:middle}
17
+ .amt{font-weight:700;color:var(--gold);white-space:nowrap}
18
+ .uri{font-family:ui-monospace,monospace;font-size:13px}
19
+ .body{color:var(--muted);font-size:12px;max-width:340px;word-break:break-word}
20
+ button{border:0;border-radius:10px;padding:12px 18px;font-size:15px;font-weight:600;cursor:pointer;display:block;width:100%;margin:4px 0}
21
+ .yes{background:var(--green);color:#08110c}
22
+ .no{background:transparent;color:var(--muted);border:1px solid var(--line)}
23
+ .st{font-size:12px;text-transform:uppercase;letter-spacing:.06em}
24
+ .settled{color:var(--green)} .denied,.failed{color:#e07a6b} .pending{color:var(--gold)}
25
+ .tx{font-family:ui-monospace,monospace;font-size:11px;color:var(--muted);word-break:break-all}
26
+ .foot{margin-top:28px;color:var(--muted);font-size:12px}
27
+ @media(max-width:640px){.body{max-width:180px}}
28
+ </style></head><body>
29
+ <h1>Paydlux — approvals</h1>
30
+ <div class="sub">vault ${vault} · ${network} · refreshed <span id="t">now</span></div>
31
+ <table><thead><tr><th>when</th><th>what</th><th>cost</th><th>status</th><th></th></tr></thead>
32
+ <tbody>${rows}</tbody></table>
33
+ <div class="foot">Approving signs a Stellar transaction with your key. The vault needs two signatures, so this device alone cannot release funds.</div>
34
+ <script>
35
+ const Q=${JSON.stringify(qs)};
36
+ async function act(kind,id){await fetch('/'+kind+'/'+id+Q,{method:'POST'});load()}
37
+ async function load(){const r=await fetch('/list'+Q);const d=await r.json();
38
+ document.querySelector('tbody').innerHTML=d.map(row).join('')||'<tr><td colspan="5" class="empty">nothing waiting</td></tr>';
39
+ document.getElementById('t').textContent=new Date().toLocaleTimeString()}
40
+ function row(x){const st='<span class="st '+x.status+'">'+x.status+'</span>'+(x.tx?'<div class="tx">'+x.tx+'</div>':'')+(x.reason?'<div class="tx">'+x.reason+'</div>':'');
41
+ const btns=x.status==='pending'?'<button class="yes" onclick="act(\\'approve\\',\\''+x.id+'\\')">Approve</button><button class="no" onclick="act(\\'deny\\',\\''+x.id+'\\')">Deny</button>':'';
42
+ return '<tr><td>'+new Date(x.at).toLocaleTimeString()+'</td><td><div class="uri">'+x.uri+'</div><div class="body">'+(x.body||'')+'</div></td><td class="amt">'+x.price+'</td><td>'+st+'</td><td style="min-width:130px">'+btns+'</td></tr>'}
43
+ setInterval(load,3000);
44
+ </script></body></html>`;
45
+ }
46
+ function render(r) {
47
+ const btns = r.status === "pending"
48
+ ? `<button class="yes" onclick="act('approve','${r.id}')">Approve</button>
49
+ <button class="no" onclick="act('deny','${r.id}')">Deny</button>`
50
+ : "";
51
+ const meta = [r.tx && `<div class="tx">${r.tx}</div>`, r.reason && `<div class="tx">${r.reason}</div>`].filter(Boolean).join("");
52
+ return `<tr>
53
+ <td>${new Date(r.at).toLocaleTimeString()}</td>
54
+ <td><div class="uri">${r.uri}</div><div class="body">${r.body || ""}</div></td>
55
+ <td class="amt">${r.price}</td>
56
+ <td><span class="st ${r.status}">${r.status}</span>${meta}</td>
57
+ <td style="min-width:130px">${btns}</td></tr>`;
58
+ }
59
+ export function servePortal(req, res, html) {
60
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
61
+ res.end(html);
62
+ }
@@ -0,0 +1,135 @@
1
+ import { Asset, Horizon, Keypair, Networks } from "stellar-sdk";
2
+ export type Network = "testnet" | "public";
3
+ /**
4
+ * Circle's own USDC issuer on Stellar. Not a wrapped token, not a bridge:
5
+ * native, and Circle is the issuer. Anyone can put "USDC" on a token name —
6
+ * only these two accounts are real money. Never accept any other issuer.
7
+ */
8
+ export declare const USDC_ISSUER: Record<Network, string>;
9
+ /** A rejected Stellar transaction, with the ledger's own reason code. */
10
+ export declare class StellarTxError extends Error {
11
+ code: string;
12
+ constructor(code: string, cause?: unknown);
13
+ }
14
+ /** The network passphrase for a network name (needed to hash transactions). */
15
+ export declare function passphrase(network: Network): string;
16
+ /** Stellar amounts are strings with at most 7 decimals. */
17
+ export type Amount = string;
18
+ export type MandateSpec = {
19
+ payee: string;
20
+ amount: Amount;
21
+ /** seconds from now the mandate may first execute */
22
+ notBefore?: number;
23
+ /** seconds from now after which it can no longer execute */
24
+ ttl?: number;
25
+ /** memo attached to the payment, so the seller can match it to an order */
26
+ memo?: string;
27
+ };
28
+ export type Mandate = {
29
+ /** hex hash; also the signer that authorises the payment */
30
+ hash: string;
31
+ /** the built, unsigned transaction — hand this to the agent */
32
+ envelope: string;
33
+ spec: MandateSpec;
34
+ /** sequence number this envelope must be submitted at */
35
+ sequence: string;
36
+ expiresAt: number;
37
+ };
38
+ export type VaultSigners = {
39
+ /** the human who owns the money */
40
+ principal: string;
41
+ /** the human approval key (phone / browser wallet) */
42
+ approver: string;
43
+ /** the agent key, handed to the software doing the buying */
44
+ agent: string;
45
+ };
46
+ export declare class StellarVault {
47
+ readonly horizon: Horizon.Server;
48
+ readonly network: Network;
49
+ readonly address: string;
50
+ token: Asset;
51
+ signers: VaultSigners;
52
+ private constructor();
53
+ /** Fund a key on testnet. No captcha, no phone, no faucet hunt. */
54
+ static fundTestnet(kp: Keypair): Promise<string>;
55
+ /**
56
+ * Create a vault. `issuer` signs the token; the vault account itself is a
57
+ * multisig whose release threshold needs principal + approver, or agent +
58
+ * a pre-authorised mandate hash. Nothing else reaches threshold.
59
+ */
60
+ static create(opts: {
61
+ network: Network;
62
+ issuer: Keypair;
63
+ principal: Keypair;
64
+ approver: Keypair;
65
+ agent: Keypair;
66
+ tokenCode: string;
67
+ vaultSeed?: string;
68
+ startingBalance?: string;
69
+ }): Promise<StellarVault>;
70
+ /**
71
+ * A vault that settles in real Circle USDC instead of a test token.
72
+ * Circle mints USDC, so there is no issuer key for us to hold: the vault
73
+ * just needs a trustline, and money arrives from an exchange or Circle's
74
+ * faucet on testnet. Everything else (mandates, thresholds, replay) is the
75
+ * same code.
76
+ */
77
+ static usdcToken(network: Network): Asset;
78
+ /** Add a USDC trustline to an existing vault (needs both human keys). */
79
+ trustUsdc(principal: Keypair, approver: Keypair, limit?: string): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
80
+ /** Point this vault at USDC instead of a custom token. */
81
+ useUsdc(): this;
82
+ /** Re-attach to an existing vault by account id. */
83
+ static load(address: string, tokenCode: string, issuer: string, signers: VaultSigners, network: Network): Promise<StellarVault>;
84
+ /** The seller must opt in to holding our token before we can pay them. */
85
+ trustToken(seller: string, limit: string | undefined, sellerKeypair: Keypair): Promise<void>;
86
+ /** Move tokens into the vault. Only the vault's signers can move them out. */
87
+ deposit(amount: Amount, from: Keypair): Promise<string>;
88
+ /**
89
+ * Has this transaction already been applied? Horizon de-duplicates an
90
+ * identical resubmission and returns the original result, so "submit
91
+ * succeeded" does NOT mean "money moved just now". Sellers must check.
92
+ */
93
+ txExists(hash: string): Promise<boolean>;
94
+ balance(account?: string): Promise<Amount>;
95
+ /**
96
+ * Build a mandate and register it on the vault.
97
+ *
98
+ * Returns the envelope (base64 XDR) to hand to the agent plus its hash. The
99
+ * agent submits it with its own signature; the pre-auth signer supplies the
100
+ * second. The agent cannot alter it: any edit changes the hash, and the hash
101
+ * is the signer.
102
+ */
103
+ createMandate(spec: MandateSpec, principal: Keypair, approver: Keypair): Promise<Mandate>;
104
+ /** The agent's side: sign the envelope and submit. That is all it can do. */
105
+ executeMandate(mandate: Mandate, agentKeypair: Keypair): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
106
+ /**
107
+ * The human path: approve a payment directly. Needs principal + approver,
108
+ * so a stolen agent key cannot use it and a stolen phone cannot use it alone.
109
+ */
110
+ release(payee: string, amount: Amount, principal: Keypair, approver: Keypair, memo?: string): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
111
+ /**
112
+ * Kill every outstanding mandate at once. Any accepted transaction bumps the
113
+ * vault's sequence, which invalidates all pre-authorized hashes pointing at a
114
+ * later sequence. This is the panic button: it needs principal + approver,
115
+ * so a stolen agent key cannot pull it and a stolen phone cannot pull it alone.
116
+ */
117
+ revokeAll(principal: Keypair, approver: Keypair): Promise<{
118
+ revoked: number;
119
+ tx: string;
120
+ } | null>;
121
+ /** Rotate the agent key: a compromised agent is replaced, mandates die with it. */
122
+ rotateAgent(oldAgent: Keypair, newAgentPub: string, human: Keypair): Promise<Horizon.HorizonApi.SubmitTransactionResponse>;
123
+ /** Who can sign for this vault right now. */
124
+ signerSummary(): Promise<{
125
+ thresholds: {
126
+ low: any;
127
+ med: any;
128
+ high: any;
129
+ };
130
+ signers: any;
131
+ }>;
132
+ private tb;
133
+ private tbAt;
134
+ }
135
+ export { Keypair, Asset, Networks };