Skip to content

@doable/marketplace-bundle

@doable/marketplace-bundle is the on-wire format and permissions model for marketplace listings. It defines the canonical bundle manifest, two codecs (doable.json.v1 and standards.zip.v1), and a pure-function permissions compute that drives the install dialog. The package has zero runtime database or filesystem dependencies, so the API server, the web client, and edge runtimes can all import it.

The package lives at packages/marketplace-bundle/ and is published as ESM.

Why two codecs?

A bundle is a portable container for skills, rules, instructions, knowledge files, and MCP connectors. Doable wants a small JSON shape for storage and transport, but the wider AI ecosystem expects specific layouts. The two codecs let one bundle satisfy both worlds.

Codec Constant Pros Cons
Doable JSON v1 JSON_V1_FORMAT ("doable.json.v1") Trivial, transport-friendly (REST/JSON), small bundles Not an industry-recognised format. Large knowledge files inflate the JSON.
Standards Zip v1 STANDARDS_ZIP_FORMAT ("standards.zip.v1") A single zip that is simultaneously a Doable bundle and a portable Anthropic Agent Skills, Cursor Rules, and Claude Code plugin pack Bigger payload, zip parsing required

Use Standards Zip v1 when interop with Cursor, Claude Code, or external MCP tools matters. Use JSON v1 inside Doable for cheap storage.

Install

pnpm add @doable/marketplace-bundle

Runtime dependencies: fflate for zip handling, zod for schema validation. Both are isomorphic (Node 18+ and modern browsers).

Encoding

import {
  encodeBundle,
  parseManifest,
  JSON_V1_FORMAT,
  STANDARDS_ZIP_FORMAT,
} from "@doable/marketplace-bundle";

const manifest = parseManifest({
  schemaVersion: "1.0.0",
  format: JSON_V1_FORMAT,
  exportedAt: new Date().toISOString(),
  metadata: { name: "Code Reviewer", description: "Reviews PRs", icon: "code", color: "purple" },
  skills: [
    {
      name: "code-reviewer",
      title: "Code Reviewer",
      description: "Reviews PRs against the team style guide",
      content: "# Code Reviewer\n\nReview each diff against ...",
      scope: "workspace",
    },
  ],
  rules: [],
  instructions: [],
  knowledge: [],
  connectors: [],
});

// Pick a codec. The encoder stamps the format into the manifest.
const json = encodeBundle(manifest, JSON_V1_FORMAT);
// json.contents is a UTF-8 string; json.byteLength is its size.

const zip = encodeBundle(manifest, STANDARDS_ZIP_FORMAT);
// zip.contents is Uint8Array (application/zip); zip.files maps inner paths to sizes.

encodeJsonV1 and encodeStandardsZip are also exported directly for callers that want the codec-specific result type.

Decoding

import { decodeBundle } from "@doable/marketplace-bundle";

const manifest = decodeBundle({
  format: STANDARDS_ZIP_FORMAT,
  contents: zipBytes, // Uint8Array
});

The decoder runs the parsed payload through bundleManifestSchema.parse, so invalid bundles throw ZodError early. Use safeParseManifest(raw) for a non-throwing tuple.

Standards Zip layout

encodeStandardsZip emits a zip with this structure (constants from src/codecs/standards-zip.ts):

doable.manifest.json        Doable-specific metadata (full manifest, pretty-printed)
plugin.json                 Claude Code plugin descriptor
mcp.json                    MCP connector definitions (only if connectors are present)
skills/<name>/SKILL.md      Anthropic Skill files with YAML front-matter
.cursor/rules/<name>.mdc    Cursor rule files with YAML front-matter
instructions/<filename>     Plain-text agent instructions
knowledge/<filename>        Knowledge files (inline content)

A single zip is therefore both a Doable bundle and a portable Cursor / Claude / Anthropic skill pack. Round-trips through decodeStandardsZip preserve everything required by Doable (extra metadata lives in doable.manifest.json).

Bundle manifest schema

Top-level shape (src/manifest.ts):

{
  schemaVersion: "1.0.0",       // bumped only on breaking changes
  format: "doable.json.v1" | "standards.zip.v1",
  exportedAt: ISO8601 string,
  publisherId?: string,          // opaque, for verification
  metadata: {
    name, slug?, description, icon, color,
    version, tags[], homepage?, license?,
  },
  skills:       SkillItem[],
  rules:        RuleItem[],
  instructions: InstructionItem[],
  knowledge:    KnowledgeItem[],
  connectors:   ConnectorItem[],
}

Item-level highlights:

  • SkillItem carries name, optional title, description, content (markdown body), scope ("workspace" | "project"), optional version and models[].
  • RuleItem has name, description, content, filePatterns[] (globs), and an optional alwaysApply flag (matches Cursor's alwaysApply semantics).
  • InstructionItem is { filename, content }.
  • KnowledgeItem allows inline content for small files; payloads above 256 KiB MUST use a sidecar path reference under the Standards Zip codec.
  • ConnectorItem is an MCP connector descriptor. It deliberately does not carry credentials. It declares type, transport (stdio | http | sse), public config, a requires[] list of secrets the installer must supply (env, oauth, apiKey, url), and capabilities[].

The schema enforces sane bounds on string lengths and array sizes so a malicious bundle cannot ship a 50 MB JSON payload.

Permissions

computePermissions(manifest) returns a human-readable summary for the install dialog. It is pure, so the web can call it before sending the bundle to the API.

import { computePermissions, requiresModeration } from "@doable/marketplace-bundle";

const entries = computePermissions(manifest);
// PermissionEntry[]: sorted danger -> warn -> info, then alphabetical

if (requiresModeration(manifest)) {
  // marketplace flow gates "List" -> "Pending review"
}

PermissionScope values:

Scope Severity When it fires
skills.read info manifest.skills.length > 0
rules.read info manifest.rules.length > 0
knowledge.read info manifest.knowledge.length > 0
connectors.network info Connector transport is http or sse, or its type is in {http, fetch, github, linear, slack, notion, stripe}
connectors.filesystem info Connector type is in {filesystem, fs, git}
connectors.shell danger Connector type is in {shell, bash, exec, process}
connectors.thirdParty warn Connector classification is none of the above (unknown vendor)
credentials.required warn Any connector has requires[].required === true

requiresModeration(manifest) returns true iff the bundle includes any connector whose type is not one of the first-party set: filesystem, github, linear, slack. The marketplace flow uses this to route uploads to admin review automatically.

Subpath exports

The package.json exposes named subpath imports:

  • @doable/marketplace-bundle aggregate index
  • @doable/marketplace-bundle/codecs/json-v1
  • @doable/marketplace-bundle/codecs/standards-zip
  • @doable/marketplace-bundle/manifest
  • @doable/marketplace-bundle/permissions
  • @doable/marketplace-bundle/* for arbitrary src/*.ts

Use the deep imports when you want to keep the bundle dependency graph minimal (for example, the permissions module alone has no fflate dependency).

Source

  • packages/marketplace-bundle/src/manifest.ts (Zod schemas)
  • packages/marketplace-bundle/src/permissions.ts (compute + moderation gate)
  • packages/marketplace-bundle/src/codecs/json-v1.ts (JSON codec)
  • packages/marketplace-bundle/src/codecs/standards-zip.ts (Standards Zip codec)
  • Related: User Guide: Marketplace, User Guide: Custom Skills and MCP