@datonow/sdk
v0.3.0
Published
Typed JavaScript/TypeScript producer and consumer SDK for Datonow.
Maintainers
Readme
@datonow/sdk
Typed JavaScript/TypeScript SDK for Datonow. It has no runtime dependencies and keeps browser consumer credentials separate from server-only producer credentials.
Install
npm install @datonow/sdkVersion 0.2 is a deliberate breaking release. It matches the current server
wire contract and does not include aliases for the old Datonow, list,
markRead, or snake_case result API.
Browser consumer
import { DatonowConsumer } from "@datonow/sdk";
const notifications = new DatonowConsumer({
endpoint: "https://notify.example.com",
appId: "YOUR_APP_ID",
publicToken: "YOUR_PUBLIC_TOKEN",
recipientId: currentUser.id,
// Required when recipient-token protection is enabled for this application:
recipientToken: tokenFromYourBackend,
});
notifications.on("notification", (notification) => {
console.log(notification.title, notification.createdAt);
});
notifications.on("stateChanged", (state) => {
console.log("Datonow connection:", state);
});
// Resolves after the authenticated `auth_ok` frame, not merely after the TCP/WS open.
await notifications.connect();
const page = await notifications.listNotifications({ limit: 20 });
console.log(page.notifications, page.nextCursor, page.unreadCount);
const affected = await notifications.markNotificationsRead(["notification-id"]);
const unread = await notifications.getUnreadCount();
notifications.disconnect();Construction has no WebSocket side effect. Call connect() explicitly and catch
its promise so authentication failures are visible to the application.
The public token is sent in the first TLS-protected WebSocket frame:
{
"type": "auth",
"token": "PUBLIC_TOKEN",
"recipient_token": "OPTIONAL_RECIPIENT_TOKEN"
}Only app_id and recipient_id are present in the WebSocket URL. REST consumer
requests use X-Datonow-Public-Token, X-Datonow-Recipient, and, when supplied,
X-Datonow-Recipient-Token.
Server producer
import { DatonowProducer } from "@datonow/sdk";
const producer = new DatonowProducer({
endpoint: "https://notify.example.com",
appId: "YOUR_APP_ID",
secretToken: process.env.DATONOW_SECRET_TOKEN!,
});
const result = await producer.publish({
eventType: "order.shipped",
recipientId: "user-123",
title: "Order shipped",
body: "Order #456 is on its way.",
data: { orderId: 456 },
});
console.log(result.accepted); // true; delivery continues asynchronouslypublish() does not return an event ID because the current ingest endpoint
accepts work into Redis and returns { "ok": true } before the event is persisted.
The SDK never retries publish automatically, avoiding duplicate submissions.
Never bundle DatonowProducer or its secret token into browser code.
Recipient-token protection
Mint the short-lived token in your trusted backend, bind it to the authenticated user, then pass only the result to the browser:
const recipientToken = await producer.mintRecipientToken({
recipientId: currentUser.id,
ttlSeconds: 900,
});
// Send to that user only:
recipientToken.token;
recipientToken.expiresIn;Web push
WebSocket delivery only reaches an open page. DatonowWebPush additionally
registers the browser with its push service, so notifications arrive while your
app is closed.
Copy the shipped service worker to your own web root first — a worker can only control pages inside its own scope, so it cannot be served from a CDN:
cp node_modules/@datonow/sdk/datonow-sw.js public/datonow-sw.jsimport { DatonowWebPush, isWebPushSupported } from "@datonow/sdk";
const push = new DatonowWebPush({
endpoint: "https://notify.example.com",
appId: "app_123",
publicToken: "pub_live_...",
recipientId: currentUser.id,
recipientToken: recipientToken.token, // always required for push
serviceWorkerUrl: "/datonow-sw.js", // default
});
if (isWebPushSupported()) {
// Call from a click handler: browsers require a user gesture for the prompt.
const subscription = await push.subscribe();
if (!subscription) {
// Push is disabled for this application; WebSocket delivery still works.
}
}
// Repairs a subscription the browser rotated while the app was closed. The
// service worker also posts { type: "datonow:pushsubscriptionchange" } to open
// pages when the browser replaces a subscription while the app is running.
await push.syncSubscription();
// Stop pushes to this device (server first, then the browser).
await push.unsubscribe();A recipient token is mandatory here even when the application does not otherwise
require one: a subscription outlives the page, so the shared public token alone
must never be enough to register a device against a recipientId.
subscribe() throws DatonowError with code PUSH_UNSUPPORTED outside a
capable secure context and PUSH_PERMISSION_DENIED when the user declined
notifications. It returns null — not an error — when the server has push
disabled for the application.
iOS/iPadOS Safari only delivers web push to sites installed to the Home Screen.
Errors and cancellation
import { DatonowApiError, DatonowConnectionError } from "@datonow/sdk";
try {
await producer.publish(event, { signal: abortController.signal });
} catch (error) {
if (error instanceof DatonowApiError) {
console.error(error.status, error.responseBody, error.retryAfterSeconds);
}
if (error instanceof DatonowConnectionError) {
console.error(error.closeCode, error.retryable);
}
}HTTP 429 responses expose Retry-After as retryAfterSeconds. Authentication
close codes 4001 and 4003 are terminal and are not reconnected automatically.
Transient closes use capped exponential backoff.
Configuration
DatonowConsumer:
| Option | Required | Default | Purpose |
| ----------------------------------- | -------: | ------: | ------------------------------------------------ |
| endpoint | yes | — | API origin; https:// or wss:// in production |
| appId | yes | — | Datonow application ID |
| publicToken | yes | — | Browser-safe application token |
| recipientId | yes | — | Current authenticated user identifier |
| recipientToken | no | — | Short-lived recipient-scoped token |
| requestTimeoutMs | no | 10000 | REST timeout |
| reconnect.enabled | no | true | Retry transient WebSocket closes |
| reconnect.initialDelayMs | no | 1000 | First retry delay |
| reconnect.maxDelayMs | no | 30000 | Backoff cap |
| reconnect.authenticationTimeoutMs | no | 10000 | Wait for auth_ok |
DatonowProducer uses endpoint, appId, secretToken, and optional
requestTimeoutMs. All clients accept allowInsecure, defaulting to false.
Plaintext is accepted automatically only for loopback hosts.
DatonowWebPush:
| Option | Required | Default | Purpose |
| -------------------- | -------: | -----------------: | --------------------------------------------- |
| endpoint | yes | — | API origin |
| appId | yes | — | Datonow application ID |
| publicToken | yes | — | Browser-safe application token |
| recipientId | yes | — | Current authenticated user identifier |
| recipientToken | yes | — | Recipient-scoped token; never optional here |
| serviceWorkerUrl | no | /datonow-sw.js | Worker path on your own origin |
| serviceWorkerScope | no | worker directory | Registration scope |
| requestTimeoutMs | no | 10000 | REST timeout |
Public data model
The SDK normalizes server snake_case fields to JavaScript camelCase:
next_cursor→nextCursorunread_count→unreadCountrecipient_token→tokenexpires_in→expiresIn- notification
created_at/is_read→createdAt/isRead
Realtime notification frames omit client_id, event_id, recipient_id, and
is_read. The SDK fills recipientId from configuration and isRead as false;
clientId and eventId remain optional.
License
MIT
