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

@psiphon-inc/expo-psiphon

v1.4.0

Published

Expo module for routing app networking through Psiphon Tunnel Core.

Readme

@psiphon-inc/expo-psiphon

@psiphon-inc/expo-psiphon is a reusable Expo module that routes React Native app traffic through Psiphon Tunnel Core, published publicly on npm and developed in the open at Psiphon-Inc/expo-psiphon (GitHub, mirrored to internal GitLab with identical commits/tags). GPL-3.0-or-later, with vendored tunnel-core binaries (ca.psiphon.aar, PsiphonTunnel.xcframework) pinned to a published upstream commit via SOURCE.md.

What it does

Apps supply their own tunnel-core config (SponsorId/PropagationChannelId, embedded server entries — nothing is baked into the module) and choose one of three routing modes:

  • explicit (default) — only psiphonFetch() / psiphonDownload() go through the tunnel.
  • global — React Native's standard networking is intercepted: fetch/XHR on both platforms, image loading on Android (OkHttp hooks installed at process start via ContentProvider), and WebSockets on both platforms — Android via the WebSocketModule client builder, iOS via a SocketRocket proxy hook (new in this release, the last piece of global coverage).
  • manual — getProxyEndpoints() exposes the local SOCKS/HTTP proxy (TCP or Unix-domain-socket) for custom stacks.

Also on board:

  • lightproxy support with byte accounting,
  • push-payload import with persistent/in-memory queues,
  • feedback upload with log collection,
  • tunnel notices as JS events,
  • a fixed-SOCKS-port option,
  • UDS-based local proxies for explicit mode.

This package requires a development build or native build. Expo Go does not include the native Psiphon artifacts.

Install

npm install @psiphon-inc/expo-psiphon

Add the config plugin to your Expo app config:

{
  "expo": {
    "plugins": ["@psiphon-inc/expo-psiphon"]
  }
}

This package is tested with Expo SDK 53 and SDK 55. The peer dependency accepts Expo SDK 53 and newer; unlisted SDKs should be validated in your app before shipping.

On iOS the module declares its SwiftNIO dependency through React Native's spm_dependency CocoaPods helper, so React Native 0.75 or newer is required (every supported Expo SDK satisfies this). pod install needs network access the first time so Xcode can resolve the Swift packages. Versions up to 1.0.x injected a block of CNIO* :modular_headers lines into the app Podfile; that block is no longer needed, and the config plugin removes a previously injected one automatically (bare-workflow apps that copied it manually should delete it).

Known issue on Expo SDK 53 / RN 0.79 (fixed upstream in newer templates): CocoaPods' sequential object-UUID generator does not collision-check, and the Swift package objects added during post_install can be assigned the root PBXProject object's UUID — the written Pods project then fails to load ("The project 'Pods' is damaged") and packages never resolve. Apps using the config plugin (any prebuild-managed app) get the repair automatically: since 1.1.1 the plugin injects a save-time repair wrapper into the Podfile at prebuild. Bare-workflow apps that maintain their own Podfile should add the following at the end of the post_install block (after react_native_post_install):

# Repair a CocoaPods UUID collision triggered by spm_dependency packages.
require 'securerandom'
project = installer.pods_project
root = project.root_object
offender = project.objects_by_uuid[root.uuid]
if offender && !offender.equal?(root)
  fresh = SecureRandom.hex(12).upcase
  fresh = SecureRandom.hex(12).upcase while project.objects_by_uuid.key?(fresh)
  offender.instance_variable_set(:@uuid, fresh)
  project.objects_by_uuid[fresh] = offender
  project.objects_by_uuid[root.uuid] = root
end

Native Artifacts

The npm package includes these native artifacts:

  • android/libs/ca.psiphon.aar
  • ios/Frameworks/PsiphonTunnel.xcframework

When installing from npm, these artifacts are part of the package tarball. When working from a git clone, run git lfs pull after cloning if those files are missing or look like small text pointer files. The config plugin validates the vendored AAR and xcframework during prebuild when the plugin is listed in an Expo app config, and fails with Git LFS guidance if the artifacts are not present.

See SOURCE.md for the corresponding Psiphon Tunnel Core source commit, public source retrieval steps, artifact build commands, and reproducibility notes for these vendored binaries.

Building Artifacts Locally

Initialize or update submodules first:

git submodule update --init --recursive

Build fresh artifacts from the related/psiphon-tunnel-core submodule and install them into the package paths:

npm run psiphon:artifacts

Android defaults to Psiphon's Docker-based build:

npm run psiphon:artifacts:android

To use host Android tooling instead, set ANDROID_HOME, ANDROID_PLATFORM_VERSION, and ensure gomobile is available:

npm run psiphon:artifacts:android -- --android-mode=host

