@gvnrdao/dh-sdk 0.0.309 → 0.0.311

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.js CHANGED
@@ -6527,12 +6527,12 @@ var LOGIN_TYPES_WITH_AUDIENCE = {
6527
6527
  { name: "audience", type: "string" }
6528
6528
  ]
6529
6529
  };
6530
- async function buildSignedLoginPayload(signer, chainId, audience) {
6530
+ async function buildSignedLoginPayload(signer, chainId, audience, reuse) {
6531
6531
  const address = await signer.getAddress();
6532
6532
  const message = {
6533
6533
  address,
6534
- issuedAt: Math.floor(Date.now() / 1e3),
6535
- nonce: (0, import_ethers4.hexlify)((0, import_ethers4.randomBytes)(32)),
6534
+ issuedAt: reuse?.issuedAt ?? Math.floor(Date.now() / 1e3),
6535
+ nonce: reuse?.nonce ?? (0, import_ethers4.hexlify)((0, import_ethers4.randomBytes)(32)),
6536
6536
  ...audience ? { audience } : {}
6537
6537
  };
6538
6538
  const signature = await signer.signTypedData(
@@ -6550,6 +6550,13 @@ var STORE_PREFIX = "dh-server-session:";
6550
6550
  function sessionStoreKey(address, chainId, endpoint) {
6551
6551
  return STORE_PREFIX + `${address.trim().toLowerCase()}:${chainId}:${endpoint.replace(/\/+$/, "")}`;
6552
6552
  }
6553
+ var PENDING_ENVELOPE_TTL_SECONDS = 8 * 60;
6554
+ function pendingEnvelopeKey(sessionKey) {
6555
+ return sessionKey + ":pending";
6556
+ }
6557
+ function isPendingEnvelopeShape(value) {
6558
+ return !!value && typeof value === "object" && typeof value.issuedAt === "number" && typeof value.nonce === "string";
6559
+ }
6553
6560
  function isPayloadShape(value) {
6554
6561
  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
6562
  }
@@ -6591,9 +6598,48 @@ var LocalStorageSessionStore = class {
6591
6598
  } catch {
6592
6599
  }
6593
6600
  }
6601
+ loadPending(key) {
6602
+ let raw;
6603
+ try {
6604
+ raw = window.localStorage.getItem(pendingEnvelopeKey(key));
6605
+ } catch {
6606
+ return null;
6607
+ }
6608
+ if (!raw)
6609
+ return null;
6610
+ let parsed;
6611
+ try {
6612
+ parsed = JSON.parse(raw);
6613
+ } catch {
6614
+ this.clearPending(key);
6615
+ return null;
6616
+ }
6617
+ const nowSec = Math.floor(Date.now() / 1e3);
6618
+ if (!isPendingEnvelopeShape(parsed) || nowSec >= parsed.issuedAt + PENDING_ENVELOPE_TTL_SECONDS) {
6619
+ this.clearPending(key);
6620
+ return null;
6621
+ }
6622
+ return parsed;
6623
+ }
6624
+ savePending(key, envelope) {
6625
+ try {
6626
+ window.localStorage.setItem(
6627
+ pendingEnvelopeKey(key),
6628
+ JSON.stringify(envelope)
6629
+ );
6630
+ } catch {
6631
+ }
6632
+ }
6633
+ clearPending(key) {
6634
+ try {
6635
+ window.localStorage.removeItem(pendingEnvelopeKey(key));
6636
+ } catch {
6637
+ }
6638
+ }
6594
6639
  };
6595
6640
  var MemorySessionStore = class {
6596
6641
  entries = /* @__PURE__ */ new Map();
6642
+ pending = /* @__PURE__ */ new Map();
6597
6643
  load(key) {
6598
6644
  return this.entries.get(key) ?? null;
6599
6645
  }
@@ -6603,6 +6649,23 @@ var MemorySessionStore = class {
6603
6649
  clear(key) {
6604
6650
  this.entries.delete(key);
6605
6651
  }
6652
+ loadPending(key) {
6653
+ const envelope = this.pending.get(key);
6654
+ if (!envelope)
6655
+ return null;
6656
+ const nowSec = Math.floor(Date.now() / 1e3);
6657
+ if (nowSec >= envelope.issuedAt + PENDING_ENVELOPE_TTL_SECONDS) {
6658
+ this.pending.delete(key);
6659
+ return null;
6660
+ }
6661
+ return envelope;
6662
+ }
6663
+ savePending(key, envelope) {
6664
+ this.pending.set(key, envelope);
6665
+ }
6666
+ clearPending(key) {
6667
+ this.pending.delete(key);
6668
+ }
6606
6669
  };
6607
6670
  function localStorageUsable() {
6608
6671
  try {
@@ -6640,6 +6703,7 @@ var ServerSession = class {
6640
6703
  chainId;
6641
6704
  store;
6642
6705
  onSignaturePrompt;
6706
+ onSignatureResolved;
6643
6707
  now;
6644
6708
  fetchImpl;
6645
6709
  cached = null;
@@ -6669,6 +6733,7 @@ var ServerSession = class {
6669
6733
  this.chainId = opts.chainId;
6670
6734
  this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6671
6735
  this.onSignaturePrompt = opts.onSignaturePrompt;
6736
+ this.onSignatureResolved = opts.onSignatureResolved;
6672
6737
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6673
6738
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
6674
6739
  }
@@ -6830,17 +6895,31 @@ var ServerSession = class {
6830
6895
  this.onSignaturePrompt?.();
6831
6896
  let session;
6832
6897
  try {
6898
+ const store = this.store;
6899
+ const storeKey = store ? this.lastStoreKey : null;
6900
+ const reuse = storeKey ? store.loadPending?.(storeKey) ?? void 0 : void 0;
6833
6901
  const payload = await buildSignedLoginPayload(
6834
6902
  this.signer,
6835
6903
  this.chainId,
6836
- this.serviceEndpoint
6904
+ this.serviceEndpoint,
6905
+ reuse
6837
6906
  );
6907
+ if (storeKey && !reuse) {
6908
+ store.savePending?.(storeKey, {
6909
+ issuedAt: payload.message.issuedAt,
6910
+ nonce: payload.message.nonce
6911
+ });
6912
+ }
6838
6913
  session = await this.login(payload);
6839
6914
  if (notCleared()) {
6840
6915
  this.persist(payload, session);
6841
6916
  this.cached = session;
6842
6917
  }
6918
+ if (storeKey)
6919
+ store.clearPending?.(storeKey);
6920
+ this.onSignatureResolved?.(true);
6843
6921
  } catch (error) {
6922
+ this.onSignatureResolved?.(false);
6844
6923
  const failures = (this.promptBackoff?.failures ?? 0) + 1;
6845
6924
  this.promptBackoff = {
6846
6925
  failures,
@@ -16580,7 +16659,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16580
16659
  chainId: config.chainId,
16581
16660
  persistSession: config.sessionPersistence?.enabled,
16582
16661
  sessionStore: config.sessionPersistence?.store,
16583
- onSignaturePrompt: config.onSessionSignaturePrompt
16662
+ onSignaturePrompt: config.onSessionSignaturePrompt,
16663
+ onSignatureResolved: config.onSessionSignatureResolved
16584
16664
  });
16585
16665
  }
16586
16666
  const contractManagerResult = createContractManager({
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 {
@@ -6559,6 +6622,7 @@ var ServerSession = class {
6559
6622
  chainId;
6560
6623
  store;
6561
6624
  onSignaturePrompt;
6625
+ onSignatureResolved;
6562
6626
  now;
6563
6627
  fetchImpl;
6564
6628
  cached = null;
@@ -6588,6 +6652,7 @@ var ServerSession = class {
6588
6652
  this.chainId = opts.chainId;
6589
6653
  this.store = opts.persistSession === false ? null : opts.sessionStore ?? createDefaultSessionStore();
6590
6654
  this.onSignaturePrompt = opts.onSignaturePrompt;
6655
+ this.onSignatureResolved = opts.onSignatureResolved;
6591
6656
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6592
6657
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
6593
6658
  }
@@ -6749,17 +6814,31 @@ var ServerSession = class {
6749
6814
  this.onSignaturePrompt?.();
6750
6815
  let session;
6751
6816
  try {
6817
+ const store = this.store;
6818
+ const storeKey = store ? this.lastStoreKey : null;
6819
+ const reuse = storeKey ? store.loadPending?.(storeKey) ?? void 0 : void 0;
6752
6820
  const payload = await buildSignedLoginPayload(
6753
6821
  this.signer,
6754
6822
  this.chainId,
6755
- this.serviceEndpoint
6823
+ this.serviceEndpoint,
6824
+ reuse
6756
6825
  );
6826
+ if (storeKey && !reuse) {
6827
+ store.savePending?.(storeKey, {
6828
+ issuedAt: payload.message.issuedAt,
6829
+ nonce: payload.message.nonce
6830
+ });
6831
+ }
6757
6832
  session = await this.login(payload);
6758
6833
  if (notCleared()) {
6759
6834
  this.persist(payload, session);
6760
6835
  this.cached = session;
6761
6836
  }
6837
+ if (storeKey)
6838
+ store.clearPending?.(storeKey);
6839
+ this.onSignatureResolved?.(true);
6762
6840
  } catch (error) {
6841
+ this.onSignatureResolved?.(false);
6763
6842
  const failures = (this.promptBackoff?.failures ?? 0) + 1;
6764
6843
  this.promptBackoff = {
6765
6844
  failures,
@@ -16509,7 +16588,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16509
16588
  chainId: config.chainId,
16510
16589
  persistSession: config.sessionPersistence?.enabled,
16511
16590
  sessionStore: config.sessionPersistence?.store,
16512
- onSignaturePrompt: config.onSessionSignaturePrompt
16591
+ onSignaturePrompt: config.onSessionSignaturePrompt,
16592
+ onSignatureResolved: config.onSessionSignatureResolved
16513
16593
  });
16514
16594
  }
16515
16595
  const contractManagerResult = createContractManager({
@@ -77,6 +77,14 @@ interface BaseSDKConfig {
77
77
  * "check your wallet" prompt.
78
78
  */
79
79
  onSessionSignaturePrompt?: () => void;
80
+ /**
81
+ * Fires once that signature request has concluded — `true` when a session was
82
+ * established, `false` when it failed or was rejected. Pairs with
83
+ * `onSessionSignaturePrompt`, which otherwise has no ending: a UI that shows
84
+ * "check your wallet" on the prompt has no way to stop showing it, and a
85
+ * contract wallet can leave that request open for minutes.
86
+ */
87
+ onSessionSignatureResolved?: (ok: boolean) => void;
80
88
  ethRpcUrl?: string;
81
89
  chainId?: number;
82
90
  networkOverride?: {
@@ -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>;
@@ -39,18 +39,62 @@ export interface ServerSessionStore {
39
39
  load(key: string): PersistedServerSession | null;
40
40
  save(key: string, session: PersistedServerSession): void;
41
41
  clear(key: string): void;
42
+ /**
43
+ * Optional pending-envelope persistence (see `PendingLoginEnvelope`). Optional
44
+ * so a custom store written before this existed still satisfies the interface;
45
+ * without it the session simply mints a fresh envelope each attempt, which is
46
+ * the pre-existing behavior and only costs a multi-owner Safe a restart.
47
+ */
48
+ loadPending?(key: string): PendingLoginEnvelope | null;
49
+ savePending?(key: string, envelope: PendingLoginEnvelope): void;
50
+ clearPending?(key: string): void;
42
51
  }
43
52
  export declare function sessionStoreKey(address: string, chainId: number, endpoint: string): string;
53
+ /**
54
+ * The UNSIGNED envelope of a login already collecting signatures.
55
+ *
56
+ * A multi-owner Safe signs across sessions — owner 1 now, owner 2 later — and
57
+ * identifies the pending message solely by its EIP-712 hash. Every field is
58
+ * hashed, so minting a fresh `nonce`/`issuedAt` for the next attempt yields a
59
+ * DIFFERENT hash, which Safe presents as a new message needing its FIRST
60
+ * signature. Whatever owner 1 signed is orphaned, and a 2-of-N Safe can never
61
+ * reach threshold: every retry resets the count to zero.
62
+ *
63
+ * Reusing the envelope makes the re-request hash identically, so Safe shows the
64
+ * same pending message and the next owner ADDS to it. Invisible for an EOA,
65
+ * which signs immediately and spends the envelope.
66
+ */
67
+ export interface PendingLoginEnvelope {
68
+ issuedAt: number;
69
+ nonce: string;
70
+ }
71
+ /**
72
+ * How long an unsigned envelope stays reusable. Must stay UNDER the server's
73
+ * contract-wallet first-use skew (`CONTRACT_ISSUED_AT_SKEW_SECONDS`, 10 min):
74
+ * beyond it the first use is rejected as stale, so reuse would guarantee failure
75
+ * instead of preventing a re-sign.
76
+ */
77
+ export declare const PENDING_ENVELOPE_TTL_SECONDS: number;
78
+ /** Storage key for the pending envelope matching a session key. */
79
+ export declare function pendingEnvelopeKey(sessionKey: string): string;
80
+ export declare function isPendingEnvelopeShape(value: any): value is PendingLoginEnvelope;
44
81
  export declare class LocalStorageSessionStore implements ServerSessionStore {
45
82
  load(key: string): PersistedServerSession | null;
46
83
  save(key: string, session: PersistedServerSession): void;
47
84
  clear(key: string): void;
85
+ loadPending(key: string): PendingLoginEnvelope | null;
86
+ savePending(key: string, envelope: PendingLoginEnvelope): void;
87
+ clearPending(key: string): void;
48
88
  }
49
89
  export declare class MemorySessionStore implements ServerSessionStore {
50
90
  private readonly entries;
91
+ private readonly pending;
51
92
  load(key: string): PersistedServerSession | null;
52
93
  save(key: string, session: PersistedServerSession): void;
53
94
  clear(key: string): void;
95
+ loadPending(key: string): PendingLoginEnvelope | null;
96
+ savePending(key: string, envelope: PendingLoginEnvelope): void;
97
+ clearPending(key: string): void;
54
98
  }
55
99
  /** `localStorage` when usable (browser, not blocked), else per-instance memory. */
56
100
  export declare function createDefaultSessionStore(): ServerSessionStore;
@@ -40,6 +40,12 @@ export interface ServerSessionOptions {
40
40
  * never on the silent paths. Lets UIs show a "check your wallet" prompt.
41
41
  */
42
42
  onSignaturePrompt?: () => void;
43
+ /**
44
+ * Fires once the prompted signature has concluded, with whether a session was
45
+ * established. Without it `onSignaturePrompt` has no ending, so a UI showing
46
+ * "check your wallet" can never take it down.
47
+ */
48
+ onSignatureResolved?: (ok: boolean) => void;
43
49
  /** Override for tests. */
44
50
  now?: () => number;
45
51
  /** Override for tests. */
@@ -56,6 +62,7 @@ export declare class ServerSession {
56
62
  private readonly chainId;
57
63
  private readonly store;
58
64
  private readonly onSignaturePrompt?;
65
+ private readonly onSignatureResolved?;
59
66
  private readonly now;
60
67
  private readonly fetchImpl;
61
68
  private cached;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.309",
3
+ "version": "0.0.311",
4
4
  "description": "TypeScript SDK for Diamond Hands Protocol - Bitcoin-backed lending with LIT Protocol PKPs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",