anonpenguin23 3b8139802c feat: APNs silent-drop guard + persistent-WS mid-session JWT refresh
#348 - APNs silent-drop guard
Apple's APNs silently returns HTTP 200 for pushes with no visible
content (no title, no body, no badge, no sound, no
content-available=1) and then drops them — which looked to the WASM
caller like a successful delivery. Now rejected up-front with the new
push.ErrEmptyContent sentinel, and the APNs provider returns the
structured push.PushError shape (HTTPStatus, Reason, Unregistered,
Wrapped) so the dispatcher can branch on Unregistered to remove dead
tokens automatically. Legacy ErrDeviceUnregistered sentinel is
preserved for errors.Is compatibility (wrapped inside PushError).

Always logs APNs HTTP response (status, reason, apns_id, token prefix)
so future silent-drop classes show up in operator logs.

content-available is also now correctly mapped from snake_case
Data["content_available"] (any truthy variant) into Apple's
canonical "content-available": 1 inside the aps dictionary.

#321 - mid-session JWT refresh on persistent WS
Long-lived persistent WS connections used to have to close+reconnect
when the JWT rolled — losing per-instance state, message queues, and
subscriptions. The handler now accepts an "auth.refresh" control
frame: client sends the new token, the gateway re-verifies it via
the new JWTVerifier interface, updates the per-instance invCtx
in-place (persistent.Instance.UpdateInvCtx), and acks. No close, no
state loss.

JWTVerifier is optional — handlers set it via SetJWTVerifier at
gateway init. When unwired the handler nack's with a "not supported
on this gateway" response and clients fall back to the old
close+reconnect path, so older deploys don't break.

Other:
- push/dispatcher.go: SendToUserDetailed returns per-device PushError
  shape so callers can act on Unregistered / HTTPStatus / Reason.
- serverless/hostfunctions/push.go: WASM host functions for the new
  detailed-error shape.
- serverless/persistent/instance.go: UpdateInvCtx mid-session.

Tests:
- ws_persistent_control_test.go: auth.refresh ack/nack paths.
- apns_test.go: empty-content rejection, PushError shape on 410 +
  generic non-200, content-available mapping.
- dispatcher_detailed_test.go: SendToUserDetailed result shape.
- instance_update_invctx_test.go: invCtx update is per-instance, not
  cross-tenant.

VERSION bumped to 0.122.27.
2026-05-19 18:19:21 +03:00
..
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00
2026-03-26 18:40:20 +02:00

@debros/network-ts-sdk - TypeScript SDK for DeBros Network

A modern, isomorphic TypeScript SDK for the DeBros Network gateway. Works seamlessly in both Node.js and browser environments with support for database operations, pub/sub messaging, and network management.

Features

  • Isomorphic: Works in Node.js and browsers (uses fetch and isomorphic-ws)
  • Database ORM-like API: QueryBuilder, Repository pattern, transactions
  • Pub/Sub Messaging: WebSocket subscriptions with automatic reconnection
  • Authentication: API key and JWT support with automatic token management
  • TypeScript First: Full type safety and IntelliSense
  • Error Handling: Unified SDKError with HTTP status and code

Installation

npm install @debros/network-ts-sdk

Quick Start

Initialize the Client

import { createClient } from "@debros/network-ts-sdk";

const client = createClient({
  baseURL: "http://localhost:6001",
  apiKey: "ak_your_api_key:namespace",
});

// Or with JWT
const client = createClient({
  baseURL: "http://localhost:6001",
  jwt: "your_jwt_token",
});

Database Operations

Create a Table

await client.db.createTable(
  "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"
);

Insert Data

const result = await client.db.exec(
  "INSERT INTO users (name, email) VALUES (?, ?)",
  ["Alice", "alice@example.com"]
);
console.log(result.last_insert_id);

Query Data

const users = await client.db.query("SELECT * FROM users WHERE email = ?", [
  "alice@example.com",
]);

Using QueryBuilder

const activeUsers = await client.db
  .createQueryBuilder("users")
  .where("active = ?", [1])
  .orderBy("name DESC")
  .limit(10)
  .getMany();

const firstUser = await client.db
  .createQueryBuilder("users")
  .where("id = ?", [1])
  .getOne();

