Skip to content

feat(loaders): add NIP-05 over Namecoin (.bit) identity loader#79

Draft
mstrofnone wants to merge 2 commits into
hzrd149:masterfrom
mstrofnone:feat/nip05-namecoin
Draft

feat(loaders): add NIP-05 over Namecoin (.bit) identity loader#79
mstrofnone wants to merge 2 commits into
hzrd149:masterfrom
mstrofnone:feat/nip05-namecoin

Conversation

@mstrofnone
Copy link
Copy Markdown

@mstrofnone mstrofnone commented May 21, 2026

NIP-05 over Namecoin (.bit) identity loader

This PR adds a transport-free helper module and matching loader for resolving
NIP-05 identifiers rooted in the Namecoin blockchain
(the .bit TLD), rather than DNS. It is the loaders-package companion to the
existing DnsIdentityLoader / dns-identity helper — same shape, different
root of trust.

Spec draft: nostr-protocol/nips#2349.

Design siblings already in flight in other Nostr ecosystems:

The parser shape (local-part priority exact → _ → first valid, d/
vs id/ namespace handling, leading nostr: prefix tolerance, simple
nostr: "<hex>" vs extended nostr: { names, relays, nip46 } form) is
ported from those references, themselves ports of the Kotlin implementation
in Amethyst and the Swift port
in Nostur.

What's added

packages/loaders/src/helpers/namecoin-identity.ts:

  • parseNamecoinAddress(input) — accepts <x>.bit, alice@<x>.bit, d/<x>,
    id/<x>, with optional leading nostr: prefix
  • isNamecoinIdentifier / isDotBit — cheap front-door checks
  • extractNostrFromValue(address, json) — extracts pubkey + optional relays +
    optional nip46 relays from a Namecoin name value, matching the reference
    local-part priority rules
  • getIdentityFromNamecoinValue — builds an Identity (the existing
    dns-identity type) keyed by the Namecoin name in domain
  • expandImports(value, lookup, maxDepth = 4) — recursively resolves
    ifa-0001 "import" directives so apex records that delegate the
    nostr.names block to a sibling name (the canonical testls.bit
    workaround for the 520-byte per-name limit) merge correctly before the
    nostr field is extracted. Supports the canonical array-of-arrays form
    plus the three real-world shorthand forms (bare string, single-element
    array, name+selector pair), null semantic suppression, the DNS-dotted
    subdomain selector walk through the imported map tree (exact → *
    "" fallback), the spec-mandated minimum recursion depth of four,
    visited-set cycle protection, and best-effort handling of failed
    sub-lookups so transient ElectrumX hiccups never nuke an otherwise
    resolvable record. Records without an "import" key pay zero extra I/O.
  • buildNameIndexScript / electrumScriptHash / parseNameUpdateScript
    Namecoin script + ElectrumX scripthash helpers for callers that drive the
    ElectrumX query themselves (handles direct push, OP_PUSHDATA1,
    OP_PUSHDATA2, and OP_PUSHDATA4 framing)
  • DEFAULT_ELECTRUMX_SERVERS — the same three operator pairs the Kotlin /
    Swift / Rust references ship

packages/loaders/src/loaders/namecoin-identity-loader.ts:

  • NamecoinIdentityLoader mirroring DnsIdentityLoader's observable surface
    — same cache?: AsyncIdentityCache, same expiration / dedupe semantics,
    same Identity return shape
  • A single transport hook loader.resolve = (namecoinName) => Promise<string>
    that the consumer wires up to an ElectrumX (WSS / TCP+TLS) client of their
    choice. The default implementation throws a clear error so misconfiguration
    is loud rather than silent.
  • An importDepth knob (default 4, the ifa-0001 minimum) that controls the
    import-chain recursion depth. Set to 0 to disable import-following
    entirely.

Why transport-free

applesauce-core and applesauce-loaders need to stay isomorphic — no
import 'ws', no node: imports, no pinned self-signed certificates. The
helpers do all the parsing / verification and the loader stitches in
caching and dedupe, but the actual ElectrumX WSS / TCP+TLS connection is
left to the consumer (browser, Tauri, Node, Bun, etc. all have different
preferences). This matches how nostr-tools/nip05namecoin is being shaped
in the upstream JS sibling PR.

Runtime deps

One small addition to applesauce-loaders/package.json:

   "dependencies": {
+    "@noble/hashes": "^1.7.1",
     "applesauce-core": "^6.1.0",
     ...
   }

@noble/hashes is already a direct dep of applesauce-relay and
applesauce-wallet-connect at the same ^1.7.1 range, and already
transitively present via nostr-tools. Used solely for sha256 in
electrumScriptHash. No other new runtime deps.

