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

@deepthix/inapp-escape

v0.1.0

Published

Escape in-app browsers (Instagram, Facebook, TikTok, X, LinkedIn…) and open links in the user's real browser. Cloudflare Worker + framework-agnostic core. Built by Deepthix.

Readme

@deepthix/inapp-escape

Escape in-app browsers. Open links in the user's real browser.

Stop losing users to Instagram, TikTok, Facebook & X webviews. A tiny, dependency-free helper that ships everywhere.

npm license bundle size

Built by Deepthix — we ship AI agents that meet users where they are. We hit this problem in production and packaged the fix so you don't have to.


The problem

When users tap your link inside Instagram, TikTok, or Facebook, the link opens in a captive in-app browser — a stripped-down WebView that:

  • Has no extension support, no password manager, no dev tools
  • Doesn't share cookies or localStorage with the user's real browser
  • Breaks OAuth flows ("Sign in with Google" refuses to load)
  • Tanks conversion: industry data shows 30–60% drop-off vs. system browser

Worse, every app has different rules. Instagram blocks intent:// redirects. Twitter/X uses SFSafariViewController which ignores most schemes. Facebook honors x-safari-https:// but Instagram doesn't.

This package handles the matrix for you.

What it does

| App | Strategy | |---|---| | Instagram, Threads (iOS) | instagram://extbrowser/?url=… — the IG app itself opens Safari | | Facebook, Messenger (iOS) | x-safari-https://… — auto-redirect via the FB WebView | | Twitter/X, TikTok, LinkedIn, Snap, Pinterest, Reddit (iOS) | Gesture-required button (their WebViews block JS auto-redirects) | | All apps on Android | intent://…#Intent;scheme=https;…end — opens in the user's default browser |

Plus:

  • Loop guard — fallback URL carries ?_inapp_escaped=1 so the interstitial doesn't re-trigger itself
  • Static assets skipped.css, .js, /api/, /_next/, etc. pass straight through
  • Loader + CTA UI — clean, language-neutral, accessible interstitial
  • Zero dependencies — pure TypeScript, ships ESM, ~3 KB gzipped

Install

npm install @deepthix/inapp-escape

Quick start (Cloudflare Worker)

The intended deployment: a Worker fronting your origin domain.

// worker.ts
import { createWorker } from "@deepthix/inapp-escape/cloudflare";

export default createWorker();

Deploy with wrangler and route your domain through it. Done.

Quick start (Express)

import express from "express";
import { handle } from "@deepthix/inapp-escape";

const app = express();

app.use((req, res, next) => {
  const fullUrl = `${req.protocol}://${req.headers.host}${req.originalUrl}`;
  const result = handle({
    url: fullUrl,
    userAgent: req.headers["user-agent"] ?? "",
    method: req.method,
    accept: req.headers.accept ?? "",
  });
  if (!result) return next();
  for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, v);
  res.status(result.status).send(result.html);
});

Quick start (Next.js middleware)

// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { handle } from "@deepthix/inapp-escape";

export function middleware(req: NextRequest) {
  const result = handle({
    url: req.url,
    userAgent: req.headers.get("user-agent") ?? "",
    method: req.method,
    accept: req.headers.get("accept") ?? "",
  });
  if (!result) return NextResponse.next();
  return new NextResponse(result.html, { status: result.status, headers: result.headers });
}

More examples in examples/.

API

handle(input, options?) => HandleResult | null

Framework-agnostic core. Inspects request strings and returns either a plan (HTML + status + headers) or null if the request should pass through.

const result = handle({
  url: "https://example.com/landing",
  userAgent: req.headers["user-agent"] ?? "",
  method: req.method,         // optional, default "GET"
  accept: req.headers.accept, // optional
});

if (result) {
  // result.html, result.status, result.headers
  // result.app — "instagram" | "facebook" | …
  // result.platform — "ios" | "android"
  // result.gestureOnly — whether the page requires a tap
  // result.primary — the custom-scheme URL
  // result.fallbackUrl — the https URL the page falls back to
}

createWorker(options?) => ExportedHandler

Cloudflare Worker default-export factory. Wraps handle() and forwards non-intercepted requests to the origin.

Options

interface HandleOptions {
  /** Path prefixes to skip. Default: ["/api/", "/_next/", "/static/", …] */
  skipPathPrefixes?: ReadonlyArray<string>;

  /** Regex to skip static asset extensions. Sensible default included. */
  skipExtensions?: RegExp;

  /** ms before the auto-redirect falls back to https. Default 1500. */
  fallbackDelayMs?: number;

  /** Footer label. `false` to hide. Default "Powered by Deepthix". */
  branding?: string | false;

  /** CTA button label. Default "Continue". */
  buttonLabel?: string;

  /** Override which apps require a user-gesture (no auto-redirect). */
  needsUserGesture?: ReadonlySet<App>;

  /** Logging hook. Default `console.log`. */
  onEvent?: (e: HandleEvent) => void;
}

Lower-level exports

import {
  detect,             // (ua) => { app, platform }
  detectApp,          // (ua) => App | null
  detectPlatform,     // (ua) => "ios" | "android" | "other"
  iosScheme,          // (app, url) => string
  androidIntent,      // (url) => string
  renderEscapePage,   // (opts) => string  — raw HTML if you want to bring your own response layer
  NEEDS_USER_GESTURE, // ReadonlySet<App>
  ESCAPED_FLAG,       // "_inapp_escaped"
} from "@deepthix/inapp-escape";

Why does this work?

The key insight is that every in-app webview has a different escape hatch:

  • Instagram's WKWebView blocks window.location = "x-safari-https://…" without a user gesture. But Instagram registered its own scheme instagram://extbrowser/?url=… that the Instagram app itself intercepts (not the WebView) and forwards to Safari. This bypasses the gesture requirement entirely.
  • Facebook's WebView is more permissive — it honors x-safari-https:// from JS.
  • Twitter/X uses SFSafariViewController, which is itself a Safari-engine view. Custom schemes from JS are silently dropped. Only a user-tapped <a href> creates the gesture context the controller forwards to the system.
  • Android has had intent:// URIs forever — but hardcoding package=com.android.chrome fails on devices without Chrome. We omit the package and rely on the user's default.

This package encodes that matrix so you don't have to rediscover it.

Limitations (be honest)

  • Twitter/X iOS: when X uses SFSafariViewController, even a user tap may not escape on every iOS version. The interstitial shows a clear button — beyond that, Apple's sandbox wins. taap.it, smartlinkfox, and friends hit the same wall.
  • Universal Links: if your domain has an iOS app with a registered associated domain, in-app browsers may bounce to your app instead of escaping. Disable the apple-app-site-association for the relevant paths if that's not what you want.
  • WebView versions: app vendors change webview behavior over time. We track the matrix in src/schemes.ts — PRs welcome.

About Deepthix

Deepthix builds AI agents that work where users are — which means we deal with web platform reality, not idealized SPAs. We open-source the infra fixes we need internally so the whole ecosystem benefits.

If this package saved you a week of debugging, give it a ⭐ and check out what else we're building at deepthix.com.

Contributing

Bug reports, scheme additions, and adapter PRs (Hono, Fastify, AWS Lambda…) very welcome. Open an issue first for non-trivial changes.

License

MIT © Deepthix