@dvmkit/sdk 0.0.0 → 0.1.0-rc.1

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.
Files changed (41) hide show
  1. package/NOTICE +2 -0
  2. package/README.md +38 -2
  3. package/dist/chunk-27V2ILSR.js +291 -0
  4. package/dist/chunk-5GFED3GJ.js +955 -0
  5. package/dist/chunk-6JZIX5WW.js +1155 -0
  6. package/dist/chunk-7IH5SG2A.js +1038 -0
  7. package/dist/chunk-AT6V3SY7.js +102 -0
  8. package/dist/chunk-DCNT4PJS.js +733 -0
  9. package/dist/chunk-DMNLFNTW.js +135 -0
  10. package/dist/chunk-FROTD5XQ.js +70 -0
  11. package/dist/chunk-H25M54MI.js +149 -0
  12. package/dist/chunk-KQAJVVZT.js +712 -0
  13. package/dist/chunk-KXWROQGK.js +74 -0
  14. package/dist/chunk-L4OYF4DQ.js +67 -0
  15. package/dist/chunk-NTK5DJ6R.js +1256 -0
  16. package/dist/chunk-RPXHKMYE.js +3808 -0
  17. package/dist/chunk-S3XAHZQY.js +63 -0
  18. package/dist/chunk-YG7G4DPZ.js +25 -0
  19. package/dist/credit-ledger-EDMEZSA2.js +28 -0
  20. package/dist/index.d.ts +144 -0
  21. package/dist/index.js +303 -0
  22. package/dist/job-store-C5n6bhap.d.ts +5090 -0
  23. package/dist/memory-credit-ledger-7TTZDSRS.js +9 -0
  24. package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
  25. package/dist/mpp-setup-MOBWGTWJ.js +30 -0
  26. package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
  27. package/dist/postgres-job-store-J5F4GUWU.js +7 -0
  28. package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
  29. package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
  30. package/dist/pricing-4CEB34RM.js +48 -0
  31. package/dist/processed-payment-store-HAA4SFNK.js +11 -0
  32. package/dist/revenue-reporter-M35KP6V7.js +435 -0
  33. package/dist/server/index.d.ts +4108 -0
  34. package/dist/server/index.js +22538 -0
  35. package/dist/ssrf-BdHsrrIb.d.ts +325 -0
  36. package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
  37. package/dist/tempo-session-store-FTEEGZXA.js +467 -0
  38. package/dist/testing/index.d.ts +135 -0
  39. package/dist/testing/index.js +151 -0
  40. package/dist/x402-35VLYFKZ.js +1272 -0
  41. package/package.json +89 -6
