Framework SDKs and edge middleware

React, Vue, Next.js and an offline session verifier for Workers and the edge, all on top of the CapchaID browser SDK and its public JWKS.

Not published to npm yet. Until it is, build it from this repository and install from the folder (`npm install ../path/to/capchacloud/packages/<package>`, after building its dependencies in this order: id-core / id-edge first).

@capchacloud/id-edge

Offline CapchaID session verification for Cloudflare Workers, edge runtimes, Node 18+ and Deno/Bun. Verifies the ES256 session JWT against the JWKS, with caching, key rotation, stale-if-error and an optional revocation check. No dependencies.

import { verifySession, sessionErrorResponse } from "@capchacloud/id-edge";
const opts = { audience: "YOUR_CAPCHAID_APPLICATION_ID" };   // the JWT's aud claim

export default {
  async fetch(request: Request): Promise<Response> {
    const session = await verifySession(request, opts);     // Authorization: Bearer <access token>
    if (!session.ok) return sessionErrorResponse(session);  // 401, or 503 if the auth backend is down
    return Response.json({ user: session.claims.sub, email: session.claims.email });
  },
};
  • Verification is offline: the JWKS (https://id.capchacloud.com/.well-known/jwks.json) is fetched once and cached (5 minutes fresh, up to 24 hours stale if the auth origin is down), then every request is a local signature check.
  • Only ES256 is accepted. `none`, HS256 and RS256 tokens are refused before any key is looked up. `iss`, `aud`, `exp`, `nbf` and `iat` are checked with a clock-skew allowance (default 30 s, max 300 s).
  • An unknown key id triggers at most one JWKS refetch per 30 seconds, so key rotation works and random-kid spraying cannot hammer the auth origin.
  • Offline verification cannot know a session was revoked. For that, pass `revocation: { url: 'https://id.capchacloud.com/v1/sessions/verify', secretKey }` (server-side secret key only): the verifier then confirms with capcha-id on each check and fails closed if it cannot.

@capchacloud/id-core

Framework-neutral CapchaID browser session store that the React and Vue bindings share: SDK loading, auth-state snapshots, optional first-party session-cookie sync, passkey autofill helpers. No dependencies.

import { createAuthStore, loadCapchaIDClient } from "@capchacloud/id-core";

const store = createAuthStore({
  loadClient: loadCapchaIDClient({ publishableKey: "cid_pk_live_..." }),
  syncCookie: { name: "cid_at" },          // optional: lets server code verify the session
});
store.subscribe(() => console.log(store.getSnapshot()));  // { isLoaded, isSignedIn, user, error }
await store.start();                        // loads the SDK and restores the session
  • You normally use this through @capchacloud/id-react or @capchacloud/id-vue rather than directly.
  • The optional session cookie is written by JavaScript, so it is not httpOnly. It carries only the short-lived (about an hour) access token, never the refresh token. Know that trade-off before enabling it.

@capchacloud/id-react

React hooks and components for CapchaID: CapchaIDProvider, useSession, useUser, SignedIn / SignedOut, SignInButton and PasskeyButton with passkey autofill (conditional UI). React 18+.

import { CapchaIDProvider, SignedIn, SignedOut, SignInButton, PasskeyButton, useUser } from "@capchacloud/id-react";

const Hello = () => <p>Signed in as {useUser().user?.email}</p>;
export const App = () => (
  <CapchaIDProvider publishableKey="cid_pk_live_...">
    <SignedOut><SignInButton provider="google" /> <PasskeyButton conditionalInputId="email" /></SignedOut>
    <SignedIn><Hello /></SignedIn>
  </CapchaIDProvider>
);
  • Nothing auth-dependent renders until the session restore finished (`isLoaded`), and the server snapshot is always signed out, so server rendering and hydration agree.
  • A failed SDK load or session restore is reported in `useSession().error`; it is not silently treated as signed out.
  • Pass `syncCookie={{ name: 'cid_at' }}` to the provider to mirror the access token into a first-party cookie for @capchacloud/id-next.
  • `PasskeyButton conditionalInputId` marks your email input for the browser's passkey autofill dropdown. The click path always works as the fallback. Needs a CapchaID SDK build that ships passkey-conditional.js.

@capchacloud/id-vue

Vue 3 plugin and composables for CapchaID: createCapchaID, useSession, useUser, SignedIn / SignedOut, PasskeyButton and usePasskeyAutofill.

import { createApp } from "vue";
import { createCapchaID, useSession } from "@capchacloud/id-vue";
import App from "./App.vue";

createApp(App).use(createCapchaID({ publishableKey: "cid_pk_live_..." })).mount("#app");

// inside any component's <script setup>:
const { isLoaded, isSignedIn, user, getToken, signOut } = useSession();   // refs
// <SignedIn>...</SignedIn> / <SignedOut>...</SignedOut> components are exported too
  • Same store, same guarantees as the React bindings (loaded gate, explicit error state, StrictMode-style remount safe).
  • `usePasskeyAutofill({ input: 'email', onSignedIn })` wires passkey autofill to an input and aborts on unmount.

@capchacloud/id-next

Next.js integration for CapchaID: an edge-safe authMiddleware, auth() for Server Components and Route Handlers, and withAuth() for API routes. Offline JWT verification via @capchacloud/id-edge. Next 13.4+.

// middleware.ts
import { authMiddleware } from "@capchacloud/id-next";
export default authMiddleware({ audience: process.env.CAPCHAID_APP_ID!, cookieName: "cid_at", publicRoutes: ["/", "/pricing", "/sign-in"], signInUrl: "/sign-in" });
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };

// app/dashboard/page.tsx (Server Component)
import { auth } from "@capchacloud/id-next";
const { userId } = await auth({ audience: process.env.CAPCHAID_APP_ID!, cookieName: "cid_at" });
  • Fail closed: with no route lists every matched path needs a session. Paths are normalised (duplicate and trailing slashes) before matching, and RegExp patterns must match the whole path.
  • Inbound `x-capcha-*` headers are always stripped; identity headers (`x-capcha-user-id`, `x-capcha-app-id`, `x-capcha-org-id`) are set only from a verified session. `auth()` re-verifies the token itself and does not trust those headers.
  • If the auth backend is unreachable the middleware answers 503, never a redirect to sign-in (which would loop) and never allow.
  • Browsers hold the access token in memory. For server-rendered pages, enable the first-party cookie with `syncCookie={{ name: 'cid_at' }}` on the React provider (JavaScript-written, short-lived, not httpOnly), or send `Authorization: Bearer` from your own fetch calls.
  • Verified against real Next 14 types; the middleware adapter itself (src/index.ts) is a thin wrapper over the tested core and has not been run inside a live Next server.

Browser SDK (script tag)

Framework-agnostic. https://capchacloud.com/integrations/capchacloud-browser.js exposes CapchaCloudBrowser.init({ clientId }) with load(), mount(), on() and typed events widget:loading, widget:loaded, widget:ready, capture:success, widget:error. It also ships in @capchacloud/sdk/browser.

All integrations