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

@meteorwallet/connect

v0.25.0

Published

Downloads

2,916

Readme

@meteorwallet/connect

Client library for connecting applications to Meteor Wallet over the Meteor Connect bridge.

The session-native API has two protocol roles over one bridge:

  • PartnerSessionClient — the requesting application (including a source wallet in a wallet-to-wallet flow), which creates turns and consumes signed results.
  • WalletSessionClient — the destination wallet, which claims one session and independently reviews/completes each turn.

One-action interactions use the same protocol through runSingleAction(). Multi-turn workflows keep the same bridge, fixed wallet, authorization level, and secure channel until explicit close or a bounded terminal condition. Both roles are typed end to end: actions, session/turn state, and errors keep their types across the network boundary.

Installation

npm install @meteorwallet/connect @meteorwallet/connect-shared
# or
bun add @meteorwallet/connect @meteorwallet/connect-shared

Two packages, and that is the whole integration surface. @meteorwallet/connect carries the two clients, the storage adapters, the helpers, and the wire payload types; @meteorwallet/connect-shared carries the protocol enums and the action implementations you build requests and results with.

Quickstart — Partner (one-action)

The fastest integration: create a session, present a wallet link, wait for the wallet's signed result, and the SDK acknowledges-and-closes atomically.

import {
  buildWalletLinkUrl, createWebStorageAdapter, describeSessionError, PartnerSessionClient,
  selectWalletLink,
} from "@meteorwallet/connect";
import { EPartnerOrigin } from "@meteorwallet/connect-shared";

const client = new PartnerSessionClient({
  backendUrl: "https://mc.meteorwallet.app",
  backendStorageScope: "meteor-production",
  httpRequestTimeoutMs: 15_000,
  partnerMetadata: { name: "My App", origin: `${EPartnerOrigin.web_url}::${location.origin}` },
  storageAdapter: createWebStorageAdapter({ keyPrefix: "my-app::" }),
});

await client.initializeClient();

// Build a server-eligible single-turn request in your feature host. `transfer_accounts` is the
// current production single-turn option; persist its host receipt before starting the session.
const initialActionRequest = await buildTransferAccountsRequestInYourFeature();

try {
  const { result } = await client.runSingleAction({
    initialActionRequest,
    onSessionReady: (session) => {
      // Pick by preference, never by index: `walletLinks` is ordered by the backend, so
      // `walletLinks[0]` is whichever app it happened to list first.
      const link = selectWalletLink(session, { preferredAppIds: MY_PREFERRED_WALLET_APP_IDS });
      if (link == null) throw new Error("No wallet link for a supported app");
      showQrCode(buildWalletLinkUrl(session, link));
    },
  });
  // result.actionResult is the wallet's verified signed result; session already closed.
} catch (error) {
  const desc = describeSessionError(error, { backendUrl: client.backendUrl });
  console.error(desc.headline, desc.detail);
} finally {
  if (client.sessionFacts != null) {
    await client.closePhaseSafe().catch(() => undefined);
    await client.disconnectBridge();
  }
}

runSingleAction creates the session with the single_turn_v1 resource profile, calls your onSessionReady callback to present the wallet link, waits for the verified signed result, acknowledges-and-closes atomically, and disconnects. On a callback throw or failure it phase-safely closes and disconnects before re-throwing your error.

Two-turn partner flow with advance

Multi-turn workflows use createSession + waitForResult + advance:

const session = await client.createSession({ initialActionRequest });
const link = selectWalletLink(session, { preferredAppIds: MY_PREFERRED_WALLET_APP_IDS });
if (link == null) throw new Error("No wallet link for a supported app");
showQrCode(buildWalletLinkUrl(session, link));

const firstResult = await client.waitForResult();
// Ordinary first links wait for confirmation in the wallet. Only fresh_pin sessions use verifyPin(pin).

await client.advance({ acknowledgedResult: firstResult.receipt, nextActionRequest });
const secondResult = await client.waitForResult();
await client.acknowledgeAndClose(secondResult.receipt);
await client.disconnectBridge();

createSession derives resourceProfile, authorizationMode, requiredWalletCapabilities, and partnerRequestId from the action's server-authoritative policy; pass any of them only to override. The generated partnerRequestId is echoed as session.partnerRequestId — reuse it only for an exact creation retry.

Quickstart — Wallet

The wallet claims a session from a parsed wallet link, iterates turns, completes each one, and phase-safely closes.

