diff --git a/.changeset/readmes-republish.md b/.changeset/readmes-republish.md new file mode 100644 index 0000000..3a70613 --- /dev/null +++ b/.changeset/readmes-republish.md @@ -0,0 +1,26 @@ +--- +"@cdr-kit/cli": patch +"@cdr-kit/agent": patch +"@cdr-kit/contracts": patch +"@cdr-kit/core": patch +"@cdr-kit/tools": patch +"@cdr-kit/mcp": patch +"@cdr-kit/react": patch +"@cdr-kit/react-ui": patch +"@cdr-kit/vercel-ai": patch +"@cdr-kit/openai": patch +"@cdr-kit/langchain": patch +"@cdr-kit/agentkit": patch +"@cdr-kit/goat": patch +"create-cdr-kit-app": patch +--- + +Republish to flush the rewritten READMEs to npm. + +Every package's README on npm was still the old short version (e.g. `@cdr-kit/cli` showed 29 bytes; the source has been 4886 bytes since the README rewrite landed). Bumping a patch on each so a single `pnpm release` flushes the new content. **No code changes** — the dist is byte-identical to the last publish; only the README in each tarball updates. + +Verified before the bump: +- `@cdr-kit/cli`: source 4886b vs npm 29b +- `@cdr-kit/agent`: source 3790b vs npm 1657b +- `@cdr-kit/react`: source 4544b vs npm 1229b +- ...and 11 more in the same shape. diff --git a/README.md b/README.md index 30a652c..712db97 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ A standard library of 9 deployed CDR condition contracts + a typed TS SDK + a Re > **Status: 0.6.0 live on npm + Aeneid.** 15 packages published to [npmjs.com/org/cdr-kit](https://www.npmjs.com/org/cdr-kit). 9 conditions deployed on Story Aeneid (5 base + 4 advanced shipped in 0.5). 110 Foundry tests + workspace lint/typecheck/build/test all green. Dashboard live at [cdrkit.xyz](https://cdrkit.xyz) (built from `apps/site/`). The full encrypt→write→read→decrypt round-trip, the dual-path MultiSig flow, the Story IP creator path (`agent.publish()`), and an autonomous LLM-driven agent paying + reading via MCP tools all run end-to-end on real chain. +## Use the skills from any agent (Claude Code · Cursor · Copilot · Cline · …) + +``` +npx skills add Blockchain-Oracle/cdr-kit +``` + +Installs the cdr-kit Agent Skills via [skills.sh](https://skills.sh). + ## Install (any of these — pick the surface you want) ```bash diff --git a/apps/site/components/docs/roadmap-table.css b/apps/site/components/docs/roadmap-table.css new file mode 100644 index 0000000..1ec9b6a --- /dev/null +++ b/apps/site/components/docs/roadmap-table.css @@ -0,0 +1,125 @@ +/* RoadmapTable — list of cards with status-coded left borders. */ + +.rm { + display: grid; + grid-template-columns: 1fr; + gap: 14px; + margin: 22px 0 30px 0; + padding: 0; + list-style: none; +} + +.rm-card { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 18px 20px 18px 22px; + background: var(--card); + border: 1px solid var(--line); + border-left: 3px solid var(--line-2); + border-radius: 10px; + scroll-margin-top: 90px; + transition: border-color 0.14s, transform 0.12s, box-shadow 0.14s; +} +.rm-card:hover { + transform: translateY(-1px); + box-shadow: 0 6px 22px -14px color-mix(in oklab, var(--ink) 22%, transparent); +} +.rm-card:target { + box-shadow: + 0 0 0 3px color-mix(in oklab, var(--primary) 18%, transparent), + 0 6px 22px -14px color-mix(in oklab, var(--ink) 22%, transparent); +} + +.rm-card--next { border-left-color: var(--primary, oklch(0.66 0.18 30)); } +.rm-card--queued { border-left-color: oklch(0.66 0.16 268); } +.rm-card--exploring { border-left-color: var(--line-2); } + +.rm-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.rm-status { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 4px 10px 4px 8px; + font-family: var(--mono); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + border-radius: 999px; + border: 1px solid var(--line); + background: var(--paper-2); + color: var(--ink-2); +} +.rm-statusdot { + width: 7px; + height: 7px; + border-radius: 999px; + background: currentColor; + box-shadow: 0 0 0 3px color-mix(in oklab, currentColor 18%, transparent); +} + +.rm-dot--next { color: var(--primary, oklch(0.66 0.18 30)); } +.rm-dot--next .rm-statusdot { background: var(--primary, oklch(0.66 0.18 30)); } + +.rm-dot--queued { color: oklch(0.6 0.18 268); } +.rm-dot--queued .rm-statusdot { background: oklch(0.6 0.18 268); } + +.rm-dot--exploring { color: var(--ink-3); } +.rm-dot--exploring .rm-statusdot { background: var(--ink-3); } + +.rm-anchor { + font-family: var(--mono); + font-size: 0.86rem; + color: var(--ink-3); + text-decoration: none; + padding: 2px 8px; + border-radius: 6px; + transition: color 0.14s, background-color 0.14s; + opacity: 0; +} +.rm-card:hover .rm-anchor, +.rm-card:focus-within .rm-anchor { opacity: 1; } +.rm-anchor:hover { color: var(--ink); background: var(--paper-2); } + +.rm-title { + margin: 0; + font-family: var(--display, inherit); + font-size: 1.08rem; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--ink); +} + +.rm-body { + margin: 0; + font-size: 0.92rem; + line-height: 1.55; + color: var(--ink-2); +} +.rm-body code { + font-family: var(--mono); + font-size: 0.88em; + padding: 1px 6px; + border-radius: 5px; + background: var(--paper-2); + border: 1px solid var(--line); + color: var(--ink); +} + +@media (min-width: 720px) { + .rm { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 540px) { + .rm-card { padding: 16px 16px 16px 18px; } + .rm-title { font-size: 1rem; } + .rm-body { font-size: 0.88rem; } + .rm-anchor { opacity: 1; } +} diff --git a/apps/site/components/docs/roadmap-table.tsx b/apps/site/components/docs/roadmap-table.tsx new file mode 100644 index 0000000..968d564 --- /dev/null +++ b/apps/site/components/docs/roadmap-table.tsx @@ -0,0 +1,66 @@ +import type { ReactNode } from "react"; +import "./roadmap-table.css"; + +export type RoadmapStatus = "next" | "queued" | "exploring"; + +export interface RoadmapRow { + /** Stable anchor id — used as the row's ``. */ + id: string; + status: RoadmapStatus; + title: ReactNode; + body: ReactNode; +} + +interface RoadmapTableProps { + rows: RoadmapRow[]; +} + +const STATUS_META: Record< + RoadmapStatus, + { label: string; dotClass: string; cardClass: string } +> = { + next: { label: "next", dotClass: "rm-dot--next", cardClass: "rm-card--next" }, + queued: { label: "queued", dotClass: "rm-dot--queued", cardClass: "rm-card--queued" }, + exploring: { + label: "exploring", + dotClass: "rm-dot--exploring", + cardClass: "rm-card--exploring", + }, +}; + +/** + * RoadmapTable — surface used by `/docs/roadmap`. Each row is a self-contained + * card with a status pill (next / queued / exploring), title, and body copy. + * + * Visual goals: + * - Status pill colour anchors the row's left border so the page scans + * vertically — you can see at a glance how much is `next` vs `exploring`. + * - Anchor id on every row so we can deep-link from issues / changelog + * entries (`/docs/roadmap#ratelimit-condition`). + * - No external CSS framework — uses the same `--cdr-*` tokens as the rest + * of the docs site for light/dark parity. + */ +export function RoadmapTable({ rows }: RoadmapTableProps) { + return ( + + ); +} diff --git a/apps/site/components/docs/sidebar.tsx b/apps/site/components/docs/sidebar.tsx index ecddb75..3c8bb7f 100644 --- a/apps/site/components/docs/sidebar.tsx +++ b/apps/site/components/docs/sidebar.tsx @@ -23,6 +23,7 @@ export const SIDEBAR: SideGroup[] = [ { href: "/docs", label: "Introduction" }, { href: "/docs/quickstart", label: "Quickstart" }, { href: "/docs/theming", label: "Theming" }, + { href: "/docs/roadmap", label: "Roadmap" }, ], }, { diff --git a/apps/site/content/docs/roadmap.mdx b/apps/site/content/docs/roadmap.mdx new file mode 100644 index 0000000..8a86be4 --- /dev/null +++ b/apps/site/content/docs/roadmap.mdx @@ -0,0 +1,138 @@ +--- +title: Roadmap +description: What ships next. Categorized by surface — conditions, storage adapters, agent kit, scaffolder, forms, dashboard. Excludes mainnet (CDR is still testnet-only). +breadcrumb: ["cdr-kit", "Roadmap"] +prev: + href: /docs + label: Introduction +next: + href: /docs/quickstart + label: Quickstart +--- + +cdr-kit ships in two-week cycles. This page is the live picture of what's in flight, what's queued, and what we've explicitly ruled out. Each item carries a confidence tag: `next` (in the upcoming release), `queued` (next 1–2 cycles), `exploring` (we know it's needed, design isn't locked yet). + + + **Mainnet support is out of scope** until Story Protocol promotes CDR off testnet. Every roadmap item below targets Aeneid (chain 1315). When mainnet lands we'll cut a 1.0 release and migrate the address registry in `@cdr-kit/contracts` in one commit; nothing else needs to change. + + +## Condition contracts + +The standard library has 9 deployed conditions today. The roadmap focuses on patterns that real CDR consumers keep reinventing. + + + +## Storage adapters + +`@cdr-kit/core` ships 8 adapters. The roadmap fills the obvious ecosystem holes. + + + +## Framework adapters + +5 agent-kit adapters today (Vercel AI · OpenAI · LangChain · AgentKit · GOAT). Mostly chasing where agents are actually being built. + + + +## Scaffolder templates + +5 templates today (starter · blog · paywall · data-marketplace · forms). The roadmap targets shippable mini-products, not category showcases. + + + +## @cdr-kit/forms + +The new package (0.7.2) has the picker. Forms still needs the obvious gaps. + +1 KB blobs. Handles chunking + progress reporting + drag-drop." }, + { id: "conditional-fields", status: "queued", title: "Conditional fields", body: "Show / hide fields based on other field values (`when={{ field: 'plan', equals: 'pro' }}`). Standard form-builder primitive; nothing CDR-specific but expected." }, + { id: "multi-step", status: "queued", title: "Multi-step wizards", body: " wrapper for breaking long forms into screens. Each step's fields encrypted together, persisted to localStorage between steps." }, + { id: "validation-zod", status: "queued", title: "Schema validation (Zod)", body: " for typed validation before encryption. Surfaces errors per-field via the existing context." }, + { id: "rich-text", status: "exploring", title: "Rich-text answer field", body: "Tiptap-style answer field for long-form responses (interviews, applications). Sanitisation + serialised JSON storage." }, +]} /> + +## Dashboard surface + +Today's docs site doubles as the dashboard. Splitting the dashboard into its own route family is in flight. + + + +## Agent kit + +The 34-tool surface is stable. Next moves are around orchestration. + + + +## Story IP integration + +`@cdr-kit/story` covers register-IP + attach-PIL + mint-license-token + register-derivative. The wrappers below fill the rest of Story's surface. + + + +## Distribution + +How consumers find + install the CLI / scaffolder. + + + +## Performance + DX + +Smaller bundles, faster build, fewer footguns. + +` entries." }, + { id: "playground", status: "queued", title: "TypeScript playground", body: " on cdrkit.xyz — try the SDK in-browser against mock CDR. Lowers the bar from 'install + clone' to 'click and run'." }, + { id: "viem-2.x-2.y", status: "exploring", title: "Stay on viem's stable channel", body: "viem moves fast. Track the major / dual-publish at every minor so consumers don't get caught by peer-dep drift." }, +]} /> + +## Out of scope (deliberately) + +- **Mainnet** — CDR isn't on mainnet yet; cdr-kit will follow. +- **Custom precompile** — we wrap CDR, we don't replace it. If you want a different threshold scheme, fork [`@piplabs/cdr-sdk`](https://github.com/piplabs/cdr-sdk). +- **Whitelabel dashboard** — the dashboard at cdrkit.xyz is for demos, not for hosting other people's data marketplaces. Use `create-cdr-kit-app --template data-marketplace`. +- **Storage gateway** — we don't host a CDN. Every adapter is "you bring the credentials". Self-hosted only. +- **AI agent for sale** — cdr-kit ships the toolkit, not a managed agent. If you want a hosted CDR agent, deploy the `vercel-ai-chatbot` example. + +## How to influence this list + +- 👍 on a [GitHub issue](https://github.com/Blockchain-Oracle/cdr-kit/issues) bumps it up. +- A PR with a working draft beats every prioritization argument. +- Sellers actually using CDR on Aeneid for real revenue get priority — drop a note in the issue describing your use-case. diff --git a/apps/site/content/docs/skill.mdx b/apps/site/content/docs/skill.mdx index 48ab97a..779f0da 100644 --- a/apps/site/content/docs/skill.mdx +++ b/apps/site/content/docs/skill.mdx @@ -10,22 +10,39 @@ next: label: Contracts --- -## Install (three paths) +## Install (four paths) - -## Skills (8) + +**How `skills.sh` actually packages cdr-kit** — the CLI treats the whole GitHub repo as the **package** (`Blockchain-Oracle/cdr-kit`), then walks the repo's top-level `skills/` directory to discover each individual **skill** inside it. So `npx skills add Blockchain-Oracle/cdr-kit` installs **all 11** by default; `--skill ` picks one or more, `--skill '*'` is shorthand for all, and `--all` installs every skill into every detected agent. + +The CLI clones the repo into a cache, copies (or symlinks with `--copy` off) the matching `SKILL.md` files into your agent's skill folder (e.g. `~/.claude/skills//`), and writes a `skills-lock.json` next to your project so re-installs are reproducible. Update everything later with `npx skills update`. + +This means cdr-kit ships **once** (one repo, one badge) but each consumer can pull in only the skills they actually use — same model as a JS monorepo. + + +## Skills (11) diff --git a/apps/site/lib/mdx-components.tsx b/apps/site/lib/mdx-components.tsx index ae6c874..cef1305 100644 --- a/apps/site/lib/mdx-components.tsx +++ b/apps/site/lib/mdx-components.tsx @@ -30,6 +30,7 @@ import { } from "@/components/docs/demos/headless-demos"; import { CdrFormDemo } from "@/components/gallery/cdr-form-demo"; import { StorageProviderPickerDemo } from "@/components/gallery/storage-provider-picker-demo"; +import { RoadmapTable } from "@/components/docs/roadmap-table"; /** * Components available to every MDX page without per-file imports. @@ -60,5 +61,6 @@ export function getMDXComponents(): MDXComponents { EmptyVaultsDemo, CdrFormDemo, StorageProviderPickerDemo, + RoadmapTable, }; } diff --git a/packages/agent/package.json b/packages/agent/package.json index c3b6434..d66d5d3 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/agent", - "version": "0.7.1", + "version": "0.7.2", "description": "Autonomous agent client for Story CDR: discover -> subscribe -> access vaults from its own wallet.", "type": "module", "sideEffects": false, diff --git a/packages/agentkit/package.json b/packages/agentkit/package.json index d212ccb..0e7e2f7 100644 --- a/packages/agentkit/package.json +++ b/packages/agentkit/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/agentkit", - "version": "0.7.0", + "version": "0.7.1", "description": "Coinbase AgentKit adapter for cdr-kit — getCdrActionProvider(agent).", "type": "module", "sideEffects": false, diff --git a/packages/cli/package.json b/packages/cli/package.json index 3308570..0494f96 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/cli", - "version": "0.7.0", + "version": "0.7.1", "description": "The cdr CLI + MCP server factory + auto-wallet + Claude Code skill installer. One binary, three surfaces (CLI / MCP / skill).", "type": "module", "bin": { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 830ab81..eccae05 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/contracts", - "version": "0.7.1", + "version": "0.7.2", "description": "Typed ABIs + verified addresses for cdr-kit contracts and Story CDR.", "type": "module", "sideEffects": false, diff --git a/packages/core/package.json b/packages/core/package.json index 802af18..870b918 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/core", - "version": "0.7.1", + "version": "0.7.2", "description": "Typed TS SDK for Story CDR: condition encoders, the CdrKitVault flow, and the 2-step pay->access helpers.", "type": "module", "sideEffects": false, diff --git a/packages/create-cdr-kit-app/package.json b/packages/create-cdr-kit-app/package.json index d2576c1..0ad266f 100644 --- a/packages/create-cdr-kit-app/package.json +++ b/packages/create-cdr-kit-app/package.json @@ -1,6 +1,6 @@ { "name": "create-cdr-kit-app", - "version": "0.7.1", + "version": "0.7.2", "description": "Scaffold a runnable cdr-kit starter (mock mode — no wallet/chain needed).", "type": "module", "bin": { diff --git a/packages/goat/package.json b/packages/goat/package.json index d821167..b3fe81c 100644 --- a/packages/goat/package.json +++ b/packages/goat/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/goat", - "version": "0.7.0", + "version": "0.7.1", "description": "GOAT SDK adapter for cdr-kit — getGoatTools(agent) / cdrPlugin.", "type": "module", "sideEffects": false, diff --git a/packages/langchain/package.json b/packages/langchain/package.json index 4e96e58..e80b215 100644 --- a/packages/langchain/package.json +++ b/packages/langchain/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/langchain", - "version": "0.7.0", + "version": "0.7.1", "description": "LangChain adapter for cdr-kit — getLangChainTools(agent).", "type": "module", "sideEffects": false, diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 29787c5..5e89b61 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/mcp", - "version": "0.7.0", + "version": "0.7.1", "description": "Stdio MCP server for Story CDR — wrapper around @cdr-kit/cli so `npx @cdr-kit/mcp` Just Works in any MCP host.", "type": "module", "bin": { diff --git a/packages/openai/package.json b/packages/openai/package.json index 46c25c9..1eba11c 100644 --- a/packages/openai/package.json +++ b/packages/openai/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/openai", - "version": "0.7.0", + "version": "0.7.1", "description": "OpenAI / Anthropic tool-calling adapter for cdr-kit — JSON-Schema tools[] + a dispatch router.", "type": "module", "sideEffects": false, diff --git a/packages/plugin/README.md b/packages/plugin/README.md index 103a857..593c58f 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -2,6 +2,14 @@ > 11 skills + 2 reference docs that teach Claude *how* to design, wire, and debug around Story Confidential Data Rails. +## Use the skills from any agent (Claude Code · Cursor · Copilot · Cline · …) + +``` +npx skills add Blockchain-Oracle/cdr-kit +``` + +Installs the cdr-kit Agent Skills via [skills.sh](https://skills.sh). + This is the plugin source. It's bundled inside `@cdr-kit/cli` and installed by: ```bash diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index e31524e..c767fe8 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/react-ui", - "version": "0.7.0", + "version": "0.7.1", "description": "Styled component variants for @cdr-kit/react — ConditionBadge, AccessStepper, SubscribeButton, VaultCard + DX primitives. CSS-variable themed; no Tailwind, no lucide, no framer-motion hard deps.", "type": "module", "sideEffects": [ diff --git a/packages/react/package.json b/packages/react/package.json index 4309dbd..aac6cf5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/react", - "version": "0.7.0", + "version": "0.7.1", "description": "React provider, , and hooks for Story CDR — built on @cdr-kit/core.", "type": "module", "sideEffects": [ diff --git a/packages/tools/package.json b/packages/tools/package.json index 1f6a34b..f5c9cba 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/tools", - "version": "0.7.0", + "version": "0.7.1", "description": "Framework-agnostic CDR agent tools ({name, description, Zod schema, invoke}) — the single source mapped to MCP + every agent framework.", "type": "module", "sideEffects": false, diff --git a/packages/vercel-ai/package.json b/packages/vercel-ai/package.json index 28fdb92..c52bef1 100644 --- a/packages/vercel-ai/package.json +++ b/packages/vercel-ai/package.json @@ -1,6 +1,6 @@ { "name": "@cdr-kit/vercel-ai", - "version": "0.7.0", + "version": "0.7.1", "description": "Vercel AI SDK adapter for cdr-kit — getVercelAITools(agent).", "type": "module", "sideEffects": false, diff --git a/skills/audit-vault-config/SKILL.md b/skills/audit-vault-config/SKILL.md new file mode 100644 index 0000000..6904792 --- /dev/null +++ b/skills/audit-vault-config/SKILL.md @@ -0,0 +1,82 @@ + +--- +name: audit-vault-config +description: Audit an existing CDR vault's on-chain config — verify read/write conditions, condition data, owner, license terms, and subscription plan match the intended design. Use this skill when (1) the user wants to vet someone else's vault before subscribing to it; (2) the user wants to verify their own deployment matches their intent; (3) the user asks "is vault X subscription-gated / what's the price / who owns it / what IP does it gate"; (4) the user pastes a uuid and asks "should I trust this". +license: MIT +--- + +# Audit a CDR vault's on-chain config + +You can't decrypt a vault's content without paying / qualifying, but you can verify every aspect of its access control + economics for free. This skill is for "should I subscribe to this?" or "did my deployment configure correctly?" questions. + +## When to use + +- "Is vault X subscription-gated? What's the price?" +- "Who owns vault X? What IP asset is it tied to?" +- "Does vault X's read condition do what its README claims?" +- "I deployed a vault — verify the on-chain state matches my intent." +- "What's the difference between this vault's licenseTermsId and another?" + +## The 4-call audit (all view-only) + +```ts +import { CdrAgent } from "@cdr-kit/agent"; +const agent = new CdrAgent({ privateKey: anyTestnetKey, network: "aeneid" }); +// (privateKey unused for view-only reads, but agent requires one for typing — use a throwaway) + +// 1. Factory metadata: tokenId, ipId, creator, licenseTermsId +const info = await agent.getVaultInfo(uuid); +// → { uuid, tokenId, ipId, creator, licenseTermsId } + +// 2. Subscription plan (if subscription-gated) +const plan = await agent.getSubscriptionPlan(uuid).catch(() => null); +// → { pricePerPeriodWei, periodSeconds, payee, mode (0=IP, 1=WIP), licensorIpId } + +// 3. Your own entitlement against this vault +const ent = await agent.getEntitlement(uuid); +// → { paidUntilUnix, isEntitled } + +// 4. Raw vault record (encryptedData hex, write tx, etc.) +const raw = await agent.getVaultRecord(uuid); +``` + +**One-shot:** `cdr vault info ` runs calls 1+2+3 and prints them as one JSON object. + +## What each field tells you + +| Field | Audit signal | +|---|---| +| `info.creator` | The original minter — must match what the listing claims. Cross-check against a known-good profile. | +| `info.ipId` | The Story IP asset — verify it's registered on `IPAssetRegistry` (call `IPAssetRegistry.isRegistered(ipId)`) and look up its license terms. | +| `info.licenseTermsId` | The PIL terms attached — pull from `LicenseRegistry` to inspect commercial use, revenue share, royalty policy. | +| `plan.payee` | Where subscription fees flow — should be the vault creator OR a known royalty-vault address. Anything else is a red flag. | +| `plan.pricePerPeriodWei` | Convert to IP (`formatEther`) — does the headline price match? | +| `plan.periodSeconds` | 86400=daily, 604800=weekly, 2592000≈monthly. Unusual values are suspicious. | +| `plan.mode` | `0` = native IP, `1` = WIP (wrapped IP ERC20). WIP means the reader needs `wipClient.deposit + approve` first. | +| `ent.paidUntilUnix` | If `> now`, you're already entitled — call `access` not `subscribeAndAccess`. | + +## Audit checks (red flags) + +1. **`plan.payee` is NOT `info.creator` or a known royalty contract** — payments leak to a third party. +2. **`plan.pricePerPeriodWei` ≪ headline price** — pricing was misconfigured during deployment; reader pays less than advertised. (Not a security issue, but the seller will be surprised.) +3. **`info.licenseTermsId === 0`** — no PIL terms attached. The vault is decoupled from Story IP licensing; reader gets data but not licensed rights. +4. **`plan.periodSeconds === 0`** — subscription never expires. Almost certainly a misconfig. +5. **`info.ipId` registered to a different creator than `info.creator`** — possible front-run / squatter. Verify ipId ownership in `IPAssetRegistry`. + +## CLI alternative + +```bash +cdr vault info --json | jq '{creator: .info.creator, price_ip: (.plan.pricePerPeriodWei | tonumber / 1e18), period_days: (.plan.periodSeconds | tonumber / 86400), payee: .plan.payee}' +``` + +## What this skill does NOT do + +- It doesn't decrypt content. Decryption requires the read condition to pass (subscription, license, etc.). +- It doesn't audit the condition contract's source code. The deployed addresses in `@cdr-kit/contracts.aeneid` are the canonical ones; if a vault uses a different `readConditionAddr`, that's a flag — but you'd need to verify the source out-of-band. +- It doesn't simulate "will my access call succeed?" — the read condition's `checkReadCondition` view is the closest, but it's read-only and free; the protocol calls it at read time. + +## See also + +- `design-condition` — what conditions the audit can encounter +- `wire-allocate-pay-read` — how to actually consume after the audit passes +- `references/conditions-cheatsheet.md` — interpret `readConditionData` for each condition diff --git a/skills/debug-cdr-precompile/SKILL.md b/skills/debug-cdr-precompile/SKILL.md new file mode 100644 index 0000000..a78cc64 --- /dev/null +++ b/skills/debug-cdr-precompile/SKILL.md @@ -0,0 +1,105 @@ + +--- +name: debug-cdr-precompile +description: Diagnose CDR precompile failures and reverts. Use this skill when (1) a CDR transaction reverts with OOG / ReentrancySentryOOG / AlreadyConfigured; (2) a read hangs or partial-collection times out; (3) the user hits "WASM not initialized" / LabelMismatch / ContentSizeExceeded; (4) checkReadCondition returns false on a uuid the user thinks should pass. +license: MIT +--- + +# Debug a CDR precompile failure + +CDR's on-chain interface is a **precompiled contract** at `0xCCCcCC0000000000000000000000000000000005` with non-standard gas characteristics. Five failure modes account for ~95% of issues. This skill walks each, with the fix. + +## When to use + +- "My createVault tx reverted with OutOfGas / OOG" +- "I got `ReentrancySentryOOG`" +- "`AlreadyConfigured` error" +- "Read returns empty bytes / hangs forever / partial collection times out" +- "`WASM not initialized` runtime error" +- "Read condition view returns false but I think it should return true" + +## Failure mode catalog + +### 1. `OutOfGas` / `OOG` on createVault / allocate / read + +**Symptom:** Tx reverts with `OutOfGas`, sometimes wrapped as `ReentrancySentryOOG`. + +**Root cause:** `eth_estimateGas` does NOT correctly estimate the gas cost of the CDR precompile's nested calls. It returns a value 50-80% too low, and the actual run runs out. + +**Fix:** +- If using `@cdr-kit/core`: `createVault` already sets `gas: 3M` (see `packages/core/src/flows.ts`). Don't override. +- If using raw cast/forge: set an explicit `--gas-limit 2000000` or higher. `@cdr-kit/contracts` exports `CDR_GAS_LIMIT = 2_000_000n` as the safe minimum. +- Never trust `eth_estimateGas` for CDR txs. + +### 2. `AlreadyConfigured` + +**Symptom:** Tx reverts with `AlreadyConfigured` when you call `setConfigFromFactory` or try to re-configure an existing vault. + +**Root cause:** CDR vault configs are **factory-only + atomic** (decision-log D1–D5). Once a uuid has a read+write condition set, it cannot be re-set. This is intentional to close a front-run window — never add a permissionless re-register path. + +**Fix:** Allocate a new vault (`createVault`). The kit's `CdrKitVault.createVault` allocates + configures in one tx — this is the only correct path. + +### 3. Unconfigured uuid → condition view returns `false` + +**Symptom:** `checkReadCondition` returns false for a uuid you just allocated, even though the caller satisfies the condition. + +**Root cause:** Every condition `view` returns `false` (never reverts) when the uuid hasn't been configured yet. Configuration is part of `createVault`'s atomic tx. + +**Fix:** +- Verify the uuid was created via `CdrKitVault.createVault`, not a raw `allocate()` against the precompile. +- `agent.getVaultInfo(uuid)` returns null for never-created uuids — use this as a precondition check. + +### 4. `WASM not initialized` runtime error + +**Symptom:** `Error: CDR crypto WASM is not initialized` thrown during encrypt or decrypt. + +**Root cause:** `@piplabs/cdr-crypto` needs `initWasm()` called once before any TDH2 operation. Forgotten in custom code that reaches around `@cdr-kit/core`. + +**Fix:** +- Stay in `@cdr-kit/core`'s flows — they call `ensureWasm()` for you. +- If you must use `@piplabs/cdr-sdk` directly: `import { initWasm } from "@piplabs/cdr-sdk"; await initWasm();` at app startup. +- In React: do this in a top-level effect or provider. `CdrProvider` from `@cdr-kit/react` already handles it. + +### 5. `PartialCollectionTimeoutError` / read hangs + +**Symptom:** Read promise never resolves; eventually `PartialCollectionTimeoutError` after the timeout. + +**Root cause:** Validators didn't return enough partial decryptions before `timeoutMs`. Causes (in order of likelihood): +1. `apiUrl` wrong/unreachable — the SDK polls Story-API REST for partial decryptions +2. Read condition reverted — validators won't produce partials for an unauthorized read +3. Validator network slow / under-quorum +4. `timeoutMs` too low (rare with our 120_000 default) + +**Fix:** +- Retry the read (`agent.access(uuid)` again) — first attempt failures are common. +- Verify `apiUrl` — `cdr config` prints the resolved value. +- Verify the condition: `agent.getEntitlement(uuid)` for subscription-gated; for license-gated check `LicenseToken.ownerOf(tokenId) === agent.address` via viem. +- If still failing, bump `timeoutMs`: `agent.access(uuid, accessAuxData)` doesn't expose timeoutMs — drop to the low-level `accessVault(client, { uuid, timeoutMs })` from `@cdr-kit/core`. + +### 6. `ContentSizeExceededError` + +**Symptom:** `agent.writeVaultData` throws `CONTENT_SIZE_EXCEEDED`. + +**Root cause:** Inline payload exceeds the `maxEncryptedDataSize` (1024 bytes on Aeneid; TDH2 overhead reduces effective plaintext to ~960 bytes). + +**Fix:** Use `agent.uploadFile({ content, addUrl, gatewayUrl })` — encrypts + IPFS-pins + writes only the CID+key reference (small) to the vault. + +### 7. `LabelMismatchError` + +**Symptom:** Decryption fails with `LABEL_MISMATCH`. + +**Root cause:** Ciphertext was encrypted with a label that doesn't match the vault's uuid. Labels bind ciphertext to its vault. + +**Fix:** Use `uuidToLabel(uuid)` from `@piplabs/cdr-sdk` when constructing labels — but `@cdr-kit/core`'s `writeVaultData` does this automatically. Don't roll your own. + +## CLI alternatives + +- `cdr fees` — verify `apiUrl` reachability (this call goes through Story-API REST) +- `cdr vault info ` — confirm a uuid is created and inspect its condition +- `cdr access --json` — get the typed error structure on failure + +## Where to look next + +- `wire-allocate-pay-read` — the canonical 4-step flow that avoids most of these errors +- `references/error-catalog.md` — full taxonomy of `CdrErrorCode`s + the SDK error they map from +- `https://docs.story.foundation/developers/cdr-sdk/encrypt-and-decrypt` — the error class table in the docs diff --git a/skills/design-condition/SKILL.md b/skills/design-condition/SKILL.md new file mode 100644 index 0000000..b1283cd --- /dev/null +++ b/skills/design-condition/SKILL.md @@ -0,0 +1,107 @@ + +--- +name: design-condition +description: Pick the right CDR access-control condition for a vault use case. Use this skill when (1) the user is designing a new CDR vault and asks "which condition should I use"; (2) the user wants to gate reads behind a paywall, license, NFT/token tier, or composite AND/OR rule; (3) the user asks how to encode readConditionData for a specific condition; (4) the user is choosing between subscription / tier-gate / composable / open / license-gated. +license: MIT +--- + +# Design a CDR access condition + +CDR vaults gate decryption with two condition contracts: a **read condition** and a **write condition**. Picking the right one up-front beats refactoring once data is encrypted. This skill walks the decision tree. + +## When to use + +- "Which CDR condition should I use for X?" +- "I want to gate reads behind a paywall / a license / a tier / an EOA / a custom check." +- "What does composable condition give me?" +- "How do I encode `conditionData` for the X condition?" + +## The 5 deployed conditions on Aeneid + +| Condition | What it gates | Typical use | +|---|---|---| +| `openCondition` | Anyone can read (write is always condition-controlled) | Public hints; teasers; gated reveal where the gate is a different vault | +| `subscriptionCondition` | Caller must have an active `paidUntil[uuid][addr] > now` | Substack/Patreon paywall, periodic data feeds, intel reports | +| `tierGateCondition` | Caller must hold an NFT/ERC20 token at min balance | "Subscribers", "VIP" tiers, allowlist-by-NFT | +| `composableCondition` | Boolean OR/AND of N child conditions | "Hold token X **AND** subscribed to vault Y", "Subscribed **OR** holds license" | +| `licenseReadCondition` | Caller must own a Story PIL license token for the vault's IP | The Story-native pattern — license-as-payment, royalties via PIL terms | +| `creatorWriteCondition` | Only the creator (vault NFT owner) can write | Default write condition for almost every flow | +| `ownerWriteCondition` | Only a hardcoded EOA can write | Demo / one-shot vaults | + +Address per condition: see `@cdr-kit/contracts`'s `aeneid` constant + `references/conditions-cheatsheet.md`. + +## Decision tree + +``` +What gates the READ? +├─ Everyone → openCondition (with readConditionData = "0x") +├─ Pay per period (recurring) → subscriptionCondition (price + period encoded in setConfigFromFactory) +├─ Pay once for a Story license → licenseReadCondition (mint license token → use tokenId as accessAuxData) +├─ Hold a token (NFT/ERC20) → tierGateCondition (token addr + min balance) +└─ Composite (AND/OR multiple) → composableCondition (children + AND/OR mask) + +What gates the WRITE? +├─ Only the creator (default) → creatorWriteCondition +├─ Only one EOA → ownerWriteCondition +└─ Custom → deploy your own condition impl +``` + +## Encoding `readConditionData` per condition + +Done in code via `encodeAbiParameters`: + +- **openCondition** → `"0x"` (no data) +- **subscriptionCondition** → factory's `setConfigFromFactory` writes `(pricePerPeriod, period, payee, mode, licensorIpId)` — handled inside `CdrKitVault.createVault`; you pass `readConditionData: encodeAbiParameters([...], [...])`. See `references/condition-encoding.md` for the field layout per condition. +- **tierGateCondition** → `encodeAbiParameters([{type:"address"}, {type:"uint256"}], [tokenAddr, minBalance])` +- **composableCondition** → encoded child conditions list + AND/OR mask +- **licenseReadCondition** → `encodeAbiParameters([{type:"address"},{type:"address"}], [licenseTokenAddr, ipId])`. Reader passes their license tokenId(s) at read-time via `accessAuxData: abi.encode(uint256[] licenseTokenIds)`. + +`@cdr-kit/contracts` exports the deployed addresses; never hardcode. + +## CLI alternative + +- `cdr vault info ` — inspect the live condition + plan + your entitlement on an existing vault +- `cdr vault create --read --read-config ` — create with the picked condition +- `cdr fees` — fetch allocate/write/read fees before you commit (useful for cost-modeling subscription pricing) + +## Pitfalls (the load-bearing ones) + +1. **EOA conditions need `skipConditionValidation: true`.** The CDR contract bypasses the check when `msg.sender === conditionAddr`, so setting your EOA as `readConditionAddr` makes a vault owner-only — but the SDK's high-level `uploadCDR()` rejects this. Use the low-level `allocate()` path or our `agent.access()` path. +2. **`writeConditionAddr` defaults to `creatorWriteCondition` if you don't pass one** in `agent.createVault()`. Don't override unless you know why. +3. **`composableCondition` configs are not symmetric.** The order of children matters for some AND/OR encodings; see `references/composable-encoding.md`. + +## Examples + +A subscription-gated trading signal vault (paid weekly): +```ts +import { aeneid } from "@cdr-kit/contracts"; +import { encodeAbiParameters, parseEther } from "viem"; + +const readConfig = encodeAbiParameters( + [{ type: "uint256" }, { type: "uint256" }, { type: "address" }, { type: "uint8" }, { type: "address" }], + [parseEther("5"), 7n * 24n * 60n * 60n /* 7 days */, payee, 0 /* native IP */, ipId], +); +await agent.createVault({ + readConditionAddr: aeneid.subscriptionCondition, + readConfig, + licenseTermsId: ipLicenseTermsId, +}); +``` + +A license-gated PDF (Story-native pattern): +```ts +const readConfig = encodeAbiParameters( + [{ type: "address" }, { type: "address" }], + [aeneid.licenseToken, ipId], +); +await agent.createVault({ readConditionAddr: aeneid.licenseReadCondition, readConfig }); +// Then: agent.uploadFile({ content: pdfBytes, addUrl, gatewayUrl }) writes the file off-chain +// Reader: mintLicenseTokens (in 0.5) → agent.accessLicenseGated({ uuid, licenseTokenId }) +``` + +## See also + +- `wire-allocate-pay-read` — the 2-step pay→read pattern across all conditions +- `audit-vault-config` — verify an existing vault's condition + data on-chain +- `references/conditions-cheatsheet.md` — full ABI + encoding per condition +- `https://docs.story.foundation/developers/cdr-sdk/overview` — official CDR overview diff --git a/skills/design-deadman-switch/SKILL.md b/skills/design-deadman-switch/SKILL.md new file mode 100644 index 0000000..7bb189c --- /dev/null +++ b/skills/design-deadman-switch/SKILL.md @@ -0,0 +1,93 @@ + +--- +name: design-deadman-switch +description: Design a Story CDR DeadManSwitchCondition vault (auto-unlock to heirs or public after creator stops poke()-ing). Use this skill when (1) the user mentions dead man switch / deadman / heartbeat / leak-on-disappearance / wallet recovery / estate planning / journalist trapdoor / posthumous unlock; (2) the user asks "what happens if I stop responding" or "auto-publish after N days of silence"; (3) the user wants to design heir lists, public-after-unlock semantics, or block-vs-timestamp countdowns; (4) the user is wiring useDeadManTimer / HeartbeatTimer in the React layer. +license: MIT +--- + +# Design a CDR dead-man-switch vault + +`DeadManSwitchCondition` (deployed Aeneid `0x37226f97e184843aB0b8d4f08A55969801B97766`) auto-unlocks to a heir set (or public) if the creator stops `poke()`-ing within `duration`. Two phases: + +- **Locked** (`now < unlockAt`): only the creator can read, and only if `creatorCanReadWhileLocked = true`. +- **Unlocked** (`now >= unlockAt`): either public (`publicAfterUnlock = true`) or restricted to `heirs`. + +The trapdoor is **one-way** — once unlocked + read, plaintext is out forever. + +## When to use + +- "Encrypt-and-deadman a wallet's recovery phrase" +- "Auto-publish my data dump if I don't check in for 90 days" +- "Journalist source — leak if I'm arrested" +- "Estate planning — heirs get my password manager after 30 days of silence" +- "Founder runs the company, but if they're hit by a bus, the board unlocks the strategy doc" + +## Creator: register the vault + +```ts +const txHash = await agent.createDeadManVault({ + duration: 90n * 86400n, // 90 days in seconds + heirs: ["0xHeir1...", "0xHeir2..."], + blockBased: false, // use timestamps, not block.number + creatorCanReadWhileLocked: true, // creator reads their own vault pre-unlock + publicAfterUnlock: false, // restrict to heirs (NOT global public) + licenseTermsId: undefined, // not a license-gated vault +}); +``` + +Constraints: +- `duration > 0` (rejected at config) +- If `heirs.length == 0`, the contract forces `publicAfterUnlock = true` (no heirs + private = unreadable forever, which is never intent) + +Read the `uuid` from the `VaultCreated` event in the receipt. + +## Creator: keep poking + +```ts +await agent.pokeDeadMan(uuid); +// → resets unlockAt to now + duration +``` + +Constraints: +- Only the original creator can poke (`NotCreator` revert otherwise) +- Cannot poke after `unlockAt` has passed (`AlreadyUnlocked` revert — the trapdoor is one-way; reviving would let a creator block heirs forever by re-poking late) + +**The biggest operational risk is forgetting to poke.** Mitigations: + +1. **Manual + calendar reminder** — fine for short durations (days), risky for long (months). +2. **Self-hosted cron** — `cdr access … && cdr poke …` in a periodic job. Simple. Free. Requires uptime. +3. **Gelato Automate** — schedule on-chain poke via Gelato. Costs ~$5/mo per vault. Robust. +4. **In the React layer** — `useDeadManTimer(uuid)` returns `{ remainingMs, isCritical, poke }`; `isCritical = true` once <25% of duration remains so the UI can prompt the creator. `` is the headless component. + +## Post-unlock semantics — the trapdoor + +After `unlockAt`, the creator gets NO special treatment. If `publicAfterUnlock = false` AND the creator is not in `heirs`, **the creator loses read access permanently** at unlock. This is intentional — the entire point of a dead-man-switch is that "alive creator" and "dead creator" produce different access rules. + +UX best practice: agent helper defaults `creatorCanReadWhileLocked = true`. Encourage callers to also add themselves to `heirs` if they want post-unlock access. The `createDeadManVault` JSDoc has the callout. + +## Block-based vs timestamp-based + +- **Timestamps (`blockBased = false`)** are the default. Wall-clock-friendly; `block.timestamp` is miner-influenced ±12s on Story but doesn't matter for durations >> 12s. +- **Block-based (`blockBased = true`)** uses `block.number`. More predictable for short horizons (<7 days) since Story's block production is rarely irregular at that scale. Less safe for long horizons because block time isn't constitutionally fixed. + +Rule of thumb: use timestamps for >24h durations, blocks for <24h. + +## Heir variants + +- **No heir set + public unlock** — global leak. Use for "publish this data publicly if I'm gone." +- **Heir set + private unlock** — restricted leak. Use for "only my lawyer / co-founder / spouse can read post-unlock." +- **Heir set + public unlock** — `pubAfter = true` overrides — heirs are documented but everyone else also reads. Confusing combo; avoid unless you want it explicit. + +## Common failure modes + +- **"AlreadyUnlocked" on poke.** The window already lapsed. The vault is now in unlocked mode permanently — no revival path. +- **Creator can't read post-unlock.** Add yourself to `heirs` at config time, or accept the trapdoor. +- **Heir reads return `false`.** Check `_configured(uuid)` (vault must be factory-configured) AND `now >= unlockAt`. The view returns false on either failure rather than reverting (per D-series rule). +- **Block-based countdown looks wrong in UI.** The React hook `useDeadManTimer` doesn't tick block.number client-side — `remainingMs` returns 0 for block-based vaults. Surface "block: X" raw in the UI instead. +- **Heirs leak after unlock and the creator wants to revoke.** Impossible — once any heir reads, they hold plaintext forever. Document this loudly. + +## Don't + +- Don't use a dead-man switch for "subscription-style" recurring access — use `SubscriptionCondition` instead. The dead-man pattern is a one-shot trapdoor. +- Don't set `duration` so short that you can't realistically poke it (e.g. 1 day) unless you have automated heartbeats. +- Don't store the only copy of a critical secret in a dead-man vault without testing the heir read path on a separate test vault first. The "I'm dead now" UX has no rollback. diff --git a/skills/design-escrow/SKILL.md b/skills/design-escrow/SKILL.md new file mode 100644 index 0000000..f19a143 --- /dev/null +++ b/skills/design-escrow/SKILL.md @@ -0,0 +1,98 @@ + +--- +name: design-escrow +description: Design a Story CDR ConditionalEscrowCondition vault (buyer pays, then confirms delivery, with arbiter dispute path and seller-side timeout claim). Use this skill when (1) the user wants to build a data marketplace with pay-per-read + delivery confirmation; (2) the user asks how to add a refund / arbiter / dispute path to a CDR vault; (3) the user asks "what happens if the buyer goes silent after paying" or wants timeoutSecs guidance; (4) the user is comparing escrow vs subscription vs multi-sig for monetizing one-off datasets. +license: MIT +--- + +# Design a CDR conditional-escrow vault + +`ConditionalEscrowCondition` (Aeneid `0x7fcDe02DB7c14fD3587aB2fED064a1D8355b7584`) gates reads behind a 2-step payment flow: buyer escrows the listing price, then either (a) confirms delivery to release funds + unlock the read, (b) goes silent and the seller `claimAfterTimeout`s after `timeoutSecs`, or (c) disputes and the arbiter refunds via `arbiterRefund`. Seller can always read their own vault. The contract is the canonical data-marketplace primitive for cdr-kit. + +## When to use + +- "How do I sell a one-off dataset and only release it after the buyer confirms?" +- "Build a data marketplace where the seller waits for delivery confirmation" +- "Add an arbiter / dispute path to my CDR vault" +- "Buyer paid but went silent — how does the seller still get the money?" +- "Refund a buyer who paid but never got what they expected" +- "Compare escrow vs subscription for monetizing data" + +## The 4 paths through one vault + +``` +1. happy path: buyer.pay() → buyer.confirmDelivery() → seller paid + buyer reads +2. timeout: buyer.pay() → (silence) → seller.claimAfterTimeout(buyer) → seller paid + buyer reads +3. dispute: buyer.pay() → arbiter.arbiterRefund() → buyer refunded + NO read +4. seller-only: (seller always reads own vault, regardless of buyer state) +``` + +The "paths" share state — once `delivered[uuid][buyer] == true`, the read passes and the contract has paid out. Once `paidAt[uuid][buyer] == 0` (post-refund), the buyer can pay again from scratch. + +## Creator: configure the vault + +```ts +const txHash = await agent.createEscrowVault({ + // seller: defaults to agent's own wallet — omit unless the agent is acting as a broker + price: 1_000_000_000_000_000n, // 0.001 IP + timeoutSecs: 86_400n, // 24h before seller can claim unilaterally + arbiter: arbiterAddress, // omit for no-arbiter mode (no refund path) + licenseTermsId: undefined, // optional PIL terms to attach +}); +// uuid lives in the VaultCreated event of the receipt +``` + +Three knobs worth thinking through: + +- **`timeoutSecs`** — too short and a buyer with a slow content-delivery pipeline gets force-closed; too long and the seller's money sits idle. Default 24h is the right starting point. +- **`arbiter`** — `address(0)` means no refund path. Skip the arbiter only when both parties are fully trusted (the seller can never be wrong, the buyer can never dispute). Most real deals want one. +- **`price`** — denominated in native IP. `pay()` refunds excess; underpayment reverts. + +## Buyer side: pay, then confirm + +```ts +await agent.payEscrow({ uuid, price }); // step 1 — escrow funds +// (verify the data off-chain — agent.access(uuid) is gated until step 2) +await agent.confirmEscrowDelivery(uuid); // step 2 — release + unlock +const bytes = await agent.access(uuid); // now succeeds +``` + +The buyer holds plaintext after the read. Document this intrinsic CDR limitation in the buyer UX — there's no take-backsies after a confirmed read. + +## Seller side: claim if buyer silent + +```ts +// After `paidAt + timeoutSecs` has lapsed: +await agent.claimEscrowAfterTimeout({ uuid, buyer }); +// Buyer can now read (the contract sets `delivered[uuid][buyer] = true`). +``` + +Why grant the buyer read access on a timeout claim? Otherwise the "I paid, seller went radio silent, I have neither funds nor data" failure mode exists — the buyer is strictly worse off than if they'd never paid. Granting read on timeout means the buyer at least gets the data they paid for. + +## Arbiter: refund disputes + +```ts +await agent.refundEscrow({ uuid, buyer }); +// paidAt reset to 0 — the buyer can call pay() again later if they change their mind. +// `delivered` stays false — buyer cannot read after a refund. +``` + +Arbiter is fire-and-forget on refund: there's no on-chain mechanism to "appeal" the refund; the seller has to seek redress off-chain. Pick arbiters carefully. + +## Dashboard / UI patterns + +`` is the buyer-side button — it reads `useEscrowState(u, buyer)` and switches between `pay` / `confirm delivery` / `delivered ✓` based on chain state. Pair with a seller-side `` (build using `agent.claimEscrowAfterTimeout` once `useEscrowState` returns `timeoutInMs === 0`). + +## Common failure modes + +- **`Underpaid` revert** — `msg.value < price`. The contract refunds excess automatically; do not require an exact match in the UI. +- **`AlreadyPaid` revert** — the buyer already paid; UI should reflect chain state and disable the `pay` button when `paidAt > 0`. +- **`TooEarly` on `claimAfterTimeout`** — `block.timestamp < paidAt + timeoutSecs`. Surface a countdown to the seller via `useEscrowState.timeoutInMs`. +- **`NoArbiter` on `arbiterRefund`** — the vault was created without an arbiter. Mode flag the dispute path off in the UI when `arbiter === address(0)`. +- **Buyer holds plaintext** — read is one-way. Document this in the listing UX before the buyer pays. + +## Don't + +- Don't use escrow for high-frequency reads. The 2-step flow has UX cost per buyer; subscription or multi-sig vaults fit better for recurring access. +- Don't set `timeoutSecs = 0` to "force immediate seller claim" — the seller can already claim instantly via the buyer's confirmation; setting timeout to 0 just removes the buyer's safety window. +- Don't store the only copy of the secret in escrow without testing the timeout path on a separate vault first. The seller-claim flow grants the buyer plaintext access; verify the encryption surface end-to-end before going live. diff --git a/skills/design-multisig-condition/SKILL.md b/skills/design-multisig-condition/SKILL.md new file mode 100644 index 0000000..3006538 --- /dev/null +++ b/skills/design-multisig-condition/SKILL.md @@ -0,0 +1,134 @@ + +--- +name: design-multisig-condition +description: Design a Story CDR MultiSigCondition vault (N-of-M EIP-712 threshold reads, signer rotation, off-chain signature collection). Use this skill when (1) the user wants to gate a CDR vault behind multiple signers, board approval, or a DAO threshold; (2) the user asks how off-chain EIP-712 sigs compose without on-chain approve(); (3) the user wants to rotate signers or invalidate in-flight sigs via epoch bump; (4) the user is integrating Safe / Gnosis or wiring the MultiSigSigner / MultiSigApprovalTracker React components. +license: MIT +--- + +# Design a CDR multi-sig vault + +`MultiSigCondition` (deployed Aeneid `0xb22EBF0481950A3c0e528A5902C4c5C69184fB78` as of 2026-06-01) gates reads behind N-of-M EIP-712 signatures collected off-chain. **First-of-kind in the CDR ecosystem** — no on-chain `approve()` tx per signer. Buyer collects threshold-many sigs, submits them as `accessAuxData = abi.encode(deadline, sigs[])`, the contract recovers + dedupes + counts. + +## When to use + +- "Board / DAO / 3 of 5 founders need to approve each read" +- "We don't want signers to pay gas for approvals" +- "Sigs should be revocable if a signer leaves" +- "How do I integrate Safe / Gnosis as a signer?" +- "Why is my multi-sig read returning false?" +- "How does epoch invalidation work?" + +## The 2-step flow + +1. **Creator** registers signers + threshold once (`createMultiSigVault`). Vault is gated; reads return false until threshold met. +2. **Buyer** collects threshold-many EIP-712 sigs from configured signers via off-chain UI / RPC / messaging. Each sig binds `(uuid, callerAddress, epoch, deadline)`. +3. **Buyer** submits all sigs as `accessAuxData` to `agent.access(uuid, aux)` or `agent.accessMultiSig({ uuid, deadline, sigs })`. + +No `approve()` on-chain. No per-signer tx fee. Buyer pays one read fee, period. + +## Creator: register the vault + +```ts +const txHash = await agent.createMultiSigVault({ + signers: ["0xAlice...", "0xBob...", "0xCarol..."], // auto-sorted ascending + threshold: 2, + licenseTermsId: 1n, // optional PIL terms to attach +}); +// uuid lives in the VaultCreated event of the receipt +``` + +Constraints: +- `signers.length >= 1` +- `1 <= threshold <= signers.length` +- Signers must be sorted strictly ascending in `_store` (helper sorts automatically). +- 0 ≤ threshold ≤ ~10 in practice — each sig recovery is ~6k gas, so `threshold = 10` adds ~60k to the read. + +## Signer: produce a signed approval + +```ts +import { signTypedData } from "viem/accounts"; +const signature = await signTypedData(account, { + domain: { + name: "cdr-kit:MultiSigCondition", + version: "1", + chainId: 1315, + verifyingContract: "0xb22EBF...", + }, + types: { + Approval: [ + { name: "uuid", type: "uint32" }, + { name: "caller", type: "address" }, + { name: "epoch", type: "uint64" }, + { name: "deadline", type: "uint64" }, + ], + }, + primaryType: "Approval", + message: { uuid, caller, epoch, deadline }, +}); +``` + +Or use the agent helper: `agent.signMultiSigApproval({ uuid, caller, deadline })` — pulls the current `epoch` from chain and signs against the right domain. + +## Buyer: read the vault + +```ts +const data = await agent.accessMultiSig({ + uuid, + deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour + sigs: [aliceSig, bobSig], // any threshold-many; the contract dedupes +}); +``` + +Sig submission order doesn't strictly matter — the contract sorts on the recovered address and rejects duplicates / out-of-order. But sending sigs already sorted ascending by recovered address is fastest (no out-of-order rejection on first scan). + +## Epoch invalidation + +Each rotation bumps `epoch`. Old sigs (signed against the previous epoch) are rejected. Use this to: +- Remove a compromised signer immediately +- Add a new signer to the set +- Increase or decrease threshold + +```ts +await agent.client.walletClient!.writeContract({ + address: multiSigAddr, + abi: multiSigConditionAbi, + functionName: "rotateSigners", + args: [uuid, [...newSignersSorted], newThreshold], +}); +// All in-flight sigs against old epoch are now invalid; buyers must re-collect +``` + +The React hook `useMultiSigStatus(uuid)` exposes `{ signers, threshold, epoch }` so dashboards can show "X of Y · epoch N". + +## Caller binding (replay protection) + +Sigs bind to `caller` (the address that will submit the read tx). A sig signed for `0xBuyer1` cannot be replayed by `0xBuyer2`. The buyer MUST tell each signer their own address when requesting a sig. UIs should default to "sign for the connected wallet" to make this idiomatic. + +## Deadline (sig expiry) + +`deadline` is a unix timestamp. The contract rejects sigs where `block.timestamp > deadline`. Pick deadlines that comfortably outlast your collection flow (e.g. 1 hour for in-meeting board approvals; 24 hours for async multi-day signing). + +## EIP-1271 (Safe-as-signer) — not in 0.5 + +The contract uses `ecrecover` only — EOA signatures. Safe / contract wallets cannot directly act as signers in 0.5.0. Workaround: have a Safe-owned EOA sign on behalf, treat that EOA as the registered signer. + +## ECDSA malleability + +Both `(r, s, v)` and `(r, n-s, v^1)` recover the SAME address. The contract dedupes by recovered address with strict-ascending order, so a malicious double-sig from the same signer is automatically rejected. No `s ≤ n/2` check needed. + +## Defensive eval + +Malformed `accessAuxData` returns `false` (not revert) — the contract wraps decode + recover in a try/catch via `this.evaluate(...)`. This means a malformed sig blob fails closed (no read), it doesn't bubble a revert into the CDR precompile. + +## Common failure modes + +- **Read returns false with valid sigs.** Check `epoch` — was the vault rotated after sigs were produced? Re-collect against the new epoch. +- **Read returns false, sigs look right.** Check `caller` binding — did each signer sign for the actual reader's address? +- **"NoSigners" / "BadThreshold" on register.** Threshold must be ≥1 and ≤ `signers.length`. Empty signer arrays are rejected. +- **"SignersNotSorted" on register.** Pass signers in strictly-ascending order — the agent helper does this automatically; raw `createVault` callers must sort themselves. + +## Don't + +- Don't store sigs on-chain — the contract is stateless on approvals. Storing them defeats the gas-savings premise. +- Don't reuse sigs across uuids — they bind to `(uuid, caller, epoch, deadline)` and the contract rejects mismatch. +- Don't use multi-sig for high-frequency reads — collecting threshold-many sigs per read has UX cost. For frequent access, use a subscription or license-gated vault instead. diff --git a/skills/design-publish-with-story/SKILL.md b/skills/design-publish-with-story/SKILL.md new file mode 100644 index 0000000..4e68184 --- /dev/null +++ b/skills/design-publish-with-story/SKILL.md @@ -0,0 +1,123 @@ + +--- +name: design-publish-with-story +description: Use the cdr-kit agent-as-publisher one-shot (agent.publish) to ship a Story-IP-registered, license-gated CDR vault in a single call. Use this skill when (1) the user wants to monetize encrypted data via Story PIL license tokens; (2) the user asks how to register IP + attach PIL terms + create vault + write data in one tx-bundle; (3) the user is picking between commercial-use, commercial-remix, non-commercial-social-remixing, or creative-commons PIL flavors; (4) the user is combining @cdr-kit/story with @cdr-kit/agent or wiring an SPG NFT contract. +license: MIT +--- + +# Publish data with Story IP — the agent-as-publisher one-shot + +`agent.publish({ data, spgNftContract, pilTerms })` collapses 4 normally-separate Story Protocol calls into a single agent method: register a fresh IP asset, attach PIL license terms, allocate a license-gated CDR vault, and write the encrypted payload. The output is everything a buyer needs to subscribe and read: `{ ipId, tokenId, licenseTermsId, vaultUuid, vaultTxHash, ipRegisterTxHash, writeTxHash }`. This is the highest-DX win in `@cdr-kit/agent` — the wedge for autonomous data sellers. + +## When to use + +- "Sell my encrypted dataset on Story Protocol" +- "Register an IP asset and gate the CDR vault to license-token holders" +- "PIL commercial-remix flow with CDR access" +- "Agent publishes original data and prices it via Story license-mint fee" +- "I want one call that does register + attach terms + create vault + write" +- "How does PIL pricing flow through to CDR reads?" + +## The 4-step flow this one-shot encapsulates + +``` +1. registerIpAsset({ nft: { type: "mint", spgNftContract }, licenseTermsData: [{ terms }] }) + → { ipId, tokenId, licenseTermsIds[0] } +2. createVault({ readConditionAddr: licenseReadCondition, readConfig: encode(ipId, licenseTermsId) }) + → vaultTxHash → extract uuid from VaultCreated event +3. writeVaultData({ uuid, dataKey: data }) + → writeTxHash +4. (return everything; buyers use ipId + licenseTermsId to mintLicenseTokens + access) +``` + +The buyer-side counterpart is `agent.accessLicenseGated({ uuid, licenseTokenId })` once they've minted a license token via Story. + +## Pre-requisites the publish() flow assumes + +- **An SPG NFT collection.** Story's Story Protocol Gateway (SPG) lets you mint into a collection without deploying your own ERC-721. Create one once at https://story.foundation/apps/spg — you get back an `spgNftContract` address that you reuse for every IP you register. Per-IP cost: just gas + mint fee. +- **PIL terms.** Use `PILFlavor.commercialUse({ ... })` / `commercialRemix({ ... })` / `nonCommercialSocialRemixing()` / `creativeCommonsAttribution()` from `@cdr-kit/story` to build the terms struct. Don't hand-construct `PILTerms` — the SDK validators are strict and the 17-field shape changes between Story versions. +- **`@cdr-kit/story` installed.** It's a peer dep — `pnpm add @cdr-kit/story @story-protocol/core-sdk`. The agent lazy-loads it; basic users who don't publish never pay the install cost. + +## Minimal example + +```ts +import { CdrAgent } from "@cdr-kit/agent"; +import { PILFlavor } from "@cdr-kit/story"; + +const agent = new CdrAgent({ privateKey: process.env.CDR_PRIVATE_KEY!, network: "aeneid" }); + +const result = await agent.publish({ + data: new TextEncoder().encode("the actual secret data"), + spgNftContract: "0xYourSpgNftCollection", + pilTerms: PILFlavor.commercialUse({ + defaultMintingFee: 1_000_000_000_000_000_000n, // 1 WIP per license + commercialRevShare: 5, // 5% of derivative revenue back to original + }), + ipMetadata: { ipMetadataURI: "ipfs://Qm..." }, // optional +}); + +// → { ipId, tokenId, licenseTermsId, vaultUuid, vaultTxHash, ipRegisterTxHash, writeTxHash } +``` + +## PIL flavors — picking the right one + +| Flavor | Mint fee | Derivative rev share | Use when | +|---|---|---|---| +| `nonCommercialSocialRemixing` | 0 | 0% | Open-source datasets, free public IP | +| `commercialUse` | per-license fee | 0% | One-off data sales; buyer can use commercially, no derivatives | +| `commercialRemix` | per-license fee | configurable % | Buyer can build derivative works; revenue flows back | +| `creativeCommonsAttribution` | 0 | 0% | Attribution-required free use (CC-BY) | + +For CDR-published data, **`commercialUse`** is the default — the data is paid-for and the buyer reads it, but you don't want derivative works without a separate license discussion. **`commercialRemix`** when you want analytics/derivative IP downstream and a cut of that revenue. + +## The buyer flow + +Once `publish()` returns, share `{ ipId, licenseTermsId, vaultUuid }` (e.g., as a URL or QR). Buyer flow: + +```ts +// 1. (optional) wrap IP → WIP for license mint fee: +await agent.wrapIp({ amountWei: 1_000_000_000_000_000_000n }); +await agent.approveWip({ spender: royaltyModuleAddress, amountWei: 1_000_000_000_000_000_000n }); + +// 2. mint a license token: +const { licenseTokenIds, txHash } = await agent.mintLicenseTokens({ + licensorIpId: ipId, + licenseTermsId, + amount: 1n, + maxMintingFee: 1_000_000_000_000_000_000n, +}); + +// 3. read the vault: +const bytes = await agent.accessLicenseGated({ uuid: vaultUuid, licenseTokenId: licenseTokenIds[0]! }); +``` + +This is the same pattern `jacob-tucker/cdr-ai-negotiate` uses end-to-end — agent-to-agent commerce where the license-mint IS the payment. + +## Composing with derivatives + +Once a buyer mints a license, they can register their derivative IP: + +```ts +await agent.registerDerivative({ + childIpId: derivIpId, + parentIpIds: [originalIpId], + licenseTermsIds: [licenseTermsId], + licenseTokenIds: [boughtLicenseTokenId], // optional — consume the token at register time +}); +``` + +Royalty flows back to the original IP per the PIL terms' `commercialRevShare`. The CDR vault doesn't participate — only Story's royalty graph does. + +## Common failure modes + +- **`SPG_INVALID_PUBLIC_MINT`** — your SPG contract isn't configured for public mints. Configure via Story's SPG factory UI, or use a non-SPG `register({ nft: { type: "minted", nftContract, tokenId } })` flow. +- **`licenseTermsIds missing from registerIpAsset response`** — `publish()` throws this; means the SDK returned ok but didn't include the terms ID, usually because you passed empty `licenseTermsData`. Always pass at least one terms entry. +- **`InvalidConditionContract` on vault create** — your network's `licenseReadCondition` address is wrong or stale. Check `@cdr-kit/contracts.aeneid.licenseReadCondition`. +- **Buyer gets `UnauthorizedRead` despite holding a license** — they're passing a token from a different `licenseTermsId` than the vault is gated on. The token ID and the vault's gating terms must match. + +## Don't + +- Don't construct `PILTerms` by hand — the SDK shape changes; use `PILFlavor.(params)`. +- Don't reuse SPG NFT collections across unrelated products without thinking about brand attribution; an SPG can host many IPs but they all share the collection metadata. +- Don't gate a low-stakes preview vault behind `LicenseReadCondition` — minting a license costs gas. For free previews, use `OpenCondition` or `TimeWindow`. +- Don't store the only copy of mission-critical data via `publish()` without dry-running the buyer flow first. CDR reads have ~7-min latency ceiling; verify end-to-end before going live. diff --git a/skills/design-storage-adapter/SKILL.md b/skills/design-storage-adapter/SKILL.md new file mode 100644 index 0000000..eb0d0d6 --- /dev/null +++ b/skills/design-storage-adapter/SKILL.md @@ -0,0 +1,94 @@ + +--- +name: design-storage-adapter +description: Pick the right cdr-kit storage backend for a CDR file vault. Use this skill when (1) the user asks where the file body goes / how cdr-kit stores the encrypted payload; (2) the user wants to wire Pinata / Supabase / S3 / R2 / IPFS-Kubo / Helia / Storacha / read-only gateway / Synapse to cdr-kit; (3) the user wants to upload a payload over the ~1KB inline cap; (4) the user is configuring storage on the buyer dashboard vs the seller / publisher side. +license: MIT +--- + +# Design a cdr-kit storage adapter + +CDR vaults route payloads by size: **≤ ~1KB → inline on-chain**, **> ~1KB → off-chain body + CDR-secured key reference**. The off-chain path needs a storage backend that implements `CdrStorageProvider { upload(bytes) → cid; download(cid) → bytes }`. This skill picks the right one. + +## When to use + +- "Which storage should I use for my CDR vault?" +- "How do I wire Pinata / Supabase / R2 / web3.storage / Helia to cdr-kit?" +- "Why is my upload failing — wrong storage shape?" +- "How big is the inline cap?" +- "Can I read a CDR file vault without pinning credentials?" +- "Buyer-side dashboard — what storage should we configure?" + +## The 6 official adapters + +All ship from `@cdr-kit/core` (`createMemoryStorage`, `createIpfsStorage`, `createPinataStorage`, `createSupabaseStorage`, `createReadOnlyGatewayStorage`) — the 3 SDK-heavy ones (Helia browser preset, S3-compat, Storacha-server) ship as separate `@cdr-kit-ecosystem/*` packages once available. + +| Adapter | Use when | Backend | Auth | Notes | +|---|---|---|---|---| +| `createMemoryStorage` | Unit tests, CI, mocks | In-process Map | none | Content-addressed; returns real CIDv1; no network | +| `createIpfsStorage` | Self-hosted Kubo, custom pinner | Any IPFS HTTP API + gateway | Bearer/JWT in `headers` | Generic; parses CID from `Hash` / `cid` / `IpfsHash` keys | +| `createPinataStorage` | Indie / hosted pinning | Pinata pinning + gateway | JWT (Pinata key) | Convenience wrapper over `createIpfsStorage` | +| `createSupabaseStorage` | Already-have-Supabase shops | Supabase Storage bucket | service-role or anon key | Path-in-bucket as the "cid"; bare REST (no `@supabase/supabase-js` dep) | +| `createReadOnlyGatewayStorage` | Buyer dashboards, no upload | Any IPFS gateway | none | Throws on `upload()`; for read-only consumers | +| Helia (browser preset) | Browser-side dogfooded IPFS | Embedded Helia node | none | Requires `@peculiar/webcrypto: 1.7.0` pnpm override | +| Storacha-server | UCAN-backed pinning | `@storacha/client` | UCAN delegation env vars | Public IPFS, robust | +| S3-compat (R2 / S3 / MinIO) | Enterprise / private | `@aws-sdk/client-s3` | IAM keys | Browser uploads need server-side signed URLs | + +## Pick by use case (decision tree) + +1. **Buying — read only?** → `createReadOnlyGatewayStorage` (any public gateway: `ipfs.io`, `cf-ipfs.com`, `w3s.link`). +2. **Selling — fastest path to hosted IPFS?** → `createPinataStorage` (get a JWT at `app.pinata.cloud/keys`, takes 2 min). +3. **Selling — already have Supabase?** → `createSupabaseStorage`. Bucket must exist; RLS rules apply per your service-role vs anon key. +4. **Self-host IPFS node?** → `createIpfsStorage({ addUrl: 'http://kubo:5001/api/v0/add', gatewayUrl: 'http://kubo:8080', headers })`. +5. **Enterprise / private (S3, R2)** → `@cdr-kit-ecosystem/storage-s3` (planned). Until then, write a 60-LOC adapter implementing `CdrStorageProvider` directly. +6. **Tests / CI?** → `createMemoryStorage`. + +## Wiring examples + +### Pinata (creator side) +```ts +import { createPinataStorage, uploadFile } from "@cdr-kit/core"; +const storage = createPinataStorage({ jwt: process.env.PINATA_JWT! }); +await uploadFile(client, { content, storage, readConditionAddr, readConditionData }); +``` + +### Supabase (creator side) +```ts +import { createSupabaseStorage } from "@cdr-kit/core"; +const storage = createSupabaseStorage({ + supabaseUrl: process.env.SUPABASE_URL!, + key: process.env.SUPABASE_SERVICE_ROLE_KEY!, + bucket: "cdr-secrets", + pathPrefix: "vaults/", // optional, default "cdr/" +}); +``` + +### Read-only gateway (buyer side) +```ts +import { createReadOnlyGatewayStorage, downloadFile } from "@cdr-kit/core"; +const storage = createReadOnlyGatewayStorage({ gatewayUrl: "https://gateway.pinata.cloud" }); +const { content } = await downloadFile(client, { uuid, storage }); +``` + +## The inline cap + +The 1KB cap is the documented practical limit. For the on-chain truth, call `getInlineLimit(client)` (added in 0.5.0) — it reads `CDR.maxEncryptedDataSize()` and caches the result. Below the cap → inline (`writeVaultData`); above → file path (`uploadFile`). Routing helper: + +```ts +import { shouldUseFile, getInlineLimit } from "@cdr-kit/core"; +const limit = await getInlineLimit(client); +const useFile = shouldUseFile(content, limit); +``` + +## Common failure modes + +- **Pinata upload returns no CID.** Check the JWT scope — needs pinning permissions, not just admin read. +- **Supabase 403.** Bucket RLS doesn't grant the anon key write access — use the service-role key, or update RLS. +- **Browser CORS on download.** Public IPFS gateways often block direct browser requests; proxy through a server route OR use Pinata's dedicated subdomain gateway. +- **CID changes between upload and download.** The storage adapter returned a path-as-CID (Supabase, custom) but a buyer is trying an IPFS gateway URL. Use the SAME adapter on both sides. +- **`download` returns "cannot upload" on the consumer side.** The buyer wired `createReadOnlyGatewayStorage` then accidentally called `uploadFile` — wire `createPinataStorage` for write paths instead. + +## Don't + +- Don't write a fresh adapter when one of the 6 already fits — `CdrStorageProvider` is a 2-method interface; the existing adapters cover ~95% of cases. +- Don't put pinning JWTs in browser-shipped code. Route through a server proxy. The JWT can drain your Pinata account. +- Don't use `createMemoryStorage` in production — it's process-local and resets on restart. diff --git a/skills/design-time-window/SKILL.md b/skills/design-time-window/SKILL.md new file mode 100644 index 0000000..7d7d296 --- /dev/null +++ b/skills/design-time-window/SKILL.md @@ -0,0 +1,89 @@ + +--- +name: design-time-window +description: Design a Story CDR TimeWindowCondition vault (read access gated by absolute timestamps or block numbers). Use this skill when (1) the user wants release-on-date / scheduled-publication / embargoed-data / NDA-expiry / time-locked-content; (2) the user asks how to auto-open a vault at timestamp X or block N; (3) the user wants to bound a read window to event hours (conference slides, polls-close, market-open); (4) the user is picking between blockBased vs timestamp gating or wiring TimeWindowBadge / useTimeWindowState. +license: MIT +--- + +# Design a CDR time-window vault + +`TimeWindowCondition` (Aeneid `0x67911435F262e7e4EC4F7FEB4e868a67b9dd90b1`) gates reads to a `[startTs, endTs]` window. Use it for release-on-date drops, embargoed data, NDA expiry, time-bound previews, and any "auto-unlock at T" pattern. The contract is stateless on participants — no per-buyer config, no payments. Time is the only key. + +## When to use + +- "Auto-publish this data after April 1" +- "Sealed bid that opens at the auction close" +- "Time-bound preview window for a paid product" +- "Embargoed press release lifted at 9am EST" +- "Token-gated content that becomes public after 30 days" +- "Conference talk slides released after the keynote" + +## Three preset shapes + +The agent helper accepts three intuitive configurations: + +| Pattern | `startTs` | `endTs` | `blockBased` | +|---|---|---|---| +| **Release on date** (open-ended after T) | T | `0` | `false` | +| **Limited window** (T1 → T2) | T1 | T2 | `false` | +| **Release after block N** | `0` | N | `true` | + +Set `endTs = 0` to mean "no upper bound" — the contract treats it as open-ended. Setting `startTs = 0` means "always-open from genesis" — almost certainly a bug; use `OpenCondition` instead if you want public-anytime reads. + +## Creator: configure the vault + +```ts +// Release-on-date: open after April 1 2026 +const txHash = await agent.createTimeWindowVault({ + startTs: 1_743_465_600n, // unix timestamp + endTs: 0n, // open-ended + blockBased: false, +}); + +// Limited window: 1-hour preview +await agent.createTimeWindowVault({ + startTs: BigInt(Math.floor(Date.now() / 1000)), + endTs: BigInt(Math.floor(Date.now() / 1000) + 3600), + blockBased: false, +}); + +// Block-based: release at block 19_500_000 +await agent.createTimeWindowVault({ + startTs: 0n, + endTs: 19_500_000n, + blockBased: true, +}); +``` + +## Block-based vs timestamp-based + +- **Timestamps** are wall-clock-friendly and survive block-time drift. Use for windows > 24h. +- **Block numbers** are predictable for short horizons (< 7d on Story's ~12s block time) and immune to validator timestamp manipulation. Use for trustless precise releases (auctions, bonding curves). +- `block.timestamp` is miner-influenced ±12s on Story Aeneid. Document. + +## Reader side + +There's no setup — anyone can `access(uuid)` and the contract returns true if the current time is within the window: + +```ts +const bytes = await agent.access(uuid); // reverts cleanly before startTs or after endTs +``` + +The dashboard reads `useTimeWindowState(uuid)` which returns `{ startTs, endTs, blockBased, isOpen, opensInMs, closesInMs }` and ticks every second locally. Wrap with `` for "opens in 3d 4h" / "closes in 59m" / "closed" rendering. + +## Common failure modes + +- **`BadWindow` on configure** — `endTs != 0 && endTs <= startTs`. The contract rejects empty / inverted windows. +- **Reader sees "closed" before the window opens** — that's correct; the badge falls through to "opens in …" if `opensInMs > 0`, otherwise "closed". +- **Block-based mode countdown shows 0** — the React hook intentionally doesn't poll `block.number` every second. Render the raw block bound from the state object instead. +- **Read seems to lapse instantly** — `block.timestamp > endTs` by even one second closes the window. Set a generous buffer if downstream consumers might be slow. + +## Composition with other conditions + +Pair with `ComposableCondition` to get "license-gated PLUS time-windowed" or "subscription PLUS embargo": the parent composable evaluates `licenseRead AND timeWindow`, so a buyer needs both the license AND the window to be open. See `design-condition` for the composable pattern. + +## Don't + +- Don't use `block.timestamp` precision for high-value cryptoeconomic windows (< 1 minute). Validators can nudge it ±12s; use block numbers if accuracy matters. +- Don't set `startTs = 0` — that's "always open" which is what `OpenCondition` exists for; using `TimeWindow` for it just burns extra SLOAD per read. +- Don't expect to "extend the window" after configuration. The contract is immutable per uuid; create a new vault if the schedule changes. diff --git a/skills/explain-cdr-error/SKILL.md b/skills/explain-cdr-error/SKILL.md new file mode 100644 index 0000000..94a090b --- /dev/null +++ b/skills/explain-cdr-error/SKILL.md @@ -0,0 +1,69 @@ + +--- +name: explain-cdr-error +description: Translate a raw CDR error (SDK class name, error code, viem revert string, stack trace) into root cause + fix. Use this skill when (1) the user pastes any error blob involving cdr-kit / @piplabs/cdr-sdk / the CDR precompile (0xC...05) or DKG precompile (0xC...04); (2) a CDR call returned { error: { code, message } } over MCP; (3) a test or runtime failure mentions CdrError / WalletClientRequiredError / LabelMismatchError / ContentSizeExceededError / PartialCollectionTimeoutError; (4) the user wants the resolution playbook for a specific CdrErrorCode. +license: MIT +--- + +# Explain a CDR error + +Paste any CDR error message, stack trace, viem revert string, or `@piplabs/cdr-sdk` class name and get back the root cause + the fix. Companion to `debug-cdr-precompile` (which covers the 7 most common failure modes in depth); this skill is the lookup table. + +## When to use + +- The user pastes a stack trace or `Error: X` message +- A CDR call returned `{ error: { code: "X", message: "..." } }` over MCP +- A test failure mentions a `CdrError`, `WalletClientRequiredError`, `LabelMismatchError`, etc. + +## The taxonomy + +`@cdr-kit/core` exports `CdrError` with a `code` field. Errors from `@piplabs/cdr-sdk` are mapped via `mapSdkError()`. Match the user's input against this table: + +| Error / code | What it means | Fix | +|---|---|---| +| `WASM_NOT_INITIALIZED` | `initWasm()` was never called | `await ensureWasm()` (or use ``) before any encrypt/decrypt | +| `WALLET_REQUIRED` / `WALLET_CLIENT_REQUIRED` / `WalletClientRequiredError` | Trying to write/decrypt without a wallet client | Set `CDR_PRIVATE_KEY` env, or pass `privateKey` to `createCdrKitClient()` / `new CdrAgent()` | +| `WRONG_NETWORK` | Connected wallet is on a different chain than the SDK expects | Switch to Story Aeneid (chain 1315) in the wallet | +| `CONDITION_NOT_MET` | Read/write condition rejected the caller | Satisfy the condition first (subscribe, mint license, hold tier token); check `agent.getEntitlement(uuid)` to verify | +| `VAULT_NOT_FOUND` / `EMPTY_VAULT` / `EmptyVaultError` | Reading a uuid that was never written to | Confirm uuid via `agent.getVaultInfo(uuid)` (returns `null` for unknown uuids) | +| `PAYLOAD_TOO_LARGE` / `CONTENT_SIZE_EXCEEDED` / `ContentSizeExceededError` | Inline payload > 1024 bytes (effective ~960 plaintext after TDH2) | Use `agent.uploadFile({ content, addUrl, gatewayUrl })` for the file path | +| `OUT_OF_GAS` / `ReentrancySentryOOG` | CDR precompile call OOG under `eth_estimateGas` | Set explicit `gas: 3M` (the kit does this for `createVault`); for raw cast: `--gas-limit 2000000`+ | +| `READ_TIMEOUT` / `PARTIAL_COLLECTION_TIMEOUT` / `PartialCollectionTimeoutError` | Validators didn't return enough partial decryptions in time | Retry; verify `apiUrl` (`cdr config`); confirm condition passes (`agent.getEntitlement(uuid)`); bump `timeoutMs` via low-level `accessVault` | +| `KEEPER_UNAVAILABLE` | Story-API REST endpoint unreachable | Retry with backoff; verify `CDR_API_URL` env (default `http://172.192.41.96:1317`) | +| `RATE_LIMITED` | Hit the keeper's rate limit | Retry with backoff per the suggested `retryAfterMs` | +| `LABEL_MISMATCH` / `LabelMismatchError` | Ciphertext label doesn't bind to the vault uuid | Use `uuidToLabel(uuid)` (the kit does this automatically in `writeVaultData`); don't construct labels by hand | +| `CID_INTEGRITY` / `CidIntegrityError` | Downloaded file from IPFS doesn't match the on-chain CID | Try a different gateway (default `https://w3s.link`); the original gateway may have served stale/tampered data | +| `INVALID_CONDITION_CONTRACT` / `InvalidConditionContractError` | `readConditionAddr` doesn't implement `checkReadCondition` | Use a deployed condition from `@cdr-kit/contracts.aeneid`; for EOA conditions use `allocate()` low-level with `skipConditionValidation: true` | +| `AlreadyConfigured` (Solidity revert) | Tried to re-configure an already-configured uuid | Allocate a NEW vault; CDR configs are factory-only + atomic by design | +| `UNKNOWN` | Generic wrapped error | Look at `.cause` for the original SDK/viem error | + +## Resolution playbook + +``` +Did the error happen… +├─ During encrypt/decrypt? → likely WASM_NOT_INITIALIZED or LABEL_MISMATCH +├─ During allocate/createVault? → likely OUT_OF_GAS or AlreadyConfigured +├─ During subscribe/pay? → check value === maxPricePerPeriod * periods; viem revert wraps the on-chain reason +├─ During access/read? → likely CONDITION_NOT_MET (check entitlement) or PARTIAL_COLLECTION_TIMEOUT +├─ During upload/download (file)? → likely CID_INTEGRITY or storage HTTP error +└─ At import / construction? → likely WALLET_CLIENT_REQUIRED or INVALID_CONDITION_CONTRACT +``` + +## Use the CLI to diagnose + +- `cdr config` — prints resolved network, RPC, API URL, wallet path (rules out env mis-config) +- `cdr wallet` — prints address + balance (rules out unfunded-wallet) +- `cdr fees` — pings Story-API (rules out unreachable apiUrl) +- `cdr vault info ` — view-only check of vault state + your entitlement (rules out CONDITION_NOT_MET, VAULT_NOT_FOUND) + +## Anti-patterns to flag + +- Catching the error and continuing silently. Every code has a fix; never swallow. +- Retrying without checking the code. Only `READ_TIMEOUT`, `KEEPER_UNAVAILABLE`, `RATE_LIMITED` are retry-friendly (`recoverable: true` on the CdrError). +- Hand-encoding `accessAuxData` for license-gated reads. Use `agent.accessLicenseGated({ uuid, licenseTokenId })` — it encodes correctly. + +## See also + +- `debug-cdr-precompile` — deeper walks of the 7 most common failures +- `references/error-catalog.md` — every code with full `suggestedAction` text +- `wire-allocate-pay-read` — the canonical flow that avoids 80% of these diff --git a/skills/wire-allocate-pay-read/SKILL.md b/skills/wire-allocate-pay-read/SKILL.md new file mode 100644 index 0000000..2963aa6 --- /dev/null +++ b/skills/wire-allocate-pay-read/SKILL.md @@ -0,0 +1,117 @@ + +--- +name: wire-allocate-pay-read +description: Wire the canonical CDR allocate → encrypt-and-write → pay → read flow end-to-end. Use this skill when (1) the user is implementing a CDR vault for the first time (creator OR consumer side); (2) the user asks the difference between agent.access vs subscribeAndAccess vs accessLicenseGated; (3) the user's read returns empty bytes / hangs / fails partial collection; (4) the user wants the idiomatic 4-tx lifecycle (createVault → writeVaultData → subscribe → access) with the right gas + uuid handling. +license: MIT +--- + +# Wire the CDR allocate → pay → read flow + +Every CDR vault follows the same 4-step lifecycle. This skill walks the canonical wiring for both the **creator** (allocate + write encrypted) and the **consumer** (pay + read decrypted) sides, with the exact gotchas the official docs and the cdr-kit decision log warn about. + +## When to use + +- "How do I create a vault and write an encrypted secret?" +- "How do I subscribe and read?" +- "How do I read a vault I've already subscribed to?" +- "What's the difference between `agent.access()` and `agent.subscribeAndAccess()`?" +- "Why is my read returning empty bytes / hanging / failing partial collection?" + +## Two-side overview + +``` +CREATOR CONSUMER +─────── ──────── +1. agent.createVault({ readConditionAddr, ┐ + readConfig, ... }) ├─ One on-chain tx; uuid lives in the + │ VaultCreated event of the receipt +2. agent.writeVaultData({ uuid, dataKey }) ┘ (TDH2-encrypt + write inline) + OR + agent.uploadFile({ content, addUrl, + gatewayUrl, ... }) (encrypt + IPFS-pin + allocate + write) + + 3. agent.subscribeAndAccess({ ← if not yet entitled + uuid, periods, maxPricePerPeriod, + value }) + OR + agent.access(uuid) ← if already entitled + OR + agent.accessLicenseGated({ ← Story-native (license-token) + uuid, licenseTokenId }) +``` + +**Total tx count:** creator = 1 (`createVault`) + 1 (`writeVaultData`); consumer = 1 (`subscribe`) + 1 (`read`) = 4 chain txs end-to-end. Read partial-collection happens off-chain and is bounded by `timeoutMs` (default `120_000`, server-side ceiling 200 blocks ≈ 7 min). + +## Creator-side wiring (idiomatic) + +```ts +import { CdrAgent } from "@cdr-kit/agent"; +import { aeneid } from "@cdr-kit/contracts"; +import { encodeAbiParameters, parseEther } from "viem"; + +const agent = new CdrAgent({ privateKey, network: "aeneid" }); + +// 1. allocate + configure the read condition (subscription example) +const readConfig = encodeAbiParameters( + [{ type: "uint256" }, { type: "uint256" }, { type: "address" }, { type: "uint8" }, { type: "address" }], + [parseEther("5"), 30n * 86400n, payee, 0, ipId], +); +const allocateTx = await agent.createVault({ + readConditionAddr: aeneid.subscriptionCondition, + readConfig, + licenseTermsId, +}); +// Pull uuid from the VaultCreated event in the receipt — do NOT predict it. +const receipt = await agent.client.publicClient.waitForTransactionReceipt({ hash: allocateTx }); +const uuid = pullUuidFromLogs(receipt.logs); + +// 2. encrypt + write (small payload) +await agent.writeVaultData({ uuid, dataKey: new TextEncoder().encode("BUY ETH @ 4200") }); +``` + +## Consumer-side wiring + +```ts +// If unsure whether you're already entitled, check first (no gas): +const ent = await agent.getEntitlement(uuid); +if (ent.isEntitled) { + const data = await agent.access(uuid); + console.log(new TextDecoder().decode(data)); +} else { + const plan = await agent.getSubscriptionPlan(uuid); + // budget check + if (plan.pricePerPeriodWei > MY_BUDGET_WEI) throw new Error("too expensive"); + const data = await agent.subscribeAndAccess({ + uuid, + periods: 1n, + maxPricePerPeriod: plan.pricePerPeriodWei, + value: plan.pricePerPeriodWei, + }); +} +``` + +## CLI alternative + +The same flow as one-shot commands: +- `cdr vault create --read --read-config ` (creator: allocate + configure) +- `cdr vault upload --pin-url --gateway ` (creator: encrypt + IPFS + allocate + write a file) +- `cdr vault info ` (consumer: pre-flight — info + plan + entitlement in one call) +- `cdr subscribe --periods 1 --max-price ` (consumer: pay + read) +- `cdr access ` (consumer: already-entitled read) +- `cdr access-license --license-token-id ` (consumer: Story-native license-gated) + +## Pitfalls (from `context/research/cdr-protocol-truth.md`) + +1. **CDR precompile calls OOG under `eth_estimateGas`** — `@cdr-kit/core`'s `createVault` sets `gas: 3M` for you. If you call the raw CdrKitVault contract from forge/cast, set a 2M+ explicit gas limit yourself (see `CDR_GAS_LIMIT` exported from `@cdr-kit/contracts`). +2. **`initWasm()` is required before any encrypt/decrypt.** `@cdr-kit/core`'s flows call `ensureWasm()` for you. If you reach into `@piplabs/cdr-sdk` directly, call `initWasm()` once at startup. +3. **`uuid` is a global counter — read it from the receipt.** Do not predict the next uuid (the SDK's `allocate()` simulates with one value but the live tx may settle on another). +4. **Read latency is variable.** Default `timeoutMs: 120_000` (was `600_000` pre-0.4 — corrected to match docs). Server cap is 200 blocks ≈ 7 min. Plan UIs for "tens of seconds typical, up to 7 min worst case". +5. **`access()` is for "already entitled".** It will revert on-chain if the read condition isn't satisfied. Check `agent.getEntitlement(uuid)` first if you don't know. +6. **`subscribeAndAccess()`'s `value` must equal `maxPricePerPeriod * periods` exactly.** Underpaying reverts; overpaying does not refund. + +## See also + +- `design-condition` — picking the right condition before you create the vault +- `debug-cdr-precompile` — what to do when `allocate` / `read` reverts or hangs +- `references/timeoutMs-tuning.md` — when to override the default +- `https://docs.story.foundation/developers/cdr-sdk/encrypt-and-decrypt` — official walkthrough
SkillWhen it firesWhat it teaches