iOS uses Psiphon's MobileLibrary/iOS/build-psiphon-framework.sh and requires Xcode. The wrapper downloads the exact Go version required by the submodule script into .toolchains/go/ when it is not already available:

npm run psiphon:artifacts:ios

Forward Go build tags with -- --build-tags="tag1 tag2". Install previously built submodule outputs without rebuilding with -- --install-only, or validate the currently installed package artifacts with npm run psiphon:artifacts:validate.

Wrapping Lightproxy Push Payloads

Generate local push payload key material:

npm run psiphon:push:generate-keys -- --out example/config/push_payload_keys.json

Add PushPayloadObfuscationKey and PushPayloadSignaturePublicKey from that file to example/config/psiphon_config.json. Keep PushPayloadSignaturePrivateKey private.

Wrap a raw encoded lightproxy entry into the signed and obfuscated push payload expected by importPushPayload:

npm run psiphon:push:wrap-lightproxy -- \
  --keys example/config/push_payload_keys.json \
  --entry-file example/config/lightproxy_entry \
  --ttl-seconds 2592000 \
  --out example/config/push_payload

Basic Usage

import {
  addStatusListener,
  addFeedbackStatusListener,
  configurePsiphon,
  enqueueFeedback,
  getProxyEndpoints,
  getStatus,
  importPushPayload,
  logError,
  logInfo,
  psiphonDownload,
  psiphonFetch,
  psiphonWebSocket,
  startTunnel,
  stopTunnel,
} from "@psiphon-inc/expo-psiphon";

await configurePsiphon({
  tunnelCoreConfig,
  embeddedServerEntries,
  routingMode: "explicit",
  failClosed: true,
});

await importPushPayload(pushPayloadBase64);
await startTunnel();

const status = await getStatus();
const proxyEndpoints = await getProxyEndpoints();
const subscription = addStatusListener((nextStatus) => {
  console.log(nextStatus.canProxyRequests);
});

const response = await psiphonFetch("https://example.com/api", {
  waitForProxy: true,
  responseType: "text",
});

await psiphonDownload("https://example.com/file.zip", "file:///path/to/file.zip", {
  waitForProxy: true,
});

const socket = await psiphonWebSocket("wss://example.com/stream", { waitForProxy: true });
const messageSub = socket.addMessageListener((message) => console.log(message.data));
await socket.send("hello");
messageSub.remove();
await socket.close();

subscription.remove();
await stopTunnel();

Routing

routingMode selects how app traffic reaches the tunnel: global intercepts React Native's standard HTTP clients, explicit (default) tunnels only psiphonFetch/psiphonDownload calls, and manual exposes local proxy endpoints for custom networking stacks.

Global routing caveats

  • Call configurePsiphon before the app issues any network request. React Native caches its HTTP clients on first use. On iOS, a URL session created before configuration permanently bypasses the proxy. On Android, the module installs its routing hook at process start, so later configuration still applies — but traffic sent before configurePsiphon completes is still direct unless failClosed engages.
  • Coverage limits: iOS global routing covers React Native fetch/XHR and SocketRocket-backed WebSocket when SocketRocket's private proxy selectors are present. Validate this when upgrading React Native or SocketRocket. Android global routing covers React Native's fetch/XHR/WebSocket and image loading via the standard OkHttp clients, but third-party SDKs with their own HTTP stacks bypass it, and installing a custom OkHttpClientFactory displaces the hook.
  • For traffic that must be tunneled, prefer psiphonFetch/psiphonDownload, which always dial the local proxy directly.

Limitations

expo-psiphon tunnels in-process app traffic only. Psiphon runs as a linked library inside the app's own process — not a system/device VPN (no NEPacketTunnelProvider on iOS, no VpnService on Android) — so it does not tunnel other apps' or system traffic, and it does not exclude its own outbound from a device-wide VPN. When another VPN is active, app traffic double-tunnels: app → in-process Psiphon → device VPN → internet.

Feedback And App Logs

expo-psiphon can collect app logs, tunnel-core notices, and upload a Psiphon feedback report using the configured tunnel-core feedback settings.

const feedbackSub = addFeedbackStatusListener((event) => {
  console.log(event.status, event.feedbackId, event.error);
});

logInfo("App", "Feedback-visible app log line");
logError("App", "Feedback-visible error line");

const feedbackId = await enqueueFeedback(id, { appName: "my-app" });

feedbackSub.remove();

Tunnel-core logs require useNoticeFiles: true; without it the upload still succeeds but the report's tunnel-core section is empty.

enqueueFeedback resolves once the upload has been accepted/started; completion or failure is delivered via addFeedbackStatusListener events. On Android it may resolve null when an upload is already pending.

App log messages passed to logInfo, logWarn, and logError may be written to local feedback logs and included in uploaded feedback reports. Do not log secrets, tokens, keys, private configs, credential-bearing URLs, or user-sensitive data through these APIs.

