@unicitylabs/nostr-js-sdk 0.5.0 → 0.6.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.
@@ -9244,6 +9244,16 @@ var NametagUtils = /*#__PURE__*/Object.freeze({
9244
9244
  */
9245
9245
  /** Default country code for phone number normalization (shared with NametagUtils) */
9246
9246
  const DEFAULT_COUNTRY = 'US';
9247
+ /**
9248
+ * UNIP-01 namespace label for nametag ownership bindings.
9249
+ *
9250
+ * A binding event carrying a NIP-32 ["L", UNICITY_NAMETAG_NAMESPACE] label opts
9251
+ * into relay-enforced single-owner semantics: the first author a UNIP-01 relay
9252
+ * accepts for a given identifier (d-tag) owns it, by relay receive order. The
9253
+ * marker is what makes ownership independent of the self-asserted `created_at`.
9254
+ * See the UNIP-01 spec (unicity-tokens-relay/docs/UNIP-01.md).
9255
+ */
9256
+ const UNICITY_NAMETAG_NAMESPACE = 'unicity:nametag';
9247
9257
  /**
9248
9258
  * Create a nametag binding event.
9249
9259
  *
@@ -9275,6 +9285,8 @@ async function createBindingEvent(keyManager, nametagId, unicityAddress, default
9275
9285
  };
9276
9286
  const tags = [
9277
9287
  ['d', hashedNametag],
9288
+ // UNIP-01: opt this binding into relay-enforced single-owner semantics.
9289
+ ['L', UNICITY_NAMETAG_NAMESPACE],
9278
9290
  ['nametag', hashedNametag],
9279
9291
  ['t', hashedNametag],
9280
9292
  ['address', unicityAddress],
@@ -9478,6 +9490,18 @@ function parseAddressFromEvent(event) {
9478
9490
  return undefined;
9479
9491
  }
9480
9492
  }
9493
+ /**
9494
+ * UNIP-01: true if this binding event carries the single-owner namespace marker
9495
+ * (a NIP-32 ["L", UNICITY_NAMETAG_NAMESPACE] label). Such bindings are vetted by
9496
+ * UNIP-01 relays for single ownership and are preferred at resolution over
9497
+ * unmarked (legacy) bindings, independent of the self-asserted `created_at`.
9498
+ *
9499
+ * @param event Binding event
9500
+ * @returns true if the UNIP-01 nametag marker is present
9501
+ */
9502
+ function hasNametagOwnershipMarker(event) {
9503
+ return event.getTagValues('L').includes(UNICITY_NAMETAG_NAMESPACE);
9504
+ }
9481
9505
  /**
9482
9506
  * Verify that a binding event is valid.
9483
9507
  * Checks signature and structure.
@@ -9510,11 +9534,13 @@ function isValidBindingEvent(event) {
9510
9534
 
9511
9535
  var NametagBinding = /*#__PURE__*/Object.freeze({
9512
9536
  __proto__: null,
9537
+ UNICITY_NAMETAG_NAMESPACE: UNICITY_NAMETAG_NAMESPACE,
9513
9538
  createAddressToBindingFilter: createAddressToBindingFilter,
9514
9539
  createBindingEvent: createBindingEvent,
9515
9540
  createIdentityBindingEvent: createIdentityBindingEvent,
9516
9541
  createNametagToPubkeyFilter: createNametagToPubkeyFilter,
9517
9542
  createPubkeyToNametagFilter: createPubkeyToNametagFilter,
9543
+ hasNametagOwnershipMarker: hasNametagOwnershipMarker,
9518
9544
  isValidBindingEvent: isValidBindingEvent,
9519
9545
  parseAddressFromEvent: parseAddressFromEvent,
9520
9546
  parseBindingInfo: parseBindingInfo,
@@ -10448,7 +10474,18 @@ class NostrClient {
10448
10474
  await this.publishEvent(event);
10449
10475
  return true;
10450
10476
  }
10451
- catch {
10477
+ catch (err) {
10478
+ // UNIP-01: a relay-enforced single-owner rejection (NIP-20 `blocked:`)
10479
+ // means the nametag is owned by another key — surface it as a hard
10480
+ // failure rather than a silent `false`, which a caller could mistake for
10481
+ // a transient publish error. Other (transient) errors keep returning
10482
+ // false, preserving the prior best-effort behavior.
10483
+ const msg = err instanceof Error ? err.message : String(err);
10484
+ // Match only the ownership-conflict message, not the generic NIP-20
10485
+ // `blocked:` prefix (which also covers spam/rate-limit/policy rejections).
10486
+ if (/owned by another key|already claimed/i.test(msg)) {
10487
+ throw new Error(`Nametag "${nametagId}" is already claimed by another key`);
10488
+ }
10452
10489
  return false;
10453
10490
  }
10454
10491
  }
@@ -10532,20 +10569,26 @@ class NostrClient {
10532
10569
  }
10533
10570
  }
10534
10571
  /**
10535
- * Query binding events with first-seen-wins anti-hijacking resolution.
10572
+ * Query binding events and select the owner.
10536
10573
  *
10537
- * Strategy: first-seen-wins across authors, latest-wins for same author.
10538
- * - Across authors: the pubkey that first published wins (earliest created_at)
10539
- * - Same author: the most recent event is used (latest created_at = most complete data)
10540
- * - Tie-breaking: deterministic by lexicographic pubkey comparison (lowest wins)
10574
+ * UNIP-01 (preferred): if any returned binding carries the single-owner
10575
+ * namespace marker (NIP-32 ["L", "unicity:nametag"]), ownership is taken from
10576
+ * the marked binding. These are vetted by UNIP-01 relays for single ownership
10577
+ * (first author by relay receive order owns the identifier), so the
10578
+ * self-asserted `created_at` is ignored entirely. With a UNIP-01 relay set
10579
+ * there is exactly one marked owner; observing more than one distinct marked
10580
+ * author (e.g. across relays in differing states) is treated as ambiguous and
10581
+ * resolves to null rather than guessing.
10541
10582
  *
10542
- * Events with invalid signatures are silently skipped to prevent relay injection attacks.
10583
+ * Legacy fallback (no marked binding present e.g. an identifier not yet
10584
+ * migrated): first-seen-wins across authors by `created_at`, latest-wins for
10585
+ * the same author, lexicographic pubkey tie-break. Retained for backward
10586
+ * compatibility during migration; superseded as soon as the owner republishes
10587
+ * a marked binding. (Self-asserted timestamps are not authoritative — this
10588
+ * path exists only so un-migrated identifiers keep resolving.)
10543
10589
  *
10544
- * Known limitations:
10545
- * - Timestamps are self-reported (NIP-01). An attacker can set created_at to 0.
10546
- * Chain-anchored proof of registration time is the only reliable defense.
10547
- * - TOCTOU: between conflict check and publish, another user can claim the same nametag.
10548
- * This is inherent to Nostr's eventually-consistent relay model.
10590
+ * Events with invalid signatures are silently skipped to prevent relay
10591
+ * injection of forged events.
10549
10592
  *
10550
10593
  * @param filter Subscription filter
10551
10594
  * @param extractResult Callback to extract the desired result from the winning event
@@ -10581,6 +10624,19 @@ class NostrClient {
10581
10624
  const authors = new Map();
10582
10625
  const allRelaysDone = (id) => this.allRelaysDoneFor(id);
10583
10626
  const pickWinner = () => {
10627
+ // UNIP-01: prefer marker-carrying (relay-vetted single-owner) bindings,
10628
+ // ignoring the self-asserted created_at. The marker is read from each
10629
+ // author's CURRENT (latest, per NIP-33 replaceable) event, so a newer
10630
+ // unmarked update correctly drops the author from the marked set, and a
10631
+ // marked owner resolves to their freshest binding.
10632
+ const marked = [...authors.values()].filter((e) => hasNametagOwnershipMarker(e.latestEvent));
10633
+ if (marked.length > 0) {
10634
+ // Exactly one marked owner is the relay-enforced norm. More than one
10635
+ // distinct marked author means cross-relay disagreement — do not guess.
10636
+ const owner = marked.length === 1 ? marked[0] : undefined;
10637
+ return owner ? extractResult(owner.latestEvent) : null;
10638
+ }
10639
+ // Legacy fallback: first-seen-wins by self-asserted created_at.
10584
10640
  let winnerEntry = null;
10585
10641
  let winnerPubkey = '';
10586
10642
  for (const [pubkey, entry] of authors) {
@@ -11387,5 +11443,5 @@ function isReadReceipt(message) {
11387
11443
  return message.kind === 15;
11388
11444
  }
11389
11445
 
11390
- export { AGENT_LOCATION, AGENT_PROFILE, APP_DATA, AUTH, bech32 as Bech32, CHAT_MESSAGE, CLOSED, CLOSING, CONNECTING, CONTACTS, CallbackEventListener, DELETION, ENCRYPTED_DM, Event, EventKinds, FILE_METADATA, Filter, FilterBuilder, GIFT_WRAP, NAMETAG_MAX_LENGTH, NAMETAG_MIN_LENGTH, nip04 as NIP04, nip17 as NIP17, nip44 as NIP44, NametagBinding, NametagUtils, NostrClient, NostrKeyManager, OPEN, PAYMENT_REQUEST, PAYMENT_REQUEST_RESPONSE, PROFILE, PaymentRequestProtocol, REACTION, READ_RECEIPT, RECOMMEND_RELAY, RELAY_LIST, SEAL, schnorr as SchnorrSigner, TEXT_NOTE, TOKEN_TRANSFER, TokenTransferProtocol, areSameNametag, createAddressToBindingFilter, createBindingEvent, createGiftWrap, createIdentityBindingEvent, createNametagToPubkeyFilter, createPubkeyToNametagFilter, createReadReceipt, createWebSocket, decode, decodeNpub, decodeNsec, decryptNametag, encode, encodeNpub, encodeNsec, encryptNametag, extractMessageData, formatForDisplay, getName, getPublicKey, getPublicKeyHex, hashAddressForTag, hashNametag, isChatMessage, isEphemeral, isParameterizedReplaceable, isPhoneNumber, isReadReceipt, isReplaceable, isValidBindingEvent, isValidNametag, normalizeNametag, parseAddressFromEvent, parseBindingInfo, parseNametagHashFromEvent, sha256Hex, sign, signHex, unwrap, verify, verifyHex };
11446
+ export { AGENT_LOCATION, AGENT_PROFILE, APP_DATA, AUTH, bech32 as Bech32, CHAT_MESSAGE, CLOSED, CLOSING, CONNECTING, CONTACTS, CallbackEventListener, DELETION, ENCRYPTED_DM, Event, EventKinds, FILE_METADATA, Filter, FilterBuilder, GIFT_WRAP, NAMETAG_MAX_LENGTH, NAMETAG_MIN_LENGTH, nip04 as NIP04, nip17 as NIP17, nip44 as NIP44, NametagBinding, NametagUtils, NostrClient, NostrKeyManager, OPEN, PAYMENT_REQUEST, PAYMENT_REQUEST_RESPONSE, PROFILE, PaymentRequestProtocol, REACTION, READ_RECEIPT, RECOMMEND_RELAY, RELAY_LIST, SEAL, schnorr as SchnorrSigner, TEXT_NOTE, TOKEN_TRANSFER, TokenTransferProtocol, UNICITY_NAMETAG_NAMESPACE, areSameNametag, createAddressToBindingFilter, createBindingEvent, createGiftWrap, createIdentityBindingEvent, createNametagToPubkeyFilter, createPubkeyToNametagFilter, createReadReceipt, createWebSocket, decode, decodeNpub, decodeNsec, decryptNametag, encode, encodeNpub, encodeNsec, encryptNametag, extractMessageData, formatForDisplay, getName, getPublicKey, getPublicKeyHex, hasNametagOwnershipMarker, hashAddressForTag, hashNametag, isChatMessage, isEphemeral, isParameterizedReplaceable, isPhoneNumber, isReadReceipt, isReplaceable, isValidBindingEvent, isValidNametag, normalizeNametag, parseAddressFromEvent, parseBindingInfo, parseNametagHashFromEvent, sha256Hex, sign, signHex, unwrap, verify, verifyHex };
11391
11447
  //# sourceMappingURL=index.js.map