@@ -0,0 +1,435 @@
1
+ import {
2
+ withSdkInitLock
3
+ } from "./chunk-S3XAHZQY.js";
4
+
5
+ // src/sdk/server/revenue-reporter.ts
6
+ import { randomUUID } from "crypto";
7
+ var ENDPOINTS = {
8
+ revenue: "/_internal/job-revenue",
9
+ deposit: "/_internal/credit-deposit",
10
+ release: "/_internal/credit-draw-release",
11
+ drain: "/_internal/credit-drain"
12
+ };
13
+ var RevenueReporter = class _RevenueReporter {
14
+ // 24h of failed retries
15
+ constructor(db, platformUrl, platformToken) {
16
+ this.db = db;
17
+ this.platformUrl = platformUrl;
18
+ this.platformToken = platformToken;
19
+ }
20
+ db;
21
+ platformUrl;
22
+ platformToken;
23
+ retryTimer = null;
24
+ static RETRY_INTERVAL_MS = 3e4;
25
+ static MAX_BACKOFF_MS = 3e5;
26
+ // 5 min
27
+ static ABANDON_AFTER_MS = 24 * 60 * 60 * 1e3;
28
+ /** Create the pending-reports table (idempotent). */
29
+ async init() {
30
+ await withSdkInitLock(this.db, () => this.createTables());
31
+ }
32
+ /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
33
+ async createTables() {
34
+ await this.db.query(`
35
+ CREATE TABLE IF NOT EXISTS pending_revenue_reports (
36
+ id TEXT PRIMARY KEY,
37
+ payload JSONB NOT NULL,
38
+ retry_count INTEGER NOT NULL DEFAULT 0,
39
+ next_retry_at BIGINT NOT NULL DEFAULT 0,
40
+ stale BOOLEAN NOT NULL DEFAULT false,
41
+ stale_reason TEXT,
42
+ created_at BIGINT NOT NULL,
43
+ kind TEXT NOT NULL DEFAULT 'revenue'
44
+ )
45
+ `);
46
+ await this.db.query(
47
+ "ALTER TABLE pending_revenue_reports ADD COLUMN IF NOT EXISTS stale_reason TEXT"
48
+ );
49
+ await this.db.query(
50
+ "ALTER TABLE pending_revenue_reports ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'revenue'"
51
+ );
52
+ }
53
+ /** Persist a revenue report and attempt immediate delivery. */
54
+ async report(payload) {
55
+ return this.enqueue("revenue", payload, payload.jobId);
56
+ }
57
+ /**
58
+ * Join a credit deposit to the caller-owned rail transaction.
59
+ *
60
+ * This method performs no network I/O: it only writes the reporter-owned
61
+ * durable queue row through `tx`. The retry drain delivers the row after the
62
+ * rail transaction commits, including after a process restart.
63
+ */
64
+ async enqueueDeposit(tx, payload) {
65
+ await this.insertPending(tx, "deposit", payload);
66
+ }
67
+ /** Join a released draw to the transaction that made the hold terminal. */
68
+ async enqueueCreditDrawRelease(tx, payload) {
69
+ await this.insertPending(tx, "release", payload);
70
+ }
71
+ /**
72
+ * Join a credit drain to the transaction that made it terminal (internal-review).
73
+ *
74
+ * Same discipline as {@link enqueueDeposit} and the same reason: no network
75
+ * I/O here, only the durable queue row, written through `tx` so the report
76
+ * and the status CAS commit or roll back together. A drain whose row was
77
+ * lost between the two would overstate outstanding liability forever —
78
+ * the mirror image of a lost deposit.
79
+ *
80
+ * Named for the payload rather than the verb because `drain` already means
81
+ * "flush the queue" in this class (see {@link drainPending}).
82
+ */
83
+ async enqueueCreditDrain(tx, payload) {
84
+ await this.insertPending(tx, "drain", payload);
85
+ }
86
+ /**
87
+ * Report a reaper-force-failed paid job to the platform (internal-review) so the
88
+ * operator receives an ambient notice that a DVM died or wedged mid-job.
89
+ * Best-effort and fire-and-forget: unlike {@link report} there is no local
90
+ * durable queue — a non-2xx or transport failure is structured-logged
91
+ * (`paid_job_death_report_failed`) and dropped, since the reaper's own
92
+ * `stale_job_failed` log is the durable record and a failed *alert* must never
93
+ * wedge the sweep.
94
+ */
95
+ async reportPaidJobDeath(payload) {
96
+ const url = `${this.platformUrl}/_internal/paid-job-death`;
97
+ try {
98
+ const res = await fetch(url, {
99
+ method: "POST",
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ Authorization: `Bearer ${this.platformToken}`
103
+ },
104
+ body: JSON.stringify(payload),
105
+ signal: AbortSignal.timeout(1e4)
106
+ });
107
+ if (!res.ok) {
108
+ console.warn(
109
+ JSON.stringify({
110
+ level: "paid_job_death_report_failed",
111
+ jobId: payload.jobId,
112
+ status: res.status
113
+ })
114
+ );
115
+ }
116
+ } catch (err) {
117
+ console.warn(
118
+ JSON.stringify({
119
+ level: "paid_job_death_report_failed",
120
+ jobId: payload.jobId,
121
+ error: err instanceof Error ? err.message : String(err)
122
+ })
123
+ );
124
+ }
125
+ }
126
+ /**
127
+ * Report a settled draw whose revenue event was skipped (internal-review).
128
+ * Best-effort and fire-and-forget: the SDK's structured
129
+ * `revenue_skipped_no_rail` log is the durable backstop, so a failed notice
130
+ * must never wedge terminal handling or the orphan-draw reconciler.
131
+ */
132
+ async reportRevenueSkippedNoRail(payload) {
133
+ const url = `${this.platformUrl}/_internal/revenue-skipped-no-rail`;
134
+ try {
135
+ const res = await fetch(url, {
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Authorization: `Bearer ${this.platformToken}`
140
+ },
141
+ body: JSON.stringify(payload),
142
+ signal: AbortSignal.timeout(1e4)
143
+ });
144
+ if (!res.ok) {
145
+ console.warn(
146
+ JSON.stringify({
147
+ level: "revenue_skipped_no_rail_report_failed",
148
+ jobId: payload.jobId,
149
+ drawId: payload.drawId,
150
+ reason: payload.reason,
151
+ status: res.status
152
+ })
153
+ );
154
+ }
155
+ } catch (err) {
156
+ console.warn(
157
+ JSON.stringify({
158
+ level: "revenue_skipped_no_rail_report_failed",
159
+ jobId: payload.jobId,
160
+ drawId: payload.drawId,
161
+ reason: payload.reason,
162
+ error: err instanceof Error ? err.message : String(err)
163
+ })
164
+ );
165
+ }
166
+ }
167
+ /** Report the self-relay's Base gas gauge for platform-side paging and recovery. */
168
+ async reportX402SelfRelayGasBalance(payload) {
169
+ const url = `${this.platformUrl}/_internal/x402-self-relay-gas-balance`;
170
+ try {
171
+ const res = await fetch(url, {
172
+ method: "POST",
173
+ headers: {
174
+ "Content-Type": "application/json",
175
+ Authorization: `Bearer ${this.platformToken}`
176
+ },
177
+ body: JSON.stringify(payload),
178
+ signal: AbortSignal.timeout(1e4)
179
+ });
180
+ if (!res.ok) {
181
+ console.warn(
182
+ JSON.stringify({
183
+ level: "x402_self_relay_gas_balance_report_failed",
184
+ address: payload.address,
185
+ status: res.status
186
+ })
187
+ );
188
+ }
189
+ } catch (err) {
190
+ console.warn(
191
+ JSON.stringify({
192
+ level: "x402_self_relay_gas_balance_report_failed",
193
+ address: payload.address,
194
+ error: err instanceof Error ? err.message : String(err)
195
+ })
196
+ );
197
+ }
198
+ }
199
+ /** Report redacted self-relay RPC health for platform-side paging and recovery. */
200
+ async reportX402SelfRelayRpcHealth(payload) {
201
+ const url = `${this.platformUrl}/_internal/x402-self-relay-rpc-health`;
202
+ try {
203
+ const res = await fetch(url, {
204
+ method: "POST",
205
+ headers: {
206
+ "Content-Type": "application/json",
207
+ Authorization: `Bearer ${this.platformToken}`
208
+ },
209
+ body: JSON.stringify(payload),
210
+ signal: AbortSignal.timeout(1e4)
211
+ });
212
+ if (!res.ok) {
213
+ console.warn(
214
+ JSON.stringify({
215
+ level: "x402_self_relay_rpc_health_report_failed",
216
+ address: payload.address,
217
+ status: res.status
218
+ })
219
+ );
220
+ }
221
+ } catch {
222
+ console.warn(
223
+ JSON.stringify({
224
+ level: "x402_self_relay_rpc_health_report_failed",
225
+ address: payload.address,
226
+ reason: "transport"
227
+ })
228
+ );
229
+ }
230
+ }
231
+ /** Report the Tempo operator's fee-token gauge for paging and recovery. */
232
+ async reportTempoSettlementBalance(payload) {
233
+ await this.postTempoSettlementReadiness("balance", payload);
234
+ }
235
+ /** Report a redacted operator fee rejection immediately. */
236
+ async reportTempoSettlementFailure(payload) {
237
+ await this.postTempoSettlementReadiness("failure", payload);
238
+ }
239
+ async postTempoSettlementReadiness(kind, payload) {
240
+ const url = `${this.platformUrl}/_internal/tempo-settlement-${kind}`;
241
+ try {
242
+ const res = await fetch(url, {
243
+ method: "POST",
244
+ headers: {
245
+ "Content-Type": "application/json",
246
+ Authorization: `Bearer ${this.platformToken}`
247
+ },
248
+ body: JSON.stringify(payload),
249
+ signal: AbortSignal.timeout(1e4)
250
+ });
251
+ if (!res.ok) {
252
+ console.warn(
253
+ JSON.stringify({
254
+ level: "tempo_settlement_readiness_report_failed",
255
+ kind,
256
+ address: payload.address,
257
+ status: res.status
258
+ })
259
+ );
260
+ }
261
+ } catch {
262
+ console.warn(
263
+ JSON.stringify({
264
+ level: "tempo_settlement_readiness_report_failed",
265
+ kind,
266
+ address: payload.address,
267
+ reason: "transport"
268
+ })
269
+ );
270
+ }
271
+ }
272
+ /** Start the background retry loop. */
273
+ startRetryLoop() {
274
+ if (this.retryTimer) return;
275
+ this.retryTimer = setInterval(() => {
276
+ void this.drainPending();
277
+ }, _RevenueReporter.RETRY_INTERVAL_MS);
278
+ }
279
+ /** Stop the background retry loop. */
280
+ stop() {
281
+ if (this.retryTimer) {
282
+ clearInterval(this.retryTimer);
283
+ this.retryTimer = null;
284
+ }
285
+ }
286
+ /**
287
+ * Operator-facing queue snapshot: distinguishes the live retry queue from
288
+ * terminal (stale) rows and breaks the latter down by classification reason
289
+ * (`token_rotated`, `client_error:<status>`, `abandoned`). Lets monitoring tell
290
+ * transient backlog apart from permanently-failed rows.
291
+ */
292
+ async getQueueStats() {
293
+ const { rows } = await this.db.query(
294
+ `SELECT stale, stale_reason, COUNT(*)::int AS count
295
+ FROM pending_revenue_reports
296
+ GROUP BY stale, stale_reason`
297
+ );
298
+ let liveRetry = 0;
299
+ let stale = 0;
300
+ const byReason = {};
301
+ for (const row of rows) {
302
+ const count = Number(row.count);
303
+ if (row.stale) {
304
+ stale += count;
305
+ const reason = row.stale_reason ?? "unknown";
306
+ byReason[reason] = (byReason[reason] ?? 0) + count;
307
+ } else {
308
+ liveRetry += count;
309
+ }
310
+ }
311
+ return { liveRetry, stale, byReason };
312
+ }
313
+ /**
314
+ * Deliver one bounded batch from the durable queue.
315
+ *
316
+ * Public so startup/recovery tests and operator tooling can drive the same
317
+ * drain the background timer uses without reaching into private state.
318
+ */
319
+ async drainPending() {
320
+ try {
321
+ const { rows } = await this.db.query(
322
+ `SELECT id, payload, retry_count, created_at, kind FROM pending_revenue_reports
323
+ WHERE stale = false AND next_retry_at <= $1
324
+ ORDER BY created_at LIMIT 10`,
325
+ [Date.now()]
326
+ );
327
+ for (const row of rows) {
328
+ const label = row.payload.jobId ?? row.payload.creditId ?? row.id;
329
+ try {
330
+ await this.attemptDelivery(row.id, row.payload, row.kind, label);
331
+ } catch {
332
+ const retryCount = row.retry_count + 1;
333
+ if (Date.now() - Number(row.created_at) >= _RevenueReporter.ABANDON_AFTER_MS) {
334
+ await this.markStale(row.id, "abandoned");
335
+ console.warn(
336
+ JSON.stringify({
337
+ level: "revenue_reporter_abandoned",
338
+ id: row.id,
339
+ kind: row.kind,
340
+ retryCount,
341
+ dvmId: row.payload.dvmId,
342
+ label,
343
+ // A drain names its payout rail `method`; every other kind
344
+ // carries `rail`. An abandoned row is the one place an
345
+ // operator sees this payload at all, so don't log it blank.
346
+ rail: row.payload.rail ?? row.payload.method
347
+ })
348
+ );
349
+ continue;
350
+ }
351
+ const backoff = Math.min(
352
+ 3e4 * Math.pow(2, retryCount - 1),
353
+ _RevenueReporter.MAX_BACKOFF_MS
354
+ );
355
+ await this.db.query(
356
+ `UPDATE pending_revenue_reports
357
+ SET retry_count = $1, next_retry_at = $2
358
+ WHERE id = $3`,
359
+ [retryCount, Date.now() + backoff, row.id]
360
+ );
361
+ }
362
+ }
363
+ } catch (err) {
364
+ console.error("[revenue-reporter] Retry loop error:", err);
365
+ }
366
+ }
367
+ /** Durably queue one report of either kind, then try it once immediately. */
368
+ async enqueue(kind, payload, label) {
369
+ const id = await this.insertPending(this.db, kind, payload);
370
+ void this.attemptDelivery(id, payload, kind, label).catch(() => {
371
+ });
372
+ }
373
+ /** Insert one pending report through either the pool or an open transaction. */
374
+ async insertPending(q, kind, payload) {
375
+ const id = randomUUID();
376
+ await q.query(
377
+ `INSERT INTO pending_revenue_reports (id, payload, created_at, kind)
378
+ VALUES ($1, $2, $3, $4)`,
379
+ [id, JSON.stringify(payload), Date.now(), kind]
380
+ );
381
+ return id;
382
+ }
383
+ async attemptDelivery(id, payload, kind, label) {
384
+ const url = `${this.platformUrl}${ENDPOINTS[kind]}`;
385
+ let res;
386
+ try {
387
+ res = await fetch(url, {
388
+ method: "POST",
389
+ headers: {
390
+ "Content-Type": "application/json",
391
+ Authorization: `Bearer ${this.platformToken}`
392
+ },
393
+ body: JSON.stringify(payload),
394
+ signal: AbortSignal.timeout(1e4)
395
+ });
396
+ } catch (err) {
397
+ const reason = err instanceof Error ? err.message : String(err);
398
+ console.warn(
399
+ `[revenue-reporter] delivery failed for ${id}: transport error (${reason}) \u2014 will retry`
400
+ );
401
+ throw err;
402
+ }
403
+ if (res.ok) {
404
+ await this.db.query("DELETE FROM pending_revenue_reports WHERE id = $1", [id]);
405
+ console.log(`[revenue-reporter] reported ${kind} ${label}`);
406
+ return;
407
+ }
408
+ if (res.status === 401) {
409
+ await this.markStale(id, "token_rotated");
410
+ console.warn(
411
+ `[revenue-reporter] Report ${id} got 401 \u2014 marking stale (token likely rotated)`
412
+ );
413
+ return;
414
+ }
415
+ if (res.status >= 400 && res.status < 500) {
416
+ await this.markStale(id, `client_error:${res.status}`);
417
+ console.warn(
418
+ `[revenue-reporter] Report ${id} got HTTP ${res.status} \u2014 marking stale (permanent client error, will not retry)`
419
+ );
420
+ return;
421
+ }
422
+ console.warn(`[revenue-reporter] delivery failed for ${id}: HTTP ${res.status} \u2014 will retry`);
423
+ throw new Error(`Platform returned ${res.status}`);
424
+ }
425
+ /** Mark a pending row terminal (stale) with a classification reason. */
426
+ async markStale(id, reason) {
427
+ await this.db.query(
428
+ "UPDATE pending_revenue_reports SET stale = true, stale_reason = $1 WHERE id = $2",
429
+ [reason, id]
430
+ );
431
+ }
432
+ };
433
+ export {
434
+ RevenueReporter
435
+ };