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

@medispeak/scribe-ts-sdk

v0.3.0

Published

Medispeak-native browser scribe SDK: record a consultation and auto-fill a form, without ever holding an account secret in the browser.

Readme

@medispeak/scribe-ts-sdk

The Medispeak-native browser scribe SDK. Record a consultation in the browser, stream it to Medispeak, and get back a transcript and/or an auto-filled form — without ever holding an account secret in the browser.

The SDK talks only to Medispeak's documented v2 API. Its public types are Medispeak-native (sessions, outputs, fields) and framework-agnostic; a thin React hook is provided as a separate, optional entry point.

  • Zero secrets in the browser. The browser authenticates with a short-lived, single-session token minted by your backend from its account key.
  • Resumable capture. Audio is chunked (~5 s segments) and uploaded as it is recorded; on a network drop the SDK consults the server and re-sends only the missing chunks.
  • Runtime forms. Define the fields you want extracted at record time; results come back keyed by your own field keys. No pre-created templates required.
  • Three entry points, tree-shakeable, shipped as ESM + CJS with types.

Install

npm install @medispeak/scribe-ts-sdk

react is an optional peer dependency — it is only needed if you import the @medispeak/scribe-ts-sdk/react entry point. The core and server entry points have no runtime dependencies.

| Import path | Runs in | Purpose | | --- | --- | --- | | @medispeak/scribe-ts-sdk | Browser | Record, upload, commit, poll for results. | | @medispeak/scribe-ts-sdk/server | Your Node backend | Create sessions + mint scoped tokens with the account secret. | | @medispeak/scribe-ts-sdk/react | Browser (React) | useScribe hook wrapping the browser client. |


The token flow (why there are two clients)

The account secret (msk_live_…) must never reach a browser. Instead your backend creates the session and mints a short-lived, session-scoped token that can only upload to and read that one session. The browser SDK holds only that token.

  ┌──────────────────────────┐                         ┌──────────────────────────┐
  │      Your app backend     │                         │     Browser (the SDK)     │
  │   holds msk_live_ secret   │                         │  holds only a scoped token │
  └────────────┬─────────────┘                         └─────────────┬────────────┘
               │                                                       │
   createSession(outputs)                                             │
   POST /scribe_sessions  ──────────────► Medispeak v2 ──► 201 { id } │
               │                                                       │
   mintSessionToken(id)                                               │
   POST /scribe_sessions/:id/tokens ────► Medispeak v2 ──► { token }  │
               │                                                       │
               │   hand { id, token } to the browser ────────────────►│
               │                                                       │
               │                        record()  ─ Bearer token ─►  POST /:id/audio/chunks
               │                        stop()     ─ Bearer token ─►  POST /:id/commit
               │                        result()   ─ Bearer token ─►  GET  /:id  (poll)
               │                                                       │
               │◄──── optional HMAC webhook (callbackUrl) ────────────┤
  1. Backend creates the session with the outputs you want (including any inline form fields) using its account key, then mints a scoped token.
  2. Backend hands { id, token } to the browser however it likes (page render, JSON endpoint, etc.). The browser's getToken provider returns that token — and can re-fetch a fresh one, which the SDK calls automatically on a 401 (scoped tokens are short-lived; a long consultation may outlast the TTL).
  3. Browser records, uploads, commits, and polls — all scoped to that session.

Server usage (@medispeak/scribe-ts-sdk/server)

Runs on your backend. Uses the account secret. Never bundle this into browser code.

import { createServerClient } from "@medispeak/scribe-ts-sdk/server";
import type { FieldSpec } from "@medispeak/scribe-ts-sdk/server";

const server = createServerClient({
  baseUrl: "https://api.medispeak.example/api/v2",
  apiKey: process.env.MEDISPEAK_API_KEY!, // msk_live_…
});

const fields: FieldSpec[] = [
  { key: "heart_rate", label: "Heart Rate", type: "number", description: "beats per minute", minimum: 0, maximum: 300 },
  { key: "on_insulin", label: "On insulin", type: "boolean" },
  { key: "severity",   label: "Severity",   type: "single_select", enum: ["mild", "moderate", "severe"] },
  { key: "symptoms",   label: "Symptoms",   type: "multi_select",  enum: ["fever", "cough", "fatigue"] },
];

// 1) Create the session (returns { id, status, expiresAt })
const session = await server.createSession({
  outputs: [{ type: "form", fields }, { type: "transcript" }],
  language: ["auto"],
  mode: "consultation",
  callbackUrl: "https://app.example/webhooks/scribe", // optional HMAC webhook
});

// 2) Mint a scoped token for the browser (returns { token, expiresAt })
const { token } = await server.mintSessionToken(session.id);

// 3) Send { id: session.id, token } to the browser.

Output types

| OutputSpec | Result | | --- | --- | | { type: "transcript" } | Verbatim transcript. | | { type: "note", templateRef? } | Narrative note, optionally from a stored template. | | { type: "form", fields } | Structured extraction against the inline fields you define. |

Field types (FieldType): string, number, boolean, single_select (needs enum), multi_select (needs enum). minimum/maximum apply to number.


Browser usage (@medispeak/scribe-ts-sdk)

import { createScribeClient } from "@medispeak/scribe-ts-sdk";

