@orbinum/sdk 1.3.1 → 1.4.0

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.mts CHANGED
@@ -240,8 +240,27 @@ declare function reserveSelfEphIndex(storage: NoteStorage): Promise<number>;
240
240
  * viewing public key. Registering the counterparty is a side effect worth
241
241
  * having: it makes the REVERSE direction cheap too, since their future payments
242
242
  * to this wallet become hash lookups instead of one trial ECDH per note.
243
+ *
244
+ * Returns `null` when this vault holds no history for that counterparty, and
245
+ * the caller must then use a random ephemeral. The reason is that "no history"
246
+ * has two causes this function cannot tell apart:
247
+ *
248
+ * - a genuine first payment, where index 0 is correct;
249
+ * - a counter that was LOST — a restored seed, a cleared IndexedDB, a wipe
250
+ * and rescan — where index 0 was already published and re-deriving it
251
+ * republishes that ephPk, linking the two notes in public.
252
+ *
253
+ * Unlike `selfEphCounter`, this one cannot be recovered: the index was
254
+ * published on a note encrypted toward someone else, so it never appears in
255
+ * this wallet's own scan, and asking a server whether a given ephPk exists
256
+ * would reveal which notes are ours. So the ambiguity is resolved the safe way
257
+ * — the caller degrades to random, costing the recipient one trial scan, and
258
+ * every later payment to the same counterparty takes the fast path again.
259
+ *
260
+ * The entry is still created: the NEXT payment has a counter, and registering
261
+ * the counterparty is what makes the reverse direction cheap.
243
262
  */
244
- declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number>;
263
+ declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number | null>;
245
264
 
246
265
  /**
247
266
  * Turning a note into a stored record and back.
@@ -4495,7 +4514,13 @@ declare function resolveSelfEphCeiling(params: {
4495
4514
  /**
4496
4515
  * Discovery window size for the next scan, given the persisted counter: at least
4497
4516
  * the default, rounded up so every index the wallet may already have used (plus
4498
- * the gap margin) falls inside the fast path.
4517
+ * the gap margin) falls inside the fast path, and never past `MAX_EPH_WINDOW`.
4518
+ *
4519
+ * The counter is read from a config that survives restores and hand-editing, so
4520
+ * a non-finite value is treated as "no history" rather than propagated: `NaN`
4521
+ * would make the builder's `i < from + count` false immediately and return an
4522
+ * EMPTY window, silently disabling the fast path, while `Infinity` would make
4523
+ * that same loop never terminate.
4499
4524
  */
4500
4525
  declare function windowSizeForCounter(counter: number): number;
4501
4526
 
package/dist/index.d.ts CHANGED
@@ -240,8 +240,27 @@ declare function reserveSelfEphIndex(storage: NoteStorage): Promise<number>;
240
240
  * viewing public key. Registering the counterparty is a side effect worth
241
241
  * having: it makes the REVERSE direction cheap too, since their future payments
242
242
  * to this wallet become hash lookups instead of one trial ECDH per note.
243
+ *
244
+ * Returns `null` when this vault holds no history for that counterparty, and
245
+ * the caller must then use a random ephemeral. The reason is that "no history"
246
+ * has two causes this function cannot tell apart:
247
+ *
248
+ * - a genuine first payment, where index 0 is correct;
249
+ * - a counter that was LOST — a restored seed, a cleared IndexedDB, a wipe
250
+ * and rescan — where index 0 was already published and re-deriving it
251
+ * republishes that ephPk, linking the two notes in public.
252
+ *
253
+ * Unlike `selfEphCounter`, this one cannot be recovered: the index was
254
+ * published on a note encrypted toward someone else, so it never appears in
255
+ * this wallet's own scan, and asking a server whether a given ephPk exists
256
+ * would reveal which notes are ours. So the ambiguity is resolved the safe way
257
+ * — the caller degrades to random, costing the recipient one trial scan, and
258
+ * every later payment to the same counterparty takes the fast path again.
259
+ *
260
+ * The entry is still created: the NEXT payment has a counter, and registering
261
+ * the counterparty is what makes the reverse direction cheap.
243
262
  */
244
- declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number>;
263
+ declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number | null>;
245
264
 
