@paydlux/agent 0.1.6 → 0.1.7
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 +56 -0
- package/dist/desk.js +26 -0
- package/dist/gateway.d.ts +13 -0
- package/dist/gateway.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,62 @@ npm install @paydlux/agent
|
|
|
114
114
|
import { StellarVault, Gateway, PaydluxAgent } from "@paydlux/agent";
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
### Seller setup, in full (the four things that bite)
|
|
118
|
+
|
|
119
|
+
A stranger following only the previous version of this README hit four walls.
|
|
120
|
+
All four are in the code; here they are up front.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { Keypair } from "@stellar/stellar-sdk";
|
|
124
|
+
import { StellarVault, Gateway } from "@paydlux/agent";
|
|
125
|
+
|
|
126
|
+
const issuer = Keypair.random(), principal = Keypair.random();
|
|
127
|
+
const approver = Keypair.random(), agent = Keypair.random();
|
|
128
|
+
const payee = Keypair.random();
|
|
129
|
+
|
|
130
|
+
await StellarVault.fundTestnet(issuer); // testnet only
|
|
131
|
+
await StellarVault.fundTestnet(payee);
|
|
132
|
+
const vault = await StellarVault.create({
|
|
133
|
+
network: "testnet", issuer, principal, approver, agent, tokenCode: "USDC",
|
|
134
|
+
});
|
|
135
|
+
await vault.trustToken(payee.publicKey(), "1000", payee); // payee must trust the token
|
|
136
|
+
await vault.deposit("50", issuer); // vault must hold the money
|
|
137
|
+
|
|
138
|
+
const gw = new Gateway({
|
|
139
|
+
vault, network: "testnet", payee: payee.publicKey(),
|
|
140
|
+
routes: [{ uri: "/api/data", price: "1.00", mode: "approval" }],
|
|
141
|
+
principal: principal.secret(),
|
|
142
|
+
approver: approver.secret(), // omit this + set `split: true` to approve on a phone
|
|
143
|
+
agentKeys: [agent.publicKey()], // (1) without it, no agent can park a payment
|
|
144
|
+
deskPort: 4022,
|
|
145
|
+
portalKey: newPortalKey(), // (2) required before the desk is public
|
|
146
|
+
deskUrl: "https://desk.example.com", // (3) what the buyer is told, not loopback
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await gw.listen(8080, (uri) => myPayload(uri));
|
|
150
|
+
await gw.desk.listen(4022); // (4) gw.listen() does NOT start the desk
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
1. **`agentKeys`** — an intent is only honoured if signed by a listed key. Empty
|
|
154
|
+
list means nothing can request approval, and the failure looks like a plain
|
|
155
|
+
402 with no explanation.
|
|
156
|
+
2. **`portalKey`** — the approval page has no login. The key *is* the
|
|
157
|
+
authentication, so it is a 22-character secret in the link. A desk configured
|
|
158
|
+
with a public hostname refuses to start without one.
|
|
159
|
+
3. **`deskUrl`** — the 202 the buyer gets says where a human approves. It
|
|
160
|
+
defaults to `http://127.0.0.1:4022`, which is only right when buyer and
|
|
161
|
+
seller share a machine.
|
|
162
|
+
4. **`gw.desk.listen(port)`** — the gateway and the desk are two HTTP servers.
|
|
163
|
+
Starting the gateway alone leaves the queue unreachable.
|
|
164
|
+
|
|
165
|
+
The buyer side needs the agent **secret**, not the vault's signer list:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const agent = new PaydluxAgent({ agentKey: process.env.AGENT_KEY! });
|
|
169
|
+
const res = await agent.get("https://seller.example.com/api/data", { mandate });
|
|
170
|
+
if (!res.ok && res.handle) console.log("parked, awaiting a human at", res.desk);
|
|
171
|
+
```
|
|
172
|
+
|
|
117
173
|
`npm run build` produces `dist/`. The package depends only on
|
|
118
174
|
`@stellar/stellar-sdk` — no transitive `toml`, so `npm audit` is clean.
|
|
119
175
|
|
package/dist/desk.js
CHANGED
|
@@ -417,6 +417,32 @@ export class ApprovalDesk {
|
|
|
417
417
|
}
|
|
418
418
|
const m = url.pathname.match(/^\/(approve|deny|signed)\/([\w-]+)$/);
|
|
419
419
|
if (m && req.method === "POST") {
|
|
420
|
+
// These routes move money, so they must not be reachable from another
|
|
421
|
+
// page. A cross-origin fetch POST is a CORS *simple request*: no
|
|
422
|
+
// preflight, so the attacker's page never sees the reply but the
|
|
423
|
+
// approval still happens. Verified end to end: a page on another port
|
|
424
|
+
// settled a parked payment and paid the seller. Browsers always send
|
|
425
|
+
// Origin on POST and Sec-Fetch-Site on every request, so checking both
|
|
426
|
+
// closes that path while leaving local automation (curl, tests, the
|
|
427
|
+
// phone app on its own origin) alone.
|
|
428
|
+
const origin = String(req.headers.origin ?? "");
|
|
429
|
+
if (origin) {
|
|
430
|
+
const here = String(req.headers.host ?? "").toLowerCase();
|
|
431
|
+
let oh;
|
|
432
|
+
try {
|
|
433
|
+
oh = new URL(origin).host.toLowerCase();
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
oh = "";
|
|
437
|
+
}
|
|
438
|
+
if (oh !== here) {
|
|
439
|
+
return send(403, { error: "cross_origin", reason: `this desk only accepts actions from itself, not from ${origin}` });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const site = String(req.headers["sec-fetch-site"] ?? "").toLowerCase();
|
|
443
|
+
if (site && site !== "same-origin" && site !== "none") {
|
|
444
|
+
return send(403, { error: "cross_origin", reason: `this desk only accepts same-site actions (got ${site})` });
|
|
445
|
+
}
|
|
420
446
|
if (m[1] === "signed") {
|
|
421
447
|
const body = await readBody(req);
|
|
422
448
|
const { signedXdr } = JSON.parse(body || "{}");
|
package/dist/gateway.d.ts
CHANGED
|
@@ -32,6 +32,19 @@ export type GatewayConfig = {
|
|
|
32
32
|
portalKey?: string;
|
|
33
33
|
/** hostname the desk is publicly served on; pins the Host header. See DeskConfig. */
|
|
34
34
|
publicHost?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Agent public keys allowed to park a payment for approval. An intent is only
|
|
37
|
+
* honoured if it is signed by one of these, so an unlisted key gets a plain
|
|
38
|
+
* 402 instead of a queue entry. Leave empty and nothing can request approval.
|
|
39
|
+
*/
|
|
40
|
+
agentKeys?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Where a human approves, as the BUYER should see it. Set this to the public
|
|
43
|
+
* desk URL (e.g. "https://desk.paydlux.com") when the desk is reachable there;
|
|
44
|
+
* it is returned in the 202 so the buyer knows where to go. Defaults to the
|
|
45
|
+
* loopback address, which is only useful on one machine.
|
|
46
|
+
*/
|
|
47
|
+
deskUrl?: string;
|
|
35
48
|
};
|
|
36
49
|
export type PaymentProof = {
|
|
37
50
|
/** pre-authorised envelope, base64 XDR, as issued by the vault */
|
package/dist/gateway.js
CHANGED
|
@@ -26,6 +26,7 @@ export class Gateway {
|
|
|
26
26
|
cfg;
|
|
27
27
|
constructor(cfg) {
|
|
28
28
|
this.cfg = cfg;
|
|
29
|
+
this.agentKeys = cfg.agentKeys ?? [];
|
|
29
30
|
for (const r of cfg.routes)
|
|
30
31
|
this.routes.set(r.uri, r);
|
|
31
32
|
this.desk = new ApprovalDesk({
|
|
@@ -79,7 +80,7 @@ export class Gateway {
|
|
|
79
80
|
return false;
|
|
80
81
|
}
|
|
81
82
|
/** Keys allowed to place approval requests. */
|
|
82
|
-
agentKeys
|
|
83
|
+
agentKeys;
|
|
83
84
|
requirements(uri) {
|
|
84
85
|
const r = this.routes.get(uri);
|
|
85
86
|
return {
|
|
@@ -191,7 +192,12 @@ export class Gateway {
|
|
|
191
192
|
}
|
|
192
193
|
const handle = this.desk.enqueue(uri, r.price, body, getContent(uri));
|
|
193
194
|
res.writeHead(202, { "content-type": "application/json", "x-payment-hold": handle });
|
|
194
|
-
|
|
195
|
+
// The buyer needs to know where a human approves. Loopback is only
|
|
196
|
+
// right when both sides are on one machine; anything real sets deskUrl
|
|
197
|
+
// (or publicHost) so this points at the https desk.
|
|
198
|
+
const deskUrl = this.cfg.deskUrl
|
|
199
|
+
?? (this.cfg.publicHost ? `https://${this.cfg.publicHost}` : `http://127.0.0.1:${this.cfg.deskPort ?? 4022}`);
|
|
200
|
+
res.end(JSON.stringify({ status: "awaiting_approval", handle, desk: deskUrl }));
|
|
195
201
|
return;
|
|
196
202
|
}
|
|
197
203
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paydlux/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Payments for AI agents that a human approves. Spending mandates enforced on the Stellar ledger: 2-of-2 vault, pre-authorised transaction envelopes, HTTP 402, USDC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|