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

@unseal-ai/unseal-space-sdk

v0.2.0

Published

Typed SDK for creating, iterating on, previewing, and publishing unseal-space projects.

Readme

@unseal-ai/unseal-space-sdk

Typed TypeScript SDK for unseal-space — create a website project, iterate on it with an AI agent, inspect its live preview, manage its environment variables and version history, publish it, and attach a domain. Everything an External Agent or your own backend needs, in one client.

The SDK is a thin, fully typed adapter over the public unseal-space API. Every method mirrors the API contract 1:1 and returns plain DTOs — no wrapper objects, no hidden state.

npm install @unseal-ai/unseal-space-sdk
pnpm add @unseal-ai/unseal-space-sdk
yarn add @unseal-ai/unseal-space-sdk
bun add @unseal-ai/unseal-space-sdk

Requirements — ESM only (import, no require). Any runtime with a global fetch and async iterators: Node.js 18+, Bun, Deno, or an edge runtime. Pass your own fetch if the global one is unsuitable.

Authentication

Create an API key on the API Keys page of the unseal-space dashboard, choosing a permission preset or explicit permissions. The key is shown once — store it as a secret. The SDK sends it on every request; you never construct headers yourself.

import { createUnsealSpaceClient } from "@unseal-ai/unseal-space-sdk";

const client = createUnsealSpaceClient({
  baseUrl: "https://api.unseal.space",
  apiKey: process.env.UNSEAL_SPACE_API_KEY!,
});

| Option | Type | Required | Notes | | --------- | -------------- | -------- | ----------------------------------------------------------------- | | baseUrl | string | yes | Absolute http(s) origin. Production: https://api.unseal.space | | apiKey | string | yes | Workspace API key | | fetch | typeof fetch | no | Custom fetch (proxy, retry, instrumentation, tests) |

Keys are scoped to one workspace, and every call is authorized against the key's permissions — see Permissions.

Quick start

Create a project, drive one agent turn, then publish it:

import { createUnsealSpaceClient } from "@unseal-ai/unseal-space-sdk";

const client = createUnsealSpaceClient({
  baseUrl: "https://api.unseal.space",
  apiKey: process.env.UNSEAL_SPACE_API_KEY!,
});

// 1. Create a project. The initial prompt starts the first turn for you.
const project = await client.projects.create({
  initialPrompt: "Build a landing page for a specialty coffee roaster",
});

// 2. Wait for that turn to reach a terminal state.
if (project.initialMessageId) {
  const turn = await client.messages.wait(project.projectId, project.initialMessageId);
  if (turn.status !== "completed") throw new Error(turn.error?.message ?? turn.status);
}

// 3. Look at the live preview.
const preview = await client.preview.get(project.projectId);
console.log(preview.status, preview.url); // "ready" https://…

// 4. Iterate.
const message = await client.messages.send(project.projectId, {
  content: "Make the hero image full-bleed and add a newsletter form",
});
await client.messages.wait(project.projectId, message.messageId);

// 5. Publish to production and wait for the deployment.
const accepted = await client.deployments.create(project.projectId);
const deployment = await client.deployments.wait(project.projectId, accepted.deploymentId);
console.log(deployment.status, deployment.url);

Every long-running operation follows the same accept → wait shape: the mutation returns an id immediately, and a wait() helper polls or streams to a terminal state. Persist the id — a lost wait() never cancels the remote work, so you can always resume by id.

API

projects

await client.projects.create({ initialPrompt: "…", idempotencyKey: "…" }); // both optional
await client.projects.list({ filter: "starred" }); // "all" | "starred" | "created" | "shared"
await client.projects.get(projectId);

create returns the project plus initialMessageId when initialPrompt was given. A project created from a marketplace template starts with provisioningState: "provisioning" — poll projects.get until it flips to "ready" (or "failed", with provisioningError) before sending messages.

preview

await client.preview.get(projectId); // → { url, status }

status is none | warming | ready | error. A warming preview means the sandbox dev server is still starting; poll until ready.

messages — the agent turn

One turn per project at a time; a second send while one is in flight fails with MESSAGE_IN_FLIGHT (the busy messageId is in error.data).

const { messageId } = await client.messages.send(projectId, {
  content: "Add a pricing section",
  // all optional:
  model: "…", // from the platform's model allowlist
  attachmentIds: [attachmentId], // see attachments
  skillNames: ["…"],
  idempotencyKey: crypto.randomUUID(), // replay guard
});

Wait for the terminal state (the common case):

const result = await client.messages.wait(projectId, messageId, {
  timeoutMs: 10 * 60 * 1000, // default 10 minutes
  maxReconnectAttempts: 5, // default 5
  cursor: savedCursor, // resume after a previous run
  onEvent: ({ chunk, cursor }) => console.log(chunk.type),
});

result.status; // "completed" | "failed" | "interrupted"
result.version; // the Version this turn produced, when completed
result.error; // { code, status, message } when failed
result.cursor; // persist this to resume later

wait streams the event feed, reconnects with exponential backoff on transient failures, and resumes from the last cursor — so a dropped connection does not lose the turn. It throws SDK_TIMEOUT when the local deadline elapses; the remote turn keeps running, so resume with the same messageId and cursor.

