@dvmkit/sdk 0.7.0-rc.9 → 0.8.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 (57) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +57 -261
  3. package/dist/{chunk-5CSR33B5.js → chunk-2U6LAASN.js} +1 -1
  4. package/dist/chunk-66HGCPBU.js +25 -0
  5. package/dist/{chunk-QM2OS3NJ.js → chunk-7PM4R4GD.js} +1 -1
  6. package/dist/{chunk-YE53E4BT.js → chunk-AGE5XZKE.js} +1 -1
  7. package/dist/{chunk-RJFBBEWQ.js → chunk-ALWVJ2RB.js} +1 -1
  8. package/dist/{chunk-FYETJDBB.js → chunk-AVV5SYGO.js} +2 -2
  9. package/dist/{chunk-5PLQOC4B.js → chunk-BFGQG4WY.js} +9 -1
  10. package/dist/{chunk-IOKEKMIU.js → chunk-FM6QAGJC.js} +2 -2
  11. package/dist/{chunk-KXWROQGK.js → chunk-FUJ36YDV.js} +1 -24
  12. package/dist/{chunk-6JZDFYQL.js → chunk-G3MPHW5I.js} +1 -1
  13. package/dist/{chunk-2HAFZ5GC.js → chunk-IHQUBATH.js} +2 -2
  14. package/dist/{chunk-YS6PWVCZ.js → chunk-ILBSZMJL.js} +1 -1
  15. package/dist/{chunk-RGSWVNQG.js → chunk-IOQIXRGM.js} +2 -43
  16. package/dist/{chunk-YNHXOONR.js → chunk-J5J63WDC.js} +1 -1
  17. package/dist/{chunk-G7IXOVKV.js → chunk-JIZ4ZIXX.js} +331 -102
  18. package/dist/{chunk-ZLWPEIE2.js → chunk-NA2KOR6G.js} +1 -1
  19. package/dist/{chunk-2DE32RAW.js → chunk-NU5RWJ5B.js} +1 -1
  20. package/dist/chunk-OP7VCGQR.js +47 -0
  21. package/dist/{chunk-QN3CQOE2.js → chunk-QKE5NDPF.js} +368 -7
  22. package/dist/{chunk-7RZEJJTN.js → chunk-RSNC5T6C.js} +2 -2
  23. package/dist/{chunk-2GTLF74N.js → chunk-UDJNZ3UG.js} +1 -1
  24. package/dist/{chunk-WGK5KZ6S.js → chunk-UVS7MRJX.js} +3 -3
  25. package/dist/{chunk-HU6ZMQDN.js → chunk-UWGBI5WK.js} +58 -1
  26. package/dist/{chunk-OFSOLHRX.js → chunk-WMGXJQI3.js} +1 -1
  27. package/dist/{chunk-5TIFEMHJ.js → chunk-ZEPJF4JC.js} +2 -2
  28. package/dist/{content-retention-U7dJM7Ph.d.ts → content-retention-B7ddtJ2y.d.ts} +1 -1
  29. package/dist/{credit-ledger-F2LWQU6N.js → credit-ledger-KIQIE6I6.js} +3 -3
  30. package/dist/{credit-menu-CUJdCfpt.d.ts → credit-menu-BH3g9GQa.d.ts} +5 -5
  31. package/dist/{fx-BvG2Wnjk.d.ts → fx-DN26_J7o.d.ts} +1 -1
  32. package/dist/index.d.ts +5 -5
  33. package/dist/index.js +4 -4
  34. package/dist/internal/caller.d.ts +10 -8
  35. package/dist/internal/caller.js +12 -9
  36. package/dist/internal/server.d.ts +7 -7
  37. package/dist/internal/server.js +22 -20
  38. package/dist/{job-store-DjgN2AOk.d.ts → job-store-Cq21JT37.d.ts} +1 -1
  39. package/dist/{lightning-backend-DeasHNOP.d.ts → lightning-backend-S8F8nf8q.d.ts} +1 -1
  40. package/dist/{memory-credit-ledger-XOOWJTBZ.js → memory-credit-ledger-ATYFAYBG.js} +4 -4
  41. package/dist/{mpp-setup-KGX6PLAF.js → mpp-setup-ZTORW3VF.js} +4 -2
  42. package/dist/{payout-reporter-W3CCLYCN.js → payout-reporter-NN6K2IVQ.js} +2 -2
  43. package/dist/{postgres-consumed-credential-store-LUJ5CUJS.js → postgres-consumed-credential-store-XJTE5PCA.js} +1 -1
  44. package/dist/{postgres-job-store-IX7IUXF4.js → postgres-job-store-AR6ZJUQV.js} +2 -2
  45. package/dist/{postgres-replay-store-Q6MEQDWZ.js → postgres-replay-store-33K33QV7.js} +2 -2
  46. package/dist/{pricing-ZUEFO3CM.js → pricing-G5IN5YJE.js} +3 -3
  47. package/dist/{revenue-reporter-NRS2FANA.js → revenue-reporter-KDUZLD5F.js} +2 -2
  48. package/dist/server/index.d.ts +10 -10
  49. package/dist/server/index.js +30 -28
  50. package/dist/{step-cache-C9JRSFhM.d.ts → step-cache-CR12EZ1Q.d.ts} +136 -4
  51. package/dist/{tempo-charge-store-NZJTJCUO.js → tempo-charge-store-XEGJYM3G.js} +2 -2
  52. package/dist/{tempo-session-store-ENFDLDKC.js → tempo-session-store-3WTDOO7H.js} +3 -3
  53. package/dist/testing/index.d.ts +3 -3
  54. package/dist/testing/index.js +8 -6
  55. package/dist/{usd-DXDkHn_8.d.ts → usd-s_VZ_eS7.d.ts} +1 -1
  56. package/dist/{x402-T7WVKDD4.js → x402-HQ5GRQZU.js} +6 -5
  57. package/package.json +18 -3
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-5PLQOC4B.js";
3
+ } from "./chunk-BFGQG4WY.js";
4
4
  import {
5
5
  withSdkInitLock
6
6
  } from "./chunk-COYKJRMM.js";