Using Repository Pattern

interface User {
  id?: number;
  name: string;
  email: string;
}

const repo = client.db.repository<User>("users");

// Find
const users = await repo.find({ active: 1 });
const user = await repo.findOne({ email: "alice@example.com" });

// Save (INSERT or UPDATE)
const newUser: User = { name: "Bob", email: "bob@example.com" };
await repo.save(newUser);

// Remove
await repo.remove(newUser);

Transactions

const results = await client.db.transaction([
  {
    kind: "exec",
    sql: "INSERT INTO users (name, email) VALUES (?, ?)",
    args: ["Charlie", "charlie@example.com"],
  },
  {
    kind: "query",
    sql: "SELECT COUNT(*) as count FROM users",
    args: [],
  },
]);

Pub/Sub Messaging

The SDK provides a robust pub/sub client with:

  • Multi-subscriber support: Multiple connections can subscribe to the same topic
  • Namespace isolation: Topics are scoped to your authenticated namespace
  • Server timestamps: Messages preserve server-side timestamps
  • Binary-safe: Supports both string and binary (Uint8Array) payloads
  • Strict envelope validation: Type-safe message parsing with error handling

Publish a Message

// Publish a string message
await client.pubsub.publish("notifications", "Hello, Network!");

// Publish binary data
const binaryData = new Uint8Array([1, 2, 3, 4]);
await client.pubsub.publish("binary-topic", binaryData);

Subscribe to Topics

const subscription = await client.pubsub.subscribe("notifications", {
  onMessage: (msg) => {
    console.log("Topic:", msg.topic);
    console.log("Data:", msg.data);
    console.log("Server timestamp:", new Date(msg.timestamp));
  },
  onError: (err) => {
    console.error("Subscription error:", err);
  },
  onClose: () => {
    console.log("Subscription closed");
  },
});

// Later, close the subscription
subscription.close();

Message Interface:

interface Message {
  data: string; // Decoded message payload (string)
  topic: string; // Topic name
  timestamp: number; // Server timestamp in milliseconds
}

Debug Raw Envelopes

For debugging, you can inspect raw message envelopes before decoding:

const subscription = await client.pubsub.subscribe("notifications", {
  onMessage: (msg) => {
    console.log("Decoded message:", msg.data);
  },
  onRaw: (envelope) => {
    console.log("Raw envelope:", envelope);
    // { data: "base64...", timestamp: 1234567890, topic: "notifications" }
  },
});

Multi-Subscriber Support

Multiple subscriptions to the same topic are supported. Each receives its own copy of messages:

// First subscriber
const sub1 = await client.pubsub.subscribe("events", {
  onMessage: (msg) => console.log("Sub1:", msg.data),
});

// Second subscriber (both receive messages)
const sub2 = await client.pubsub.subscribe("events", {
  onMessage: (msg) => console.log("Sub2:", msg.data),
});

// Unsubscribe independently
sub1.close(); // sub2 still active
sub2.close(); // fully unsubscribed

List Topics

const topics = await client.pubsub.topics();
console.log("Active topics:", topics);

Presence Support

The SDK supports real-time presence tracking, allowing you to see who is currently subscribed to a topic.

Subscribe with Presence

Enable presence by providing presence options in subscribe:

const subscription = await client.pubsub.subscribe("room.123", {
  onMessage: (msg) => console.log("Message:", msg.data),
  presence: {
    enabled: true,
    memberId: "user-alice",
    meta: { displayName: "Alice", avatar: "URL" },
    onJoin: (member) => {
      console.log(`${member.memberId} joined at ${new Date(member.joinedAt)}`);
      console.log("Meta:", member.meta);
    },
    onLeave: (member) => {
      console.log(`${member.memberId} left`);
    },
  },
});

Get Presence for a Topic

Query current members without subscribing:

const presence = await client.pubsub.getPresence("room.123");
console.log(`Total members: ${presence.count}`);
presence.members.forEach((member) => {
  console.log(`- ${member.memberId} (joined: ${new Date(member.joinedAt)})`);
});

Subscription Helpers

Get presence information from an active subscription:

