@wishcore/wish-sdk
v0.5.0
Published
Wish API for node. Used for building Wish Apps. TS framework layers over the native wish-sdk-rs core (napi).
Maintainers
Readme
Wish SDK
Node.js SDK for building peer-to-peer applications on the Wish protocol.
- Cryptographic identity-based P2P networking
- Protocol class pattern for defining endpoints and peer lifecycle
- Document sync via
SyncProtocol - WebSocket + CBOR server for frontends (
createWebRpc) - Promise-based ready state and identity management (
RpcApp)
Install
npm install @wishcore/wish-sdkThe SDK is currently beta-tagged. To install the latest beta explicitly:
npm install @wishcore/wish-sdk@betaThe document-store dependency is the Rust-backed napi package in this
repo (wish/bindings/node/document-store); inside wish-stack everything
resolves via file: paths — see an example app's package.json
(apps/notes/backend). The npm registry publish (stable versions for
external use) is pending.
Two things to know before your first app
1. Your app doesn't own identities — it asks for one
Identities are minted and managed by the Wish app (Dashboard). Your app
requests an identity grant and the user approves it from the tray. The
SDK exposes this as app.requestIdentity(). Before the grant arrives,
app.identities is empty.
The exception is admin tooling (e.g. the Dashboard itself, the wish CLI).
Those pass permissions: { admin: true } and can call admin RPCs like
identity.create directly. Regular apps never need that.
2. A Wish app is a long-running process, not a script
Peer connections, document sync, and presence all live inside the running app process. There's no "one-shot CLI" shape — the typical layout is:
- Daemon:
RpcApp+ yourProtocols +createWebRpcfor a UI/CLI to attach - Clients: anything that talks to the daemon's WebRpc (a webview, a CLI, …)
If you want one-shot commands, write a thin CLI that connects to the daemon
over WebRpc, not a fresh App per invocation.
Project setup
// package.json — top-level await needs "type": "module"
{
"type": "module",
"dependencies": {
"@wishcore/wish-sdk": "^0.4.0-beta-26"
}
}Quick start
import { RpcApp, Protocol } from '@wishcore/wish-sdk';
class ChatProtocol extends Protocol {
online(peer, client) { console.log('Peer online:', peer.toString()); }
offline(peer) { console.log('Peer offline:', peer.toString()); }
}
const app = new RpcApp({
name: 'Chat',
// Auto-detects ~/.wish/core.sock; override with coreUnixSocket or corePort.
protocols: { chat: new ChatProtocol() },
});
await app.whenReady;
if (app.identities.length === 0) {
// Dashboard will prompt the user. Resolves once granted.
await app.requestIdentity();
}
console.log('Identity:', app.identities[0].name);Requires a running wish-core. Get it via the Wish desktop app, or download the standalone binary from developer.wishtech.fi/downloads/wish-core/.
Protocol pattern
Define peer-facing endpoints by extending Protocol. Each peer that runs the
same protocol gets a typed session.
class MyProtocol extends Protocol {
constructor() {
super();
this.endpoint('greet', { doc: 'Send greeting' }, async (req, res) => {
res.send({ message: 'Hello from ' + req.args[0] });
});
}
online(peer, client) {
client.request('greet', ['Alice'], (err, data) => {
console.log(data?.message);
});
}
offline(peer) { /* peer left */ }
}SyncProtocol
Bridges document-store to wish-core. The app declares which root
documents to pull via getRoots; the sync engine handles discovery,
incremental transfer, and signature checks.
import { RpcApp, SyncProtocol, createWebRpc } from '@wishcore/wish-sdk';
import { DocumentStore, SqlitePersistence } from 'document-store';
import Database from 'better-sqlite3';
const db = new Database('./data/app.db');
const store = new DocumentStore({ storage: new SqlitePersistence(db) as any });
store.registerType('note');
const webRpc = createWebRpc({ port: 8090 });
const sync = new SyncProtocol({
store,
// Required: return the set of root hashes you want to sync, per identity.
// Typical pattern: maintain a `pullRoots` document type the user controls
// (e.g. by accepting invites or following others), and query it here.
getRoots: async (uid) => {
const rows = db.prepare(
`SELECT json_extract(doc, '$.roots') as roots
FROM pullRoots
WHERE json_extract(doc, '$.uid.__buf') = ?`,
).all(uid.toString('hex')) as any[];
return rows.flatMap(r => JSON.parse(r.roots || '[]')
.map((h: any) => Buffer.from(h.__buf, 'hex')));
},
onSync: ({ injected }) => {
if (injected) webRpc.emit('signals', ['document.changed']);
},
});
const app = new RpcApp({
name: 'Notes',
protocols: { notes: sync },
});
await app.whenReady;For a richer setup (custom sign/verify callbacks, schema validation,
share resolvers) see the
document-store guide
or the Reason app source.
Notifying peers after a write
When your app writes a document, tell sync to push the change:
const [errors, hash] = await store.add('note', {
uid: app.identities[0].uid,
title: 'Hello',
share: { self: true },
});
if (!errors) sync.notifyPeers();Subclassing
class MyAppProtocol extends SyncProtocol {
constructor(store: DocumentStore, getRoots: SyncProtocolOptions['getRoots']) {
super({ store, getRoots });
this.endpoint('custom.method', { doc: 'My endpoint' }, async (_req, res) => {
res.send({ result: 'ok' });
});
}
online(peer, client) {
console.log('Peer online:', peer.toString());
super.online(peer, client); // Start sync for this peer
}
}createWebRpc
WebSocket + CBOR server for frontends and CLIs:
import { createWebRpc } from '@wishcore/wish-sdk';
const webRpc = createWebRpc({ port: 8090 });
webRpc.endpoint('note.list', async (_req, res) => {
const notes = await store.collection('note').find({}).toArray();
res.send(notes);
});
webRpc.emit('signals', ['document.changed']);RpcApp
Manages the wish-core connection, identity lifecycle, and protocol routing.
const app = new RpcApp({
name: 'MyApp',
// Auto-detects ~/.wish/core.sock. Override only if needed:
// coreUnixSocket: '/custom/path/core.sock',
// corePort: 9094,
protocols: { myapp: new MyProtocol() },
});
await app.whenReady;
console.log(app.identities); // [] until the user grants one
if (app.identities.length === 0) {
await app.requestIdentity(); // Resolves on user approval in Dashboard
}
app.on('identity', (identities) => { /* updated grant list */ });Testing
npm testpretest downloads the stable wish-core binary for your platform
(x64/arm64 Linux, arm64 Darwin) from
developer.wishtech.fi/downloads/wish-core/,
verifies sha256, and drops it at test/.bin/wish-core (gitignored). A small
mocha bootstrap (test/_setup.cjs) points WISH at it. To force a refresh,
delete test/.bin/; to pin a specific build, set WISH explicitly before
running.
# Switch to the rolling test channel (newest unreleased SHAs)
CHANNEL=test npm run fetch-wish-core
# Override entirely
WISH=/abs/path/to/wish-core npm test
WISH=/abs/path/to/wish-core npm run test-only -- test/identity.spec.tsSee test/README.md for the multi-peer test patterns
(WishCoreRunner, importContactWithTransport, sync E2E).
Documentation
- SDK overview: developer.wishtech.fi/sdk
- Document Store: ds.wishtech.fi
- Reason app source — full pattern for sharing, invites, pullRoots, sync
License
Apache-2.0