import {
  createWebStorageAdapter, hydrateAndValidateActionRequest, parseWalletLinkUrl, WalletSessionClient,
} from "@meteorwallet/connect";
import {
  act_impl_meteor_wallet_core, EMeteorAppId, EWalletProtocolCapability,
} from "@meteorwallet/connect-shared";

const client = new WalletSessionClient({
  backendUrl: "https://mc.meteorwallet.app",
  backendStorageScope: "meteor-production",
  httpRequestTimeoutMs: 15_000,
  meteorAppId: EMeteorAppId.meteor_wallet_mobile,
  additionalWalletCapabilities: [EWalletProtocolCapability.new_key_account_transfer_v1],
  // On React Native: createAsyncStorageAdapter({ asyncStorage: AsyncStorage })
  storageAdapter: createWebStorageAdapter({ keyPrefix: "my-wallet::" }),
});

await client.initializeClient();
// If the identity was previously deleted, initializeClient throws — call createWallet() to
// create a fresh account, or resumePendingCompletion() if a completion was interrupted.

// Parse the wallet link (QR/deep link) — the fragment secret is handled inside.
const claimInput = parseWalletLinkUrl(walletLinkUrl);
if (claimInput == null) throw new Error("Invalid wallet link");

const claimed = await client.claimSession(claimInput);
if (client.requiresPartnerConfirmation) {
  // Show client.linkConfirmation.partnerMetadata as app-provided (unverified) details.
  // Warn: continue only if you initiated this connection; it remembers the app for future requests.
  // Wire explicit Link / Cancel buttons to confirmPartnerLink() / rejectPartnerLink().
  // Do not confirm automatically on claim, push, focus or reload.
}
if (claimed.pinCode != null) {
  // Show this PIN to the user. The partner/source role is the one that submits it.
  await showPinCodeToUser(claimed.pinCode);
}

// Iterate turns — the async generator finishes on terminal.
for await (const turn of client.turns()) {
  // The SDK validates first-party wire input before yielding; hydration restores the typed API.
  const hydrated = hydrateAndValidateActionRequest(
    act_impl_meteor_wallet_core,
    turn.actionRequest,
  );
  const actionResult = await reviewAndResolveWalletCoreAction(hydrated);

  await client.completeAction(actionResult);
  // completeAction persists the result before sending; an ambiguous response can call
  // resumePendingCompletion() without executing the action twice.
}

await client.closePhaseSafe();
await client.disconnectBridge();

Wallet event subscription

Subscribe to session lifecycle events without subclassing:

client.events.on("factsChanged", ({ facts }) => updateUi(facts));
client.events.on("terminal", ({ outcome }) => handleTerminal(outcome));
client.events.on("pinRequired", () => showPinPrompt());
client.events.on("linkStatusChanged", ({ status }) => updateLinkBanner(status));

The event names are a closed literal union: factsChanged, terminal, linkStatusChanged, realmStatus, attachError, pinRequired. Handlers must not invoke client mutations during dispatch (same re-entrancy expectation as the old protected hooks); a handler throw is caught and reported, never corrupting client state.

Framework-free state snapshot

For React/Vue/Svelte stores that need a snapshot of the wallet session lifecycle:

import { createWalletSessionState } from "@meteorwallet/connect";

const state = createWalletSessionState(client);
// state.getSnapshot() → { clientStatus, identity, facts, turn, error, ... }
// state.subscribe(listener) → unsubscribe
// state.initialize() / state.applyClaimedSession(claimed) / state.complete(input) / state.clear()

Links that name their backend

Not in 0.12.0. buildWalletLinkUrl's backendUrlHint option and resolveTrustedBackendUrlHint ship in the next release. On 0.12.0 the two-argument buildWalletLinkUrl(session, link) is the whole surface — until then, a multi-backend integration has to resolve its own backend from configuration it already trusts, and must not take one from a scanned link.

A wallet built against one backend cannot claim a bridgeId that only exists on another — the claim fails as a plain "not found" that says nothing about why. When your wallet and partner can legitimately meet on more than one backend (a staging partner presenting to a wallet whose default is production, a locally-run wallet claiming from a deployed environment), the partner can say which backend the session lives on:

import {
  buildWalletLinkUrl, parseWalletLinkUrl, resolveTrustedBackendUrlHint,
} from "@meteorwallet/connect";

// Partner side — opt-in; a single-backend integration should leave this unset.
const url = buildWalletLinkUrl(session, session.walletLinks[0], {
  backendUrlHint: partnerClient.backendUrl,
});