API Notes

configurePsiphon accepts a tunnel-core config object or JSON string. SponsorId and PropagationChannelId are required. Defaults are routingMode: "explicit", failClosed: true, and persistPushPayloadQueue: true.

Additional config options:

  • proxyType selects which local tunnel-core proxy module traffic uses: "socks" (default) or "http" (HTTP CONNECT). It applies to all module traffic for the configuration's lifetime; there is no per-request override.
  • useUnixDomainSockets routes the local proxies over Unix domain sockets instead of TCP ports. Only supported with routingMode: "explicit"; configurePsiphon rejects it with global routing.
  • fixedSocksPort pins the local SOCKS proxy to a specific TCP port (1–65535) instead of an ephemeral one. On iOS, if that port is already in use the tunnel reports ERR_SOCKS_PROXY_PORT_IN_USE and stays fail-closed; the module does not auto-rotate, so handle the error by reconfiguring with a different port.
  • With useNoticeFiles: true, notice-driven byte accounting for the light proxy is unavailable on iOS (known limitation).

psiphonFetch and psiphonDownload accept http and https URLs only. psiphonDownload destinations must be file:// URLs or absolute filesystem paths; empty and relative destinations are rejected before native calls. Request options default to method: "GET", timeoutMs: 30000, isolated: true, and responseType: "text". String bodies are supported; Uint8Array bodies are rejected until native bridging support is added. psiphonFetch response bodies are capped at 32 MiB (native-enforced); larger responses fail with an error directing you to psiphonDownload, which streams to disk.

psiphonWebSocket opens a WebSocket tunneled over the local proxy in any routing mode — no global interception or SocketRocket private API is involved. URLs must be ws or wss. waitForProxy and timeoutMs have the same semantics as psiphonFetch; the timeout covers proxy wait, connect, and handshake, defaulting to 30000 ms. The resolved handle exposes send (strings send as text; binary messages use { kind: "binary", data } with base64 data), close, and addMessageListener/addCloseListener subscriptions; call .remove() on subscriptions you add. Sockets are fail-closed: a dropped tunnel errors the socket (close event with error) — it never falls back to a direct connection. One platform asymmetry: plain ws:// is not supported in Unix-domain-socket mode on Android; use wss:// there.

The module automatically signals tunnel-core to resume when the app returns to the foreground; no configuration is required.

Other exports:

  • restartTunnel() stops and restarts the tunnel with the current configuration, forcing reconnection.
  • getStatus() resolves the current PsiphonStatus snapshot (state, proxy readiness, proxyEndpoints, byte totals, last error).
  • getProxyEndpoints() resolves { socks?, http?, selected? }. TCP endpoints include { transport: "tcp", protocol, host, port }; Unix-domain-socket endpoints include { transport: "unix", protocol, path }. In UDS mode, selected is the HTTP endpoint when available because explicit helpers use HTTP CONNECT over UDS.
  • getSocksProxy() resolves the TCP SOCKS proxy { host, port }, or null when it is not running. This is a legacy TCP-only convenience for manual integrations; prefer getProxyEndpoints() or getStatus().proxyEndpoints.
  • getPushPayloadQueueStatus() resolves the pending push payload queue count and the timestamp of the oldest queued payload.
  • clearPushPayloadQueue() drops all queued push payloads.
  • clearPsiphonData() clears persisted Psiphon data (server entries, persisted queues) for a clean-slate restart.
  • addPushPayloadStatusListener(listener) subscribes to push payload import progress events; call .remove() on the returned subscription.
  • addTunnelNoticeListener(listener) subscribes to forwarded tunnel-core notice messages when forwardNoticesToEvents is enabled.

Example Test Harness

The example/ app is a native control room for verifying module fundamentals from UI controls. It exposes configuration, lifecycle, data reset, status/proxy observation, push queueing, live notices, explicit request helpers, raw fetch comparison, download testing, and one-tap smoke/cold-start race runs.

Use local environment values for real Psiphon configs:

cp example/.env.example example/.env.local

The example is a test harness. Fixture values loaded through example/config/, Expo public env vars, or app config are bundled into the app and must not be treated as secret storage. Its traffic simulation can intentionally run ordinary untunneled networking when the tunnel is unavailable so proxy and baseline paths can be compared during local testing.

Then run a native development build from example/:

npm install
npm run prebuild:clean
npm run ios

Native UI automation uses Maestro flows under example/maestro/, driven by the scripts/dev-sim.sh harness, which owns a dedicated simulator and headless Metro so tests do not depend on a foreground npm run ios. From example/:

npm run dev:up           # boot sim + Metro + build/install/launch
npm run dev:test:all     # full local aggregate suite
npm run dev:test:ui
npm run dev:test:native

dev:test:all runs the full local aggregate suite; dev:test:benchmark stays separate. See example/README.md for the full command list.