@@ -0,0 +1,47 @@
1
+ import {
2
+ emitDiagnostic,
3
+ projectDiagnosticFields
4
+ } from "./chunk-BFGQG4WY.js";
5
+
6
+ // src/sdk/logger.ts
7
+ function createConsoleLogger(_jobId) {
8
+ return {
9
+ debug(_message, data) {
10
+ emitDiagnostic("debug", "diagnostic.message", data);
11
+ },
12
+ info(_message, data) {
13
+ emitDiagnostic("info", "diagnostic.message", data);
14
+ },
15
+ warn(_message, data) {
16
+ emitDiagnostic("warn", "diagnostic.message", data);
17
+ },
18
+ error(_message, data) {
19
+ emitDiagnostic("error", "diagnostic.message", data);
20
+ }
21
+ };
22
+ }
23
+ function createStdoutLogger(context) {
24
+ const emit = (level) => (_message, data) => {
25
+ emitDiagnostic(level, "diagnostic.message", {
26
+ ...projectDiagnosticFields(context),
27
+ ...projectDiagnosticFields(data)
28
+ });
29
+ };
30
+ return {
31
+ debug: emit("debug"),
32
+ info: emit("info"),
33
+ warn: emit("warn"),
34
+ error: emit("error")
35
+ };
36
+ }
37
+ function createNoopLogger() {
38
+ const noop = () => {
39
+ };
40
+ return { debug: noop, info: noop, warn: noop, error: noop };
41
+ }
42
+
43
+ export {
44
+ createConsoleLogger,
45
+ createStdoutLogger,
46
+ createNoopLogger
47
+ };
@@ -1,6 +1,9 @@
1
+ import {
2
+ loggers
3
+ } from "./chunk-IOQIXRGM.js";
1
4
  import {
2
5
  diagnosticConsole
3
- } from "./chunk-5PLQOC4B.js";
6
+ } from "./chunk-BFGQG4WY.js";
4
7
 
5
8
  // src/sdk/server/mpp-setup.ts
6
9
  import { createHash, randomUUID } from "crypto";
@@ -533,7 +536,7 @@ function buildMppMethods(opts, env) {
533
536
  })
534
537
  );
535
538
  if (opts.tempoSession) {
536
- methods.push(
539
+ methods.push(withAuthenticationOnlyValidation(
537
540
  tempo.session({
538
541
  recipient: opts.tempoRecipient,
539
542
  account: opts.tempoSession.account,
@@ -554,15 +557,51 @@ function buildMppMethods(opts, env) {
554
557
  chainId: opts.tempoSession.chainId ?? chainId,
555
558
  ...opts.tempoSession.getClient && { getClient: opts.tempoSession.getClient },
556
559
  ...opts.tempoSession.onSessionSettlement && {
557
- onSessionSettlement: opts.tempoSession.onSessionSettlement
560
+ onSessionSettlement: claimingCloseSettlement(opts.tempoSession)
558
561
  }
559
562
  })
560
- );
563
+ ));
561
564
  }
562
565
  }
563
566
  if (methods.length === 0 || !tempoConfiguration) return void 0;
564
567
  return { methods, tempoConfiguration };
565
568
  }
