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

@quantiya/quorum-core

v1.0.0

Published

Quorum orchestration client SDK — typed TypeScript client for the AppSync-hosted orchestration engine. Drives multi-agent peer review, revision loops, and tamper-evident audit logging.

Downloads

49

Readme

@quantiya/quorum-core

Typed TypeScript client for the Quorum orchestration engine — an AppSync-hosted, multi-agent peer-review service that drives the two-gate review flow: primary agent proposes, peer reviewers vote, consensus resolves or escalates to the user, with a tamper-evident audit log of every decision.

Homepage: quorum.quantiya.ai

Status: prerelease. 1.0.0 is the first npm publish (no 2e.4 source ever shipped to the registry). The API below is what the 1.0.0 tarball will expose.


Requirements

  • Node 18+ — uses globalThis.fetch and globalThis.crypto.subtle, both of which Node 18+ ships natively. Subscriptions need a WebSocket implementation. Node 22+ and modern browsers expose globalThis.WebSocket natively; Node 18/20 do NOT ship a stable global WebSocket, so callers on those runtimes must pass webSocketFactory in ConnectParams. The factory MUST forward the protocols argument — AppSync's realtime endpoint requires the graphql-ws subprotocol, which the SDK passes via protocols; dropping it breaks the subscription handshake. Example: (url, protocols) => new (require("ws"))(url, protocols). Without a factory, client.onGateResolved(...) throws a typed EngineEvalError with payload.kind === "client_error" pointing at webSocketFactory. No third-party crypto, HTTP, or GraphQL deps; no AWS SDK, no Amplify.
  • An AppSync HTTPS endpoint URL (the 2.0 backend's GraphQLApiUrl CloudFormation output) and a Cognito ID token (JWT) for the signed-in user. Token refresh is the caller's responsibility.
  • A pre-derived 32-byte AES-256-GCM session key for the session this client is bound to. The session key is decrypted from Session.encryptedKeys[deviceId] using the caller's ECDH private key — that pipeline already lives in @quantiya/codevibe-core under src/crypto/.

The SDK is a thin client. It does not spawn reviewer subprocesses, manage device keys, or own session lifecycle — those are @quantiya/codevibe-core's job. Reviewer fan-out happens in-process on the caller side via @quantiya/codevibe-core/reviewer (see Reviewer dispatch below).


Quick start

import { randomUUID } from "node:crypto";
import {
  OrchestrationClient,
  type ResolvedGateEvent,
} from "@quantiya/quorum-core";

const client = await OrchestrationClient.connect({
  appsyncUrl: "https://abc.appsync-api.us-east-1.amazonaws.com/graphql",
  cognitoIdToken: cognitoSession.idToken,
  tokenProvider: async () => (await cognitoSession.refresh()).idToken,
  sessionId,                     // Session.sessionId
  sessionKeyB64,                 // 32-byte AES key, base64-encoded
});

try {
  // Step 1: prepare. Same idempotencyKey on retries — the SDK
  // auto-retries transient network errors (§7.4).
  const prepared = await client.prepareProposalGate({
    rawAgentOutput: agentResponse,
    taskId,
    roundNumber: 0,
    triggeredBy: "initial",
    prepareIdempotencyKey: randomUUID(),
  });

  if (prepared.kind === "resolved") {
    // Parser fast-path — outcome already in hand, no fan-out needed.
    handleOutcome(prepared.outcome);
    return;
  }

  // Step 2: caller dispatches reviewers in parallel. Use
  // `@quantiya/codevibe-core/reviewer` to spawn the real CLIs;
  // see Appendix B of the design for the FanOutRequest →
  // ReviewerSpec adapter.
  const verdicts = await Promise.all(
    prepared.fanOutRequests.map((req) => dispatchReviewer(req, prepared.gateId)),
  );

  // Step 3: submit each verdict. The mutation response is the
  // AUTHORITATIVE outcome signal — onGateResolved is supplementary.
  const responses = await Promise.all(
    verdicts.map((v) =>
      client.submitReviewerVerdict({
        gateId: prepared.gateId,
        seatId: v.seat_id,
        verdict: v,
      }),
    ),
  );

  // ONE OR MORE responses may carry kind: "gate1Resolved" — concurrent
  // submitters can all land in the resolved branch via the lost-race
  // idempotent path (§5.3 / Appendix B).
  const resolved = responses.find(
    (r) => r.kind === "gate1Resolved" || r.kind === "gate2Resolved",
  );
  if (resolved && resolved.kind === "gate1Resolved") {
    handleOutcome(resolved.outcome);
  }
} finally {
  await client.destroy();
}

A complete worked example — including the reviewer-dispatch adapter, parallel-submit dedupe, and the optional observer subscription — is the Appendix B sample in codevibe-backend/docs/2.0/PHASE-2F-5-DESIGN.md.


OrchestrationClient.connect(params)

connect() is async, but does no network I/O — the client is constructed synchronously and the returned Promise resolves on the next microtask. Network-resolved failures (wrong URL, wrong region, expired JWT) surface on the first real call, not at connect time, so plugin startup at scale won't bottleneck on a mandatory health-ping.

A malformed or non-32-byte sessionKeyB64 rejects the connect() Promise before any network I/O with a typed EngineEvalError whose payload.kind === "encryption_failed"connect() returns a Promise (it's async), so the failure is a rejection rather than a synchronous throw, but it's still deterministic and immediate. The SDK can't sensibly defer the check, since every method needs the key. Catch this if sessionKeyB64 is sourced from caller-controlled state.

Note on base64 validity: under Node, Buffer.from(b64, "base64") is permissive — it may silently accept non-canonical input as long as the decoded byte length is correct. So the trigger for the rejection is the decoded byte length not equaling 32, not strict base64 well-formedness; pass canonical base64 if you need strict validation, or run a pre-check in caller code.

interface ConnectParams {
  /** AppSync HTTPS endpoint URL (the GraphQLApiUrl output). */
  appsyncUrl: string;

  /**
   * Cognito ID token (JWT). Caller refreshes; the SDK passes the
   * current value as `Authorization` on every request.
   */
  cognitoIdToken: string;

  /**
   * Optional: function returning the current ID token. Called per
   * request so a long-lived client survives token rotation. If
   * omitted, `cognitoIdToken` is used statically.
   */
  tokenProvider?: () => Promise<string>;

  /** Session this client is bound to. One client = one session. */
  sessionId: string; // UUID

  /**
   * 32-byte AES-256-GCM key for the session, base64-encoded. Held
   * in a `SessionKeyHandle` opaque wrapper internally; zero-filled
   * (best-effort) on `destroy()`.
   */
  sessionKeyB64: string;

  /**
   * Optional key version (matches `EncryptedPayload.keyVersion`).
   * Bumped when the caller rotates session keys. Absent → Lambda
   * treats as v1.
   */
  keyVersion?: number;

  /**
   * Optional WebSocket factory. Default uses `globalThis.WebSocket`.
   * Pass a custom factory in environments without one (e.g. older
   * Node bundles before the global landed).
   */
  webSocketFactory?: (url: string, protocols?: string[]) => WebSocket;
}

Optional health probe

If you do want a connect-time round-trip:

await client.ping();
// Internally: queryAudit on a sentinel taskId, swallowing
// task_not_found. Throws on auth or routing failures.

Cleanup

await client.destroy();
// Closes the subscription WebSocket, drops the bound session key,
// zero-fills the in-memory key buffer (best-effort — see §4.4 of
// the design on JS GC limits). Subsequent calls throw
// `EngineEvalError.client_destroyed`.

API surface

Every method takes a typed parameter object and returns a typed result or discriminated union. There are no JSON strings exposed at the surface — the SDK transforms wire shapes into TypeScript-ergonomic form on the way out.

| Method | Purpose | |---|---| | prepareProposalGate(params) | Open Gate 1. Returns either a Resolved fast-path outcome (parser short-circuit) or a Pending handle plus the fan-out requests. Encrypts rawAgentOutput. | | prepareCompletionGate(params) | Open Gate 2. Encrypts proposal, artifact, optional progressEvents. | | submitReviewerVerdict(params) | Submit a single seat's verdict. Encrypts verdict. Response is gate1Resolved / gate2Resolved / acknowledged. | | applyUserDecision(params) | Dispatch the user's choice at an escalated gate. Encrypts optional notes. | | recordExecutionEvent(params) | Append a host-emitted audit entry (progress_event, tool_use, etc.). Encrypts payload. | | flushAudit() | Force-flush buffered audit entries server-side. Takes no arguments — the session is bound at connect() time. | | queryAudit(taskId) | Read every audit entry for a task. Payloads remain ciphertext — see Decrypting audit payloads. | | onGateResolved(callback) | Subscribe to gate1Resolved / gate2Resolved events on this session. SDK filters out kind: "acknowledged" server pings before your callback fires. Returns an unsubscribe(). |

Method-by-method wire mapping, error semantics, and idempotency rules are in §5 / §7 of the design doc.


Reviewer dispatch

The SDK does not spawn reviewer subprocesses. After a successful prepareProposalGate / prepareCompletionGate returns a Pending handle, the caller dispatches each FanOutRequestWire itself — typically through @quantiya/codevibe-core/reviewer's createSubprocessReviewerRegistry().

The Lambda's Rust adapter wraps each FanOutRequest into a ReviewerSpec with locked defaults; the TS SDK caller MUST match those defaults so backend audit / gate-policy guarantees hold:

// Match codevibe-engine/src/orchestration.rs:581,587
const REVIEWER_TOOL_ALLOWLIST = ["Read", "Grep", "Glob"] as const;
const REVIEWER_TIMEOUT_MS = 30_000;

A drift here (relaxed allowlist, longer timeout) silently breaks the read-only-reviewer invariant or the gate timeout budget. The full adapter — including the model_hint passthrough and the seat_id / role / agent field mapping — is the fanOutRequestToReviewerSpec helper in Appendix B of the design.


Per-session E2E

Seven request fields are encrypted client-side before they reach the wire (rawAgentOutput, proposal, artifact, progressEvents, verdict, notes, payload). The SDK encrypts with AES-256-GCM under the bound session key, using a fresh random nonce per call, and emits the inline-nonce envelope:

ciphertextB64 = base64( nonce(12) || ciphertext(N) || authTag(16) )
nonceB64      absent

This is byte-for-byte the format produced by @quantiya/codevibe-core/src/crypto/crypto-service.ts:encryptString and accepted by the Lambda's crypto::decrypt_payload. A cross-impl parity gate (§11.2 of the design) keeps Rust and TS aligned.

Per-field size caps

| Field | Max ciphertextB64 bytes | |---|---| | rawAgentOutput | 180 KB | | proposal | 100 KB | | artifact | 180 KB | | progressEvents | 100 KB | | verdict | 50 KB | | notes | 16 KB | | payload | 50 KB |

The SDK enforces these post-encryption — over-cap inputs throw EngineEvalError.payload_too_large locally before any network call. The exact bytes ship as CIPHERTEXT_B64_MAX_BYTES for reference.

SessionKeyHandle

Wraps the 32-byte session key in an opaque object. JS has no RAII / Drop — once a Uint8Array is GC'd, clearing is a runtime detail, and any base64 round-trip leaves a copy in the V8 string heap with no zeroable handle. SessionKeyHandle exposes an explicit destroy() that zero-fills the underlying bytes immediately, on a best-effort basis. client.destroy() calls it for you.


Decrypting audit payloads

queryAudit returns entries with the encrypted payload field intact — payloads are session-bound ciphertext written by the engine's audit emit paths. To read them back:

import { decryptAuditPayload, type SessionKeyHandle } from "@quantiya/quorum-core";

const entries = await client.queryAudit(taskId);
for (const entry of entries) {
  if (entry.payload?.ciphertextB64) {
    const plaintext = await decryptAuditPayload(entry, sessionKey);
    // plaintext is the JSON string the engine emitted
  }
}

decryptAuditPayload accepts both inline-nonce and detached-nonce envelopes, since the audit emit path predates the 2f.5 inline-only lock and may have written either shape.


Audit-key helpers (separate package)

recordExecutionEvent requires a 64-char lowercase hex dedupKeyHex so Lambda retries / DLQ replays don't produce duplicate audit entries (Phase 2f.2's locked dedup-key taxonomy). Computing the right hash for each kind is non-trivial — the formulas are in §5.2 of the Phase 2f.2 design.

To avoid drift, the helpers ship in @quantiya/codevibe-core, not here:

import { AuditKeys } from "@quantiya/codevibe-core";

await client.recordExecutionEvent({
  taskId,
  kind: "progress_event",
  payload: { step: "build", status: "started" },
  dedupKeyHex: AuditKeys.dedupKeyForProgressEvent(taskId, "evt-1"),
});

@quantiya/codevibe-core exposes the helpers as the AuditKeys namespace from the package's main entry — no subpath import needed, matching the existing Reviewer namespace pattern.

A parity test in @quantiya/codevibe-core pins the helpers' output byte-for-byte against the Rust codevibe-engine::audit_keys::* fixtures — drift on either side breaks both sides at CI.


Errors

All SDK errors derive from QuorumError. Two concrete subclasses match the wire taxonomy:

  • EngineInitError — reserved for connect() / ping() failures whose .payload.kind is one of wrong_endpoint, auth, or network. The lock #3 SDK does no network I/O at connect() time, so today this is only thrown by ping(); first-call failures surface as EngineEvalError.
  • EngineEvalError — thrown from every other method. .payload.kind narrows to one of the documented buckets:
    • client_error — bad input (UUID shape, enum value, JSON-stringify failure on caller-supplied verdict / payload, missing webSocketFactory on Node 18/20, etc.)
    • payload_too_large — local preflight or server-side rejection; enriched with field / sizeBytes / maxBytes
    • unsupported_key_version — caller's keyVersion ahead of what the Lambda accepts
    • unauthorized — auth header rejected
    • tier_not_permitted — caller's tier disallows this gate
    • session_orchestration_disabled — session toggle is off
    • identity_rejected — caller is not the session owner
    • session_error — session not in a valid state
    • crypto_error — server-side decrypt failed
    • plaintext_not_utf8 / plaintext_json_invalid — server-side plaintext-validation failure
    • audit_error / engine_error / storage_error / internal — server-side failures
    • resource_not_found / task_not_found / gate_not_found
    • not_implemented
    • SDK-synthesized: network (with transient: true|false), auth (with reason: "expired" | "missing" | "rejected"), encryption_failed, client_destroyed

Discriminator details live on err.payload (the kind tag is at err.payload.kind; enriched fields like field / sizeBytes / maxBytes on payload_too_large, reason on auth, and transient on network are also on err.payload). Switch on err.payload.kind for exhaustive handling. Server-emitted envelopes are normalized through envelopeToErrorKind / envelopeToEvalError; you generally don't need to call them directly.

import { EngineEvalError } from "@quantiya/quorum-core";

try {
  await client.prepareProposalGate({ ... });
} catch (err) {
  if (err instanceof EngineEvalError) {
    switch (err.payload.kind) {
      case "payload_too_large":
        console.error(
          `field ${err.payload.field} too large: ` +
            `${err.payload.sizeBytes} > ${err.payload.maxBytes}`,
        );
        break;
      case "auth":
        if (err.payload.reason === "expired") await refreshAndRetry();
        break;
      case "network":
        if (err.payload.transient) await backoffAndRetry();
        else throw err;
        break;
      // …
    }
  } else {
    throw err;
  }
}

The SDK auto-retries network errors with transient: true for the five idempotency-keyed methods (prepareProposalGate, prepareCompletionGate, submitReviewerVerdict, applyUserDecision, recordExecutionEvent). Three attempts with 500ms / 2s / 8s backoff. All other errors surface immediately.


Subscriptions

const unsubscribe = await client.onGateResolved((event: ResolvedGateEvent) => {
  // event.outcome is the same Gate1Outcome / Gate2Outcome you'd get
  // back from a submit response.
  console.log("observed", event.gateId, event.outcome);
});

// Later:
await unsubscribe();

The subscription connects via graphql-ws over WebSocket against the AppSync realtime endpoint. Two-phase reconnect (urgent exponential backoff for 10 attempts, then persistent 5-minute retry indefinitely) — same shape as @quantiya/codevibe-core's AppSyncClient.

kind: "acknowledged" server pings are filtered out before your callback fires; you only see real gate resolutions.

The mutation-response path is authoritative for your own submission's outcome. Use onGateResolved as an observer for UI that watches gate resolutions across multiple sessions or out-of-band state changes — never as the primary outcome signal for your own submit.


Types

Full TypeScript declarations ship with the package — every parameter object, return value, discriminated union, and enum literal is typed. dist/types.d.ts is the authoritative reference.

Key types to know:

  • ConnectParams — what connect() takes
  • PrepareProposalGateOutput / PrepareCompletionGateOutput — discriminated unions on kind: "resolved" | "pending"
  • Gate1Outcome / Gate2Outcome — evaluation results
  • GateDecision / EscalateReason / PostDecisionAction — discriminated unions for caller logic
  • SubmitReviewerVerdictPayloadgate1Resolved / gate2Resolved / acknowledged
  • ResolvedGateEvent — what the subscription callback receives
  • FanOutRequestWire — engine-snake_case shape passed back from prepare* for caller-side dispatch
  • RawAuditEntryWire — what queryAudit returns

Every discriminated union uses kind as its tag.


Development

cd packages/quorum-core
npm install
npm run build      # rm -rf dist && tsc
npm test           # vitest — wire-format + contract layers

Three test layers:

  • Layer 1__tests__/wire-format.test.ts: encrypt-then-decrypt parity, error-envelope detection, every method's params shape. Pure offline.
  • Layer 2__tests__/contract.test.ts: mock-fetch + mock-WebSocket. Exercises happy-path, retry, every error mapping in §7.3, the acknowledged-filter, idempotency-key passthrough, and the destroy lifecycle.
  • Layer 3integration/smoke.test.ts: opt-in. Hits the dev-stack endpoint with a service-account Cognito user. Skipped unless RUN_INTEGRATION=1 and the four QUORUM_DEV_* env vars are set.
RUN_INTEGRATION=1 \
QUORUM_DEV_APPSYNC_URL=... \
QUORUM_DEV_COGNITO_TOKEN=... \
QUORUM_DEV_SESSION_ID=... \
QUORUM_DEV_SESSION_KEY_B64=... \
npm test -- integration/

License

MIT

Support

[email protected] · quorum.quantiya.ai