npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eternl/hub-wallet-sdk

v0.31.3

Published

Eternl Hub SDK for wallet developers.

Readme

@eternl/hub-wallet-sdk

A composables-based SDK for wallet developers to integrate with Eternl Hub. It provides the wallet-side building blocks for secure device/dApp pairing, end-to-end-encrypted messaging, presence, and remote signing over NATS WebSocket transport.

The primary, high-level entry point is createWalletConnectorSession — it composes the symmetric pairing protocol from @eternl/hub-core and adds the wallet-role logic (responding to dApp apiCalls, account switching, presence). Lower-level composables (useNatsClient, useEncryption, useReplayProtection, …) are also exported for advanced or custom integrations.

Status: source-available under BUSL-1.1 (see License). The API is stabilizing toward a 1.0 release; while on 0.x, minor versions may carry breaking changes — check the CHANGELOG before upgrading.

Installation

npm install @eternl/hub-wallet-sdk
# or
pnpm add @eternl/hub-wallet-sdk

It depends on @eternl/hub-core and the modular @nats-io/*@3.x clients (@nats-io/nats-core, @nats-io/jetstream, @nats-io/kv), which are installed automatically as dependencies.

Runtime requirements

The SDK requires a secure WebCrypto environment (a modern browser or Node.js 18+). initHub() calls assertCryptoCapabilities() and throws if crypto.subtle / crypto.getRandomValues are unavailable. There is no Math.random() fallback anywhere in the SDK.

You can probe capabilities yourself:

import { checkCryptoCapabilities } from '@eternl/hub-wallet-sdk'

const caps = checkCryptoCapabilities()
// {
//   hasSubtleCrypto: boolean
//   hasRandomValues: boolean
//   hasRandomUUID:   boolean
//   isSecure:        boolean   // hasSubtleCrypto && hasRandomValues
// }
if (!caps.isSecure) {
  throw new Error('WebCrypto unavailable — SDK cannot run here')
}

Limitations — module-level singleton state

Read this before designing your integration.

Several core modules hold process-wide singleton state, not per-instance state:

  • useNatsClient — one NATS connection, one set of callbacks, and one subscription map per process. Calling useNatsClient() repeatedly returns handles onto the same underlying connection. configure() overwrites the previously-configured providers/callbacks. There is exactly one connection per process.
  • useEncryptiongetCurrentKeyPair() / getCurrentSessionKey() track a single "current" key pair / session key per process. generateKeyPair() and deriveSessionKeyHKDF() overwrite them.
  • signingTransportconfigureSigningTransport(...) sets one global config consumed by all signing composables.
  • getWalletPresenceBeacon() — returns a process-wide singleton beacon (so all sessions share one lifecycle-dispatcher subscription).
  • useReplayProtection / useSessionKeyCache — module-level caches keyed by pairId / deviceId.

Practical consequence: this SDK is designed for one wallet client per JS context (one browser tab / one Node process). It is not safe to run two independent, differently-configured wallet clients in the same process. Tests that need isolation should reset state via the exported test helpers (e.g. __resetWalletPresenceBeaconForTests, clearAllReplayCaches, clearAllCachedKeys) or run in separate worker processes.

createWalletConnectorSession itself returns an independent per-pairing object, but it composes the singleton NATS transport and (by default) the singleton presence beacon underneath.

Configuration

1. Initialize the SDK

import { initHub, isInitialized, getConfig } from '@eternl/hub-wallet-sdk'

initHub({ network: 'mainnet' })

// With options:
initHub({
  network: 'preprod',                              // 'mainnet' | 'preprod' | 'preview'
  hubUrl: 'wss://custom-hub.example.com/nats',     // default: wss://hub.de-5.eternl.art/nats
  storage: myStorage,                              // optional HubStorage adapter
  storagePrefix: 'my-wallet',                      // optional localStorage key prefix
  constants: { CONNECTION_TIMEOUT_MS: 20_000 },    // optional protocol-constant overrides
})

if (isInitialized()) {
  console.log('network:', getConfig().network)
}

2. Required NATS configuration — getNatsUrl + getAuthenticator

useNatsClient does not read the hub URL from initHub. You must call configure() with at least a getNatsUrl provider before connecting. Production brokers run deny-by-default ACLs, so you must also supply a getAuthenticator that mints a per-pairing JWT.

import { useNatsClient } from '@eternl/hub-wallet-sdk'
import { jwtAuthenticator } from '@nats-io/nats-core'

const nats = useNatsClient()

nats.configure({
  // Required. Returns the broker WebSocket URL.
  getNatsUrl: () => 'wss://hub.de-5.eternl.art/nats',

  // Per-pairing JWT model (R1.5 / C1.e).
  // Called immediately before EVERY connect attempt — including reconnects —
  // so you can mint a fresh per-pairing CLIENTS user JWT (e.g. POST /nats/token
  // on be-hub) each time. Returning `undefined` produces an anonymous
  // connection, which only works against local-dev / pre-R1.2 brokers.
  //
  // YOU are responsible for refresh-before-expiry: when the JWT nears `exp`,
  // call `disconnect()` then `connect()` to swap in a fresh token. The SDK
  // re-invokes this provider on the new connect.
  getAuthenticator: async () => {
    const { jwt, seed } = await mintPerPairingToken()  // your call to be-hub
    return jwtAuthenticator(jwt, new TextEncoder().encode(seed))
  },

  // Optional lifecycle / error callbacks:
  onConnected: () => console.log('connected'),
  onDisconnected: () => console.log('disconnected'),
  onError: (msg: string) => console.error('nats error:', msg),

  // Broker rejected the whole connection (AUTHORIZATION_VIOLATION). The SDK
  // does NOT auto-retry — clear your cached JWT, then either mint a fresh one
  // and call retry(), or surface a re-pair prompt.
  onPairRevoked: () => clearCachedJwtAndPromptRepair(),

  // Broker rejected a publish/subscribe (PERMISSIONS_VIOLATION). The
  // connection stays up; this fires per-subject for ACL denials.
  onPermissionsViolation: (subject?: string) => console.warn('ACL denied:', subject),
})

await nats.connect()

Quick Start

A minimal end-to-end flow: connect, confirm the connection is live, then run a wallet connector session for a pairing.

import {
  initHub,
  useNatsClient,
  configureSigningTransport,
  createWalletConnectorSession,
  type ApiCallResult,
} from '@eternl/hub-wallet-sdk'

// 1. Init
initHub({ network: 'mainnet' })

// 2. Configure + connect transport
const nats = useNatsClient()
nats.configure({
  getNatsUrl: () => 'wss://hub.de-5.eternl.art/nats',
  getAuthenticator: async () => myJwtAuthenticator(),  // see "Required NATS configuration"
})
await nats.connect()

// connectionStatus() is a getter, not getConnectionStatus()
if (nats.connectionStatus() !== 'connected') {
  throw new Error(`NATS not connected: ${nats.connectionStatus()}`)
}

// 3. Run a wallet connector session for an established pairing.
//    `encryption` and `transport` here implement hub-core's SessionEncryptionApi
//    and Transport — typically built from the pairing's derived session key and
//    the NATS client. See "WalletConnectorSession" below.
const session = createWalletConnectorSession({
  pairId,                       // string — the established pairing id
  encryption,                   // SessionEncryptionApi (from @eternl/hub-core)
  transport,                    // Transport (from @eternl/hub-core)
  grantedPermissions,           // Permission[]
  walletName: 'My Wallet',
  apiCallHandler: async ({ method, args, accountId }): Promise<ApiCallResult> => {
    // Invoke the underlying CIP-30 method and return its result.
    const result = await myWallet.invoke(method, args, accountId)
    return { success: true, result }
  },
})

session.on('apiCallReceived', ({ method }) => console.log('dApp called', method))

Primary API — WalletConnectorSession

createWalletConnectorSession(options) builds a wallet-role session over a single pairing. It owns inbound apiCall dispatch, account-switch handling, access/consent state, and presence. The exported WalletConnectorSession class is a thin wrapper that returns the factory result.

Options (WalletConnectorSessionOptions)

interface WalletConnectorSessionOptions {
  pairId: string
  encryption: SessionEncryptionApi          // from @eternl/hub-core
  transport: Transport                       // from @eternl/hub-core
  grantedPermissions: Permission[]           // enforced for signing-method ACLs

  requireConsent?: boolean                   // default false → auto-grants all categories on start
  autoEstablishAllowed?: boolean             // default true
  walletName?: string

  apiCallHandler: ApiCallHandler             // REQUIRED
  accountSwitchHandler?: AccountSwitchHandler

  onPeerDisconnect?: (reason: string) => void
  onPeerPairingRemoved?: (reason: string) => void
  onError?: (err: Error) => void

  // Opt-in raw-stream presence (Option C):
  presenceC?: { deviceId: string; tabId: string; getNc: () => NatsConnection | null }
  presenceBeacon?: WalletPresenceBeaconApi   // override singleton (tests only)
}

type ApiCallResult =
  | { success: true; result: unknown }
  | { success: false; errorCode: string; errorMessage: string }

type ApiCallHandler = (params: {
  method: string
  args: unknown[]
  accountId: string
  requestId: string
}) => Promise<ApiCallResult>

type AccountSwitchHandler = (params: { accountId: string }) => Promise<boolean>

Returned API (WalletConnectorSessionApi)

const session = createWalletConnectorSession(options)

session.pairId                  // string
session.accessGranted           // boolean
session.grantedCategories       // CIP30Category[] (copy)
session.grantedPermissions      // readonly Permission[]

// Consent (only needed when requireConsent: true; otherwise auto-granted on start)
await session.grantAccess(['core', 'governance', 'extension'], /* autoEstablishAllowed */ true)
await session.denyAccess('User declined')

// Push account changes to the dApp
await session.notifyAccountsChanged(accounts, activeAccountId, 'updated')

// Presence
await session.requestPresenceCheck(/* timeoutMs? */ 5000)        // Promise<boolean>
await session.requestPresenceCSnapshot()                         // Option C only; else resolves null
await session.notifyOnline()
await session.notifyOffline('tab hidden')

// Teardown
await session.disconnect('Wallet disconnected')   // sends DISCONNECT, then disposes
await session.remove('Pairing removed')           // sends PAIRING_REMOVED, then disposes
session.dispose()                                 // local cleanup, no message

// Events (returns an unsubscribe fn)
const off = session.on('apiCallReceived', ({ method, accountId, requestId }) => { /* … */ })
session.on('accountSwitchRequested', ({ accountId }) => { /* … */ })
session.on('pingReceived', ({ timestamp }) => { /* … */ })
session.on('heartbeatReceived', ({ timestamp }) => { /* … */ })
session.on('peerPresence', ({ status, reason, sentAt }) => { /* … */ })
session.on('peerPresenceC', (event) => { /* … */ })          // Option C only
session.on('accessStateChange', ({ granted, reason }) => { /* … */ })
session.on('error', (err) => { /* … */ })
off()

Notes:

  • With requireConsent omitted/false, the session auto-grants ['core','governance','extension'] on start.
  • When access is not granted (or the method's category is not granted), inbound apiCalls are rejected with errorCode: '-3' before your apiCallHandler runs.
  • A handler that throws is surfaced to the dApp as errorCode: '-2'.
  • The session implements Symbol.dispose, so using session = createWalletConnectorSession(...) works where supported.

Remote signing (requester side)

useSigningRequestSender lets a wallet ask another paired instance to sign a transaction. It requires configureSigningTransport(...) to be called once at boot, and an active NATS connection.

import {
  configureSigningTransport,
  useSigningRequestSender,
} from '@eternl/hub-wallet-sdk'

// Configure once, globally:
configureSigningTransport({
  getDeviceInfo:        () => myDeviceInfo,
  getMyDeviceId:        () => myDeviceId,
  getMyPeerId:          () => myPeerId,
  getSessionKey:        (deviceId) => sessionKeysByDevice[deviceId],   // base64 raw key | undefined
  getPairCryptoContext: async (deviceId) => ({
    pairingId,
    pinAadBytes,        // Uint8Array — first 8 bytes of SHA-256(pin)
  }),
  getNextSeq:           (pairingId) => nextSeqFor(pairingId),          // bigint, monotonic per pairing
})

const { requestRemoteSigning } = useSigningRequestSender()

const result = await requestRemoteSigning(targetInstance, transactionCBOR, {
  walletPubKeyHash: 'sha256-hex-of-wallet-pubkey',
  // walletId?: explicit id to skip the pubKeyHash lookup
  // timeoutMs?: overrides SIGNING_TIMEOUT_MS (default 120_000)
})

// result.status: 'idle' | 'requesting' | 'waiting' | 'approved' | 'rejected' | 'timeout' | 'error'
if (result.status === 'approved') {
  console.log('signed tx:', result.signedTxCBOR, 'witness:', result.witnessSet)
} else {
  console.error('signing failed:', result.error)
}

For the responder side, see useSigningRequestHandler (decode/decrypt inbound signing.request.encrypted) and useSigningResponseSender (publish the encrypted response).

Presence beacon & raw-stream presence

import {
  getWalletPresenceBeacon,
  createWalletPresenceCClient,
} from '@eternl/hub-wallet-sdk'

// Process-wide beacon — fans tab-visibility / page-unload transitions to all
// registered sessions. WalletConnectorSession registers itself automatically.
const beacon = getWalletPresenceBeacon()
console.log('registered sessions:', beacon.size)

// Standalone raw-stream (Option C) presence client (usually you'd use the
// session's `presenceC` option instead of building this directly):
const presenceC = createWalletPresenceCClient({
  pairId,
  deviceId,
  tabId,
  getNc: () => useNatsClient().getNatsConnection(),  // live nats.ws connection or null
  onPeerEvent: (event) => console.log('peer presence:', event.classification),
})
presenceC.start()
// … later
presenceC.stop()

Lower-level composables

useNatsClient

One process-wide NATS WebSocket client. Status is read via getters, and request/response uses subscribeAndRequest (there is no request() method).

import { useNatsClient } from '@eternl/hub-wallet-sdk'

const nats = useNatsClient()
nats.configure({ getNatsUrl: () => 'wss://hub.de-5.eternl.art/nats', getAuthenticator })
await nats.connect()

// State getters
nats.connectionStatus()   // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'
nats.isConnected()        // boolean
nats.errorMessage()       // string
nats.reconnectAttempts()  // number

// Subscribe (JSON-parsed + schema-validated). Returns a Subscription | null.
const sub = nats.subscribe('hub.device.peer-id.message', (data) => {
  console.log('received:', data)
})

// Raw subscribe (no JSON parse/validation) — returns { unsubscribe } | null
const rawSub = nats.subscribeRaw('presence_out.pair.dapp', (bytes: Uint8Array) => { /* … */ })

// Publish
await nats.publish('hub.device.peer-id.message', { type: 'hello' })
await nats.publishRaw('subject', new Uint8Array([1, 2, 3]))

// Request/response (unique reply subject per call)
const res = await nats.subscribeAndRequest<MyResponse>(
  'hub.responses',          // responseSubject (a unique suffix is appended)
  'hub.requests',           // requestSubject
  { query: 'status' },      // requestData (a `replyTo` field is injected)
  { timeout: 5000 },
)
if (res.success) console.log(res.data)
else console.error(res.error)

// Cleanup
nats.unsubscribe('hub.device.peer-id.message')
nats.unsubscribeAll()
await nats.disconnect()

useEncryption

ECDH (P-256) + HKDF-SHA256 session-key derivation and AES-256-GCM AEAD, interoperable with @eternl/hub-core v2. There is no deriveSharedSecret; the v2 pipeline is deriveSharedSecretRawderiveSessionKeyHKDF. Encryption is PIN-bound: every frame's AAD is utf8(pairingId) || SHA-256(pin)[:8] || be64(seq).

import { useEncryption } from '@eternl/hub-wallet-sdk'

const enc = useEncryption()

// Handshake
const keyPair    = await enc.generateKeyPair()                       // KeyPair | null
const myPubB64   = await enc.exportPublicKey(keyPair!.publicKey)      // string | null
const peerPub    = await enc.importPublicKey(peerPubB64)             // CryptoKey | null

// v2 derivation
const shared     = await enc.deriveSharedSecretRaw(keyPair!.privateKey, peerPub!) // Uint8Array | null
const sas6       = await enc.deriveSAS6(shared!, pin, pairingId)     // 6-digit string for UI confirm
const session    = await enc.deriveSessionKeyHKDF(shared!, pairingId, pin)        // SessionKey | null
// session.key — non-extractable CryptoKey;  session.raw — base64 raw key (persist with care)

// Encrypt — pass a CryptoKey and a context { pairingId, pin | pinAadBytes, seq }
let seq = 1n
const payload = await enc.encryptJSON(
  { hello: 'world' },
  session!.key,
  { pairingId, pin, seq },          // OR { pairingId, pinAadBytes, seq } for PIN-less callers
)

// Decrypt — seq is read from the payload, so the context omits it
const plain = await enc.decryptJSON<{ hello: string }>(
  payload!,
  session!.key,
  { pairingId, pin },               // OR { pairingId, pinAadBytes }
)

// Re-import a persisted raw key as a non-extractable CryptoKey
const imported = await enc.importSessionKey(session!.raw)            // CryptoKey | null

enc.cleanup()

EncryptedPayload is a v2 envelope: { version: 2, iv, authTag, encrypted, algorithm, timestamp, messageId, pairingId, seq } (where seq is a decimal string). Decrypt hard-fails on any non-v2 payload.

useReplayProtection

In-memory, per-pairId message de-duplication with a TTL window. checkAndRecordMessage is synchronous and returns { valid, error? } (there is no .reason field and nothing to await).

import {
  generateMessageId,
  checkAndRecordMessage,
  clearReplayCache,
} from '@eternl/hub-wallet-sdk'

const messageId = generateMessageId()              // 'msg_…'
const timestamp = new Date().toISOString()

// Signature: checkAndRecordMessage(pairId, messageId, timestamp)
const check = checkAndRecordMessage('pair-id-123', messageId, timestamp)
if (!check.valid) {
  console.warn('rejected:', check.error)           // e.g. 'Replay attack detected: duplicate messageId'
}

clearReplayCache('pair-id-123')

(For naming compatibility, checkAndRecordMessage is also re-exported as checkAndRecordMessageAsync — it is still synchronous despite the name.)

useSessionKeyCache

Caches non-extractable CryptoKeys keyed by deviceId. Note importSessionKey takes deviceId first, then the base64 raw key.

import {
  importSessionKey,
  getCachedKey,
  hasCachedKey,
  clearCachedKey,
  getCacheStats,
} from '@eternl/hub-wallet-sdk'

const key = await importSessionKey(deviceId, rawBase64)   // (deviceId, rawBase64) → CryptoKey | null

if (hasCachedKey(deviceId)) {
  const cached = getCachedKey(deviceId)                   // CryptoKey | null
}

const stats = getCacheStats()                             // { size: number; deviceIds: DeviceId[] }
clearCachedKey(deviceId)

useMessageAuth

HMAC-SHA256 authentication over RFC-8785-canonicalized messages, keyed by the base64 raw session key string.

import {
  signMessage,
  verifyMessage,
  createSignedMessage,
  verifyAndUnwrap,
} from '@eternl/hub-wallet-sdk'

const sig    = await signMessage({ type: 'presence', online: true }, sessionKeyRaw) // string | null
const ok     = await verifyMessage({ type: 'presence', online: true, signature: sig! }, sessionKeyRaw)
const signed = await createSignedMessage({ type: 'presence', online: true }, sessionKeyRaw) // T & { signature } | null
const orig   = await verifyAndUnwrap(signed!, sessionKeyRaw)  // T | null (null if invalid)

useNatsValidation

Message-structure validation and type guards.

import {
  validateNatsMessage,
  isValidEncryptedPayload,
  parseAndValidatePayload,
} from '@eternl/hub-wallet-sdk'

const v = validateNatsMessage(message)   // { valid: boolean; error?: string; type?: string }
if (isValidEncryptedPayload(payload)) { /* … */ }
const parsed = parseAndValidatePayload(jsonString)

Storage

The HubStorage interface uses getItem / setItem / removeItem (with an optional clear), and supports both sync (localStorage) and async (IndexedDB) backends.

import {
  createLocalStorage,
  createMemoryStorage,
  isLocalStorageAvailable,
  type HubStorage,
} from '@eternl/hub-wallet-sdk'

interface HubStorage {
  getItem(key: string): string | null | Promise<string | null>
  setItem(key: string, value: string): void | Promise<void>
  removeItem(key: string): void | Promise<void>
  clear?(): void | Promise<void>
}

// Built-in adapters
initHub({ network: 'mainnet', storage: createLocalStorage('my-wallet') })
initHub({ network: 'mainnet', storage: createMemoryStorage() })

// Custom (e.g. IndexedDB) adapter
const customStorage: HubStorage = {
  getItem:    (key) => myDB.get(key),
  setItem:    (key, value) => myDB.set(key, value),
  removeItem: (key) => myDB.delete(key),
}
initHub({ network: 'mainnet', storage: customStorage })

If no storage is provided, initHub uses localStorage when available and falls back to in-memory storage otherwise.

Utilities

Canonicalization (RFC 8785)

import { canonicalize, createSigningPayload } from '@eternl/hub-wallet-sdk'

canonicalize({ b: 2, a: 1 })                       // '{"a":1,"b":2}'
createSigningPayload(message, ['signature'])       // canonical string with fields excluded

Secure IDs

import { generateSecureId, generateSecureMessageId, assertCryptoCapabilities } from '@eternl/hub-wallet-sdk'

assertCryptoCapabilities()         // throws if WebCrypto is unavailable
const id  = generateSecureId()     // 32-char hex (derived from crypto.randomUUID / getRandomValues)
const mid = generateSecureMessageId()  // 'msg_…' (alias of generateMessageId)

Protocol constants

All protocol constants are exported as getter functions that return the resolved value (defaults, or overrides passed via initHub({ constants: … })):

import {
  CONNECTION_TIMEOUT_MS,           // () => 10_000
  SIGNING_TIMEOUT_MS,              // () => 120_000
  REPLAY_PROTECTION_TTL_MS,        // () => 300_000
  MAX_PAYLOAD_SIZE_BYTES,          // () => 1_048_576
  PRESENCE_HEARTBEAT_INTERVAL_MS,  // () => 5_000
  DEFAULT_HUB_URL,                 // 'wss://hub.de-5.eternl.art/nats' (string constant)
} from '@eternl/hub-wallet-sdk'

const timeout = CONNECTION_TIMEOUT_MS()   // call them

Protocol types & permissions

The SDK re-exports the full set of protocol types from @eternl/hub-core and its own modules, plus the Permission enum and AccountInfo:

import type {
  DeviceId, PeerId, PairId, PairingId,
  DeviceInfo, PairedInstance, WalletRegistryEntry,
  SigningRequestMessage, EncryptedSigningRequestMessage,
  EncryptedPayload, SessionKey, KeyPair,
  ValidationResult, ReplayValidationResult,
  AccountInfo,
} from '@eternl/hub-wallet-sdk'

import { Permission } from '@eternl/hub-wallet-sdk'

const granted = [Permission.ADDRESS_READ, Permission.SIGN_TX, Permission.SIGN_DATA]

Security model (summary)

  • End-to-end encryption — AES-256-GCM with random 12-byte IVs and a 16-byte auth tag.
  • PIN-bound session keysHKDF-SHA256 over the ECDH (P-256) shared secret, salted with SHA-256(utf8(pin) || pairingId); the AAD binds pairingId, SHA-256(pin)[:8], and a monotonic seq. Without the PIN the session key cannot be derived from the ECDH transcript alone.
  • SAS6 — a 6-digit short authentication string for out-of-band pairing confirmation.
  • Replay protection — per-pairId message de-duplication with a TTL window and clock-skew tolerance.
  • HMAC authentication — HMAC-SHA256 over RFC-8785-canonicalized messages.
  • Payload validation — size limits and message-schema checks on inbound NATS messages.
  • Strict v2 wire — pre-v2 encrypted payloads are hard-rejected (no silent downgrade).

License

Business Source License 1.1 (BUSL-1.1). See LICENSE.

The Licensor is Tastenkunst GmbH. Additional Use Grant: None — you may copy, modify, and make non-production use of this package. Production/commercial use requires a separate license from Tastenkunst. On the Change Date (the fourth anniversary of each version's first publication) that version converts to GPL-2.0-or-later.

Contact: https://tastenkunst.com/