569
+ function claimingCloseSettlement(session) {
570
+ const callback = session.onSessionSettlement;
571
+ const store = session.store;
572
+ if (session.hostedObligations || !store.getClose || !store.claimCloseReference) return callback;
573
+ return async (context) => {
574
+ if (context.trigger === "close") {
575
+ const intent = await store.getClose(context.channelId);
576
+ if (intent) {
577
+ let claimed = true;
578
+ try {
579
+ claimed = (await store.claimCloseReference(
580
+ context.channelId,
581
+ intent.drainId,
582
+ context.txHash
583
+ )).claimed;
584
+ } catch {
585
+ }
586
+ if (!claimed) return;
587
+ }
588
+ }
589
+ await callback(context);
590
+ };
591
+ }
592
+ function withAuthenticationOnlyValidation(method) {
593
+ if (method.validate) return method;
594
+ const validate = async ({ credential, request }) => ({
595
+ challenge: credential.challenge,
596
+ credential,
597
+ details: void 0,
598
+ intent: method.intent,
599
+ method: method.name,
600
+ request,
601
+ ...credential.source !== void 0 && { source: credential.source }
602
+ });
603
+ return { ...method, validate };
604
+ }
566
605
  function resolveTempoNetwork(env) {
567
606
  const testnet = env.DVMKIT_TEMPO_TESTNET === "1" || env.DVMKIT_TEMPO_TESTNET === "true";
568
607
  const rawChainId = env.DVMKIT_TEMPO_CHAIN_ID?.trim();
@@ -612,6 +651,9 @@ function wrapMppx(mppx) {
612
651
  };
613
652
  return Object.assign(server, { issueChallenge });
614
653
  }
654
+ var TEMPO_CLOSE_LOG_WINDOW = 5000n;
655
+ var TEMPO_CLOSE_LOG_SLACK_MS = 10 * 60 * 1e3;
656
+ var TEMPO_CLOSE_LOG_MAX_WINDOWS = 500;
615
657
  var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
616
658
  getChannelStatesBatch: TempoSession.Precompile.Chain.getChannelStatesBatch,
617
659
  settle: TempoSession.Server.settle,
@@ -621,7 +663,57 @@ var DEFAULT_TEMPO_SESSION_CHAIN_OPS = {
621
663
  "ChannelClosed",
622
664
  channelId
623
665
  )
624
- )
666
+ ),
667
+ findChannelClose: async (client, channelId, escrowContract, notBeforeMs, startBlock) => {
668
+ const { getBlock, getBlockNumber, getLogs, getTransaction } = await import("viem/actions");
669
+ const eventNamed = (name) => {
670
+ const event = TempoSession.Precompile.escrowAbi.find(
671
+ (item) => item.type === "event" && item.name === name
672
+ );
673
+ if (!event || event.type !== "event") throw new Error(`Tempo escrow ABI has no ${name} event`);
674
+ return event;
675
+ };
676
+ const closedEvent = eventNamed("ChannelClosed");
677
+ const requestedEvent = eventNamed("CloseRequested");
678
+ const cancelledEvent = eventNamed("CloseRequestCancelled");
679
+ const beforeBound = async (blockNumber) => Number((await getBlock(client, { blockNumber })).timestamp) * 1e3 < notBeforeMs - TEMPO_CLOSE_LOG_SLACK_MS;
680
+ if (startBlock !== void 0 && await beforeBound(startBlock)) {
681
+ return { outcome: "none", stoppedAt: startBlock };
682
+ }
683
+ let toBlock = startBlock ?? await getBlockNumber(client);
684
+ for (let window = 0; window < TEMPO_CLOSE_LOG_MAX_WINDOWS; window += 1) {
685
+ const fromBlock = toBlock >= TEMPO_CLOSE_LOG_WINDOW ? toBlock - TEMPO_CLOSE_LOG_WINDOW + 1n : 0n;
686
+ const range = { address: escrowContract, args: { channelId }, fromBlock, toBlock };
687
+ const [closed, requested, cancelled] = await Promise.all([
688
+ getLogs(client, { ...range, event: closedEvent }),
689
+ getLogs(client, { ...range, event: requestedEvent }),
690
+ getLogs(client, { ...range, event: cancelledEvent })
691
+ ]);
692
+ const newest = [
693
+ ...closed.map((log) => ({ log, kind: "closed" })),
694
+ ...requested.map((log) => ({ log, kind: "requested" })),
695
+ ...cancelled.map((log) => ({ log, kind: "cancelled" }))
696
+ ].filter(({ log }) => log.transactionHash && log.blockNumber !== null).sort((left, right) => left.log.blockNumber === right.log.blockNumber ? Number(right.log.logIndex ?? 0) - Number(left.log.logIndex ?? 0) : right.log.blockNumber > left.log.blockNumber ? 1 : -1)[0];
697
+ if (newest?.kind === "closed") {
698
+ const transaction = await getTransaction(client, {
699
+ hash: newest.log.transactionHash
700
+ });
701
+ return { outcome: "closed", txHash: newest.log.transactionHash, sender: transaction.from };
702
+ }
703
+ if (newest?.kind === "requested") {
704
+ const graceEnd = newest.log.args.closeGraceEnd;
705
+ const head = await getBlock(client, { blockTag: "latest" });
706
+ if (graceEnd === void 0 || head.timestamp < graceEnd) return { outcome: "none" };
707
+ return { outcome: "exit_requested" };
708
+ }
709
+ if (newest) return { outcome: "none", stoppedAt: 0n };
710
+ if (fromBlock === 0n || await beforeBound(fromBlock)) {
711
+ return { outcome: "none", stoppedAt: fromBlock };
712
+ }
713
+ toBlock = fromBlock - 1n;
714
+ }
715
+ return { outcome: "paused", resumeAt: toBlock };
716
+ }
625
717
  };