const client = createScribeClient({
  baseUrl: "https://api.medispeak.example/api/v2",
  // Return the scoped token for this session from your backend.
  // Called lazily, cached, and re-invoked once on a 401 to refresh.
  getToken: async (sessionId) => {
    const res = await fetch(`/api/scribe/${sessionId}/token`);
    const { token } = await res.json();
    return token;
  },
});

const session = client.session(sessionIdFromBackend);

// Live status + partial transcript subscriptions (both return unsubscribers).
const off = session.onStatusChange((s) => console.log("status:", s));
session.onPartialTranscript((text) => console.log("partial:", text));

await session.record();          // asks for the mic, streams ~5 s chunks
// session.pause(); session.resume();
await session.stop();            // flush the final chunk, then commit

const result = await session.result(); // polls until a terminal status
console.log(result.status);             // "completed" | "partial" | "failed"
console.log(result.transcript);         // string | undefined
console.log(result.structuredData);     // { heart_rate: 72, severity: "mild", ... }

off();

createScribeClient(config)

| Option | Type | Notes | | --- | --- | --- | | baseUrl | string | The v2 API root, e.g. https://…/api/v2. | | getToken | (sessionId) => string \| Promise<string> | Returns the scoped token. | | fetch | typeof fetch | Optional; defaults to the global fetch. |

ScribeSession

| Member | Description | | --- | --- | | id | The session id. | | record(opts?) | Start mic capture + chunk streaming. opts.chunkMs (default 5000). Resumes automatically if the session already has chunks. | | pause() / resume() | Pause/resume capture. In-flight uploads continue. | | stop() | Finalize the upload, then commit the session for processing. | | cancel() | Stop capture and release the mic without committing. | | result(opts?) | Poll GET /:id until a terminal status. opts.pollIntervalMs (default 2000), opts.timeoutMs (default 120000). Resolves to a ScribeResult. | | onPartialTranscript(cb) | Subscribe to partial transcript text. Returns an unsubscribe fn. | | onStatusChange(cb) | Subscribe to ScribeStatus changes. Returns an unsubscribe fn. |

ScribeStatus: idle → recording ⇄ paused → processing → completed | failed.

Result mapping

result() maps Medispeak's raw outputs[] into a friendly shape:

  • transcript — the transcript output's result.text (falling back to any output whose result carries a text).
  • structuredData — a shallow merge of every form output's result object (each already { key: value }), so multiple forms combine into one map.
  • outputs — the raw per-output results are always available for advanced use.

React usage (@medispeak/scribe-ts-sdk/react)

import { createScribeClient } from "@medispeak/scribe-ts-sdk";
import { useScribe } from "@medispeak/scribe-ts-sdk/react";

const client = createScribeClient({ baseUrl, getToken });

function ScribeButton({ sessionId }: { sessionId: string }) {
  const scribe = useScribe({
    client,
    sessionId,
    onResult: (r) => console.log("done", r.structuredData),
    onError: (e) => console.error(e),
  });

  return (
    <div>
      <p>Status: {scribe.status} · {scribe.duration}s</p>
      {!scribe.isRecording && !scribe.isPaused && (
        <button onClick={scribe.start}>Record</button>
      )}
      {scribe.isRecording && <button onClick={scribe.pause}>Pause</button>}
      {scribe.isPaused && <button onClick={scribe.resume}>Resume</button>}
      {(scribe.isRecording || scribe.isPaused) && (
        <button onClick={scribe.stop}>Stop</button>
      )}
      {scribe.error && <p role="alert">{scribe.error}</p>}
      {scribe.result?.transcript && <pre>{scribe.result.transcript}</pre>}
    </div>
  );
}

useScribe(options) returns { status, isRecording, isPaused, duration, result, error, start, pause, resume, stop, cancel }. stop() commits and resolves the result (calling onResult). duration counts seconds while recording and freezes on pause.


Errors

All HTTP and SDK failures throw a ScribeError (exported from . and ./server) with optional status and parsed body:

import { ScribeError } from "@medispeak/scribe-ts-sdk";

try {
  await session.stop();
} catch (e) {
  if (e instanceof ScribeError) console.error(e.status, e.body);
}

Wire protocol

The SDK targets Medispeak's v2 routes:

| Call | Route | Auth | | --- | --- | --- | | server.createSession | POST /scribe_sessions | Bearer msk_live_… | | server.mintSessionToken | POST /scribe_sessions/:id/tokens | Bearer msk_live_… | | record() chunk | POST /scribe_sessions/:id/audio/chunks (multipart: seq, chunk, optional final) | Bearer <session token> | | record() resume | GET /scribe_sessions/:id/audio/status | Bearer <session token> | | stop() | POST /scribe_sessions/:id/commit | Bearer <session token> | | result() | GET /scribe_sessions/:id (202 processing · 200 completed · 206 partial) | Bearer <session token> |


Development

npm install
npm run build       # tsup → ESM + CJS + .d.ts for all three entry points
npm test            # vitest (jsdom), mocked fetch + MediaRecorder
npm run typecheck   # tsc --noEmit (strict)

Tests mock fetch (injected via config) and the microphone (an injectable recorder factory, since jsdom has no MediaRecorder).

License

MIT