@meerkly/sdk
A plug-and-play browser for Electron apps. Two things in one package:
- A local browser API — render any page in a hidden, hardened Chromium window and get its HTML
(or raw JSON) back. No configuration, no account, no
app.whenReady() sequencing.
- A Meerkly network worker — share the same browser's idle capacity with the Meerkly network
and earn credits for the crawls it serves. One
start() call: the SDK handles identity,
enrollment, the gateway connection, job serving, and crash recovery.
npm install @meerkly/sdk
Requires Electron ≥ 28 (peer dependency) — the SDK runs in your app's main process.
Quick start
import { MeerklySDK } from '@meerkly/sdk';
const sdk = new MeerklySDK({ workerKey: 'mk_wk_…' });
void sdk.start();
const page = await sdk.fetch('https://example.com');
console.log(page.title, page.httpStatus, page.html?.length);
That's a complete integration. You don't need to wait for app.whenReady() — the SDK does that
internally. Local requests and network jobs share one engine and are serialized internally, so they
never interleave. A runnable app lives in examples/basic-worker.
The local browser API
Works with new MeerklySDK({}) — no auth, no start().
const result = await sdk.fetch(url, {
waitFor: 'stable',
settleMs: 5000,
waitRules: [
{ if: '#captcha', then: '#content' }
],
detectMs: 800
});
fetch() resolves to a FetchResult:
success | The page loaded and HTML was extracted |
html | Page HTML — or the raw JSON payload when the document was a JSON media type |
format | 'html' or 'json' (how to read html) |
finalUrl, title | After redirects |
httpStatus | Main-document HTTP status (0 if not captured) |
loadedMs | Wall-clock load time |
waitTimedOut, matchedRule | Wait-condition outcome (matchedRule = index into waitRules, -1 = none) |
error | Failure reason when success is false |
Wait semantics: stable resolves once the DOM has been structurally quiet for 500 ms (ignoring
attribute churn, so CSS animations don't block it), capped by settleMs; networkidle waits for
zero in-flight requests for 500 ms; a CSS selector waits for the element to be visible, then
settles. These are the same spec-defined semantics gateway jobs use.
Need more control? sdk.browser exposes the underlying HiddenBrowser:
navigateToUrl(url), getStatus(), setVisible(true) / toggleVisible() (a debug view that shows
the hidden window so you can watch it work). Never construct a second HiddenBrowser yourself —
the engine serializes all work through one window and owns its session partition.
The engine is deliberately hardened: hidden window, persistent app-scoped session (never your
users' real browser profile), downloads blocked, all permission prompts denied, window.open
denied, sandboxed renderer, automatic recovery when a page crashes the renderer.
Joining the Meerkly network
start() needs one auth mode:
workerKey: 'mk_wk_…' | Third-party apps (default) | Hard-code your key. Every end-user install enrolls itself as a device under your Meerkly account and earns credits for you. The key is enrollment-only: it grants no account reads, serves no crawls, and is never sent to the gateway. Per-key device limits cap misuse if it's extracted from your binary. |
oauth: { clientId } | First-party Meerkly apps | Interactive PKCE sign-in (system browser + loopback callback) + device pairing; each end user pairs to their own account. Adds signIn() / signOut() / getAuthStatus() / getAccessToken() and the 'auth' event. Requires a Doorkeeper client with seeded loopback redirect URIs on the account service. |
getDeviceToken: async () => … | Custom pairing | Bring your own device token; re-read on every (re)connect so rotation needs no restart. |
Lifecycle:
const sdk = new MeerklySDK({ workerKey: 'mk_wk_…' });
sdk.on('status', (s) => console.log('worker:', s));
sdk.on('job', ({ jobId, url, success, error }) => console.log('served', url, success));
sdk.on('error', (err) => console.warn(err.message));
await sdk.start();
await sdk.stop();
What start() does for you: waits for Electron app-ready → resolves a stable per-install
machine id (random UUID persisted in stateDir, never derived from hardware) → obtains a device
token (enrollment for workerKey, stored pairing for oauth) → connects to the gateway with
auto-reconnect/backoff → serves fetch jobs through the shared engine. Remotely-dispatched jobs are
SSRF-guarded (private/loopback/link-local targets are refused before the browser ever sees them) and
bounded (max 3 pending jobs).
Options reference
All optional. Defaults are production-ready.
interface MeerklySDKOptions {
workerKey?: string;
oauth?: { clientId: string; callbackPorts?: number[] };
getDeviceToken?: () => Promise<string | null>;
gatewayUrl?: string;
accountBaseUrl?: string;
stateDir?: string;
machineId?: string;
workerId?: string;
tokenStore?: DeviceTokenStore;
oauthTokenStore?: OAuthTokenStore;
workerName?: string;
deviceInfo?: Partial<DeviceInfo>;
platform?: 'desktop' | 'android' | 'server';
browser?: HiddenBrowser;
logger?: BrowserLogger;
allowInsecure?: boolean;
}
Token storage prefers OS encryption (Electron safeStorage — Keychain/DPAPI/libsecret) and falls
back to 0600 plaintext files where no keychain exists. Inject tokenStore/oauthTokenStore to
change that policy.
const sdk = new MeerklySDK({ oauth: { clientId: 'my-first-party-app' } });
await sdk.start();
sdk.on('auth', (status) => render(status));
const status = await sdk.signIn();
const token = await sdk.getAccessToken();
await sdk.signOut();
Transient pairing failures retry automatically (30 s doubling to a 10 min cap); terminal ones
(device claimed by another account, stale token scope) surface in getAuthStatus().deviceLinkError.
Good to know
- Main process only. The SDK creates
BrowserWindows and uses safeStorage — don't import it
in a renderer.
- One SDK instance per app. The nav chain and session-partition ownership assume it.
- The engine reports its honest identity. The User-Agent is Electron's own with only the app
name token stripped (see
src/browser/userAgent.ts for the measured reasoning) — don't layer UA
spoofing on top; it gets detected.
- Wait/extraction semantics are spec-enforced (
api-gateway/spec/, shared with the Android and
headless workers). Protocol changes must update the spec + vectors + all workers together.
Development (this repo)
Part of the meerkly-desktop npm workspace; the Meerkly desktop app is the reference consumer.
npm run build -w @meerkly/sdk
npm test -w @meerkly/sdk
npm run publish:sdk