Tests

60 new tests across two files:

  • packages/loaders/src/helpers/__tests__/namecoin-identity.test.ts — 40
    tests covering parser positive/negative cases, the nostr: prefix, the
    simple vs extended JSON forms, the _ and first-valid fallbacks for
    domain vs identity namespaces, Identity construction, script layout,
    scripthash reversibility, OP_PUSHDATA1 framing, and the full
    expandImports matrix (all four "import" shapes, selector walk +
    wildcard, importer-wins, null suppression, depth-4 happy path,
    depth-truncation, cycles, malformed-input + lookup-failure lenience)
  • packages/loaders/src/loaders/__tests__/namecoin-identity-loader.test.ts
    — 20 tests covering the default-resolver error, extended/simple JSON
    parsing, error/missing branches, cache hit/miss/expired, the
    single-identifier overload, concurrent-request dedupe via the
    requesting map, end-to-end import-chain resolution of bare and named
    .bit identifiers, the zero-extra-cost guarantee for non-import
    records, importer-wins on the nostr.names map, graceful degradation
    when the imported sibling is unreachable, and the importDepth = 0
    disable knob

pnpm --filter applesauce-loaders test: 139/139 (was 79; 60 new).
pnpm --filter applesauce-core test: 562/562.

Snapshot updates

packages/loaders/src/helpers/__tests__/exports.test.ts and
packages/loaders/src/loaders/__tests__/exports.test.ts regenerated to
include the new symbols (DEFAULT_IMPORT_DEPTH, expandImports).

Files touched

.changeset/nip05-namecoin.md                                      (new)
.changeset/nip05-namecoin-import-chain.md                         (new)
packages/loaders/package.json                                     (+1 dep)
packages/loaders/src/helpers/index.ts                             (+1)
packages/loaders/src/helpers/namecoin-identity.ts                 (new, ~650 lines)
packages/loaders/src/helpers/__tests__/exports.test.ts            (snapshot)
packages/loaders/src/helpers/__tests__/namecoin-identity.test.ts  (new, ~440 lines)
packages/loaders/src/loaders/index.ts                             (+1)
packages/loaders/src/loaders/namecoin-identity-loader.ts          (new, ~220 lines)
packages/loaders/src/loaders/__tests__/exports.test.ts            (snapshot)
packages/loaders/src/loaders/__tests__/namecoin-identity-loader.test.ts (new, ~270 lines)

Out of scope

  • No DNS-identity changes — dns-identity.ts / dns-identity-loader.ts
    are untouched; this is purely additive
  • No ElectrumX client / WSS transport (intentionally consumer-owned)
  • No applesauce-core helper barrel for Namecoin — the parser lives in
    applesauce-loaders/helpers only (the upstream nostr-tools sibling
    is still in PR; once that lands, a thin core re-export can follow)

Draft until the upstream JS sibling settles, but the API is intended to be
stable as-is.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: 9968f5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
applesauce-loaders Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Apex records bump against the 520-byte per-name limit on Namecoin and
commonly delegate their `nostr.names` block to a sibling name (e.g.
`dd/<name>`) via the ifa-0001 `"import"` directive. Without import-chain
handling, NIP-05 resolution sees no `nostr` field at the apex and
silently fails for these records (the canonical `testls.bit` demo
target is one).

This commit ports the import-chain resolver between the parsed apex
JSON and the existing `nostr` extractor:

  - `expandImports(value, lookup, maxDepth = 4)` in
    `helpers/namecoin-identity.ts` recursively merges imported names
    (canonical array-of-arrays plus the three real-world shorthand
    forms) into the importing object, with importer-wins precedence,
    `null` semantic suppression, DNS-dotted subdomain selector walk
    through the imported `map` tree (exact / `*` / `""` fallback),
    visited-set cycle protection, and best-effort handling of failed
    sub-lookups so transient ElectrumX hiccups never nuke an otherwise
    resolvable record.
  - `NamecoinIdentityLoader.fetchIdentity` calls `expandImports` once
    between `JSON.parse` and `getIdentityFromNamecoinValue`, using the
    consumer-provided `resolve` hook as the import lookup.
  - Records without an `"import"` key pay zero extra I/O.
  - A new `importDepth` knob (default 4, the spec minimum) lets
    consumers disable or tune the recursion.

22 new tests across the helper (`expandImports` unit cases for each
shorthand, selector / wildcard, depth, cycle, malformed input,
lenient I/O) and the loader (apex + sibling fetched, named local-part
resolves through, exactly one query for non-import records,
importer-wins on the `nostr.names` map, local data survives a missing
import). `pnpm -F applesauce-loaders test`: 139/139 (was 117).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant