Skip to content

feat: paid plugin marketplace — entitlement spine, Stripe Connect, FSL policy (0196) - #157

Merged
crs48 merged 7 commits into
mainfrom
feat/paid-plugin-marketplace-0196
Jun 17, 2026
Merged

feat: paid plugin marketplace — entitlement spine, Stripe Connect, FSL policy (0196)#157
crs48 merged 7 commits into
mainfrom
feat/paid-plugin-marketplace-0196

Conversation

@crs48

@crs48 crs48 commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Implements the foundation of the paid plugin marketplace from exploration 0196: pay plugin authors a one-time or monthly fee, "bring your own Stripe" and capture a small marketplace fee, and ship source-available plugins that auto-open after 2 years.

The key reframing

"Use your own Stripe" and "capture an xNet fee" are the same product via Stripe Connect Standard: the author keeps their own dashboard/payouts/KYC, and xNet attaches a 10% application_fee. There is no way to skim a fee from a standalone account — Connect is the supported version of that wish.

What ships here (all tested, MIT)

Phase 0 — paid-aware catalog + FSL policy

  • XNetExtension gains license / pricing / publisherDid; new PluginPricing type + isPaidPricing + manifest validation. MarketplaceEntry mirrors them.
  • license-policy.ts: ALLOWED_PLUGIN_LICENSES (default FSL-1.1-MIT) + pluginLicenseText() generating real FSL/MIT LICENSE files; the scaffolder emits them.
  • scripts/check-plugin-licenses.mjs: CI gate that fails a paid listing with a non-approved license (wired into the lint job). Proven against good/bad fixtures.
  • docs/guides/sell-a-plugin.md.