if (subscription.hasPresence()) {
  const members = await subscription.getPresence();
  console.log("Current members:", members);
}

Authentication

Switch API Key

client.auth.setApiKey("ak_new_key:namespace");

Switch JWT

client.auth.setJwt("new_jwt_token");

Get Current Token

const token = client.auth.getToken(); // Returns API key or JWT

Get Authentication Info

const info = await client.auth.whoami();
console.log(info.authenticated, info.namespace);

Logout

await client.auth.logout();

Network Operations

Check Health

const healthy = await client.network.health();

Get Network Status

const status = await client.network.status();
console.log(status.healthy, status.peers);

List Peers

const peers = await client.network.peers();
peers.forEach((peer) => {
  console.log(peer.id, peer.addresses);
});

Proxy Requests Through Anyone Network

Make anonymous HTTP requests through the Anyone network:

// Simple GET request
const response = await client.network.proxyAnon({
  url: "https://api.example.com/data",
  method: "GET",
  headers: {
    Accept: "application/json",
  },
});

console.log(response.status_code); // 200
console.log(response.body); // Response data as string
console.log(response.headers); // Response headers

// POST request with body
const postResponse = await client.network.proxyAnon({
  url: "https://api.example.com/submit",
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ key: "value" }),
});

// Parse JSON response
const data = JSON.parse(postResponse.body);

Note: The proxy endpoint requires authentication (API key or JWT) and only works when the Anyone relay is running on the gateway server.

Configuration

ClientConfig

interface ClientConfig {
  baseURL: string; // Gateway URL
  apiKey?: string; // API key (optional, if using JWT instead)
  jwt?: string; // JWT token (optional, if using API key instead)
  timeout?: number; // Request timeout in ms (default: 30000)
  maxRetries?: number; // Max retry attempts (default: 3)
  retryDelayMs?: number; // Delay between retries (default: 1000)
  debug?: boolean; // Enable debug logging with full SQL queries (default: false)
  storage?: StorageAdapter; // For persisting JWT/API key (default: MemoryStorage)
  wsConfig?: Partial<WSClientConfig>; // WebSocket configuration
  fetch?: typeof fetch; // Custom fetch implementation
}

Storage Adapters

By default, credentials are stored in memory. For browser apps, use localStorage:

import { createClient, LocalStorageAdapter } from "@debros/network-ts-sdk";

const client = createClient({
  baseURL: "http://localhost:6001",
  storage: new LocalStorageAdapter(),
  apiKey: "ak_your_key:namespace",
});

Cache Operations

The SDK provides a distributed cache client backed by Olric. Data is organized into distributed maps (dmaps).

Put a Value

// Put with optional TTL
await client.cache.put("sessions", "user:alice", { role: "admin" }, "1h");

Get a Value

// Returns null on cache miss (not an error)
const result = await client.cache.get("sessions", "user:alice");
if (result) {
  console.log(result.value); // { role: "admin" }
}

Delete a Value

await client.cache.delete("sessions", "user:alice");

Multi-Get

const results = await client.cache.multiGet("sessions", [
  "user:alice",
  "user:bob",
]);
// Returns Map<string, any | null> — null for misses
results.forEach((value, key) => {
  console.log(key, value);
});

Scan Keys

// Scan all keys in a dmap, optionally matching a regex
const scan = await client.cache.scan("sessions", "user:.*");
console.log(scan.keys); // ["user:alice", "user:bob"]
console.log(scan.count); // 2

Health Check

const health = await client.cache.health();
console.log(health.status); // "ok"

Storage (IPFS)

Upload, pin, and retrieve files from decentralized IPFS storage.

Upload a File

// Browser
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const result = await client.storage.upload(file, file.name);
console.log(result.cid); // "Qm..."

// Node.js
import { readFileSync } from "fs";
const buffer = readFileSync("image.jpg");
const result = await client.storage.upload(buffer, "image.jpg", { pin: true });

Retrieve Content

// Get as ReadableStream
const stream = await client.storage.get(cid);
const reader = stream.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // Process chunk
}

// Get full Response (for headers like content-length)
const response = await client.storage.getBinary(cid);
const contentLength = response.headers.get("content-length");

Pin / Unpin / Status