246
265
  /**
247
266
  * Turning a note into a stored record and back.
@@ -4495,7 +4514,13 @@ declare function resolveSelfEphCeiling(params: {
4495
4514
  /**
4496
4515
  * Discovery window size for the next scan, given the persisted counter: at least
4497
4516
  * the default, rounded up so every index the wallet may already have used (plus
4498
- * the gap margin) falls inside the fast path.
4517
+ * the gap margin) falls inside the fast path, and never past `MAX_EPH_WINDOW`.
4518
+ *
4519
+ * The counter is read from a config that survives restores and hand-editing, so
4520
+ * a non-finite value is treated as "no history" rather than propagated: `NaN`
4521
+ * would make the builder's `i < from + count` false immediately and return an
4522
+ * EMPTY window, silently disabling the fast path, while `Infinity` would make
4523
+ * that same loop never terminate.
4499
4524
  */
4500
4525
  declare function windowSizeForCounter(counter: number): number;
4501
4526
 
package/dist/index.js CHANGED
@@ -5414,13 +5414,29 @@ function mergeCounters(current, fallback) {
5414
5414
  ...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
5415
5415
  };
5416
5416
  }
5417
- var higher = (a, b) => a === void 0 ? b : b === void 0 ? a : Math.max(a, b);
5417
+ var counterOrNone = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
5418
+ var higher = (a, b) => {
5419
+ const x = counterOrNone(a);
5420
+ const y = counterOrNone(b);
5421
+ return x === void 0 ? y : y === void 0 ? x : Math.max(x, y);
5422
+ };
5423
+ function usableEntry(entry) {
5424
+ if (typeof entry !== "object" || entry === null) return void 0;
5425
+ const { nextIndex, addedAt } = entry;
5426
+ const index = counterOrNone(nextIndex);
5427
+ if (index === void 0) return void 0;
5428
+ return { nextIndex: index, addedAt: counterOrNone(addedAt) ?? Date.now() };
5429
+ }
5418
5430
  function mergePairwise(current, fallback) {
5419
5431
  if (!current && !fallback) return void 0;
5420
- const merged = {
5421
- ...fallback ?? {}
5422
- };
5423
- for (const [key, entry] of Object.entries(current ?? {})) {
5432
+ const merged = {};
5433
+ for (const [key, entry] of Object.entries(fallback ?? {})) {
5434
+ const usable = usableEntry(entry);
5435
+ if (usable) merged[key] = usable;
5436
+ }
5437
+ for (const [key, raw] of Object.entries(current ?? {})) {
5438
+ const entry = usableEntry(raw);
5439
+ if (!entry) continue;
5424
5440
  const prev = merged[key];
5425
5441
  merged[key] = prev ? {
5426
5442
  nextIndex: Math.max(prev.nextIndex, entry.nextIndex),
@@ -5431,33 +5447,42 @@ function mergePairwise(current, fallback) {
5431
5447
  }
5432
5448
 
5433
5449
  // src/wallet/vault/storage/ephemeralIndex.ts
5450
+ var MAX_EPH_INDEX = 4294967294;
5451
+ function usableCounter(value) {
5452
+ if (typeof value !== "number" || !Number.isInteger(value)) return null;
5453
+ if (value < 0 || value > MAX_EPH_INDEX) return null;
5454
+ return value;
5455
+ }
5434
5456
  async function reserveSelfEphIndex(storage) {
5435
5457
  const updated = await storage.updateConfig((config) => ({
5436
5458
  ...config,
5437
- selfEphCounter: (config.selfEphCounter ?? 0) + 1,
5459
+ selfEphCounter: (usableCounter(config.selfEphCounter) ?? 0) + 1,
5438
5460
  updatedAt: Date.now()
5439
5461
  }));
5440
5462
  if (!updated) throw new Error("vault not initialized");
5441
- return (updated.selfEphCounter ?? 1) - 1;
5463
+ return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
5442
5464
  }
5443
5465
  async function reservePairwiseIndex(storage, ivkHex) {
5444
5466
  const key = ivkHex.toLowerCase();
5445
5467
  const updated = await storage.updateConfig((config) => {
5446
- const current = config.pairwiseCounterparties?.[key];
5468
+ const stored = usableCounter(config.pairwiseCounterparties?.[key]?.nextIndex);
5469
+ const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
5447
5470
  return {
5448
5471
  ...config,
5449
5472
  pairwiseCounterparties: {
5450
5473
  ...config.pairwiseCounterparties,
5451
5474
  [key]: {
5452
- nextIndex: (current?.nextIndex ?? 0) + 1,
5453
- addedAt: current?.addedAt ?? Date.now()
5475
+ nextIndex: (stored ?? 0) + 1,
5476
+ addedAt: typeof addedAt === "number" ? addedAt : Date.now()
5454
5477
  }
5455
5478
  },
5456
5479
  updatedAt: Date.now()
5457
5480
  };
5458
5481
  });
5459
5482
  if (!updated) throw new Error("vault not initialized");
5460
- return (updated.pairwiseCounterparties?.[key]?.nextIndex ?? 1) - 1;
5483
+ const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
5484
+ if (nextIndex === null || nextIndex <= 1) return null;
5485
+ return nextIndex - 1;
5461
5486
  }
5462
5487
 
5463
5488
  // src/wallet/vault/notes/record.ts
@@ -6235,7 +6260,7 @@ function gapMargin(windowSize) {
6235
6260
  }
6236
6261
  function resolveSelfEphCeiling(params) {
6237
6262
  const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
6238
- const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
6263
+ const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
6239
6264
  if (scanMaxIndex === null) return null;
6240
6265
  if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
6241
6266
  const ownEphPks = /* @__PURE__ */ new Set();
@@ -6260,9 +6285,12 @@ function resolveSelfEphCeiling(params) {
6260
6285
  }
6261
6286
  return max;
6262
6287
  }
6288
+ var MAX_EPH_WINDOW = SELF_EPH_WINDOW * 64;
6263
6289
  function windowSizeForCounter(counter) {
6264
- const needed = counter + gapMargin(SELF_EPH_WINDOW);
6265
- return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
6290
+ const used = Number.isFinite(counter) ? counter : 0;
6291
+ const needed = used + gapMargin(SELF_EPH_WINDOW);
6292
+ const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
6293
+ return Math.min(MAX_EPH_WINDOW, Math.max(SELF_EPH_WINDOW, rounded));
6266
6294
  }
6267
6295
 
6268
6296
  // src/wallet/scanner/phases/spentStatus.ts
@@ -6520,11 +6548,13 @@ async function buildZkNote(params, deps) {
6520
6548
  } else if (params.viewingPublicKey && storage && keys.viewingSecretKey) {
6521
6549
  try {
6522
6550
  const index = await reservePairwiseIndex(storage, toHex(params.viewingPublicKey));
6523
- const pairSecret = derivePairwiseSharedSecret(
6524
- keys.viewingSecretKey,
6525
- params.viewingPublicKey
6526
- );
6527
- ephSkOverride = derivePairwiseEphSk(pairSecret, index);
6551
+ if (index !== null) {
6552
+ const pairSecret = derivePairwiseSharedSecret(
6553
+ keys.viewingSecretKey,
6554
+ params.viewingPublicKey
6555
+ );
6556
+ ephSkOverride = derivePairwiseEphSk(pairSecret, index);
6557
+ }
6528
6558
  } catch {
6529
6559
  }
6530
6560
  }
package/dist/index.mjs CHANGED
@@ -4188,13 +4188,29 @@ function mergeCounters(current, fallback) {
4188
4188
  ...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
4189
4189
  };
4190
4190
  }
4191
- var higher = (a, b) => a === void 0 ? b : b === void 0 ? a : Math.max(a, b);
4191
+ var counterOrNone = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
4192
+ var higher = (a, b) => {
4193
+ const x = counterOrNone(a);
4194
+ const y = counterOrNone(b);
4195
+ return x === void 0 ? y : y === void 0 ? x : Math.max(x, y);
4196
+ };
4197
+ function usableEntry(entry) {
4198
+ if (typeof entry !== "object" || entry === null) return void 0;
4199
+ const { nextIndex, addedAt } = entry;
4200
+ const index = counterOrNone(nextIndex);
4201
+ if (index === void 0) return void 0;
4202
+ return { nextIndex: index, addedAt: counterOrNone(addedAt) ?? Date.now() };
4203
+ }
4192
4204
  function mergePairwise(current, fallback) {
4193
4205
  if (!current && !fallback) return void 0;
4194
- const merged = {
4195
- ...fallback ?? {}
4196
- };
4197
- for (const [key, entry] of Object.entries(current ?? {})) {
4206
+ const merged = {};
4207
+ for (const [key, entry] of Object.entries(fallback ?? {})) {
4208
+ const usable = usableEntry(entry);
4209
+ if (usable) merged[key] = usable;
4210
+ }
4211
+ for (const [key, raw] of Object.entries(current ?? {})) {
4212
+ const entry = usableEntry(raw);
4213
+ if (!entry) continue;
4198
4214
  const prev = merged[key];
4199
4215
  merged[key] = prev ? {
4200
4216
  nextIndex: Math.max(prev.nextIndex, entry.nextIndex),
@@ -4205,33 +4221,42 @@ function mergePairwise(current, fallback) {
4205
4221
  }
4206
4222
 
4207
4223
  // src/wallet/vault/storage/ephemeralIndex.ts
4224
+ var MAX_EPH_INDEX = 4294967294;
4225
+ function usableCounter(value) {
4226
+ if (typeof value !== "number" || !Number.isInteger(value)) return null;
4227
+ if (value < 0 || value > MAX_EPH_INDEX) return null;
4228
+ return value;
4229
+ }
4208
4230
  async function reserveSelfEphIndex(storage) {
4209
4231
  const updated = await storage.updateConfig((config) => ({
4210
4232
  ...config,
4211
- selfEphCounter: (config.selfEphCounter ?? 0) + 1,
4233
+ selfEphCounter: (usableCounter(config.selfEphCounter) ?? 0) + 1,
4212
4234
  updatedAt: Date.now()
4213
4235
  }));
4214
4236
  if (!updated) throw new Error("vault not initialized");
4215
- return (updated.selfEphCounter ?? 1) - 1;
4237
+ return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
4216
4238
  }
4217
4239
  async function reservePairwiseIndex(storage, ivkHex) {
4218
4240
  const key = ivkHex.toLowerCase();
4219
4241
  const updated = await storage.updateConfig((config) => {
4220
- const current = config.pairwiseCounterparties?.[key];
4242
+ const stored = usableCounter(config.pairwiseCounterparties?.[key]?.nextIndex);
4243
+ const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
4221
4244
  return {
4222
4245
  ...config,
4223
4246
  pairwiseCounterparties: {
4224
4247
  ...config.pairwiseCounterparties,
4225
4248
  [key]: {
4226
- nextIndex: (current?.nextIndex ?? 0) + 1,
4227
- addedAt: current?.addedAt ?? Date.now()
4249
+ nextIndex: (stored ?? 0) + 1,
4250
+ addedAt: typeof addedAt === "number" ? addedAt : Date.now()
4228
4251
  }
4229
4252
  },
4230
4253
  updatedAt: Date.now()
4231
4254
  };
4232
4255
  });
4233
4256
  if (!updated) throw new Error("vault not initialized");
4234
- return (updated.pairwiseCounterparties?.[key]?.nextIndex ?? 1) - 1;
4257
+ const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
4258
+ if (nextIndex === null || nextIndex <= 1) return null;
4259
+ return nextIndex - 1;
4235
4260
  }
4236
4261
 
4237
4262
  // src/wallet/vault/notes/record.ts
@@ -4998,7 +5023,7 @@ function gapMargin(windowSize) {
4998
5023
  }
4999
5024
  function resolveSelfEphCeiling(params) {
5000
5025
  const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
5001
- const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
5026
+ const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
5002
5027
  if (scanMaxIndex === null) return null;
5003
5028
  if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
5004
5029
  const ownEphPks = /* @__PURE__ */ new Set();
@@ -5023,9 +5048,12 @@ function resolveSelfEphCeiling(params) {
5023
5048
  }
5024
5049
  return max;
5025
5050
  }
5051
+ var MAX_EPH_WINDOW = SELF_EPH_WINDOW * 64;
5026
5052
  function windowSizeForCounter(counter) {
5027
- const needed = counter + gapMargin(SELF_EPH_WINDOW);
5028
- return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
5053
+ const used = Number.isFinite(counter) ? counter : 0;
5054
+ const needed = used + gapMargin(SELF_EPH_WINDOW);
5055
+ const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
5056
+ return Math.min(MAX_EPH_WINDOW, Math.max(SELF_EPH_WINDOW, rounded));
5029
5057
  }
5030
5058
 
5031
5059
  // src/wallet/scanner/phases/spentStatus.ts
@@ -5283,11 +5311,13 @@ async function buildZkNote(params, deps) {
5283
5311
  } else if (params.viewingPublicKey && storage && keys.viewingSecretKey) {
5284
5312
  try {
5285
5313
  const index = await reservePairwiseIndex(storage, toHex(params.viewingPublicKey));
5286
- const pairSecret = derivePairwiseSharedSecret(
5287
- keys.viewingSecretKey,
5288
- params.viewingPublicKey
5289
- );
5290
- ephSkOverride = derivePairwiseEphSk(pairSecret, index);
5314
+ if (index !== null) {
5315
+ const pairSecret = derivePairwiseSharedSecret(
5316
+ keys.viewingSecretKey,
5317
+ params.viewingPublicKey
5318
+ );
5319
+ ephSkOverride = derivePairwiseEphSk(pairSecret, index);
5320
+ }
5291
5321
  } catch {
5292
5322
  }
5293
5323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbinum/sdk",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Official TypeScript SDK for Orbinum.",
5
5
  "author": "Orbinum",
6
6
  "license": "MIT",