@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 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.13",
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",