Files
Shade/packages/shade-observer
Sterister 96c20cb4b2 fix(session): remember where aliasSession moved a session
aliasSession knew that two labels name the same peer, then threw that
knowledge away. The binding lived only in the caller's memory, so a
restart lost it — and the peer could not repair it from its side.

First contact forces the receiver to label a session by the only sender
hint a relay surfaces, an 8-byte signing-key fingerprint (`fp:<hex>`).
Once the peer announces its canonical address, aliasSession moves the
session there. But the peer keeps sending under `fp:<hex>`, because its
transport derives the same label from the same hint every time. After a
restart the session sat under the canonical address, inbound frames
resolved to `fp:<hex>`, and nothing matched. The peer held a valid
session so it never re-ran X3DH: the failure was permanent, and only a
manual re-link cleared it.

Observed in Prism as `No session for address: fp:579c3b335d66e2c0` on
every receive for three days, with a phone whose every RPC timed out.

StorageProvider gains saveSessionAlias / getSessionAlias /
removeSessionAliasesFor, optional so third-party implementations keep
compiling, and implemented across all seven backends. Lookups resolve
through resolveLabel(), which runs BEFORE the peer mutex — locking the
alias while mutating the canonical session would let an aliased and a
canonical caller ratchet the same state concurrently.

A live session under a label always wins over an alias, and prekey
envelopes never resolve: both keep a re-link establishing a fresh
session instead of being redirected into the stale one. Aliases are
dropped in resetSession and acceptIdentityChange, and memoized so the
hot path costs no extra read.

The sdk.test.ts case that asserted a dead fp-label encoded the old
behaviour; it now pins the new contract.

Verified: 1166 tests pass (from 1160). With alias persistence disabled
as a negative control, 5 of the 6 new tests fail, including both
restart cases.

Also drops `baseUrl` from the consumer-strict tsconfig — removed in
TS 6.0, and it was failing the typecheck that gates publishing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 19:36:31 +02:00
..

@shade/observer

Live observability backend for Shade — exposes a snapshot endpoint, an SSE event stream, and serves the bundled dashboard SPA.

Install

bun add @shade/observer @shade/server @shade/core

Usage

import { createObserver } from '@shade/observer';
import { ShadeEventEmitter, ShadeSessionManager } from '@shade/core';
import { PrekeyServerEvents, createPrekeyServer } from '@shade/server';

// 1. Create event emitters
const clientEvents = new ShadeEventEmitter();
const serverEvents = new PrekeyServerEvents();

// 2. Wire them into your session manager and prekey server
const manager = new ShadeSessionManager(crypto, storage, { events: clientEvents });
const prekeyServer = createPrekeyServer({ crypto, events: serverEvents });

// 3. Create the observer
const observer = createObserver({
  token: process.env.SHADE_OBSERVER_TOKEN!,
  clientEvents,
  serverEvents,
});

// 4. Mount or serve standalone
import { Hono } from 'hono';
const app = new Hono();
app.route('/shade-observer', observer);

Bun.serve({ port: 3900, fetch: app.fetch });

After this, visit http://localhost:3900/shade-observer/dashboard/ and enter your bearer token to see the dashboard.

Endpoints

Method Path Auth Description
GET /api/state Bearer Current snapshot (identity, sessions, prekeys, server stats)
GET /api/events Bearer (or ?token=) SSE stream of live events
GET /dashboard/ None Bundled web UI
GET /health None Liveness check

Configuration

Env var Required Description
SHADE_OBSERVER_TOKEN Yes Bearer token (min 16 chars). Refuses to start if shorter.

The token is checked with constant-time comparison.

Security notes

  • Event payloads contain NO key material, plaintext, or signatures — only structural facts (counters, addresses, short hashes for display).
  • The observer is intended for internal/debugging use. Put it behind a reverse proxy and authenticate access.
  • The dashboard stores the bearer token in localStorage for convenience. Don't load the dashboard on shared computers.