Lightweight SDK to trigger Reclaim verification flows from **your website** or **your own extension UI (popup/panel)**. It wires content ↔ background, opens the provider tab, generates proofs via an **offscreen** document (WebAssembly), and emits comple
npm install @reclaimprotocol/browser-extension-sdkLightweight SDK to trigger Reclaim verification flows from your website or your own extension UI (popup/panel).
It wires content ↔ background, opens the provider tab, generates proofs via an offscreen document (WebAssembly), and emits completion events.
> Chrome Manifest V3 compatible (Vite/CRA builds included).
---
1. Install
2. One-time Asset Setup
3. Manifest (MV3) — Security & Permissions
4. Manifest (MV3) — Required Entries
5. Load the SDK Content Bridge
6. Common Step (Both Approaches): Initialize Background
7. Approach 1: Use the SDK inside your own extension (popup/panel)
8. Approach 2: Basic setup in your extension + start from a web app
9. Optional: Don’t expose keys client-side — generate a request config on your server
10. Vite/CRX specifics
11. Troubleshooting
12. Checklist
13. Types
---
``bash`
npm i @reclaimprotocol/browser-extension-sdk
---
Copies the SDK’s prebuilt classic bundles into your extension’s public/ (so they are not re-bundled; Chrome content scripts must be classic, not ESM).
package.json
`json`
{
"scripts": {
"reclaim-extension-setup": "node node_modules/@reclaimprotocol/browser-extension-sdk/build/scripts/install-assets.js --public-dir=public"
}
}
Run
`bash`
npm run reclaim-extension-setup
This ensures the following exist in your final build (no hashing, no ESM):
- reclaim-browser-extension-sdk/content/content.bundle.jsreclaim-browser-extension-sdk/offscreen/offscreen.html
- reclaim-browser-extension-sdk/offscreen/offscreen.bundle.js
- reclaim-browser-extension-sdk/interceptor/network-interceptor.bundle.js
- reclaim-browser-extension-sdk/interceptor/injection-scripts.bundle.js
- reclaim-browser-extension-sdk/content/components/reclaim-provider-verification-popup.(css|html)
-
---
Add before other entries:
`json`
{
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; worker-src 'self';"
},
"host_permissions": ["
"permissions": ["offscreen", "cookies"]
}
If you use dynamic content script registration (recommended for Vite/CRA), add "scripting":
`json`
{
"permissions": ["offscreen", "cookies", "scripting"]
}
Why these:
- CSP enables WebAssembly for proof generation.
- host_permissions lets the SDK interact with provider sites.
- offscreen is required for background proof generation.
- cookies gives access to provider auth cookies.
- scripting is required for dynamic content script registration.
---
`json`
{
"web_accessible_resources": [
{
"resources": [
"reclaim-browser-extension-sdk/offscreen/offscreen.html",
"reclaim-browser-extension-sdk/offscreen/offscreen.bundle.js",
"reclaim-browser-extension-sdk/interceptor/network-interceptor.bundle.js",
"reclaim-browser-extension-sdk/interceptor/injection-scripts.bundle.js",
"reclaim-browser-extension-sdk/content/components/reclaim-provider-verification-popup.css",
"reclaim-browser-extension-sdk/content/components/reclaim-provider-verification-popup.html"
],
"matches": ["
}
]
}
What they’re for
- offscreen.*: proof generation + WebAssemblyinterceptor/*
- : network interception for provider authcontent/components/*
- : built-in verification popup UI
---
Choose one: static (manifest) or dynamic (service worker).
`json`
{
"content_scripts": [
{
"js": ["reclaim-browser-extension-sdk/content/content.bundle.js", "yourContent.js"],
"run_at": "document_start",
"matches": ["
}
]
}
`ts
// background (service_worker)
chrome.runtime.onInstalled.addListener(() => {
chrome.scripting.registerContentScripts(
[
{
id: "reclaim-sdk",
matches: ["
js: ["reclaim-browser-extension-sdk/content/content.bundle.js"],
runAt: "document_start",
world: "ISOLATED",
},
],
() => void chrome.runtime.lastError,
);
});
chrome.scripting.getRegisteredContentScripts((scripts) => {
if (!scripts?.find((s) => s.id === "reclaim-sdk")) {
chrome.scripting.registerContentScripts(
[
{
id: "reclaim-sdk",
matches: ["
js: ["reclaim-browser-extension-sdk/content/content.bundle.js"],
runAt: "document_start",
world: "ISOLATED",
},
],
() => void chrome.runtime.lastError,
);
}
});
`
> Dynamic registration requires "scripting" permission.
---
`js
// background entry (service_worker)
import { reclaimExtensionSDK } from "@reclaimprotocol/browser-extension-sdk";
reclaimExtensionSDK.initializeBackground(); // idempotent
`
---
Minimal popup markup
` html`
`js
// popup.js
import { reclaimExtensionSDK } from "@reclaimprotocol/browser-extension-sdk";
document.getElementById("start").onclick = async () => {
const appId = document.getElementById("appId").value.trim();
const appSecret = document.getElementById("appSecret").value.trim();
const providerId = document.getElementById("providerId").value.trim();
const out = document.getElementById("out");
out.textContent = "";
try {
const req = await reclaimExtensionSDK.init(appId, appSecret, providerId);
request.on("started", ({ sessionId }) => console.log("started", sessionId));
req.on("completed", (p) => (out.textContent = JSON.stringify(p, null, 2)));
req.on("error", (e) => (out.textContent = Error: ${e?.message || e}));Error: ${e?.message || String(e)}
} catch (e) {
out.textContent = ;`
}
};
---
When you trigger from a webpage, pass your Extension ID.
`tsx
// Example React component (Vite)
import React, { useState } from "react";
import { reclaimExtensionSDK } from "@reclaimprotocol/browser-extension-sdk";
const APP_ID = import.meta.env.VITE_RECLAIM_APP_ID;
const APP_SECRET = import.meta.env.VITE_RECLAIM_APP_SECRET;
const EXTENSION_ID = import.meta.env.VITE_RECLAIM_EXTENSION_ID;
export default function ReclaimButton({ providerId }: { providerId: string }) {
const [loading, setLoading] = useState(false);
const [proofs, setProofs] = useState(null);
const [error, setError] = useState("");
const [statusUrl, setStatusUrl] = useState("");
const [req, setReq] = useState(null);
const start = async () => {
try {
setLoading(true);
setError("");
setProofs(null);
// Optional - You can also check if the extension is installed before starting:
// const installed = await reclaimExtensionSDK.isExtensionInstalled({
// extensionID: EXTENSION_ID,
// });
// if (!installed) {
// alert("Please install the extension first.");
// return;
// }
const request = await reclaimExtensionSDK.init(APP_ID, APP_SECRET, providerId, {
extensionID: EXTENSION_ID,
});
setReq(request);
setStatusUrl(request.getStatusUrl());
request.on("completed", (p) => {
setProofs(p);
setLoading(false);
});
request.on("error", (e) => {
setError(e?.message || String(e));
setLoading(false);
});
const p = await request.startVerification();
setProofs(p);
} catch (e) {
setError(e?.message || String(e));
setLoading(false);
}
};
return (
{statusUrl && (
Track status:{" "}
status
{error}}{JSON.stringify(proofs, null, 2)}}> Important: From a web page, the SDK cannot call
chrome.runtime.sendMessage unless you provide extensionID.---
Optional: Don’t expose keys client-side — generate a request config on your server
Use
@reclaimprotocol/js-sdk to generate a signed request config on the server.Server (Node/Express)
`js
const express = require("express");
const { ReclaimProofRequest } = require("@reclaimprotocol/js-sdk");const app = express();
const port = 3000;
app.use(express.json());
app.use(express.text({ type: "/", limit: "50mb" }));
const BASE_URL = "https://your-domain.com";
app.get("/generate-config", async (_req, res) => {
const APP_ID = "YOUR_APPLICATION_ID";
const APP_SECRET = "YOUR_APPLICATION_SECRET";
const PROVIDER_ID = "YOUR_PROVIDER_ID";
try {
const reclaimProofRequest = await ReclaimProofRequest.init(APP_ID, APP_SECRET, PROVIDER_ID);
reclaimProofRequest.setAppCallbackUrl(
${BASE_URL}/receive-proofs);
const reclaimProofRequestConfig = reclaimProofRequest.toJsonString();
res.json({ reclaimProofRequestConfig });
} catch (error) {
res.status(500).json({ error: "Failed to generate request config" });
}
});app.listen(port, () => {
console.log(
Server running at http://localhost:${port});
});
`Client (web or popup)
`ts
import { reclaimExtensionSDK } from "@reclaimprotocol/browser-extension-sdk";const EXTENSION_ID = "";
async function startFromServerConfig() {
const r = await fetch("/generate-config").then((x) => x.json());
const { reclaimProofRequestConfig } = r;
const request = await reclaimExtensionSDK.fromJsonString(reclaimProofRequestConfig, {
extensionID: EXTENSION_ID,
});
request.on("started", ({ sessionId }) => console.log("started", sessionId));
request.on("completed", (p) => console.log("completed", p));
request.on("error", console.error);
}
`Docs: https://docs.reclaimprotocol.org/web/backend/usage
---
Vite/CRX specifics
- Ensure SDK assets are copied 1:1 to your
dist without hashing.
- Always load content.bundle.js (classic), not an ESM bundle.
- Use vite-plugin-static-copy if needed.`ts
import { viteStaticCopy } from "vite-plugin-static-copy";viteStaticCopy({
targets: [
{
src: "node_modules/@reclaimprotocol/browser-extension-sdk/build/*/",
dest: "reclaim-browser-extension-sdk",
},
],
});
`---
Troubleshooting
- “Unexpected token 'export'” → Load classic
content.bundle.js instead of an ESM file.
- “chrome.runtime.sendMessage called from a web page must specify Extension ID” → Pass { extensionID }.
- Provider tab doesn’t open → Check assets, permissions, background init, and content script registration.---
Checklist
- [x] Ran
npm run reclaim-extension-setup
- [x] Manifest includes CSP, host_permissions, and permissions (offscreen, cookies, scripting if dynamic)
- [x] Added web_accessible_resources
- [x] Content bundle loaded (static/dynamic)
- [x] Background initialized once
- [x] Passed extensionID from web usage (if applicable)---
Types
`ts
import type { reclaimExtensionSDK } from "@reclaimprotocol/browser-extension-sdk";
``