npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dicode-relay

v0.1.7

Published

OAuth broker + WebSocket relay server for dicode

Readme

dicode-relay

A production-ready TypeScript/Node.js service that combines an OAuth broker and a WebSocket relay tunnel in a single process. It lets local dicode daemons (running behind NAT on developer laptops) receive OAuth callbacks and inbound webhooks without a public port, ngrok, or per-user OAuth app registration — the broker holds dicode's shared client_id/client_secret for each provider, executes the full authorization-code flow, and delivers the encrypted access token directly to the daemon over the relay tunnel.


Architecture

┌──────────────────────────────────────────────────────────────┐
│  User's machine                                              │
│                                                              │
│  ┌──────────────────┐   WSS (persistent)                    │
│  │  dicode daemon   │◄──────────────────────────────────┐   │
│  │                  │                                   │   │
│  │  relay.Client    │   /hooks/oauth-complete delivery  │   │
│  │  (Go, PR #79)    │◄── forwarded over WS ─────────────┤   │
│  │                  │                                   │   │
│  │  OAuth task.ts   │                                   │   │
│  └────────┬─────────┘                                   │   │
│           │ open browser                                │   │
└───────────┼─────────────────────────────────────────────┼───┘
            │                                             │
            ▼                             ┌───────────────┴──────────────┐
   ┌──────────────────────────────────┐   │  dicode-relay (Node.js)      │
   │  Browser                         │   │                              │
   │                                  │   │  ┌────────────────────────┐  │
   │  GET /auth/github                │──►│  │  Relay Server (ws)     │  │
   │    ?session=...                  │   │  │  - challenge/response  │  │
   │    &relay_uuid=...               │   │  │  - client registry     │  │
   │    &sig=...                      │   │  │  - request forwarding  │  │
   │                                  │   │  └────────────────────────┘  │
   │  ← redirect to GitHub            │   │                              │
   │  ← redirect back to /callback    │   │  ┌────────────────────────┐  │
   │                                  │   │  │  OAuth Broker (Grant)  │  │
   │  ← "Authorization complete"      │   │  │  - holds client creds  │  │
   └──────────────────────────────────┘   │  │  - code exchange       │  │
                                          │  │  - token encryption    │  │
                                          │  │  - delivers via relay  │  │
               ┌──────────────────────┐  │  └────────────────────────┘  │
               │  GitHub / Slack / …  │◄─┤                              │
               │  (provider OAuth)    │  │  PORT 443 (WSS + HTTPS)      │
               └──────────────────────┘  └──────────────────────────────┘

Install & run

The fastest path — no clone, no Node setup beyond a recent Node:

npx dicode-relay
# or install globally
npm install -g dicode-relay && dicode-relay

Configuration is read from relay.yaml (or --config / $RELAY_CONFIG). With no file, the process falls back to process.env, so for a quick local run export BASE_URL + at least one provider's CLIENT_ID / CLIENT_SECRET and go.

From source

git clone https://github.com/dicode-ayo/dicode-relay
cd dicode-relay
cp .env.example .env
# Edit .env: set BASE_URL and at least one provider's CLIENT_ID/SECRET
npm install
npm run dev

Docker

docker pull dicodeayo/dicode-relay
docker run -p 5553:5553 --env-file .env dicodeayo/dicode-relay

Also mirrored at ghcr.io/dicode-ayo/dicode-relay if you prefer to pull from GitHub's registry.


Environment variable reference

| Variable | Required | Description | |---|---|---| | PORT | No | Port to listen on (default: 5553) | | BASE_URL | Yes | Public base URL, e.g. https://relay.dicode.app — used in relay welcome messages | | TLS_CERT_FILE | No | Path to PEM TLS certificate (skip if TLS terminated externally) | | TLS_KEY_FILE | No | Path to PEM TLS private key | | GITHUB_CLIENT_ID | Per-provider | GitHub OAuth app client ID | | GITHUB_CLIENT_SECRET | Per-provider | GitHub OAuth app client secret | | SLACK_CLIENT_ID | Per-provider | Slack OAuth app client ID (PKCE-only, no secret) | | GOOGLE_CLIENT_ID | Per-provider | Google OAuth app client ID | | GOOGLE_CLIENT_SECRET | Per-provider | Google OAuth app client secret | | SPOTIFY_CLIENT_ID | Per-provider | Spotify app client ID (PKCE-only) | | LINEAR_CLIENT_ID | Per-provider | Linear app client ID (PKCE-only) | | DISCORD_CLIENT_ID | Per-provider | Discord app client ID (PKCE-only) | | GITLAB_CLIENT_ID | Per-provider | GitLab app client ID | | GITLAB_CLIENT_SECRET | Per-provider | GitLab app client secret | | AIRTABLE_CLIENT_ID | Per-provider | Airtable app client ID | | AIRTABLE_CLIENT_SECRET | Per-provider | Airtable app client secret | | NOTION_CLIENT_ID | Per-provider | Notion integration client ID | | NOTION_CLIENT_SECRET | Per-provider | Notion integration client secret | | CONFLUENCE_CLIENT_ID | Per-provider | Atlassian app client ID (PKCE-only) | | SALESFORCE_CLIENT_ID | Per-provider | Salesforce connected app client ID (PKCE-only) | | STRIPE_CLIENT_ID | Per-provider | Stripe Connect platform client ID | | STRIPE_CLIENT_SECRET | Per-provider | Stripe Connect platform client secret | | OFFICE365_CLIENT_ID | Per-provider | Azure AD app client ID | | OFFICE365_CLIENT_SECRET | Per-provider | Azure AD app client secret | | AZURE_CLIENT_ID | Per-provider | Azure AD app client ID | | AZURE_CLIENT_SECRET | Per-provider | Azure AD app client secret |

See .env.example for registration links per provider.


Relay protocol reference

All WebSocket messages are JSON text frames.

Handshake

Server → Client:
  { "type": "challenge", "nonce": "<64 lowercase hex chars>" }

Client → Server:
  {
    "type":           "hello",
    "uuid":           "<64 lowercase hex>",   // hex(sha256(uncompressed_pubkey))
    "pubkey":         "<base64 std>",         // 65 bytes: 0x04 || X || Y — ECDSA signing key
    "decrypt_pubkey": "<base64 std>",         // 65 bytes: 0x04 || X || Y — ECIES recipient (OAuth token delivery)
    "sig":            "<base64 std>",         // ECDSA P-256 ASN.1 DER over sha256(nonce_bytes || timestamp_be_uint64)
    "timestamp":      <unix seconds integer>
  }

Server → Client (success):
  {
    "type":          "welcome",
    "url":           "wss://relay.dicode.app/u/<uuid>/hooks/",
    "protocol":      2,                        // broker advertises split sign/decrypt key support
    "broker_pubkey": "<base64 SPKI DER>"       // broker's delivery-signing key; daemons pin on first connect (TOFU)
  }

Server → Client (failure):
  { "type": "error", "message": "<reason>" }

Webhook forwarding

Server → Client (inbound request):
  {
    "type":    "request",
    "id":      "<uuidv4>",
    "method":  "POST",
    "path":    "/hooks/some-task",
    "headers": { "Content-Type": ["application/json"] },
    "body":    "<base64 encoded bytes>"
  }

Client → Server (response):
  {
    "type":    "response",
    "id":      "<same uuidv4>",
    "status":  200,
    "headers": { "Content-Type": ["application/json"] },
    "body":    "<base64 encoded bytes>"
  }

OAuth token delivery

When the broker completes a code exchange, it sends a request message to the daemon at path /hooks/oauth-complete:

{
  "type":    "request",
  "id":      "<uuidv4>",
  "method":  "POST",
  "path":    "/hooks/oauth-complete",
  "headers": { "Content-Type": ["application/json"] },
  "body":    "<base64 of OAuthTokenDeliveryPayload JSON>"
}

Where OAuthTokenDeliveryPayload is:

{
  "type":             "oauth_token_delivery",
  "session_id":       "<uuid>",
  "ephemeral_pubkey": "<base64, 65-byte uncompressed P-256>",
  "ciphertext":       "<base64, AES-256-GCM ciphertext + 16-byte auth tag>",
  "nonce":            "<base64, 12-byte GCM nonce>"
}

See docs/providers.md for the full ECIES decryption procedure.


Security model

  • ECDSA authentication: Every broker auth request is signed by the daemon's P-256 identity key. The broker verifies the signature against the public key registered in the relay client registry — no API key or shared secret required.
  • ECIES token encryption: Tokens are encrypted with the daemon's decrypt_pubkey (the ECIES-only half of the split sign/decrypt identity, sent on hello) before entering the relay code path. The relay server never sees plaintext tokens.
  • PKCE binding: The PKCE challenge is signed into the broker request and bound to the session. The verifier stays on the daemon and is never transmitted.
  • Single-use sessions: Sessions are deleted immediately after the token is delivered. Replay attacks require re-running the full OAuth flow.
  • Timestamp + nonce replay prevention: Auth requests must be within ±30 s of server time. Relay handshake nonces are tracked for 60 s.

See the OAuth broker design document in the dicode-core repository for the full threat model.


Deployment

Docker (recommended)

docker run -d \
  -p 5553:5553 \
  -e BASE_URL=https://relay.dicode.app \
  -e GITHUB_CLIENT_ID=xxx \
  -e GITHUB_CLIENT_SECRET=yyy \
  dicodeayo/dicode-relay

Also available at ghcr.io/dicode-ayo/dicode-relay if you prefer GitHub's registry.

Cloudflare

Point a Cloudflare-proxied A record at your server. Enable "WebSocket" under the Cloudflare Network settings for the domain. Cloudflare terminates TLS; the service listens on plain HTTP (omit TLS_CERT_FILE/TLS_KEY_FILE).

Enable Session Affinity in the Cloudflare load balancer if you run multiple instances — sessions are stored in-process.

Self-host (nginx)

server {
    listen 443 ssl;
    server_name relay.dicode.app;

    location / {
        proxy_pass http://127.0.0.1:5553;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Client library

This package also publishes a TypeScript/Web-Crypto client library at dicode-relay/client, used by dicode-core's built-in tasks to maintain the WSS tunnel and run OAuth flows. The library is pure protocol + crypto — consumers own all persistence. Example:

import { RelayClient, Identity, type StoredIdentity } from "dicode-relay/client";

// Consumer owns persistence — example using a hypothetical KV.
const stored = await myKv.get<StoredIdentity>("identity");
const identity = stored
  ? await Identity.import(stored)
  : await (async () => {
      const id = await Identity.generate();
      // StoredIdentity contains PRIVATE key material — treat it like a TLS
      // private key. Use encrypted storage (e.g. dicode.kv with the daemon's
      // secret-store backing).
      await myKv.set("identity", await id.export());
      return id;
    })();

const tofuCheckAndPin = async (brokerPubkeyB64: string) => {
  const pinned = await myKv.get<string>("broker_pubkey");
  if (pinned === null) {
    await myKv.set("broker_pubkey", brokerPubkeyB64);
    return "new" as const;
  }
  return pinned === brokerPubkeyB64 ? "match" as const : "mismatch" as const;
};

const client = new RelayClient({
  serverURL: "wss://relay.example/",
  localPort: 8080,
  identity,
  tofuCheckAndPin,
  log: console,
  onStatus: (s) => console.log("status:", s),
});

await client.run();

The client targets Node.js 22+ and Deno (both expose node:crypto). It is not browser-compatible — node:crypto primitives are used for HKDF, AES-GCM decrypt, and broker signature verification. In dicode tasks, use dicode.kv from the SDK to persist the StoredIdentity blob and the pinned broker pubkey.


Contributing

npm install
npm run typecheck   # tsc --noEmit
npm run lint        # eslint
npm run format:check
npm run test        # vitest
npm run test:coverage  # must pass 90% threshold
npm run build       # tsc

All checks must pass before opening a PR.