@paydlux/agent 0.1.13 → 0.1.15
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/dist/desk.d.ts +16 -1
- package/dist/desk.js +43 -2
- package/package.json +1 -1
package/dist/desk.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type DeskRecord = {
|
|
|
11
11
|
uri: string;
|
|
12
12
|
price: string;
|
|
13
13
|
body: string;
|
|
14
|
-
status: "pending" | "awaiting_phone" | "approved" | "denied" | "settled" | "failed";
|
|
14
|
+
status: "pending" | "awaiting_phone" | "approved" | "denied" | "settled" | "failed" | "expired";
|
|
15
15
|
content?: string;
|
|
16
16
|
tx?: string;
|
|
17
17
|
reason?: string;
|
|
@@ -80,6 +80,21 @@ export declare class ApprovalDesk {
|
|
|
80
80
|
private server?;
|
|
81
81
|
cfg: DeskConfig;
|
|
82
82
|
constructor(cfg: DeskConfig);
|
|
83
|
+
/**
|
|
84
|
+
* A request the human never answered is not waiting forever.
|
|
85
|
+
*
|
|
86
|
+
* The transaction the phone signs carries Stellar time bounds, so past
|
|
87
|
+
* `expiresAt` the ledger rejects it no matter who signs it. Until this sweep
|
|
88
|
+
* existed such a request stayed `awaiting_phone` for good: it showed in the
|
|
89
|
+
* queue as approvable, held its price against the daily budget so later
|
|
90
|
+
* payments were refused for a money that could never move, and the buyer's
|
|
91
|
+
* retry kept reusing the dead handle instead of parking a fresh one. Three
|
|
92
|
+
* wrong behaviours from one missing transition, which is why the transition
|
|
93
|
+
* lives here rather than in the page that draws the word "expired".
|
|
94
|
+
*
|
|
95
|
+
* Called from every read path, so no caller can observe a stale state.
|
|
96
|
+
*/
|
|
97
|
+
private sweepExpired;
|
|
83
98
|
enqueue(uri: string, price: string, body: string, content: string, key?: string): string;
|
|
84
99
|
/** Every record carrying this key, newest first. */
|
|
85
100
|
allByKey(key: string): DeskRecord[];
|
package/dist/desk.js
CHANGED
|
@@ -159,7 +159,45 @@ export class ApprovalDesk {
|
|
|
159
159
|
server;
|
|
160
160
|
cfg;
|
|
161
161
|
constructor(cfg) { this.cfg = cfg; }
|
|
162
|
+
/**
|
|
163
|
+
* A request the human never answered is not waiting forever.
|
|
164
|
+
*
|
|
165
|
+
* The transaction the phone signs carries Stellar time bounds, so past
|
|
166
|
+
* `expiresAt` the ledger rejects it no matter who signs it. Until this sweep
|
|
167
|
+
* existed such a request stayed `awaiting_phone` for good: it showed in the
|
|
168
|
+
* queue as approvable, held its price against the daily budget so later
|
|
169
|
+
* payments were refused for a money that could never move, and the buyer's
|
|
170
|
+
* retry kept reusing the dead handle instead of parking a fresh one. Three
|
|
171
|
+
* wrong behaviours from one missing transition, which is why the transition
|
|
172
|
+
* lives here rather than in the page that draws the word "expired".
|
|
173
|
+
*
|
|
174
|
+
* Called from every read path, so no caller can observe a stale state.
|
|
175
|
+
*/
|
|
176
|
+
sweepExpired() {
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
// A request nobody has even opened yet gets the same window as one the
|
|
179
|
+
// phone is holding: the seller said how long a human has to decide, and a
|
|
180
|
+
// parked request past that window is not going to be answered. Leaving it
|
|
181
|
+
// `pending` forever means the buyer's retry keeps reusing a handle that
|
|
182
|
+
// will never move, instead of parking a fresh one a human will actually see.
|
|
183
|
+
const decideWindow = (this.cfg.decideWithinSeconds ?? 1800) * 1000;
|
|
184
|
+
for (const r of this.records.values()) {
|
|
185
|
+
let deadline = null;
|
|
186
|
+
if (r.status === "pending") {
|
|
187
|
+
deadline = r.at + decideWindow;
|
|
188
|
+
}
|
|
189
|
+
else if (r.status === "awaiting_phone" || r.status === "approved") {
|
|
190
|
+
deadline = r.challenge?.summary.expiresAt ? Number(r.challenge.summary.expiresAt) * 1000 : null;
|
|
191
|
+
}
|
|
192
|
+
if (deadline === null || deadline > now)
|
|
193
|
+
continue;
|
|
194
|
+
r.status = "expired";
|
|
195
|
+
r.reason = `nobody approved it before the deadline (${new Date(deadline).toISOString()})`;
|
|
196
|
+
console.log(`[desk] ${r.id} expired: the deadline passed with no approval`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
162
199
|
enqueue(uri, price, body, content, key) {
|
|
200
|
+
this.sweepExpired();
|
|
163
201
|
// Re-use an existing request for the same buyer + body + price. A buyer who
|
|
164
202
|
// retries after a human approved must get their content, not a second
|
|
165
203
|
// payment waiting in the queue. Settled records are NOT reused here — the
|
|
@@ -176,6 +214,7 @@ export class ApprovalDesk {
|
|
|
176
214
|
}
|
|
177
215
|
/** Every record carrying this key, newest first. */
|
|
178
216
|
allByKey(key) {
|
|
217
|
+
this.sweepExpired();
|
|
179
218
|
if (!key)
|
|
180
219
|
return [];
|
|
181
220
|
return [...this.records.values()].filter((r) => r.key === key).sort((a, b) => b.at - a.at);
|
|
@@ -183,8 +222,8 @@ export class ApprovalDesk {
|
|
|
183
222
|
findByKey(key) {
|
|
184
223
|
return this.allByKey(key)[0] ?? null;
|
|
185
224
|
}
|
|
186
|
-
get(id) { return this.records.get(id) ?? null; }
|
|
187
|
-
list() { return [...this.records.values()]; }
|
|
225
|
+
get(id) { this.sweepExpired(); return this.records.get(id) ?? null; }
|
|
226
|
+
list() { this.sweepExpired(); return [...this.records.values()]; }
|
|
188
227
|
/**
|
|
189
228
|
* The human clicked Approve.
|
|
190
229
|
*
|
|
@@ -240,6 +279,7 @@ export class ApprovalDesk {
|
|
|
240
279
|
});
|
|
241
280
|
}
|
|
242
281
|
async approve(id) {
|
|
282
|
+
this.sweepExpired();
|
|
243
283
|
const rec = this.records.get(id);
|
|
244
284
|
if (!rec)
|
|
245
285
|
throw new Error("unknown handle " + id);
|
|
@@ -307,6 +347,7 @@ export class ApprovalDesk {
|
|
|
307
347
|
* ledger rejects it if the signature is not over this exact transaction.
|
|
308
348
|
*/
|
|
309
349
|
async complete(id, signedXdr) {
|
|
350
|
+
this.sweepExpired();
|
|
310
351
|
const rec = this.records.get(id);
|
|
311
352
|
if (!rec)
|
|
312
353
|
throw new Error("unknown handle " + id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paydlux/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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",
|