@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¶
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, optionaltitle,description,content(markdown body),scope("workspace" | "project"), optionalversionandmodels[]. - RuleItem has
name,description,content,filePatterns[](globs), and an optionalalwaysApplyflag (matches Cursor'salwaysApplysemantics). - InstructionItem is
{ filename, content }. - KnowledgeItem allows inline
contentfor small files; payloads above 256 KiB MUST use a sidecarpathreference under the Standards Zip codec. - ConnectorItem is an MCP connector descriptor. It deliberately does not carry credentials. It declares
type,transport(stdio|http|sse), publicconfig, arequires[]list of secrets the installer must supply (env, oauth, apiKey, url), andcapabilities[].
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-bundleaggregate 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 arbitrarysrc/*.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