626
718
  function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
627
719
  let creditLedger;
@@ -642,6 +734,102 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
642
734
  }
643
735
  if (store) {
644
736
  Object.assign(mppx, {
737
+ assertTempoSessionCloseVoucher: async (voucher) => {
738
+ const descriptor = readChannelDescriptor(voucher.descriptor);
739
+ if (!descriptor) {
740
+ throw new MppxErrors.VerificationFailedError({
741
+ reason: "descriptor required for TIP-1034 session action"
742
+ });
743
+ }
744
+ const details = isStateRecord(voucher.request.methodDetails) ? voucher.request.methodDetails : {};
745
+ const stored = await store.get(voucher.channelId.toLowerCase());
746
+ const state = readTempoSessionState(stored);
747
+ const chainId = typeof details.chainId === "number" ? details.chainId : state?.chainId;
748
+ const escrow = typeof details.escrowContract === "string" ? details.escrowContract : state?.escrowContract;
749
+ if (chainId === void 0 || escrow === void 0) {
750
+ throw new MppxErrors.VerificationFailedError({ reason: "invalid session request" });
751
+ }
752
+ const computed = TempoSession.Precompile.Channel.computeId({
753
+ ...descriptor,
754
+ chainId,
755
+ escrow
756
+ });
757
+ if (computed.toLowerCase() !== voucher.channelId.toLowerCase()) {
758
+ throw new MppxErrors.VerificationFailedError({
759
+ reason: "channel descriptor does not match channelId"
760
+ });
761
+ }
762
+ if (!sameHex(descriptor.payee, voucher.request.recipient)) {
763
+ throw new MppxErrors.VerificationFailedError({
764
+ reason: "channel descriptor payee does not match server destination"
765
+ });
766
+ }
767
+ if (!sameHex(descriptor.token, voucher.request.currency)) {
768
+ throw new MppxErrors.VerificationFailedError({
769
+ reason: "channel descriptor token does not match server token"
770
+ });
771
+ }
772
+ if (!sameHex(descriptor.operator, details.operator ?? ZERO_ADDRESS)) {
773
+ throw new MppxErrors.VerificationFailedError({
774
+ reason: "channel descriptor operator does not match server operator"
775
+ });
776
+ }
777
+ if (!state || !isStateRecord(stored)) {
778
+ throw new MppxErrors.ChannelNotFoundError({ reason: "channel not found" });
779
+ }
780
+ if (stored.backend !== "precompile") {
781
+ throw new MppxErrors.VerificationFailedError({
782
+ reason: "channel is not precompile-backed"
783
+ });
784
+ }
785
+ const storedDescriptor = readChannelDescriptor(stored.descriptor);
786
+ if (!storedDescriptor || !sameDescriptor(descriptor, storedDescriptor)) {
787
+ throw new MppxErrors.VerificationFailedError({
788
+ reason: "credential descriptor does not match stored channel"
789
+ });
790
+ }
791
+ const signer = stored.authorizedSigner;
792
+ if (typeof signer !== "string" || !HEX_ADDRESS_RE.test(signer)) {
793
+ throw new MppxErrors.ChannelNotFoundError({ reason: "channel not found" });
794
+ }
795
+ if (state.finalized) {
796
+ throw new MppxErrors.ChannelClosedError({ reason: "channel is already finalized" });
797
+ }
798
+ if (state.closeRequestedAt !== 0n) {
799
+ throw new MppxErrors.ChannelClosedError({ reason: "channel has a pending close request" });
800
+ }
801
+ if (state.deposit === 0n && (voucher.cumulativeAmount !== 0n || state.spent !== 0n)) {
802
+ throw new MppxErrors.ChannelClosedError({ reason: "channel deposit is zero (settled)" });
803
+ }
804
+ if (voucher.cumulativeAmount < state.spent) {
805
+ throw new MppxErrors.VerificationFailedError({
806
+ reason: `close voucher amount must be >= ${state.spent} (spent)`
807
+ });
808
+ }
809
+ if (voucher.cumulativeAmount < state.settledOnChain) {
810
+ throw new MppxErrors.VerificationFailedError({
811
+ reason: `close voucher amount must be >= ${state.settledOnChain} (on-chain settled)`
812
+ });
813
+ }
814
+ const valid = TempoSession.Precompile.Voucher.verifyVoucher(
815
+ escrow,
816
+ chainId,
817
+ {
818
+ channelId: voucher.channelId,
819
+ cumulativeAmount: voucher.cumulativeAmount,
820
+ signature: voucher.signature
821
+ },
822
+ signer
823
+ );
824
+ if (!valid) {
825
+ throw new MppxErrors.InvalidSignatureError({ reason: "invalid voucher signature" });
826
+ }
827
+ if (maxBigInt(state.spent, state.settledOnChain) > state.deposit) {
828
+ throw new MppxErrors.AmountExceedsDepositError({
829
+ reason: "close capture amount exceeds on-chain deposit"
830
+ });
831
+ }
832
+ },
645
833
  getTempoSessionSpent: async (channelId) => {
646
834
  const state = readTempoSessionState(await store.get(channelId.toLowerCase()));
647
835
  if (!state) throw new Error(`Tempo session channel ${channelId} not found`);
@@ -963,6 +1151,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
963
1151
  }
964
1152
  }
965
1153
  }
1154
+ await reconcileUnsubmittedCloses();
966
1155
  return { scanned: rows.length, closeRequested, settled, inFlight };
967
1156
  };
968
1157
  const recoverTempoSessionClose = async (channelId, drainId) => {
@@ -970,7 +1159,8 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
970
1159
  const intent = await store.getClose(channelId);
971
1160
  if (intent?.drainId !== drainId) return null;
972
1161
  if (intent.reference) return intent.reference;
973
- const current = readTempoSessionState(await store.get(channelId));
1162
+ const stored = await store.get(channelId);
1163
+ const current = readTempoSessionState(stored);
974
1164
  if (!current) throw new Error(`Tempo session channel ${channelId} not found`);
975
1165
  let finalized = current.finalized;
976
1166
  if (!finalized) {
@@ -984,9 +1174,150 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
984
1174
  finalized = chainState.deposit === 0n;
985
1175
  }
986
1176
  if (!finalized) return null;
987
- const recovered = await store.confirmClose(channelId, drainId, `channel:${channelId}`);
1177
+ const pending = await config.hostedObligations?.pendingTempoClose(channelId, drainId);
1178
+ const selfHostedReport = !config.hostedObligations && !!config.onSessionSettlement && !!store.claimCloseReference && !!store.getCloseRecovery;
1179
+ let reference = pending?.txHash ?? void 0;
1180
+ if (pending && !reference || selfHostedReport) {
1181
+ const recovery = await store.getCloseRecovery?.(channelId);
1182
+ const notBeforeMs = pending?.reservedAtMs ?? recovery?.updatedAtMs;
1183
+ if (notBeforeMs === void 0) throw new Error(`Tempo session close intent ${channelId} not found`);
1184
+ const obligations = config.hostedObligations;
1185
+ const found = await findFinalizedClose(current, stored, drainId, notBeforeMs, pending ? {
1186
+ from: pending.searchBefore,
1187
+ save: (resumeAt) => obligations.recordTempoCloseSearch(pending.id, resumeAt)
1188
+ } : {
1189
+ from: recovery?.searchBefore ?? null,
1190
+ save: (resumeAt) => store.recordCloseSearch?.(channelId, drainId, resumeAt)
1191
+ });
1192
+ if (pending) {
1193
+ reference = await settleUnsubmittedClose(current.channelId, drainId, pending.id, found);
1194
+ } else if (found.outcome === "ours") {
1195
+ const claimed = await store.claimCloseReference(channelId, drainId, found.txHash);
1196
+ if (claimed.claimed) {
1197
+ await reportRecoveredClose(current, found.txHash, recovery?.settledBeforeClose ?? null);
1198
+ }
1199
+ return claimed.reference;
1200
+ } else if (found.outcome === "none") {
1201
+ loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1202
+ channel_id: current.channelId,
1203
+ drain_id: drainId
1204
+ });
1205
+ }
1206
+ }
1207
+ const recovered = await store.confirmClose(
1208
+ channelId,
1209
+ drainId,
1210
+ reference ?? `channel:${channelId}`
1211
+ );
988
1212
  return recovered.reference;
989
1213
  };
1214
+ const findFinalizedClose = async (current, stored, drainId, notBeforeMs, cursor) => {
1215
+ const client = await getClient({ chainId: current.chainId });
1216
+ const search = await chainOps.findChannelClose(
1217
+ client,
1218
+ current.channelId,
1219
+ current.escrowContract,
1220
+ notBeforeMs,
1221
+ cursor.from ?? void 0
1222
+ );
1223
+ if (search.outcome === "paused") {
1224
+ await cursor.save(search.resumeAt);
1225
+ loggers.tempo.warn("tempo_session_close_recovery_search_paused", {
1226
+ channel_id: current.channelId,
1227
+ drain_id: drainId,
1228
+ resume_block: search.resumeAt.toString()
1229
+ });
1230
+ throw new Error(`Tempo close search for channel ${current.channelId} continues on the next poll`);
1231
+ }
1232
+ if (search.outcome === "closed") {
1233
+ return cooperativeCloseSenders(stored, config.account.address).has(search.sender.toLowerCase()) ? { outcome: "ours", txHash: search.txHash } : { outcome: "exit" };
1234
+ }
1235
+ if (search.outcome === "exit_requested") return { outcome: "exit" };
1236
+ if (search.stoppedAt !== void 0) await cursor.save(search.stoppedAt);
1237
+ return { outcome: "none" };
1238
+ };
1239
+ const settleUnsubmittedClose = async (channelId, drainId, obligationId, found) => {
1240
+ const obligations = config.hostedObligations;
1241
+ if (found.outcome === "ours") {
1242
+ await obligations.recordSubmitted(obligationId, found.txHash);
1243
+ return found.txHash;
1244
+ }
1245
+ if (found.outcome === "exit") {
1246
+ if (await obligations.resolveTempoCloseExit(obligationId)) {
1247
+ loggers.tempo.warn("tempo_session_close_exit_resolved", {
1248
+ channel_id: channelId,
1249
+ drain_id: drainId,
1250
+ obligation_id: obligationId
1251
+ });
1252
+ }
1253
+ return void 0;
1254
+ }
1255
+ loggers.tempo.warn("tempo_session_close_recovery_unmatched", {
1256
+ channel_id: channelId,
1257
+ drain_id: drainId,
1258
+ obligation_id: obligationId
1259
+ });
1260
+ return void 0;
1261
+ };
1262
+ const reconcileUnsubmittedCloses = async () => {
1263
+ const obligations = config.hostedObligations;
1264
+ if (!obligations) return;
1265
+ for (const row of await obligations.unsubmittedTempoCloses()) {
1266
+ try {
1267
+ const stored = await store.get(row.channelId);
1268
+ const current = readTempoSessionState(stored);
1269
+ if (!current) continue;
1270
+ if (!current.finalized) {
1271
+ const client = await getClient({ chainId: current.chainId });
1272
+ const [chainState] = await chainOps.getChannelStatesBatch(
1273
+ client,
1274
+ [current.channelId],
1275
+ current.escrowContract
1276
+ );
1277
+ await terminalizeLostBacking(current, chainState);
1278
+ if (!await writeBackChainState(current, chainState)) continue;
1279
+ }
1280
+ const found = await findFinalizedClose(current, stored, row.drainId, row.reservedAtMs, {
1281
+ from: row.searchBefore,
1282
+ save: (resumeAt) => obligations.recordTempoCloseSearch(row.id, resumeAt)
1283
+ });
1284
+ await settleUnsubmittedClose(current.channelId, row.drainId, row.id, found);
1285
+ } catch (error) {
1286
+ loggers.tempo.warn("tempo_session_close_reconcile_failed", {
1287
+ channel_id: row.channelId,
1288
+ drain_id: row.drainId,
1289
+ obligation_id: row.id,
1290
+ error: error instanceof Error ? error.message : String(error)
1291
+ });
1292
+ }
1293
+ }
1294
+ };
1295
+ const reportRecoveredClose = async (channel, txHash, settledBeforeClose) => {
1296
+ const client = await getClient({ chainId: channel.chainId });
1297
+ const { settledToPayee } = await chainOps.readChannelClose(client, channel.channelId, txHash);
1298
+ if (settledBeforeClose === null || settledToPayee < settledBeforeClose) {
1299
+ loggers.tempo.warn("tempo_session_close_settlement_unreported", {
1300
+ channel_id: channel.channelId,
1301
+ tx_hash: txHash
1302
+ });
1303
+ return;
1304
+ }
1305
+ try {
1306
+ await config.onSessionSettlement({
1307
+ txHash,
1308
+ channelId: channel.channelId,
1309
+ trigger: "close",
1310
+ amount: settledToPayee,
1311
+ delta: settledToPayee - settledBeforeClose
1312
+ });
1313
+ } catch (error) {
1314
+ loggers.tempo.warn("tempo_session_close_settlement_callback_failed", {
1315
+ channel_id: channel.channelId,
1316
+ tx_hash: txHash,
1317
+ error: error instanceof Error ? error.message : String(error)
1318
+ });
1319
+ }
1320
+ };
990
1321
  const getClose = store.getClose?.bind(store) ?? (() => Promise.resolve(null));
991
1322
  const beginClose = store.beginClose?.bind(store);
992
1323
  const withCloseIntent = store.withCloseIntent?.bind(store);
@@ -995,6 +1326,7 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
995
1326
  const confirmClose = store.confirmClose?.bind(store);
996
1327
  return Object.assign(mppx, {
997
1328
  sweepTempoSessions,
1329
+ ...config?.hostedObligations && { reconcileTempoSessionCloses: reconcileUnsubmittedCloses },
998
1330
  ...beginClose && {
999
1331
  beginTempoSessionClose: async (channelId, drainId) => {
1000
1332
  await beginClose(channelId, drainId);
@@ -1014,6 +1346,14 @@ function attachTempoSessionRuntime(mppx, config, chainOpOverrides = {}) {
1014
1346
  }
1015
1347
  });
1016
1348
  }
1349
+ function cooperativeCloseSenders(stored, fallback) {
1350
+ const record = stored && typeof stored === "object" ? stored : {};
1351
+ const descriptor = record.descriptor && typeof record.descriptor === "object" ? record.descriptor : {};
1352
+ const senders = new Set(
1353
+ [record.operator, record.payee, descriptor.operator, descriptor.payee].filter((value) => typeof value === "string" && HEX_ADDRESS_RE.test(value)).map((value) => value.toLowerCase())
1354
+ );
1355
+ return senders.size > 0 ? senders : /* @__PURE__ */ new Set([fallback.toLowerCase()]);
1356
+ }
1017
1357
  function readTempoSessionState(value) {
1018
1358
  if (!value || typeof value !== "object") return void 0;
1019
1359
  const state = value;
@@ -1028,6 +1368,27 @@ function maxBigInt(left, right) {
1028
1368
  function isStateRecord(value) {
1029
1369
  return !!value && typeof value === "object" && !Array.isArray(value);
1030
1370
  }
1371
+ var ZERO_ADDRESS = `0x${"00".repeat(20)}`;
1372
+ var DESCRIPTOR_ADDRESSES = ["payer", "payee", "operator", "token", "authorizedSigner"];
1373
+ var DESCRIPTOR_WORDS = ["salt", "expiringNonceHash"];
1374
+ function readChannelDescriptor(value) {
1375
+ if (!isStateRecord(value)) return void 0;
1376
+ for (const field of DESCRIPTOR_ADDRESSES) {
1377
+ if (typeof value[field] !== "string" || !HEX_ADDRESS_RE.test(value[field])) return void 0;
1378
+ }
1379
+ for (const field of DESCRIPTOR_WORDS) {
1380
+ if (typeof value[field] !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value[field])) {
1381
+ return void 0;
1382
+ }
1383
+ }
1384
+ return value;
1385
+ }
1386
+ function sameHex(left, right) {
1387
+ return typeof left === "string" && typeof right === "string" && left.toLowerCase() === right.toLowerCase();
1388
+ }
1389
+ function sameDescriptor(left, right) {
1390
+ return DESCRIPTOR_ADDRESSES.every((field) => sameHex(left[field], right[field])) && DESCRIPTOR_WORDS.every((field) => sameHex(left[field], right[field]));
1391
+ }
1031
1392
  function tempoSettlementDue(args) {
1032
1393
  const { channel, chainSettled, forcedClose, intervalMs } = args;
1033
1394
  const settlementBoundary = Date.parse(channel.lastSettlementAt ?? channel.createdAt ?? "");
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  callerLoggers
3
- } from "./chunk-KXWROQGK.js";
3
+ } from "./chunk-66HGCPBU.js";
4
4
  import {
5
5
  diagnosticConsole,
6
6
  projectSpanRow
7
- } from "./chunk-5PLQOC4B.js";
7
+ } from "./chunk-BFGQG4WY.js";
8
8
 
9
9
  // src/lib/x402/env.ts
10
10
  import { isAddress } from "viem";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  validateCurrency
3
- } from "./chunk-5CSR33B5.js";
3
+ } from "./chunk-2U6LAASN.js";
4
4
 
5
5
  // src/sdk/pricing/upfront.ts
6
6
  function buildUpfront(input) {
@@ -2,7 +2,7 @@ import {
2
2
  createX402FacilitatorAuthHeaders,
3
3
  settleWithFacilitator,
4
4
  verifyWithFacilitator
5
- } from "./chunk-FYETJDBB.js";
5
+ } from "./chunk-AVV5SYGO.js";
6
6
  import {
7
7
  buildPaymentRequirements,
8
8
  decodePayment,
@@ -12,10 +12,10 @@ import {
12
12
  } from "./chunk-62FI3NJF.js";
13
13
  import {
14
14
  callerLoggers
15
- } from "./chunk-KXWROQGK.js";
15
+ } from "./chunk-66HGCPBU.js";
16
16
  import {
17
17
  diagnosticConsole
18
- } from "./chunk-5PLQOC4B.js";
18
+ } from "./chunk-BFGQG4WY.js";
19
19
 
20
20
  // src/lib/x402/verify.ts
21
21
  async function verifyX402Payment(paymentHeader, config, requiredUsdcMicro, resource, btcUsdRate, exactVersions = { v1: true, v2: true }) {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-YG7G4DPZ.js";
5
5
  import {
6
6
  withPostgresTransactionRecovery
7
- } from "./chunk-QM2OS3NJ.js";
7
+ } from "./chunk-7PM4R4GD.js";
8
8
  import {
9
9
  withSdkInitLock
10
10
  } from "./chunk-COYKJRMM.js";
@@ -69,6 +69,11 @@ var PostgresTempoSessionStore = class {
69
69
  `ALTER TABLE mpp_tempo_close_intents
70
70
  ADD COLUMN IF NOT EXISTS forecast_pending BOOLEAN NOT NULL DEFAULT FALSE`
71
71
  );
72
+ await this.pool.query(
73
+ `ALTER TABLE mpp_tempo_close_intents
74
+ ADD COLUMN IF NOT EXISTS close_search_before BIGINT,
75
+ ADD COLUMN IF NOT EXISTS settled_before_close NUMERIC(78,0)`
76
+ );
72
77
  });
73
78
  }
74
79
  async get(key) {
@@ -273,6 +278,17 @@ var PostgresTempoSessionStore = class {
273
278
  if (rows[0].forecast_pending !== false) {
274
279
  throw new Error(`Tempo session drain ${drainId} refund forecast is still pending`);
275
280
  }
281
+ const channel = await tx.query(
282
+ "SELECT state_json FROM mpp_tempo_channels WHERE channel_key = $1",
283
+ [channelId.toLowerCase()]
284
+ );
285
+ const state = channel.rows[0] ? parseStoreValue(channel.rows[0].state_json) : null;
286
+ const settled = state?.settledOnChain;
287
+ await tx.query(
288
+ `UPDATE mpp_tempo_close_intents SET settled_before_close = $3
289
+ WHERE channel_id = $1 AND drain_id = $2`,
290
+ [channelId.toLowerCase(), drainId, typeof settled === "bigint" ? settled.toString() : null]
291
+ );
276
292
  return record;
277
293
  });
278
294
  }
@@ -357,6 +373,47 @@ var PostgresTempoSessionStore = class {
357
373
  if (!rows[0]) throw new Error(`Tempo session close intent not found`);
358
374
  return closeRecord(rows[0]);
359
375
  }
376
+ /** Set a close's reference only if none is set, reporting whether this call set it. */
377
+ async claimCloseReference(channelId, drainId, reference) {
378
+ const { rows } = await this.pool.query(
379
+ `UPDATE mpp_tempo_close_intents
380
+ SET reference = $3, updated_at = $4
381
+ WHERE channel_id = $1 AND drain_id = $2 AND forecast_pending = FALSE
382
+ AND reference IS NULL
383
+ RETURNING reference`,
384
+ [channelId.toLowerCase(), drainId, reference, Date.now()]
385
+ );
386
+ if (rows[0]) return { reference: rows[0].reference, claimed: true };
387
+ const current = await this.getClose(channelId);
388
+ if (current?.drainId !== drainId || current.reference === null) {
389
+ throw new Error(`Tempo session close intent not found`);
390
+ }
391
+ return { reference: current.reference, claimed: false };
392
+ }
393
+ async getCloseRecovery(channelId) {
394
+ const { rows } = await this.pool.query(
395
+ `SELECT updated_at, close_search_before, settled_before_close
396
+ FROM mpp_tempo_close_intents
397
+ WHERE channel_id = $1`,
398
+ [channelId.toLowerCase()]
399
+ );
400
+ const row = rows[0];
401
+ if (!row) return null;
402
+ return {
403
+ updatedAtMs: Number(row.updated_at),
404
+ searchBefore: row.close_search_before === null ? null : BigInt(row.close_search_before),
405
+ settledBeforeClose: row.settled_before_close === null ? null : BigInt(row.settled_before_close)
406
+ };
407
+ }
408
+ /** Record where a paused close search resumes; the cursor only moves back. */
409
+ async recordCloseSearch(channelId, drainId, resumeAt) {
410
+ await this.pool.query(
411
+ `UPDATE mpp_tempo_close_intents
412
+ SET close_search_before = LEAST(COALESCE(close_search_before, $3), $3)
413
+ WHERE channel_id = $1 AND drain_id = $2`,
414
+ [channelId.toLowerCase(), drainId, resumeAt.toString()]
415
+ );
416
+ }
360
417
  async runCloseIntentTransaction(channelId, drainId, operation) {
361
418
  return this.transaction("tempo_session.close_intent", async (client) => {
362
419
  const record = await this.writeCloseIntent(client, channelId, drainId);
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  redactUrl,
8
8
  redactUrlsInText
9
- } from "./chunk-KXWROQGK.js";
9
+ } from "./chunk-FUJ36YDV.js";
10
10
  import {
11
11
  DvmError,
12
12
  x402NetworkByCaip2,
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-UKD3R2KU.js";
4
4
  import {
5
5
  callerLoggers
6
- } from "./chunk-KXWROQGK.js";
6
+ } from "./chunk-66HGCPBU.js";
7
7
  import {
8
8
  DvmError
9
9
  } from "./chunk-BPYMEMQX.js";
10
10
  import {
11
11
  diagnosticConsole
12
- } from "./chunk-5PLQOC4B.js";
12
+ } from "./chunk-BFGQG4WY.js";
13
13
 
14
14
  // src/lib/cashu/lock-pubkey.ts
15
15
  function toLockPubkey(raw) {
@@ -1,4 +1,4 @@
1
- import { b as JobStore, J as JobRecord } from './job-store-DjgN2AOk.js';
1
+ import { b as JobStore, J as JobRecord } from './job-store-Cq21JT37.js';
2
2
 
3
3
  /** Authenticated caller metadata; omitted fields on older services mean unknown. */
4
4
  interface JobContentMetadata {