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

esoul-sdk

v0.25.5

Published

Build a full product on ExternalSoul: your own tables with per-person rules, a viewer on every seam, app roles, access levels, realtime with audiences, durable tasks, and bindings to other apps.

Readme

esoul-sdk

esoul-sdk is a toolkit for building applications on ExternalSoul. An application written with it is compiled into the platform and runs as a native app: it has its own database tables, its own server endpoints, its own background tasks, its own realtime channel, its own vocabulary of roles, and a set of tools that agents call from chat, voice, the agent builder and MCP. Access control is declared in the application's manifest and enforced by the platform at every entry point; application code does not check permissions itself.

The package provides the types, the manifest schema and validator, the rule compiler, the request helpers and an in-memory test harness. Inside ExternalSoul the same import names resolve to the platform's implementations.

Useful links

Features

Data model

  • Tables are declared in the manifest (db) and created on install; migrations are additive-only
  • Every query is scoped to the application instance and filtered for the caller by compiled rules
  • Rows can be owned by their creator, by the workspace, or by a user across instances
  • Fields can be sealed (encrypted at rest, readable only by the row's owner)
  • Index kinds for equality, list containment and case-insensitive text search, with cursor paging
  • Application state on the workspace timeline as a fold of typed events (replayable, scrubbable)

Access control

  • A viewer on every entry point: owner, member, visitor, anonymous, agent, or the app itself
  • Roles as the application's own words (customer, staff, …), mapped from the platform's kinds
  • Per-surface access levels: write, read, public, token, or a list of roles
  • Rules scoped by a person's typed attributes — a customer linked to a number sees only rows carrying it
  • Roles composed by the workspace owner at runtime, inside an envelope the manifest declares
  • Refusals distinguish login-required from forbidden, so a UI can show a sign-in wall

Server

  • Ops: server-side functions with a declared input schema, shared with the derived tools
  • Routes: HTTP endpoints mounted per instance, including server-sent event streams
  • Token routes: endpoints a machine reaches with a bearer token the application minted
  • Background tasks on a durable executor, with steps, retries and concurrency limits
  • Webhooks, OAuth connections held by the platform, workspace files and file providers
  • Access to other applications through bindings (typed contracts) and workspace tools

Realtime

  • Per-instance channels with topics; each topic declares its audience (everyone, one person, one role)
  • Tokens are issued per channel, so a subscriber never receives another audience's messages

Tools

  • One declaration per op is used by the server, the derived tool and the UI
  • Tools run on every surface: typed chat, voice, agent builder, MCP, and the browser

Development

  • The Forge: a cloud workbench with a live preview, a persona switcher (view as), the application's tools callable before installation, tests and the full checks
  • An in-memory database built from the manifest, with the same compiled rules production uses
  • A validator for the package folder (esoul-app validate)

Deployment

  • Installed from the application's own GitHub repository at a commit
  • Additive migrations applied on install; the install card lists every public surface

Installation

npm install --save-dev esoul-sdk

Outside ExternalSoul the package gives you types, the manifest validator, the rule compiler and the test helpers. Functions that need the platform (pluginDb, viewerProfile, mintRouteToken, …) throw host only when called outside it; they are exercised through the test harness or in a Forge workbench.

Requirements: Node.js 20 or newer (22 for esoul-device on a computer), TypeScript 5, React 18 or newer for the UI entry point.

Concepts

Application. A folder containing a manifest (plugin.json), a schema (app.tsx), a UI, and optionally a server module (server.ts) and tests. The folder name is the application id. The application type is plugin_ followed by the id with underscores.

Manifest. plugin.json declares everything the platform enforces: tables, roles, access levels of ops, routes and tasks, realtime topics, bindings, connections and dependencies. It is validated against schemas/plugin.schema.json.

Events and state. Application state is the result of folding typed events from the workspace timeline. Each event has a dataCreator (mints ids and timestamps) and a processor (a pure reducer). The same events are dispatched by the UI, by tools and by tasks.

Viewer. The identity of the caller, resolved by the platform for every op, route, task, tool and UI render. A viewer has a kind, an account id, a set of ids it has acted under, the application's role word for it, and whether it may write.

Op. A server-side function of the application, called from the UI or from a tool. Its input schema is declared once with defineOps; the server parses with handleOp and the tool is derived with opTool.

Route. An HTTP endpoint the application mounts at /api/plugins/<id>/route/<name>.

Task. A background job on the platform's durable executor. A task is kicked by an event, runs in steps that are memoised across retries, and may dispatch events and publish realtime messages.

Topic. A named message stream on the application's realtime channel. Its audience is declared in the manifest.

Binding. A slot the application declares (uses) and the workspace owner fills with another application that provides the named contract.

Package entry points

| Import | Contents | |---|---| | esoul-sdk | Schema and event types, defineOps, handleOp, opTool, definePluginChannel, defineBindingEvent, checkBinding, resolveAppRole, chartSvg, incompleteStateNotice, deterministicReducerId, callPluginOp, kickPluginTask, pluginRouteUrl, nanoid, the manifest schema, compileEnvelope, compileCustomRole | | esoul-sdk/server | pluginDb, viewerProfile, sseStream, mintRouteToken, readAppState, callWorkspaceTool, computer, devices, questions, credentials, llm, filesForOp, pluginFiles, emitPluginAppEvent, generateAppImage, renderChartImage, setAppRole, listAppRoles, defineAppRole, removeAppRole, getPluginConnectionCredentials, and the context types (PluginOpContext, PluginRouteContext, PluginViewer, PluginServerModule) | | esoul-sdk/react | useViewer, useSignInWall, useAppCanEdit, usePluginEventDispatch, runOptimistic, useAppNav, usePluginRealtime, useWorkspaceTools, useRemoteReconcile, useCredential, <ConnectAccount/>, <ConnectComputer/>, useDevices, useDeviceRun, useDeviceActions, the file hooks (useFileSources, useFileSourceEntries, useFolderAutocomplete, useFileUrls, useTransfer, usePluginWorkspaceFiles, usePluginFileUpload), ImageLabeler; the types PluginViewerPublic, SignInWall | | esoul-sdk/machine | what runs ON the person's computer: the Program type for a device program (types only), and the esoul-device runtime | | esoul-sdk/testing | memoryDb, fakeViewer, runOp, fakeApps, capture, memoryFiles, scriptedModel, simGmail, fakeCredentials, startMockOAuth, listFailedRequests | | esoul-sdk/schemas/plugin.schema.json | The JSON Schema of the manifest | | esoul-app validate <dir> | Command-line validator for a package folder | | esoul-device | The runtime on a person's computer: connect, status, sh, config set, disconnect, uninstall (docs/18) |

Getting started

The recommended way to build an application is the Forge, a workbench inside ExternalSoul. It does not require a checkout of the platform.

  1. Add a Forge board to a workspace.
  2. Ask the assistant to open a workbench for your application. The platform starts a cloud machine with the platform's source, scaffolds the application folder, and shows a live preview on the board.
  3. Edit files with the board's tools (write_app_file, edit_app_file). Each change reloads the preview and reports its health.
  4. Inspect the result: look_at_app renders the application on desktop and phone, light and dark, as any persona (owner, member, visitor-a, anonymous, or a composed role such as role:packer). call_app_tool runs the application's tools before installation. test_app runs the tests; check_app runs the full gate.
  5. Push the application to its own GitHub repository from the board, and install it from Settings → Apps in any workspace.

From your own editor, npm install --save-dev esoul-sdk gives you the same types and the validator, and lets you run tests locally with esoul-sdk/testing.

Package layout

<id>/
  plugin.json            the manifest
  ops.ts                 op input schemas (defineOps)
  app.tsx                the schema: events, state, tools, state description
  ui/<id>-ui.tsx         the React UI ("use client")
  server.ts              ops, routes, webhooks
  <id>.test.ts           tests
  .esoul/                generated: db.d.ts, rules.json, migration.sql

Example: a shop

The reference application is a shop. Anyone with the link may browse the catalogue. A signed-in customer places orders and sees only their own. Staff see every order of the shop. The owner sets prices and decides who is staff, and may compose narrower roles at runtime — for example a packer who sees only orders being prepared, never the customer's note, and may only mark them shipped.

Manifest

{
  "manifestVersion": 1,
  "id": "shop",
  "name": "Shop",
  "version": "1.0.0",
  "applicationType": "plugin_shop",
  "entry": "app",
  "roles": {
    "vocabulary": ["customer", "staff", "owner"],
    "default": {
      "owner": "owner",
      "member-edit": "staff",
      "member-readonly": "staff",
      "visitor": "customer",
      "anonymous": "customer",
      "agent": "inherit"
    },
    "custom": {
      "models": {
        "Order": { "where": ["status"], "hide": ["note"], "update": { "transitions": "status" } }
      },
      "ops": ["set-order-status"]
    }
  },
  "ops": {
    "browse": { "access": "public" },
    "place-order": { "access": "public", "requires": "account" },
    "list-orders": { "access": "public", "requires": "account" },
    "set-order-status": { "access": ["staff", "owner"] },
    "add-product": {}
  },
  "routes": {
    "order-updates": { "access": "read" }
  },
  "channel": {
    "topics": {
      "catalogue": {},
      "order-status": { "audience": "viewer", "mayAddress": ["staff", "owner"] },
      "new-order": { "audience": "role:staff", "mayAddress": ["customer", "staff", "owner"] }
    }
  },
  "db": {
    "Product": {
      "scope": "instance",
      "fields": { "name": "string", "priceCents": "int", "active": "boolean=true", "tags": "string[]" },
      "indexes": [["active", "createdAt"], { "fields": ["tags"], "kind": "contains" }, { "fields": ["name"], "kind": "text" }],
      "rules": { "read": "anyone", "write": ["owner"] }
    },
    "Order": {
      "scope": "instance",
      "owner": "creator",
      "fields": { "status": "string=new", "totalCents": "int", "lines": "json", "shipTo": "json", "note": "text?" },
      "indexes": [["status"], ["createdAt"]],
      "rules": {
        "read": ["creator", "staff", "owner"],
        "create": { "roles": ["customer", "staff", "owner"], "requires": "account" },
        "update": { "roles": ["staff", "owner"], "creatorMay": ["note"] },
        "delete": ["owner"]
      }
    },
    "Address": {
      "scope": "user",
      "fields": { "label": "string", "lines": "json" },
      "sealed": ["lines"]
    }
  }
}

An op with no access is write: only the owner and edit members may call it. "access": "public" admits anyone who can reach the application, including through a share link; "requires": "account" makes an anonymous call fail with login-required instead of forbidden.

Op inputs

// ops.ts
import { z } from "zod";
import { defineOps } from "esoul-sdk";

export const ORDER_STATUSES = ["new", "preparing", "shipped", "fulfilled", "refunded"] as const;

export const ops = defineOps({
  browse: z.object({}),
  "add-product": z.object({ name: z.string().min(1).max(80), priceCents: z.number().int().min(0) }),
  "place-order": z.object({
    lines: z.array(z.object({ productId: z.string().min(1), qty: z.number().int().min(1).max(99) })).min(1),
    shipTo: z.object({ name: z.string().min(1), street: z.string().min(1), city: z.string().min(1) }),
    note: z.string().max(280).optional(),
  }),
  "list-orders": z.object({}),
  "set-order-status": z.object({
    orderId: z.string().min(1).describe("The order id, as list_orders shows it"),
    status: z.enum(ORDER_STATUSES),
  }),
});

Server

// server.ts
import "server-only";
import { handleOp } from "esoul-sdk";
import { pluginDb, type PluginOpContext, type PluginServerModule } from "esoul-sdk/server";
import type { ShopDb } from "./.esoul/db";
import { ops } from "./ops";

const db = (ctx: PluginOpContext) => pluginDb<ShopDb>(ctx);

export const pluginServer: PluginServerModule = {
  ops: {
    browse: handleOp(ops, "browse", async (ctx) => {
      const d = await db(ctx);
      return d.product.findMany({ where: { active: true }, orderBy: { createdAt: "asc" }, take: 200 });
    }),

    "add-product": handleOp(ops, "add-product", async (ctx, input) => {
      const d = await db(ctx);
      return d.product.create({ data: { name: input.name, priceCents: input.priceCents, tags: [] } });
    }),

    "place-order": handleOp(ops, "place-order", async (ctx, input) => {
      const d = await db(ctx);
      // Price on the server, from the catalogue.
      const products = await d.product.findMany({ where: { id: { in: input.lines.map((l) => l.productId) } } });
      const totalCents = input.lines.reduce((sum, l) => sum + l.qty * (products.find((p) => p.id === l.productId)?.priceCents ?? 0), 0);
      const order = await d.order.create({ data: { status: "new", totalCents, lines: input.lines, shipTo: input.shipTo, note: input.note } });
      await ctx.notify("new-order", { orderId: order.id });
      return { orderId: order.id, totalCents };
    }),

    "list-orders": handleOp(ops, "list-orders", async (ctx) => {
      const d = await db(ctx);
      return d.order.findMany({ orderBy: { createdAt: "desc" } });
    }),

    "set-order-status": handleOp(ops, "set-order-status", async (ctx, input) => {
      const d = await db(ctx);
      const order = await d.order.update({ where: { id: input.orderId }, data: { status: input.status } });
      await ctx.notify("order-status", { orderId: order.id, status: order.status }, { to: { viewerIds: [order.ownerId] } });
      return order;
    }),
  },
};

There is no access check in this file. pluginDb(ctx) returns a client scoped to the instance and filtered for ctx.viewer. A customer's order.findMany returns their own rows; a staff member's returns every row of the shop. A customer calling set-order-status is refused before the handler runs, because the manifest opens it to staff and owner only. A packer — a composed role scoped to status: ["preparing"] — receives only those rows, with note blanked, and an update from preparing to anything other than shipped is refused with the allowed move named.

Tools

// app.tsx (excerpt)
import { opTool } from "esoul-sdk";
import { ops } from "./ops";

toolkitCreator: (identifier) => {
  const base = identifier.instanceName.replace(/[^a-zA-Z0-9]/g, "_");
  const cfg = { pluginId: "shop", nodeId: identifier.nodeId };
  return {
    [`browse_${base}`]: opTool(ops, "browse", {
      ...cfg,
      description: `List the products of "${identifier.instanceName}".`,
      readOnly: true,
      publicSafe: true,
      say: (rows) => ({ text: rows.map((p) => `${p.name} — ${(p.priceCents / 100).toFixed(2)} (id ${p.id})`).join("\n") || "No products." }),
    }),
    [`set_order_status_${base}`]: opTool(ops, "set-order-status", {
      ...cfg,
      description: `Move an order of "${identifier.instanceName}" to a new status.`,
      say: (o) => ({ text: `Order ${o.id} is now ${o.status}.` }),
    }),
  };
},

The tool's parameters are the op's input schema; a field the op does not declare is refused with the field named. The tool runs as the person who invoked it, on every surface.

UI

// ui/shop-ui.tsx (excerpt)
"use client";
import { callPluginOp } from "esoul-sdk";
import { useSignInWall, useViewer } from "esoul-sdk/react";

export function ShopUi({ nodeId }: { nodeId: string }) {
  const viewer = useViewer();           // { kind, userId, role, canEdit, signedIn, customRole, can }
  const wall = useSignInWall();
  const order = async (lines, shipTo) => {
    try {
      return await callPluginOp("shop", "place-order", nodeId, { lines, shipTo });
    } catch (err) {
      wall.raise(err);                   // shows the sign-in wall on `login-required`, rethrows otherwise
    }
  };
  // viewer.can.models.Order.moves tells a packer's screen which buttons to show;
  // the server enforces the same rule regardless.
  …
}

Test

// shop.test.ts
import { fakeViewer, memoryDb, runOp } from "esoul-sdk/testing";
import manifest from "./plugin.json";
import { pluginServer } from "./server";

const owner = fakeViewer("owner");
const ada = fakeViewer("visitor", { userId: "u_ada", role: "customer" });
const lin = fakeViewer("visitor", { userId: "u_lin", role: "customer" });

it("a customer sees only their own orders", async () => {
  const db = memoryDb(manifest);
  const { result: p } = await runOp(pluginServer, "add-product", { viewer: owner, args: { name: "Tea", priceCents: 350 }, db: db.as(owner) });
  const shipTo = { name: "Ada", street: "Main 1", city: "Brno" };
  await runOp(pluginServer, "place-order", { viewer: ada, args: { lines: [{ productId: p.id, qty: 1 }], shipTo }, db: db.as(ada) });
  const { result } = await runOp(pluginServer, "list-orders", { viewer: lin, args: {}, db: db.as(lin) });
  expect(result).toEqual([]);
});

it("an anonymous caller is asked to sign in", async () => {
  const db = memoryDb(manifest);
  const anon = fakeViewer("anonymous");
  await expect(runOp(pluginServer, "list-orders", { viewer: anon, args: {}, db: db.as(anon) })).rejects.toMatchObject({ code: "login-required" });
});

memoryDb(manifest) builds an in-memory database from the manifest with the same compiled rules the platform applies to the real database. runOp calls the op with a platform-shaped context and records what it notified and emitted.

API reference

Manifest (plugin.json)

| Field | Type | Description | |---|---|---| | manifestVersion | 1 | Schema version. | | id | string | Application id: lower-case letters, digits, hyphens. Equals the folder name. | | name, description, version, icon | string | Shown in the store and the install card. version is semver. | | applicationType | string | plugin_ + id with underscores. Never changes after release. | | entry | string | The schema module (app). | | roles.vocabulary | string[] | The application's role words. | | roles.default | object | Mapping from platform kinds (owner, member-edit, member-readonly, visitor, anonymous, agent) to role words. "inherit" for agent uses the role of the person it acts for. | | roles.describe | object | One line per role, for the install card. | | roles.attributes | object | Name → string, int or boolean: what a grant may say about a person. Typed when granted; carried on viewer.attrs; read by scoped rules as viewer.<name>. | | roles.custom | object | The envelope for roles the owner composes: per model, which indexed fields may scope (where), which fields may be hidden (hide), which fields and transition field may be updated (update); ops a composed role may be handed; attributes a grant may carry. | | ops | object | Op name → { access, requires }. access: write (default), read, public, or a list of role words. requires: "account" turns an anonymous refusal into login-required. | | routes | object | Route name → { access }, additionally token for routes reached with a minted bearer token. | | kickableTasks | object | Tasks the browser may kick, with their access level. | | pollTasks | array | [{ task, everyMinutes }]: tasks the platform kicks on a schedule (5 to 1440 minutes). | | channel.topics | object | Topic name → { audience, mayAddress, description }. audience: all (default), viewer, or role:<word>. mayAddress: roles that may publish on the topic. | | db | object | Model name → table declaration (see below). | | uses | object | Slot name → { contract, label, optional }. | | provides | object | Contracts this application provides, with the tools, events and tables each names. | | connections | array | OAuth or API-key connections the platform holds for the application. | | webhooks | string[] | Webhook names handled in server.ts. | | workspaceTools | array | Tools of other applications the server side may call. | | platformApi | object | { min, max? }: the platform contract versions the application accepts. |

npm packages the application needs are declared in a package.json in the application folder with a dependencies field only.

Table declaration:

| Field | Description | |---|---| | scope | instance (rows belong to this instance), workspace, or user (rows follow the account across instances). | | owner | creator marks each row with the creator's ids so rules can name creator. | | fields | Name → type: string, text, int, float, boolean, json, datetime, string[], ref:<Model>; ? for optional, =value for a default. | | unique, indexes | Field groups. An index entry may be { fields, kind } with kind btree (default), contains (list membership) or text (case-insensitive substring). | | sealed | Fields encrypted at rest and readable only by the row's owner. | | rules | read, create, update, delete: "anyone", a list of principals (creator, role words, or a scoped role { "role": "customer", "where": { "customer": "viewer.customer" } }), or { roles, requires, creatorMay }. A scoped role reads only the rows its where names, creates inside them, and grants nothing to a holder without the attribute. A model without rules is writable by the owner role only. |

Platform columns on every table: id, workspaceId, nodeId, ownerId, createdBy, createdAt, updatedAt, deletedAt.

esoul-sdk

| Export | Description | |---|---| | ApplicationSchema, EventDefinition, EventTypes, ApplicationIdentifier, ApplicationPort | The schema contract: events, stateCreator, toolkitCreator, getStateDescription, tasks, channel, reconstructStateFromEventLog. | | defineOps(inputs) | Declares the input schema of every op once. | | handleOp(ops, name, fn) | Wraps an op handler so it receives parsed input; an undeclared or malformed field is refused as invalid, naming the field. | | opTool(ops, name, { pluginId, nodeId, description, say, then?, readOnly?, publicSafe? }) | Derives an agent tool from an op. say turns the op's result into the tool's text; readOnly admits a read-scoped token; publicSafe allows use on a public storefront. | | definePluginChannel({ applicationType, topics }) | Declares the realtime channel placed on the schema as channel. | | defineBindingEvent, checkBinding, bindingsOf | Bindings: the event that records a slot being filled, the check that a provider satisfies a contract, and the current bindings from state. | | resolveAppRole, roleKeyFor | The mapping from platform kinds to role words that the platform itself uses. | | compileEnvelope, compileCustomRole, CustomRoleError | The compiler for composed roles, for tests that assert on them. | | coerceAttr, coerceAttrs, attrProblem, ATTR_TYPES | Attribute typing: what a typed value becomes, and the sentence for one that does not fit. | | chartSvg(spec) | Renders a time-series chart with panels, markers and gaps to SVG; text is drawn as glyph outlines so it renders on a server without fonts. | | incompleteStateNotice, missingStateKeys | For getStateDescription: reports a state that did not load instead of describing it as empty. | | deterministicReducerId(prefix, seed) | A stable id for use inside a reducer. | | callPluginOp(pluginId, op, nodeId, args?) | Calls an op from the browser or from a tool. Throws PluginCallError carrying code (invalid, forbidden, login-required), detail and signInPath. | | kickPluginTask(args), pluginRouteUrl(...) | Kicks a task; builds a route's URL. | | timingSafeEqual, nanoid | Utilities. |

esoul-sdk/server

| Export | Description | |---|---| | PluginServerModule | { ops, routes, webhooks } exported as pluginServer from server.ts. | | PluginOpContext | pluginId, opName, workspaceId, nodeId, instanceName, viewer, args, origin, cloudConnectionId, apps (bound applications), notify(topic, data, { to? }), emit(eventName, eventData). | | PluginRouteContext | The op context plus request, method, searchParams, canWrite. Handlers return a Response. | | PluginViewer | kind, userId, viewerIds, role, customRole, attrs, canWrite, shareId, agent. | | pluginDb<T>(ctx) | A typed client over the application's tables, scoped and filtered for ctx.viewer. Per model: findMany, findUnique, count, aggregate, groupBy, create, createMany, update, updateMany, upsert, delete, deleteMany; $transaction. Refusals throw with code forbidden, login-required or invalid. | | viewerProfile(viewer) | The caller's own account (name, email, picture), or null. | | sseStream(fn) | A server-sent event response for a route. | | mintRouteToken(ctx, { route, ttlSeconds?, label? }) | Mints a bearer token for a route declared "access": "token". Returns { token, url, expiresAt }; url is built on ctx.origin. Expiry is the only revocation; 30 days at most. | | readAppState(nodeId) | The folded state of another application in the workspace. | | callWorkspaceTool({ ... }) | Calls a tool of another application, gated by the manifest's workspaceTools. | | computer(ctx, machineNodeId) | A paired machine: status, run, runToEnd, claude, claudeToEnd, readFile, fetchJson, python, runJson. Waits are capped and the approval gate is reported. | | emitPluginAppEvent(args) | Appends an event to the application's own timeline from server code. | | generateAppImage(ctx, args) | Generates an image, scales it to web weight, stores it and returns its URL. Refused in a Forge preview. | | renderChartImage(ctx, { name, svg }) | Renders an SVG to PNG and returns { url } (installed) or { base64 } (preview). | | setAppRole(ctx, { email, role, attrs? }) | The owner gives an account one of the application's role words, or a composed role, with attribute values ({ customer: "204" }) validated against roles.attributes. Recorded as an event. | | listAppRoles(ctx) | { people, roles, custom, envelope }. | | defineAppRole(ctx, definition), removeAppRole(ctx, name) | The owner composes or removes a role inside roles.custom. | | credentials(ctx).slot(name) | The person's account for a declared slot — Google, any OAuth 2.0 provider (Microsoft, Dropbox…) or a pasted API key: status() and fetch(url, init?), with the token or key added by the platform and sent only where the slot may reach. Your code never holds it (docs/08). | | getPluginConnectionCredentials(connectionId, pluginId) | Older: the token of one of the app's own connections, readable only from the app's own running call for the workspace owner's connection. Prefer a credentials slot (docs/08 §9). | | pluginFiles(ctx), filesForOp(ctx) | Workspace files and file sources from server code: read by ref or address ("workspace:/Mail/a.pdf"), save to a path, and transfer a list of files into a folder with platform progress, Stop and a clean Retry (docs/09). |

esoul-sdk/react

| Hook | Description | |---|---| | useViewer() | The public viewer: kind, userId, role, canEdit, signedIn, attrs, customRole, can. can lists the ops a composed role may call and, per model, the hidden fields, updatable fields and allowed moves. | | useSignInWall() | { needed, reason, signIn, raise, ask, serverSays }. raise(err) shows the wall when err.code === "login-required" and rethrows anything else; ask(call) attempts a call and returns null (wall raised) on login-required; serverSays is the server's answer so far (null, "account", "no-account"). | | useAppCanEdit() | Whether the current viewer may write. | | usePluginEventDispatch() | Dispatches one of the application's events from the UI. | | usePluginRealtime({ channel, workspaceId, nodeId, topics, enabled? }) | Subscribes to the instance's channel. Returns { data, latestData, error, state }. | | useWorkspaceTools(identity) | Lists and calls tools of other applications from the UI, subject to workspaceTools. | | usePluginWorkspaceFiles(), usePluginFileUpload(), useFileSources(), useFileSourceEntries() | Workspace files, upload, and file sources (workspace, Google Drive, providers). | | useTransfer(transferId) | A file transfer's progress, with retry() and cancel(). |

esoul-sdk/testing

| Export | Description | |---|---| | memoryDb(manifest, options?) | An in-memory database compiled from the manifest. Starts as the application's own code; .as(viewer) returns the same rows as another caller. $rules exposes the compiled rules. | | fakeViewer(kind, { userId?, viewerIds?, role?, name?, email?, attrs? }) | A viewer of a kind. name/email are what viewerProfile returns for it; attrs are what a scoped rule reads. | | runOp(server, opName, { viewer, args?, db?, apps?, notifyFails? }) | Calls pluginServer.ops[opName] with a platform-shaped context. Returns { result, notified, emitted }; throws the op's refusals. | | fakeApps(fixtures) | Bound applications for ctx.apps. | | capture() | A recorder for callbacks. | | fakeCredentials(slots) | A stand-in for credentials(ctx): each slot answers as the test says (status, hosts, fetch), the platform's refusals apply, and calls records every request (docs/08 §7). | | startMockOAuth(opts?) | A local OAuth server for connection tests. |

Command line

npx esoul-app validate <dir>

Exits 0 when the folder is a well-formed application package; otherwise prints each problem.

Rules

Note. State is a fold of events. A processor must be a pure, idempotent function of (state, event). Ids and timestamps are minted in dataCreator, never in a processor. An event that replaces a whole value carries a collapse key so a burst folds to one event.

Note. Access is declared, not checked. An op, route or task that tests the viewer itself has a second answer to a question the platform already answered; the platform's answer is the one enforced. Use viewer.role and viewer.can to decide what a screen shows, not what a request may do.

Warning. A task handler is re-run from the top after every step boundary. Every side effect — a dispatch, a fetch, an emit, a random id — belongs inside ctx.step.run. A side effect outside a step runs once per replay.

Warning. Application code imports the platform only through esoul-sdk. A relative import into the platform's source is refused by the checks and by the install.

Note. viewer.signedIn in the UI is the page's estimate. Whether a request needs an account is decided by the op; attempt the call and let useSignInWall().raise act on login-required.

Documentation

| | | |---|---| | 1. Getting started | The Forge loop, package layout, the smallest complete application | | 2. The manifest | Every field of plugin.json | | 3. Events and state | dataCreator, processor, collapse keys, replay | | 4. Tools | Tools on every surface; deriving a tool from its op | | 5. The UI | React, hooks, theme, responsive rules | | 6. The server | Ops, routes, webhooks, computer, charts, calling other applications | | 7. Background tasks | The durable executor, the replay model, concurrency | | 8. Accounts | The person's Google, Microsoft or any OAuth 2.0 / API-key account, used through the platform without your code holding the token | | 9. Files | Workspace files, Drive, file providers | | 10. Testing | The fold contract, ops, tasks and rules as tests | | 11. Shipping | The repository, review, release, install, dependencies | | 12. Rules and failures | Each rule with the failure it prevents | | 13. People and access | Viewer, roles, access levels, composed roles, the sign-in wall | | 14. Your own tables | db, rules, scopes, sealed fields, indexes, migrations | | 15. Realtime | Topics, audiences, addressing | | 16. Bindings | Slots, contracts, reaching another application | | 17. Editing and merging | An editor that takes changes from other devices and agents; merges | | 18. Your computer | The device arm: commands, the workspace, programs, esoul-device | | 19. Model calls | llm(ctx): billed model calls, budgets, scriptedModel() for tests | | 20. A service on your computer | A walk-through: a program that keeps a service running on the person's computer, talking to the app both ways — built in the Forge, then installed |

Versions

From 0.20.0 on, every version is in CHANGELOG.md (accounts through the platform — Google, then any OAuth 2.0 or API-key provider in 0.25.0, model calls, file transfers, opened-from-outside navigation, server-only events, the questions bell, mail waits). Earlier:

| Version | Changes | |---|---| | 0.16.0 – 0.19.1 | runOptimistic and the failed-request banner; stable list paging; editor sync (useRemoteReconcile, mergeLines / mergeFields / mergeRecordsById); the device arm — devices(ctx), <ConnectComputer/>, the esoul-device runtime, declared commands, the shared command line, an app's own program on the computer (docs/17, 18). | | 0.15.0 | Files work in a Forge box, and do more: FilesApi.list(…, opts) narrowed at the source, listAll, resolvePath, readMany, write (make or replace by name — a label file beside its image; manifest fileSources.write), readGrant + fileGrantUrl (signed, expiring URLs for an <img> in a preview and for a machine). UI: useFolderAutocomplete, useFileUrls, useFileSourceEntries(…, opts) with loadMore, listFileEntries, resolveFilePath. ImageLabeler / PolygonCanvas — the Explorer's polygon editor as a component. Pure: pairImagesWithLabels, serializeLabelMe, parseLabelMe, labelFileNameFor. Testing: memoryFiles. | | 0.14.0 | Attribute-scoped access: roles.attributes (typed; validated when granted; viewer.attrs); scoped rule principals { role, where } with viewer.<attribute>, applied to reads, aggregates, updates and deletes and filling or refusing creates, in both database clients; a rule combining creator with a scoped role is refused. compileManifestRules is the one reader of a manifest's rules. VIEW AS personas carry attributes (visitor-a?customer=204). | | 0.13.0 | Composed roles: roles.custom envelope; defineAppRole, removeAppRole; setAppRole with attributes; listAppRoles returns composed roles and the envelope. Reads, updates and deletes are narrowed by the composition in both database clients; surfaces not handed to the role are refused; useViewer().customRole and can. access: [<roles>] on a surface. Composed roles are personas in the Forge. | | 0.12.0 | Token routes: "access": "token" and mintRouteToken. ctx.origin on ops. | | 0.11.0 | chartSvg, renderChartImage. computer().python() and runJson() with truncated outputs. APPROVAL_WAIT. Table refusals name the constraint. | | 0.10.0 | computer(ctx, machineNodeId). Server code in a Forge preview reaches the workspace (callWorkspaceTool, readAppState, computer), gated by workspaceTools. | | 0.9.0 | defineOps, handleOp, opTool: one declaration for an op's input, its server parser and its tool. | | 0.8.0 | generateAppImage; setAppRole and listAppRoles; useSignInWall().ask() and serverSays. | | 0.7.0 | Index kinds (contains, text) and cursor paging; ctx.emit; list field defaults; aggregate, groupBy, $transaction, *Many documented. | | 0.6.0 | Tables (db, pluginDb, rules, scopes, sealed fields, additive migrations). viewer on every entry point, roles, access levels, the sign-in wall. Realtime audiences. Bindings. viewerProfile. Testing: memoryDb, fakeViewer, runOp. | | 0.5.0 | Routes, sseStream, realtime channels. | | 0.3.0 | Renamed from @externalsoul/plugin-sdk. readAppState, callWorkspaceTool. The import wall. | | 0.2.0 | File sources and providers. |

Releasing (maintainers)

cd packages/esoul-sdk
npm run release:check   # fresh docs + build, dist completeness, pack, clean-room install, every entry point, the CLI
npm publish             # prepack rebuilds docs and dist; the owner's npm login
npm view esoul-sdk version

The check refuses a version that is already on npm and a dist/ that lacks a module src/ has. Commit the regenerated llms.txt, llms-full.txt and api-reference.md with the version bump.