Phase 1 — entitlement spine

  • New @xnetjs/licenses package: Ed25519, DID-bound PluginLicense tokens — sign/verify/mint/checkLicenseFor, expiry + grace, kid for rotation. Asymmetric on purpose (the client is the adversary; it can't forge).
  • PluginRegistry.install runs a fail-closed paid-license gate via an injected checkLicense callback — @xnetjs/plugins stays runtime-dependency-free of the verifier.

Phase 2 — adapter half

  • @xnetjs/billing: CheckoutRequest.connect routes funds to the seller's connected account (transfer_data[destination]) and keeps the platform application_fee (percent for subs, amount for one-time). connect.ts fee math with a 10% (1000 bps) default.

Deferred (documented in the exploration's checklists)

Hub wiring that needs live Stripe / larger integration: seller-onboarding Connect OAuth, POST /marketplace/checkout, webhook→license-mint + refresh, payouts dashboard, the BYO-billing path (Phase 3), and lifecycle/revocation/2-year auto-open (Phase 4).

Validation

  • @xnetjs/licenses (16 tests), billing connect+stripe (16), plugins gates+manifest+scaffold+license-policy (40) — all green; full plugins suite 499 pass.
  • typecheck green for licenses/billing/plugins; check:cloud-boundary + check:plugin-licenses green; prettier clean on changed TS.
  • New workspace package → pnpm install + committed lockfile.

🤖 Generated with Claude Code

xNet Test and others added 6 commits June 17, 2026 13:50
…icensing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…okens (0196)

New MIT package: signs/verifies/mints offline-verifiable PluginLicense tokens
bound to the buyer's DID. Asymmetric (Ed25519) so an adversarial client cannot
forge a license — the verify+types ship to the client, the private key stays
hub-side. Mirrors the @xnetjs/entitlements sign/verify shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (0196)

- XNetExtension gains license / pricing / publisherDid; PluginPricing type +
  isPaidPricing helper; validateManifest checks the new fields.
- MarketplaceEntry mirrors license/pricing/publisherDid for badge + CI policy.
- PluginRegistry.install runs a paid-license gate via an injected checkLicense
  callback (host wires it to @xnetjs/licenses; plugins stays runtime-dep-free).
  LicenseRequiredError; fail-closed when a priced plugin has no provider.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ath (0196)

CheckoutRequest gains an optional `connect` (ConnectCharge): the Stripe adapter
routes funds to the seller's connected account via transfer_data[destination]
and keeps the platform application fee (application_fee_percent for subs,
application_fee_amount for one-time). New connect.ts: applicationFeeMinor /
feeBpsToPercent / sellerNetMinor with a 10% (1000 bps) default. This is the
'bring your own Stripe + captured marketplace fee' path (Connect Standard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + CI gate (0196)

- license-policy.ts: ALLOWED_PLUGIN_LICENSES (FSL-1.1-MIT default) +
  pluginLicenseText() generating real FSL/MIT LICENSE files.
- scaffolder emits license/pricing/publisherDid into the manifest + a matching
  LICENSE file.
- scripts/check-plugin-licenses.mjs: CI gate that fails a paid listing with a
  non-approved license; wired into the lint job + 'check:plugin-licenses'.
- docs/guides/sell-a-plugin.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark Phase 0, the @xnetjs/licenses spine + install gate (Phase 1), and the
Stripe Connect adapter + fee math (Phase 2 adapter) as shipped; record hub
wiring + Phase 3/4 as deferred. Tidy test import order + format the guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-157 June 17, 2026 20:54 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #157.

…(0196)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-157 June 17, 2026 21:05 — with GitHub Actions Inactive
const root = resolve(process.cwd())

/** Recursively collect `registry.json` files living under a `marketplace/` dir. */
function findRegistries(dir, underMarketplace, out) {
const BILLING_KINDS = ['managed', 'byo']

/** Validate the optional `pricing` descriptor (exploration 0196). */
function validatePricing(pricing: unknown, issues: string[]): void {
@crs48
crs48 merged commit 76441ae into main Jun 17, 2026
9 of 11 checks passed
@crs48
crs48 deleted the feat/paid-plugin-marketplace-0196 branch June 17, 2026 21:16
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
crs48 pushed a commit that referenced this pull request Jun 17, 2026
…caffold test

Changelog: none (CI/test hygiene).

- apps/web task-filter.test.ts: apply Prettier formatting (CI format-check).
- packages/cli connector.test.ts: the scaffolder emits a LICENSE (marketplace
  requirement, #157) but this expected-file list was stale and red on main
  already — align it with plugin.test.ts, which already expects LICENSE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crs48 added a commit that referenced this pull request Jun 18, 2026
Backfills the notable user-facing PRs that merged before the changelog
automation existed (or were flagged `needs-changelog`):

| Entry | PR |
|---|---|
| A richer, interactive changelog | #151 |
| xNet adapts to your phone (mobile workbench) | #156 |
| See your local agent in the AI panel | #163 |
| Paid plugins | #157 |
| Agent-native Connectors | #160 |
| A new look — cosmic-web app icons | #153 |

Each fragment carries its **original** PR number, so `resolve-prs`
leaves it untouched and the gallery/link point at the right PR. The
**#156 entry auto-pulls its real mobile-vs-desktop before/after
sliders** from `visuals/pr/156/` — confirming the auto-gallery works on
a backfilled entry.

This PR also exercises the new flow itself: it **adds fragments**, so
the required `changelog-section` check passes via the fragment-present
path (no `skip-changelog` label).

Verified locally: `validate:changelog` → 19 fragments valid; `astro
build` clean; all 6 articles + the #156 before/after gallery render.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48 added a commit that referenced this pull request Jun 18, 2026
## What

Backfills the changelog so it reflects what's live on `main`. The per-PR
fragment system (exploration 0197) was current through PR #163 (PR #178
backfilled #151/#153/#156/#157/#160/#163), but the recent **0198/0199
batch** and a couple of stragglers shipped without entries. This adds 7
fragments, with prose lifted from each PR's own `## Changelog` block
where present.

| PR | Entry | Tags |
|---|---|---|
| #143 | AI chat that knows your workspace | `ai` |
| #167 | Linear-grade Tasks | `tasks` `app` |
| #169 | Cleaner page editor typography | `editor` |
| #170 | Bring your Slack data into xNet | `plugins` `platform` |
| #171 | Smoother, more consistent motion | `app` |
| #173 | Airtable-grade database grids | `app` |
| #174 | A redesigned chat & channels experience | `app` |

## Deliberately excluded

- **In-editor AI (#154 / #158)** — shipped the transform engine +
diff/approval logic, but both PRs state the `/ai` UI wiring into the
running app is **deferred**, so it isn't user-visible yet. An entry
would overclaim.
- **Slack (#170)** — entry scoped to the migration connector +
compatibility layer (genuinely live); avoids claiming the
webhook/slash-command hub endpoints work, since they're
generic-over-an-injected-sink and not mounted in a running server yet.
- Pure infra/ops/docs PRs (cloud M1/M2, CI, runbooks) — not user-facing.

## Verification

- `validate-changelog.ts` → **26 fragments valid** (19 existing + 7 new)
- `prettier --check` on all new files → clean
- Newest-first ordering confirmed — the 7 entries slot in at the top

## Changelog

<!-- This PR only adds changelog fragments for already-merged work; it
ships no user-facing change of its own. -->

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants