@gvnrdao/dh-sdk 0.0.309 → 0.0.312

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/index.d.ts CHANGED
@@ -35,6 +35,8 @@ export type { BitcoinOperationsConfig, BitcoinNetwork, EnrichedBitcoinBalance, }
35
35
  export type { SupportedStablecoinData } from './graphs/diamond-hands';
36
36
  export { validateSDKConfig, validateServiceModeConfig, validateStandaloneModeConfig, isServiceModeConfig, isStandaloneModeConfig, } from './interfaces/chunks/config.i';
37
37
  export { assertSafeServiceEndpoint } from './utils/service-endpoint-policy';
38
+ export { buildLoginDomain, buildSignedLoginPayload, LOGIN_TYPES, LOGIN_TYPES_WITH_AUDIENCE, } from './utils/eip712-login';
39
+ export type { DhServerLoginMessage, DhServerLoginPayload, } from './utils/eip712-login';
38
40
  export { DEFAULT_LIT_NETWORK, VALID_LIT_NETWORKS, SDK_DEFAULTS, } from './constants/chunks/sdk-config';
39
41
  export { ALL_CONTRACTS, ALL_DEPLOYMENTS, getContractsByNetwork, getDeploymentByNetwork, LOCALHOST_CONTRACTS, SEPOLIA_CONTRACTS, } from './constants/chunks/deployment-addresses';
40
42
  export type { DeploymentContracts, DeploymentData, DeploymentLatestEnv, } from './constants/chunks/deployment-addresses';
package/dist/index.js CHANGED
@@ -5574,6 +5574,8 @@ __export(src_exports, {
5574
5574
  ErrorSeverity: () => ErrorSeverity,
5575
5575
  EventHelpers: () => EventHelpers,
5576
5576
  LOCALHOST_CONTRACTS: () => LOCALHOST_CONTRACTS,
5577
+ LOGIN_TYPES: () => LOGIN_TYPES,
5578
+ LOGIN_TYPES_WITH_AUDIENCE: () => LOGIN_TYPES_WITH_AUDIENCE,
5577
5579
  LRUCache: () => LRUCache,
5578
5580
  LoanCreator: () => LoanCreator,
5579
5581
  LoanQuery: () => LoanQuery,
@@ -5596,6 +5598,8 @@ __export(src_exports, {
5596
5598
  buildBtcExecuteEnvelope: () => buildBtcExecuteEnvelope,
5597
5599
  buildInvalidateMessageHash: () => buildInvalidateMessageHash,
5598
5600
  buildInvalidateStaleSpendEnvelope: () => buildInvalidateStaleSpendEnvelope,
5601
+ buildLoginDomain: () => buildLoginDomain,
5602
+ buildSignedLoginPayload: () => buildSignedLoginPayload,
5599
5603
  collectFailures: () => collectFailures,
5600
5604
  collectSuccesses: () => collectSuccesses,
5601
5605
  combine: () => combine,
@@ -6527,12 +6531,12 @@ var LOGIN_TYPES_WITH_AUDIENCE = {
6527
6531
  { name: "audience", type: "string" }
6528
6532
  ]
6529
6533
  };
6530
- async function buildSignedLoginPayload(signer, chainId, audience) {
6534
+ async function buildSignedLoginPayload(signer, chainId, audience, reuse) {
6531
6535
  const address = await signer.getAddress();
6532
6536
  const message = {
6533
6537
  address,
6534
- issuedAt: Math.floor(Date.now() / 1e3),
6535
- nonce: (0, import_ethers4.hexlify)((0, import_ethers4.randomBytes)(32)),
6538
+ issuedAt: reuse?.issuedAt ?? Math.floor(Date.now() / 1e3),
6539
+ nonce: reuse?.nonce ?? (0, import_ethers4.hexlify)((0, import_ethers4.randomBytes)(32)),
6536
6540
  ...audience ? { audience } : {}
6537
6541
  };
6538
6542
  const signature = await signer.signTypedData(
@@ -6550,6 +6554,13 @@ var STORE_PREFIX = "dh-server-session:";
6550
6554
  function sessionStoreKey(address, chainId, endpoint) {
6551
6555
  return STORE_PREFIX + `${address.trim().toLowerCase()}:${chainId}:${endpoint.replace(/\/+$/, "")}`;
6552
6556
  }
6557
+ var PENDING_ENVELOPE_TTL_SECONDS = 8 * 60;
6558
+ function pendingEnvelopeKey(sessionKey) {
6559
+ return sessionKey + ":pending";
6560
+ }
6561
+ function isPendingEnvelopeShape(value) {
6562
+ return !!value && typeof value === "object" && typeof value.issuedAt === "number" && typeof value.nonce === "string";
6563
+ }
6553
6564
  function isPayloadShape(value) {
6554
6565
  return !!value && typeof value === "object" && typeof value.chainId === "number" && typeof value.signature === "string" && !!value.message && typeof value.message.address === "string" && typeof value.message.issuedAt === "number" && typeof value.message.nonce === "string";
6555
6566
  }
@@ -6591,9 +6602,48 @@ var LocalStorageSessionStore = class {
6591
6602
  } catch {
6592
6603
  }
6593
6604
  }
6605
+ loadPending(key) {
6606
+ let raw;
6607
+ try {
6608
+ raw = window.localStorage.getItem(pendingEnvelopeKey(key));
6609
+ } catch {
6610
+ return null;
6611
+ }
6612
+ if (!raw)
6613
+ return null;
6614
+ let parsed;
6615
+ try {
6616
+ parsed = JSON.parse(raw);
6617
+ } catch {
6618
+ this.clearPending(key);
6619
+ return null;
6620
+ }
6621
+ const nowSec = Math.floor(Date.now() / 1e3);
6622
+ if (!isPendingEnvelopeShape(parsed) || nowSec >= parsed.issuedAt + PENDING_ENVELOPE_TTL_SECONDS) {
6623
+ this.clearPending(key);
6624
+ return null;
6625
+ }
6626
+ return parsed;
6627
+ }
6628
+ savePending(key, envelope) {
6629
+ try {
6630
+ window.localStorage.setItem(
6631
+ pendingEnvelopeKey(key),
6632
+ JSON.stringify(envelope)
6633
+ );
6634
+ } catch {
6635
+ }
6636
+ }
6637
+ clearPending(key) {
6638
+ try {
6639
+ window.localStorage.removeItem(pendingEnvelopeKey(key));
6640
+ } catch {
6641
+ }
6642
+ }
6594
6643
  };
6595
6644
  var MemorySessionStore = class {
6596
6645
  entries = /* @__PURE__ */ new Map();
6646
+ pending = /* @__PURE__ */ new Map();
6597
6647
  load(key) {
6598
6648
  return this.entries.get(key) ?? null;
6599
6649
  }
@@ -6603,6 +6653,23 @@ var MemorySessionStore = class {
6603
6653
  clear(key) {
6604
6654
  this.entries.delete(key);
6605
6655
  }
6656
+ loadPending(key) {
6657
+ const envelope = this.pending.get(key);
6658
+ if (!envelope)
6659
+ return null;
6660
+ const nowSec = Math.floor(Date.now() / 1e3);
6661
+ if (nowSec >= envelope.issuedAt + PENDING_ENVELOPE_TTL_SECONDS) {
6662
+ this.pending.delete(key);
6663
+ return null;
6664
+ }
6665
+ return envelope;
6666
+ }
6667
+ savePending(key, envelope) {
6668
+ this.pending.set(key, envelope);
6669
+ }
6670
+ clearPending(key) {
6671
+ this.pending.delete(key);
6672
+ }
6606
6673
  };
6607
6674
  function localStorageUsable() {
6608
6675
  try {
@@ -6637,9 +6704,11 @@ function isLoginRejection(error) {
6637
6704
  var ServerSession = class {
6638
6705
  signer;
6639
6706
  serviceEndpoint;
6707
+ loginAudience;
6640
6708
  chainId;
6641
6709
  store;
6642
6710
  onSignaturePrompt;
6711
+ onSignatureResolved;
6643
6712
  now;
6644
6713
  fetchImpl;
6645
6714
  cached = null;
@@ -6666,9 +6735,14 @@ var ServerSession = class {
6666
6735
  assertSafeServiceEndpoint(opts.serviceEndpoint);
6667
6736
  this.signer = opts.signer;
6668
6737
  this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
6738
+ this.loginAudience = (opts.loginAudience ?? this.serviceEndpoint).replace(
6739
+ /\/+$/,
6740
+ ""
6741
+ );
6669
6742
  this.chainId = opts.chainId;
6670
6743
  this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6671
6744
  this.onSignaturePrompt = opts.onSignaturePrompt;
6745
+ this.onSignatureResolved = opts.onSignatureResolved;
6672
6746
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6673
6747
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
6674
6748
  }
@@ -6764,6 +6838,53 @@ var ServerSession = class {
6764
6838
  );
6765
6839
  }
6766
6840
  }
6841
+ /**
6842
+ * Adopt a login envelope signed ELSEWHERE and exchange it for a session now.
6843
+ *
6844
+ * The browser frontend collects one wallet signature and spends it at both the
6845
+ * api and this service, so the envelope is minted outside the SDK. Without this
6846
+ * seam the SDK would mint its own — a second wallet prompt, which for a
6847
+ * multi-sig Safe is another full propose-and-confirm ceremony across owners.
6848
+ *
6849
+ * Must be called PROMPTLY after signing. Each server pins the nonce on FIRST
6850
+ * use and requires that first use to be fresh (±120s EOA, ±600s contract
6851
+ * wallet); the stores are independent, so a late hand-off is rejected as
6852
+ * `stale_first_use` even though the other service already accepted it. After a
6853
+ * successful first use, silent re-mint covers the next 24h.
6854
+ *
6855
+ * No-ops the prompt path entirely: on success the session is cached and
6856
+ * persisted exactly as a self-minted login would be.
6857
+ */
6858
+ adoptLoginPayload(payload) {
6859
+ const gen = this.generation;
6860
+ const notCleared = () => gen === this.generation;
6861
+ const attempt = (async () => {
6862
+ if (payload?.chainId !== this.chainId) {
6863
+ throw new Error(
6864
+ `ServerSession: cannot adopt a login for chain ${payload?.chainId} \u2014 this session is on ${this.chainId}`
6865
+ );
6866
+ }
6867
+ const payloadAddress = payload.message?.address?.toLowerCase();
6868
+ const signerAddress = (await this.signer.getAddress()).toLowerCase();
6869
+ if (!payloadAddress || payloadAddress !== signerAddress) {
6870
+ throw new Error(
6871
+ "ServerSession: cannot adopt a login signed by a different address"
6872
+ );
6873
+ }
6874
+ const session = await this.login(payload);
6875
+ if (notCleared()) {
6876
+ this.persist(payload, session);
6877
+ this.cached = session;
6878
+ this.promptBackoff = null;
6879
+ }
6880
+ return session;
6881
+ })();
6882
+ this.inFlight = attempt;
6883
+ return attempt.then(() => void 0).finally(() => {
6884
+ if (notCleared())
6885
+ this.inFlight = null;
6886
+ });
6887
+ }
6767
6888
  /**
6768
6889
  * Resolve a session WITHOUT ever prompting for a wallet signature: a live
6769
6890
  * cached token, else the persisted JWT (adopted if still fresh, otherwise
@@ -6830,17 +6951,31 @@ var ServerSession = class {
6830
6951
  this.onSignaturePrompt?.();
6831
6952
  let session;
6832
6953
  try {
6954
+ const store = this.store;
6955
+ const storeKey = store ? this.lastStoreKey : null;
6956
+ const reuse = storeKey ? store.loadPending?.(storeKey) ?? void 0 : void 0;
6833
6957
  const payload = await buildSignedLoginPayload(
6834
6958
  this.signer,
6835
6959
  this.chainId,
6836
- this.serviceEndpoint
6960
+ this.loginAudience,
6961
+ reuse
6837
6962
  );
6963
+ if (storeKey && !reuse) {
6964
+ store.savePending?.(storeKey, {
6965
+ issuedAt: payload.message.issuedAt,
6966
+ nonce: payload.message.nonce
6967
+ });
6968
+ }
6838
6969
  session = await this.login(payload);
6839
6970
  if (notCleared()) {
6840
6971
  this.persist(payload, session);
6841
6972
  this.cached = session;
6842
6973
  }
6974
+ if (storeKey)
6975
+ store.clearPending?.(storeKey);
6976
+ this.onSignatureResolved?.(true);
6843
6977
  } catch (error) {
6978
+ this.onSignatureResolved?.(false);
6844
6979
  const failures = (this.promptBackoff?.failures ?? 0) + 1;
6845
6980
  this.promptBackoff = {
6846
6981
  failures,
@@ -16466,6 +16601,32 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16466
16601
  await this.serverSession.clearPersisted();
16467
16602
  }
16468
16603
  }
16604
+ /**
16605
+ * Establish the lit-ops-server session from a login envelope the CALLER already
16606
+ * had signed, instead of prompting the wallet for one.
16607
+ *
16608
+ * For a host that authenticates several services from one wallet signature (the
16609
+ * browser frontend signs once and spends the same envelope at the api and here),
16610
+ * this is what turns two wallet prompts into one — and for a multi-sig Safe each
16611
+ * prompt saved is a whole propose-and-confirm ceremony across owners.
16612
+ *
16613
+ * Call it PROMPTLY after signing: each server pins the nonce on first use and
16614
+ * requires that first use to be fresh (±120s EOA, ±600s contract wallet), and
16615
+ * the two servers' replay stores are independent, so a late hand-off is rejected
16616
+ * as stale even though the other service already accepted the same envelope.
16617
+ * Once accepted, silent re-mint covers the next 24h with no further prompts.
16618
+ *
16619
+ * Requires `loginAudience` to be configured to a value BOTH servers accept —
16620
+ * otherwise this service rejects the envelope as `audience_mismatch`.
16621
+ *
16622
+ * Throws if the exchange fails, so a caller can fall back to the prompt path.
16623
+ * No-op in standalone mode (no server session to prime).
16624
+ */
16625
+ async primeServerSession(payload) {
16626
+ if (!this.serverSession)
16627
+ return;
16628
+ await this.serverSession.adoptLoginPayload(payload);
16629
+ }
16469
16630
  /**
16470
16631
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
16471
16632
  * the post-write state. We clear the entire loan-query cache (not just
@@ -16577,10 +16738,12 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16577
16738
  this.serverSession = new ServerSession({
16578
16739
  signer: config.authSigner ?? config.contractSigner,
16579
16740
  serviceEndpoint: config.serviceEndpoint,
16741
+ loginAudience: config.loginAudience,
16580
16742
  chainId: config.chainId,
16581
16743
  persistSession: config.sessionPersistence?.enabled,
16582
16744
  sessionStore: config.sessionPersistence?.store,
16583
- onSignaturePrompt: config.onSessionSignaturePrompt
16745
+ onSignaturePrompt: config.onSessionSignaturePrompt,
16746
+ onSignatureResolved: config.onSessionSignatureResolved
16584
16747
  });
16585
16748
  }
16586
16749
  const contractManagerResult = createContractManager({
@@ -23786,6 +23949,8 @@ async function getPositionDelegate(positionId, provider, registryAddress) {
23786
23949
  ErrorSeverity,
23787
23950
  EventHelpers,
23788
23951
  LOCALHOST_CONTRACTS,
23952
+ LOGIN_TYPES,
23953
+ LOGIN_TYPES_WITH_AUDIENCE,
23789
23954
  LRUCache,
23790
23955
  LoanCreator,
23791
23956
  LoanQuery,
@@ -23808,6 +23973,8 @@ async function getPositionDelegate(positionId, provider, registryAddress) {
23808
23973
  buildBtcExecuteEnvelope,
23809
23974
  buildInvalidateMessageHash,
23810
23975
  buildInvalidateStaleSpendEnvelope,
23976
+ buildLoginDomain,
23977
+ buildSignedLoginPayload,
23811
23978
  collectFailures,
23812
23979
  collectSuccesses,
23813
23980
  combine,
package/dist/index.mjs CHANGED
@@ -6446,12 +6446,12 @@ var LOGIN_TYPES_WITH_AUDIENCE = {
6446
6446
  { name: "audience", type: "string" }
6447
6447
  ]
6448
6448
  };
6449
- async function buildSignedLoginPayload(signer, chainId, audience) {
6449
+ async function buildSignedLoginPayload(signer, chainId, audience, reuse) {
6450
6450
  const address = await signer.getAddress();
6451
6451
  const message = {
6452
6452
  address,
6453
- issuedAt: Math.floor(Date.now() / 1e3),
6454
- nonce: hexlify(randomBytes(32)),
6453
+ issuedAt: reuse?.issuedAt ?? Math.floor(Date.now() / 1e3),
6454
+ nonce: reuse?.nonce ?? hexlify(randomBytes(32)),
6455
6455
  ...audience ? { audience } : {}
6456
6456
  };
6457
6457
  const signature = await signer.signTypedData(
@@ -6469,6 +6469,13 @@ var STORE_PREFIX = "dh-server-session:";
6469
6469
  function sessionStoreKey(address, chainId, endpoint) {
6470
6470
  return STORE_PREFIX + `${address.trim().toLowerCase()}:${chainId}:${endpoint.replace(/\/+$/, "")}`;
6471
6471
  }
6472
+ var PENDING_ENVELOPE_TTL_SECONDS = 8 * 60;
6473
+ function pendingEnvelopeKey(sessionKey) {
6474
+ return sessionKey + ":pending";
6475
+ }
6476
+ function isPendingEnvelopeShape(value) {
6477
+ return !!value && typeof value === "object" && typeof value.issuedAt === "number" && typeof value.nonce === "string";
6478
+ }
6472
6479
  function isPayloadShape(value) {
6473
6480
  return !!value && typeof value === "object" && typeof value.chainId === "number" && typeof value.signature === "string" && !!value.message && typeof value.message.address === "string" && typeof value.message.issuedAt === "number" && typeof value.message.nonce === "string";
6474
6481
  }
@@ -6510,9 +6517,48 @@ var LocalStorageSessionStore = class {
6510
6517
  } catch {
6511
6518
  }
6512
6519
  }
6520
+ loadPending(key) {
6521
+ let raw;
6522
+ try {
6523
+ raw = window.localStorage.getItem(pendingEnvelopeKey(key));
6524
+ } catch {
6525
+ return null;
6526
+ }
6527
+ if (!raw)
6528
+ return null;
6529
+ let parsed;
6530
+ try {
6531
+ parsed = JSON.parse(raw);
6532
+ } catch {
6533
+ this.clearPending(key);
6534
+ return null;
6535
+ }
6536
+ const nowSec = Math.floor(Date.now() / 1e3);
6537
+ if (!isPendingEnvelopeShape(parsed) || nowSec >= parsed.issuedAt + PENDING_ENVELOPE_TTL_SECONDS) {
6538
+ this.clearPending(key);
6539
+ return null;
6540
+ }
6541
+ return parsed;
6542
+ }
6543
+ savePending(key, envelope) {
6544
+ try {
6545
+ window.localStorage.setItem(
6546
+ pendingEnvelopeKey(key),
6547
+ JSON.stringify(envelope)
6548
+ );
6549
+ } catch {
6550
+ }
6551
+ }
6552
+ clearPending(key) {
6553
+ try {
6554
+ window.localStorage.removeItem(pendingEnvelopeKey(key));
6555
+ } catch {
6556
+ }
6557
+ }
6513
6558
  };
6514
6559
  var MemorySessionStore = class {
6515
6560
  entries = /* @__PURE__ */ new Map();
6561
+ pending = /* @__PURE__ */ new Map();
6516
6562
  load(key) {
6517
6563
  return this.entries.get(key) ?? null;
6518
6564
  }
@@ -6522,6 +6568,23 @@ var MemorySessionStore = class {
6522
6568
  clear(key) {
6523
6569
  this.entries.delete(key);
6524
6570
  }
6571
+ loadPending(key) {
6572
+ const envelope = this.pending.get(key);
6573
+ if (!envelope)
6574
+ return null;
6575
+ const nowSec = Math.floor(Date.now() / 1e3);
6576
+ if (nowSec >= envelope.issuedAt + PENDING_ENVELOPE_TTL_SECONDS) {
6577
+ this.pending.delete(key);
6578
+ return null;
6579
+ }
6580
+ return envelope;
6581
+ }
6582
+ savePending(key, envelope) {
6583
+ this.pending.set(key, envelope);
6584
+ }
6585
+ clearPending(key) {
6586
+ this.pending.delete(key);
6587
+ }
6525
6588
  };
6526
6589
  function localStorageUsable() {
6527
6590
  try {
@@ -6556,9 +6619,11 @@ function isLoginRejection(error) {
6556
6619
  var ServerSession = class {
6557
6620
  signer;
6558
6621
  serviceEndpoint;
6622
+ loginAudience;
6559
6623
  chainId;
6560
6624
  store;
6561
6625
  onSignaturePrompt;
6626
+ onSignatureResolved;
6562
6627
  now;
6563
6628
  fetchImpl;
6564
6629
  cached = null;
@@ -6585,9 +6650,14 @@ var ServerSession = class {
6585
6650
  assertSafeServiceEndpoint(opts.serviceEndpoint);
6586
6651
  this.signer = opts.signer;
6587
6652
  this.serviceEndpoint = opts.serviceEndpoint.replace(/\/+$/, "");
6653
+ this.loginAudience = (opts.loginAudience ?? this.serviceEndpoint).replace(
6654
+ /\/+$/,
6655
+ ""
6656
+ );
6588
6657
  this.chainId = opts.chainId;
6589
6658
  this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6590
6659
  this.onSignaturePrompt = opts.onSignaturePrompt;
6660
+ this.onSignatureResolved = opts.onSignatureResolved;
6591
6661
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6592
6662
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
6593
6663
  }
@@ -6683,6 +6753,53 @@ var ServerSession = class {
6683
6753
  );
6684
6754
  }
6685
6755
  }
6756
+ /**
6757
+ * Adopt a login envelope signed ELSEWHERE and exchange it for a session now.
6758
+ *
6759
+ * The browser frontend collects one wallet signature and spends it at both the
6760
+ * api and this service, so the envelope is minted outside the SDK. Without this
6761
+ * seam the SDK would mint its own — a second wallet prompt, which for a
6762
+ * multi-sig Safe is another full propose-and-confirm ceremony across owners.
6763
+ *
6764
+ * Must be called PROMPTLY after signing. Each server pins the nonce on FIRST
6765
+ * use and requires that first use to be fresh (±120s EOA, ±600s contract
6766
+ * wallet); the stores are independent, so a late hand-off is rejected as
6767
+ * `stale_first_use` even though the other service already accepted it. After a
6768
+ * successful first use, silent re-mint covers the next 24h.
6769
+ *
6770
+ * No-ops the prompt path entirely: on success the session is cached and
6771
+ * persisted exactly as a self-minted login would be.
6772
+ */
6773
+ adoptLoginPayload(payload) {
6774
+ const gen = this.generation;
6775
+ const notCleared = () => gen === this.generation;
6776
+ const attempt = (async () => {
6777
+ if (payload?.chainId !== this.chainId) {
6778
+ throw new Error(
6779
+ `ServerSession: cannot adopt a login for chain ${payload?.chainId} \u2014 this session is on ${this.chainId}`
6780
+ );
6781
+ }
6782
+ const payloadAddress = payload.message?.address?.toLowerCase();
6783
+ const signerAddress = (await this.signer.getAddress()).toLowerCase();
6784
+ if (!payloadAddress || payloadAddress !== signerAddress) {
6785
+ throw new Error(
6786
+ "ServerSession: cannot adopt a login signed by a different address"
6787
+ );
6788
+ }
6789
+ const session = await this.login(payload);
6790
+ if (notCleared()) {
6791
+ this.persist(payload, session);
6792
+ this.cached = session;
6793
+ this.promptBackoff = null;
6794
+ }
6795
+ return session;
6796
+ })();
6797
+ this.inFlight = attempt;
6798
+ return attempt.then(() => void 0).finally(() => {
6799
+ if (notCleared())
6800
+ this.inFlight = null;
6801
+ });
6802
+ }
6686
6803
  /**
6687
6804
  * Resolve a session WITHOUT ever prompting for a wallet signature: a live
6688
6805
  * cached token, else the persisted JWT (adopted if still fresh, otherwise
@@ -6749,17 +6866,31 @@ var ServerSession = class {
6749
6866
  this.onSignaturePrompt?.();
6750
6867
  let session;
6751
6868
  try {
6869
+ const store = this.store;
6870
+ const storeKey = store ? this.lastStoreKey : null;
6871
+ const reuse = storeKey ? store.loadPending?.(storeKey) ?? void 0 : void 0;
6752
6872
  const payload = await buildSignedLoginPayload(
6753
6873
  this.signer,
6754
6874
  this.chainId,
6755
- this.serviceEndpoint
6875
+ this.loginAudience,
6876
+ reuse
6756
6877
  );
6878
+ if (storeKey && !reuse) {
6879
+ store.savePending?.(storeKey, {
6880
+ issuedAt: payload.message.issuedAt,
6881
+ nonce: payload.message.nonce
6882
+ });
6883
+ }
6757
6884
  session = await this.login(payload);
6758
6885
  if (notCleared()) {
6759
6886
  this.persist(payload, session);
6760
6887
  this.cached = session;
6761
6888
  }
6889
+ if (storeKey)
6890
+ store.clearPending?.(storeKey);
6891
+ this.onSignatureResolved?.(true);
6762
6892
  } catch (error) {
6893
+ this.onSignatureResolved?.(false);
6763
6894
  const failures = (this.promptBackoff?.failures ?? 0) + 1;
6764
6895
  this.promptBackoff = {
6765
6896
  failures,
@@ -16395,6 +16526,32 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16395
16526
  await this.serverSession.clearPersisted();
16396
16527
  }
16397
16528
  }
16529
+ /**
16530
+ * Establish the lit-ops-server session from a login envelope the CALLER already
16531
+ * had signed, instead of prompting the wallet for one.
16532
+ *
16533
+ * For a host that authenticates several services from one wallet signature (the
16534
+ * browser frontend signs once and spends the same envelope at the api and here),
16535
+ * this is what turns two wallet prompts into one — and for a multi-sig Safe each
16536
+ * prompt saved is a whole propose-and-confirm ceremony across owners.
16537
+ *
16538
+ * Call it PROMPTLY after signing: each server pins the nonce on first use and
16539
+ * requires that first use to be fresh (±120s EOA, ±600s contract wallet), and
16540
+ * the two servers' replay stores are independent, so a late hand-off is rejected
16541
+ * as stale even though the other service already accepted the same envelope.
16542
+ * Once accepted, silent re-mint covers the next 24h with no further prompts.
16543
+ *
16544
+ * Requires `loginAudience` to be configured to a value BOTH servers accept —
16545
+ * otherwise this service rejects the envelope as `audience_mismatch`.
16546
+ *
16547
+ * Throws if the exchange fails, so a caller can fall back to the prompt path.
16548
+ * No-op in standalone mode (no server session to prime).
16549
+ */
16550
+ async primeServerSession(payload) {
16551
+ if (!this.serverSession)
16552
+ return;
16553
+ await this.serverSession.adoptLoginPayload(payload);
16554
+ }
16398
16555
  /**
16399
16556
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
16400
16557
  * the post-write state. We clear the entire loan-query cache (not just
@@ -16506,10 +16663,12 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16506
16663
  this.serverSession = new ServerSession({
16507
16664
  signer: config.authSigner ?? config.contractSigner,
16508
16665
  serviceEndpoint: config.serviceEndpoint,
16666
+ loginAudience: config.loginAudience,
16509
16667
  chainId: config.chainId,
16510
16668
  persistSession: config.sessionPersistence?.enabled,
16511
16669
  sessionStore: config.sessionPersistence?.store,
16512
- onSignaturePrompt: config.onSessionSignaturePrompt
16670
+ onSignaturePrompt: config.onSessionSignaturePrompt,
16671
+ onSignatureResolved: config.onSessionSignatureResolved
16513
16672
  });
16514
16673
  }
16515
16674
  const contractManagerResult = createContractManager({
@@ -23714,6 +23873,8 @@ export {
23714
23873
  ErrorSeverity,
23715
23874
  EventHelpers,
23716
23875
  LOCALHOST_CONTRACTS,
23876
+ LOGIN_TYPES,
23877
+ LOGIN_TYPES_WITH_AUDIENCE,
23717
23878
  LRUCache,
23718
23879
  LoanCreator,
23719
23880
  LoanQuery,
@@ -23736,6 +23897,8 @@ export {
23736
23897
  buildBtcExecuteEnvelope,
23737
23898
  buildInvalidateMessageHash,
23738
23899
  buildInvalidateStaleSpendEnvelope,
23900
+ buildLoginDomain,
23901
+ buildSignedLoginPayload,
23739
23902
  collectFailures,
23740
23903
  collectSuccesses,
23741
23904
  combine,
@@ -71,12 +71,33 @@ interface BaseSDKConfig {
71
71
  /** Custom store (e.g. a file-backed store for CLI daemons). */
72
72
  store?: ServerSessionStore;
73
73
  };
74
+ /**
75
+ * Audit M-8 audience the server-session login binds to. Defaults to
76
+ * `serviceEndpoint`.
77
+ *
78
+ * Set this only when ONE signature must authenticate several services — the
79
+ * browser frontend signs a single envelope and spends it at both
80
+ * lit-ops-server and the api, and a per-endpoint audience cannot name two
81
+ * endpoints. Every server the envelope is presented to must list this value as
82
+ * an accepted audience, so changing it requires a server-side config change
83
+ * FIRST. Node callers (CLI, MCP, cr-monitor) omit it and keep binding to the
84
+ * endpoint they call.
85
+ */
86
+ loginAudience?: string;
74
87
  /**
75
88
  * Fires immediately before the server-session login requests a wallet
76
89
  * signature — never on the silent re-mint paths. Lets UIs show a
77
90
  * "check your wallet" prompt.
78
91
  */
79
92
  onSessionSignaturePrompt?: () => void;
93
+ /**
94
+ * Fires once that signature request has concluded — `true` when a session was
95
+ * established, `false` when it failed or was rejected. Pairs with
96
+ * `onSessionSignaturePrompt`, which otherwise has no ending: a UI that shows
97
+ * "check your wallet" on the prompt has no way to stop showing it, and a
98
+ * contract wallet can leave that request open for minutes.
99
+ */
100
+ onSessionSignatureResolved?: (ok: boolean) => void;
80
101
  ethRpcUrl?: string;
81
102
  chainId?: number;
82
103
  networkOverride?: {
@@ -18,6 +18,7 @@ import { SDKError } from "../utils/error-handler";
18
18
  import type { CreateLoanRequest, CreateLoanResult, LoanDataDetail, UCDMintRequest, UCDMintResult, PartialPaymentRequest, PartialPaymentResult, BTCWithdrawalResult, RenewPositionRequest, RenewPositionResult, LiquidationRequest, LiquidationResult, ConfirmBalanceRequest, ConfirmBalanceResult, TermsWithFeesResult } from "../interfaces/chunks/loan-operations.i";
19
19
  import type { DiamondHandsSDKConfig } from "../interfaces/chunks/config.i";
20
20
  import type { PKPData } from "../interfaces/chunks/pkp-integration.i";
21
+ import type { DhServerLoginPayload } from "../utils/eip712-login";
21
22
  import { type ReconciledWithdrawal } from "../utils/withdrawal-reconciliation.utils";
22
23
  import { ContractManager } from "./contract/contract-manager.module";
23
24
  import { WithdrawalAddressModule } from "./withdrawal-address/withdrawal-address.module";
@@ -125,6 +126,28 @@ export declare class DiamondHandsSDK {
125
126
  * and never prompts the wallet.
126
127
  */
127
128
  clearServerSession(): Promise<void>;
129
+ /**
130
+ * Establish the lit-ops-server session from a login envelope the CALLER already
131
+ * had signed, instead of prompting the wallet for one.
132
+ *
133
+ * For a host that authenticates several services from one wallet signature (the
134
+ * browser frontend signs once and spends the same envelope at the api and here),
135
+ * this is what turns two wallet prompts into one — and for a multi-sig Safe each
136
+ * prompt saved is a whole propose-and-confirm ceremony across owners.
137
+ *
138
+ * Call it PROMPTLY after signing: each server pins the nonce on first use and
139
+ * requires that first use to be fresh (±120s EOA, ±600s contract wallet), and
140
+ * the two servers' replay stores are independent, so a late hand-off is rejected
141
+ * as stale even though the other service already accepted the same envelope.
142
+ * Once accepted, silent re-mint covers the next 24h with no further prompts.
143
+ *
144
+ * Requires `loginAudience` to be configured to a value BOTH servers accept —
145
+ * otherwise this service rejects the envelope as `audience_mismatch`.
146
+ *
147
+ * Throws if the exchange fails, so a caller can fall back to the prompt path.
148
+ * No-op in standalone mode (no server session to prime).
149
+ */
150
+ primeServerSession(payload: DhServerLoginPayload): Promise<void>;
128
151
  /**
129
152
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
130
153
  * the post-write state. We clear the entire loan-query cache (not just
@@ -31,4 +31,16 @@ export declare const LOGIN_TYPES_WITH_AUDIENCE: Record<string, TypedDataField[]>
31
31
  * Audit M-8: pass `audience` (the service endpoint URL being logged into) to bind the login to
32
32
  * one service. Omit it for legacy compatibility during the migration window.
33
33
  */
34
- export declare function buildSignedLoginPayload(signer: Signer, chainId: number, audience?: string): Promise<DhServerLoginPayload>;
34
+ export declare function buildSignedLoginPayload(signer: Signer, chainId: number, audience?: string,
35
+ /**
36
+ * Reuse the envelope of a login already collecting signatures instead of
37
+ * minting a fresh one. Every field is hashed into the EIP-712 message, so a
38
+ * new nonce produces a new hash — which a multi-owner Safe shows as a new
39
+ * message needing its FIRST signature, discarding the ones already collected.
40
+ * Passing the pending envelope back reproduces the same hash, so the next
41
+ * owner adds to it. See `server-session-store.ts`.
42
+ */
43
+ reuse?: {
44
+ issuedAt: number;
45
+ nonce: string;
46
+ }): Promise<DhServerLoginPayload>;