// Wallet side — the hint is UNTRUSTED input off the same QR as everything else.
const parsed = parseWalletLinkUrl(url);
if (parsed != null) {
  const backendUrl = resolveTrustedBackendUrlHint({
    hint: parsed.untrustedBackendUrlHint,
    allowedBackendUrls: MY_APP_BACKENDS, // your configuration, never the link
  });
  // `backendUrl` is one of MY_APP_BACKENDS, or null. Only now decide whether to act on it.
}

Treat the hint as routing advice, never authorization. Dialing a backend named by a scanned link would hand that host the session's bridgeId, bridgeLease, and partnerSecret, so resolveTrustedBackendUrlHint returns your allowlist entry rather than the hint string — a look-alike host cannot match, and the value you act on is always one you configured. The hint is deliberately not part of the claimSession input, so passing a parsed link straight to claimSession ignores it; and most apps should ignore it outright in production builds.

Switching backends switches identity: backendStorageScope derives from the backend URL, so each backend keeps its own client keys, trust pins, and stored partners. Build a new client rather than trying to move one.

Error handling

Every failure in a session flow is classifiable through one function:

import { describeSessionError } from "@meteorwallet/connect";

const desc = describeSessionError(error, { backendUrl: client.backendUrl });
// desc.kind → "terminal" | "session" | "bridge" | "connection" | "local" | "unknown"
// desc.headline → one plain-language sentence
// desc.detail → what it means and what to do next
// desc.originalMessage → the untouched underlying message
  • terminal — the session ended permanently; create a new session to continue.
  • session — a typed protocol rejection with ids[] and a retry hint ("exact", "no", or "new_operation").
  • bridge — a typed bridge-level rejection (allocation, lease, transport authorization).
  • connection — a connection-level failure (embeds the full describeBackendConnectionError output, whose own kind is protocol_mismatch / network_unreachable / handshake_rejected / bridge_gone / unknown, plus the exact endpoint dialed when the error names one).
  • local — an SDK fail-closed guard (SessionLocalGuardError) raised before/after transmission; address the stated precondition and rerun.
  • unknown — read the original message for the specific cause.

See the error handling guide for the full EErr_Bridge_Session catalogue with retryability and the turn_already_active/session_closing precedence note.

Session lifecycle and recovery contract

Each logical session uses a fresh unpredictable partnerRequestId (generated for you and echoed on the created session when omitted); reuse it only for exact allocation and staging retries. The immutable server-approved profile fixes turn, idle, absolute-lifetime, and redial bounds. The SDK carries the lease on authenticated HTTP/WebSocket traffic, never extends a deadline through traffic, and treats action bridge_gone and WebSocket close 4410 as the same non-retryable terminal state. Create a fresh session rather than reviving an expired one.

Turn completion is not session completion. The wallet persists the exact encrypted/signed completion before sending it, so an ambiguous response can call resumePendingCompletion() without executing the wallet action twice. The partner exposes exact result receipts plus prepare/submit methods for host-owned D33 journals. advance() acknowledges the old result and installs the next turn atomically; acknowledgeAndClose() performs the final acknowledgement and close atomically.

The bridge secret and lease remain memory-only. A live partner can create an authenticated D24 current-turn handoff or send a paired-wallet notification to wake the fixed claimed wallet; the handoff contains no action/account plaintext and expires with the idle deadline. If both processes lose the session or the bridge expires, use the feature's durable journal and a fresh session. Never log the lease, partner secret, PIN, device token, durable IDs, sealed payload, action, or result.

One session per process

Each client instance binds at most one bridge session at a time. Claiming a new session while one is live throws a SessionLocalGuardError — disconnect the current session first. This is a protocol invariant, not a configuration choice: the bridge enforces one claimed wallet per session, and the SDK's mutation locks, event hub, and turn iterators are all scoped to the bound session.

Acknowledgement is receipt, not acceptance

acknowledgeResult tells the backend "I received this result" — it is transport receipt, never acceptance of the result's content. A declined result (signed typed-error) is still acknowledged so the session is not parked in result_ready; the partner then decides whether to close, replay, or advance. acknowledgeAndClose is the final receipt-and-close; advance is receipt-plus-next-turn.

Same-process and multi-tab mutation locks

Where the platform provides the Web Locks API (navigator.locks — multi-tab browsers), the SDK locks durable session mutations by default under one stable name keyed by backend storage scope + role, so every tab or client instance sharing one identity serializes on one lock. Platforms without Web Locks (React Native/AsyncStorage hosts, tests) fall back to per-instance serialization. Supply your own callback only to override the default.