// Pin an existing CID
await client.storage.pin("QmExampleCid", "my-file");

// Check pin status
const status = await client.storage.status("QmExampleCid");
console.log(status.status); // "pinned", "pinning", "queued", "unpinned", "error"

// Unpin
await client.storage.unpin("QmExampleCid");

Serverless Functions (WASM)

Invoke WebAssembly serverless functions deployed on the network.

// Configure functions namespace
const client = createClient({
  baseURL: "http://localhost:6001",
  apiKey: "ak_your_key:namespace",
  functionsConfig: {
    namespace: "my-namespace",
  },
});

// Invoke a function with typed input/output
interface PushInput {
  token: string;
  message: string;
}
interface PushOutput {
  success: boolean;
  messageId: string;
}

const result = await client.functions.invoke<PushInput, PushOutput>(
  "send-push",
  { token: "device-token", message: "Hello!" }
);
console.log(result.messageId);

Vault (Distributed Secrets)

The vault client provides Shamir-split secret storage across guardian nodes. Secrets are split into shares, distributed to guardians, and reconstructed only when enough shares are collected (quorum).

const client = createClient({
  baseURL: "http://localhost:6001",
  apiKey: "ak_your_key:namespace",
  vaultConfig: {
    guardians: [
      { address: "10.0.0.1", port: 8443 },
      { address: "10.0.0.2", port: 8443 },
      { address: "10.0.0.3", port: 8443 },
    ],
    identityHex: "your-identity-hex",
  },
});

// Store a secret (Shamir-split across guardians)
const data = new TextEncoder().encode("my-secret-data");
const storeResult = await client.vault.store("api-key", data, 1);
console.log(storeResult.quorumMet); // true if enough guardians ACKed

// Retrieve and reconstruct a secret
const retrieved = await client.vault.retrieve("api-key");
console.log(new TextDecoder().decode(retrieved.data)); // "my-secret-data"

// List all secrets for this identity
const secrets = await client.vault.list();
console.log(secrets.secrets);

// Delete a secret from all guardians
await client.vault.delete("api-key");

Wallet-Based Authentication

For wallet-based auth (challenge-response flow):

// 1. Request a challenge
const challenge = await client.auth.challenge();

// 2. Sign the challenge with your wallet (external)
const signature = await wallet.signMessage(challenge.message);

// 3. Verify signature and get JWT
const session = await client.auth.verify(challenge.id, signature);
console.log(session.token);

// 4. Get an API key for long-lived access
const apiKey = await client.auth.getApiKey();

Error Handling

The SDK throws SDKError for all errors:

import { SDKError } from "@debros/network-ts-sdk";

try {
  await client.db.query("SELECT * FROM nonexistent");
} catch (error) {
  if (error instanceof SDKError) {
    console.log(error.httpStatus); // e.g., 400
    console.log(error.code); // e.g., "HTTP_400"
    console.log(error.message); // Error message
    console.log(error.details); // Full error response
  }
}

Browser Usage

The SDK works in browsers with minimal setup:

// Browser example
import { createClient } from "@debros/network-ts-sdk";

const client = createClient({
  baseURL: "https://gateway.example.com",
  apiKey: "ak_browser_key:my-app",
});

// Use like any other API client
const data = await client.db.query("SELECT * FROM items");

Note: For WebSocket connections in browsers with authentication, ensure your gateway supports either header-based auth or query parameter auth.

Testing

Run E2E tests against a running gateway:

# Set environment variables
export GATEWAY_BASE_URL=http://localhost:6001
export GATEWAY_API_KEY=ak_test_key:default

# Run tests
npm run test:e2e

Examples

See the tests/e2e/ directory for complete examples of:

  • Authentication (auth.test.ts)
  • Database operations (db.test.ts)
  • Transactions (tx.test.ts)
  • Pub/Sub messaging (pubsub.test.ts)
  • Network operations (network.test.ts)

Building

npm run build

Output goes to dist/ with ESM and type declarations.

Development

npm run dev      # Watch mode
npm run typecheck # Type checking
npm run lint     # Linting (if configured)

License

MIT

Support

For issues, questions, or contributions, please open an issue on GitHub or visit DeBros Network Documentation.