Stream the events yourself when you want to render progress. The chunks are AI SDK v5 UI Message Stream chunks (text, reasoning, dynamic tool calls, data-* parts), so they fold directly into a UIMessage:

for await (const { chunk, cursor } of client.messages.events(projectId, messageId, { cursor })) {
  if (chunk.type === "text-delta") process.stdout.write(String(chunk.delta));
  if (chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort") break;
}

attachments

Two steps: reserve a slot (returns a direct upload URL), then PUT the bytes. Pass the returned attachmentId to messages.send.

const bytes = await readFile("hero.png");
const reservation = await client.attachments.reserve(projectId, {
  filename: "hero.png",
  contentType: "image/png", // png | jpeg | webp | gif
  size: bytes.byteLength,
});
await client.attachments.upload(reservation, bytes);

await client.messages.send(projectId, {
  content: "Use this as the hero image",
  attachmentIds: [reservation.attachmentId],
});

The byte length must match the reserved size exactly — a mismatch throws SDK_INVALID_ARGUMENT before any upload happens.

environment — write-only values

Manages the project's environment variables (database URLs, third-party API keys). Values are write-only: you can create, replace, or delete a value, but the platform never returns one. Every response carries only metadata (name, kind, targets, version) plus runtime freshness.

const env = await client.environment.get(projectId);

await client.environment.create(projectId, {
  name: "DATABASE_URL",
  value: process.env.DATABASE_URL!, // write-only: cannot be read back later
  kind: "secret", // "secret" | "plain"
  preview: true,
  production: true,
});

// Mutations take the variable id + optimistic-lock version from a fresh get().
const variable = env.variables.find((v) => v.name === "DATABASE_URL")!;
await client.environment.replace(projectId, {
  eid: variable.id,
  expectedVersion: variable.version,
  value: "postgres://rotated",
  kind: variable.kind,
});
await client.environment.updateTargets(projectId, {
  eid: variable.id,
  expectedVersion: variable.version + 1,
  preview: true,
  production: false,
});
await client.environment.delete(projectId, {
  eid: variable.id,
  expectedVersion: variable.version + 2,
});

A stale expectedVersion fails with engine.ENVIRONMENT_CHANGED; re-read with environment.get and retry. After a write, preview.stale === true until the preview dev server restarts with the new variables, and production.pending === true until the next production deployment ships them.

versions

const versions = await client.versions.list(projectId); // newest history first

await client.versions.restore(projectId, {
  targetSha: versions[3].sha,
  confirmedSha: versions[3].sha, // must equal targetSha — an explicit confirmation
  idempotencyKey: crypto.randomUUID(),
});

restore rewrites the working tree the project keeps building on. To ship a historical version to production without touching the working tree, use deployments.create(projectId, { versionId }) instead. Do not mix the two.

deployments

const accepted = await client.deployments.create(projectId, {
  versionId: "…", // optional: deploy a historical version
  idempotencyKey: "…", // optional replay guard
});
const deployment = await client.deployments.wait(projectId, accepted.deploymentId, {
  timeoutMs: 10 * 60 * 1000, // default
  pollIntervalMs: 1000, // default
});
deployment.status; // "queued" | "building" | "uploading" | "ready" | "failed"
deployment.url; // set once ready

registeredDomains — bought through unseal-space

Purchases are money movements, so they are gated by a short-lived price challenge: quote first, then purchase against the returned nonce.

const suggestions = await client.registeredDomains.search({ query: "roastery", limit: 10 });
const [check] = await client.registeredDomains.check(["roastery.coffee"]);

const challenge = await client.registeredDomains.createPurchaseChallenge("roastery.coffee");
// challenge.maximumPriceMicroUsd / .currency / .expiresAt — show this before charging

await client.registeredDomains.purchase({
  nonce: challenge.nonce,
  idempotencyKey: crypto.randomUUID(),
  autoRenew: true,
  privacyMode: "redaction",
  contact: { name, email, phone, street, city, state, postalCode, country },
});

const registration = await client.registeredDomains.wait("roastery.coffee");
if (registration.status === "succeeded") {
  await client.registeredDomains.bind("roastery.coffee", projectId);
}

Also: list(), get(name), registrationStatus(name), unbind(name). bind and unbind accept { idempotencyKey } in their options argument.

connectedDomains — owned at another registrar

const connected = await client.connectedDomains.connect({ hostname: "example.com", projectId });
connected.instructions; // DNS records the owner must create (CNAME / TXT)

await client.connectedDomains.get("example.com"); // poll ownership/cert/routing status
await client.connectedDomains.assign("example.com", projectId); // or null to unassign
await client.connectedDomains.disconnect("example.com");
await client.connectedDomains.list();

Classify before routing. A hostname purchased through unseal-space appears in registeredDomains.list() and is routed with bind; a hostname you own elsewhere appears in connectedDomains.list() and is routed with assign. Check both inventories before changing routing.

Permissions

Each API key carries explicit permissions; a call outside them fails with app.FORBIDDEN (403). Presets in the dashboard: build, publish, domain.

| SDK method | Required permission | | ------------------------------------------------------------------------------------- | ------------------------------------------------------ | | projects.create | projects:write | | projects.list · projects.get · versions.list | projects:read | | versions.restore | projects:restore | | preview.get | preview:read | | environment.get | environment:read | | environment.create · replace · updateTargets · delete | environment:write | | messages.send | messages:write | | messages.events | messages:read | | messages.wait | messages:read + projects:read | | attachments.reserve | attachments:write | | deployments.create | deployments:write | | deployments.get · deployments.wait | deployments:read | | registeredDomains.search · check · list · get · registrationStatus · wait | registeredDomains:read | | registeredDomains.createPurchaseChallenge · getPurchaseChallenge · purchase | registeredDomains:purchase | | registeredDomains.bind / unbind | registeredDomains:bind / :unbind | | connectedDomains.list · get | connectedDomains:read | | connectedDomains.connect / assign / disconnect | connectedDomains:connect / :assign / :disconnect |

messages.wait reads version history to report the version a turn produced, so it needs projects:read on top of messages:read.

Idempotency

projects.create, messages.send, deployments.create, versions.restore, registeredDomains.purchase, and the domain bind/assign mutations accept an idempotencyKey. Reuse a key only when retrying the exact same mutation; generate a new one whenever any argument changes. Replaying a key with different input fails with engine.IDEMPOTENCY_CONFLICT, and replaying while the original is still running fails with engine.IDEMPOTENCY_IN_PROGRESS.

Errors

Every failure throws UnsealSpaceError — never a bare Error, never an oRPC internal.

import { UnsealSpaceError } from "@unseal-ai/unseal-space-sdk";

try {
  await client.messages.send(projectId, { content: "…" });
} catch (error) {
  if (error instanceof UnsealSpaceError) {
    error.code; // "MESSAGE_IN_FLIGHT"
    error.status; // 409
    error.why; // machine-written cause, when the server provides one
    error.fix; // the suggested recovery action
    error.link; // documentation link, when there is one
    error.data; // full structured payload (e.g. { messageId })
  }
  throw error;
}

Most server codes are namespaced namespace.CODE — app.* (auth, validation, rate limits), engine.* (project / turn / environment lifecycle), billing.*, domain.*. A few errors declared directly on the API contract use a bare UPPER_SNAKE_CASE code and carry a typed payload in data. Common ones:

| Code | Status | Meaning | | -------------------------------- | ------ | --------------------------------------------------- | | app.UNAUTHORIZED | 401 | Missing, revoked, or expired API key | | app.FORBIDDEN | 403 | Key lacks the permission for this call | | app.NOT_FOUND | 404 | Unknown project, message, deployment, or domain | | app.VALIDATION_FAILED | 400 | Rejected input | | app.RATE_LIMITED | 429 | Too many requests — back off before retrying | | MESSAGE_IN_FLIGHT | 409 | A turn is already running (data.messageId) | | engine.IDEMPOTENCY_CONFLICT | 409 | Key reused with different input | | engine.IDEMPOTENCY_IN_PROGRESS | 409 | The original request with this key is still running | | engine.ENVIRONMENT_CHANGED | 409 | Stale expectedVersion — re-read and retry | | billing.INSUFFICIENT_CREDIT | 402 | Workspace is out of credit |

Failures raised locally by the SDK carry SDK_* codes and status: 0 (except where noted): SDK_INVALID_CONFIG, SDK_INVALID_ARGUMENT, SDK_REQUEST_FAILED (network), SDK_TIMEOUT (408), SDK_ABORTED (499), SDK_RECONNECT_EXHAUSTED (504), SDK_UPLOAD_FAILED.

Cancellation and timeouts

Every method takes an options argument with an AbortSignal; the wait helpers additionally take timeoutMs (and pollIntervalMs where they poll).

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
await client.projects.list({}, { signal: controller.signal });

Aborting or timing out cancels your local wait only — the remote turn, deployment, or registration keeps running. Persist the id and resume.

Types

The root entry exports the client, UnsealSpaceError, and every DTO. Type-only subpaths let you import DTOs without pulling in runtime code:

import type { UnsealSpaceProject } from "@unseal-ai/unseal-space-sdk/projects";
import type { MessageEvent } from "@unseal-ai/unseal-space-sdk/messages";

Available subpaths: /projects, /preview, /messages, /attachments, /environment, /versions, /deployments, /domains.

Types are exported under both UnsealSpace* and legacy Builder* names, and createBuilderClient / BuilderError remain as deprecated aliases of createUnsealSpaceClient / UnsealSpaceError. New code should use the UnsealSpace* names.

Other ways in

  • CLI — unseal-space is a standalone binary built on this SDK, with no Node.js requirement. Install with curl -fsSL https://assets.unseal.space/cli/install.sh | sh.
  • REST / OpenAPI — the same surface is documented at https://api.unseal.space/api-reference (spec at /api-reference/spec.json) for non-JavaScript consumers.

Versioning

Semantic versioning. The SDK's DTOs are checked against the server contract in CI, so a published type shape and the live API cannot drift.