Phase-safe close

closePhaseSafe() on either role inspects the current phase and chooses the correct close verb — never a blind destructive operation. Use describeCloseOptions(facts) to label close buttons in UI, and isTerminalPhase(facts) to detect when the session is already over.

Push wake (mobile wallets)

The SDK persists claimed-partner public keys on every successful claim, so push-delivered session handoffs can be unsealed without a consumer-built key store:

import { parsePushSessionClaim, parsePushSessionTurnWake } from "@meteorwallet/connect";

// On an initial push notification. Both parsers are fail-closed: `null` means the payload was not
// a well-formed, in-bounds push, and the only correct response is to drop it.
const claimPayload = parsePushSessionClaim(notification);
if (claimPayload != null) {
  const claimed = await client.claimInitialPush({ payload: claimPayload });
}

// On a per-turn wake push:
const wakePayload = parsePushSessionTurnWake(notification);
if (wakePayload != null) {
  const { claimed, turn } = await client.claimTurnWake({ payload: wakePayload });
}

The SessionClaimRouter unifies link/QR/push/manual claim inputs through one serial chain with live-session refusal (a manual claim while a session is live emits refused, never a silent no-op). See the push guide for the full integration.

Migration from pre-cut APIs

If you are migrating from the pre-P6 snake_case API, see MIGRATION.md for the old→new name/shape table.

Backend-scoped persistence

Every backend-coupled client record is stored below a versioned backend-and-role prefix before the client reads or creates anything. This includes the persistent Durable Object ID, signing/exchange keys and trust pins, paired-wallet records, and channel state. Switching between local, development, and production backends therefore preserves three independent identities instead of presenting an ID minted by one Cloudflare Durable Object namespace to another.

If backendStorageScope is omitted, the SDK uses a versioned SHA-256-derived identifier for the canonical HTTP(S) origin of backendUrl. Set it explicitly when several URL aliases intentionally reach the same backend, or when separate backends are mounted below different paths on the same origin. The value must be a stable 1–64 character identifier containing only letters, digits, ., _, or -. Never reuse one scope across development and production.

Network trust boundary

The SDK uses the platform fetch and WebSocket implementations without attaching a static client-identifying header or exposing a host option to do so. Browser and native clients reach the same narrowly scoped signed/leased routes under the backend's normal WAF, rate, and body-size controls. If a native production build cannot reach those routes, correct the edge policy — do not work around it by adding an SDK header, environment tag, or custom socket constructor.

Set httpRequestTimeoutMs on partner and wallet clients so an unreachable backend settles into your normal describeSessionError connection path instead of relying on a platform-specific fetch deadline. The timeout aborts only the local HTTP attempt; retries must still follow the operation's documented idempotency and retryability contract.

Session eligibility

Session creation is fail-closed per exact action input. The current first-party admitted workflows are the journaled new-key start/verify pair and transfer_accounts; the latter uses single_turn_v1, fresh PIN authorization, claimant-encrypted delivery, and a wallet-owned transactional import receipt. Other NEAR and third-party actions remain ineligible until their host recovery journals and reconciliation seams are implemented and reviewed.

Consumer guides

License

MIT

Wallet-owned first-link consent (protocol 3)

Use createWalletSessionState(client) to render requiresPartnerConfirmation, linkConfirmation, confirmingPartner and confirmationError. Bind the Link button to state.confirmPartnerLink() and Cancel to state.rejectPartnerLink(). The helper owns error/busy state; claiming never approves. Show the app name/address as self-asserted, warn users to cancel unexpected connections, explain that the link is remembered and expose authoritative connected-app revocation. Action consent remains separate. partnerConfirmationRequired and pinRequired are distinct events.

Ordinary/new-key requests default to partner_link; first consent creates trust and subsequent requests from the same signing identity skip linking prompts. Existing-secret transfer_accounts defaults to mandatory fresh_pin on every new session, including remembered partners. Explicit fresh_pin for ordinary requests remains supported. Rejecting the action after pairing does not revoke the relationship. All sessions now bind a revocable link incarnation.

This is a pre-production protocol cut: upgrade backend, SDK and wallet hosts together. Protocol-2 sessions and trusted_allowed are unsupported; there is no fallback or storage migration for them. Do not advertise wallet protocol 3 before implementing both consent and required-PIN screens.