papercliped 1.1.0-beta.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -11
- package/.claude-plugin/plugin.json +17 -8
- package/.mcp.json +8 -14
- package/CHANGELOG.md +64 -1
- package/LICENSE +21 -21
- package/README.md +80 -80
- package/commands/paperclip-report.md +5 -5
- package/commands/paperclip-status.md +4 -4
- package/commands/paperclip-sync.md +5 -5
- package/dist/accounts/alias.js +1 -1
- package/dist/accounts/username.js +1 -1
- package/dist/cli.js +11 -11
- package/dist/http.js +17 -3
- package/dist/manage/routes.js +60 -82
- package/dist/oauth/pages.js +28 -18
- package/dist/oauth/pg-store.js +0 -13
- package/dist/oauth/provider.js +27 -63
- package/dist/oauth/scopes.js +19 -16
- package/dist/openapi.js +19 -2
- package/dist/public-api/health.js +46 -0
- package/dist/public-api/openapi.js +51 -0
- package/dist/public-api/routes.js +251 -0
- package/dist/public-api/types.js +14 -0
- package/dist/server.js +4 -0
- package/dist/site/landing.js +3 -3
- package/dist/site/markdown.js +2 -1
- package/dist/site/site.js +16 -4
- package/dist/telemetry/recorder.js +1 -1
- package/dist/tools.js +48 -2
- package/dist/web/csp.js +16 -0
- package/dist/web/hosts.js +23 -0
- package/dist/web/routes.js +132 -0
- package/docs/ARCHITECTURE.md +74 -73
- package/docs/COMMUNITY.md +86 -0
- package/docs/DEPLOY-RENDER.md +3 -3
- package/docs/DOMAINS.md +74 -0
- package/docs/LAUNCH.md +50 -34
- package/docs/RELEASING.md +37 -0
- package/docs/SECURITY.md +82 -65
- package/docs/SITEMAP.md +92 -0
- package/docs/V3-SUBDOMAINS.md +106 -0
- package/docs/chatgpt.md +20 -20
- package/docs/least-privilege.sql +23 -23
- package/docs/legal/PRIVACY.md +37 -37
- package/docs/legal/TERMS.md +11 -11
- package/docs/oauth.md +61 -62
- package/docs/supabase-schema.sql +8 -1
- package/migrations/001_init.sql +84 -84
- package/migrations/002_audit.sql +41 -41
- package/migrations/003_accounts.sql +66 -66
- package/migrations/004_privacy_panel.sql +57 -57
- package/migrations/006_two_levels.sql +5 -0
- package/package.json +10 -14
- package/site/docs/chatgpt.md +1 -1
- package/site/docs/cli.md +27 -0
- package/site/docs/connect-your-paperclip.md +120 -0
- package/site/docs/environment.md +64 -0
- package/site/docs/faq.md +31 -0
- package/site/docs/getting-started.md +11 -5
- package/site/docs/limits-and-errors.md +32 -0
- package/site/docs/manage-api.md +33 -0
- package/site/docs/manage.md +12 -21
- package/site/docs/other-ai-apps.md +28 -0
- package/site/docs/paperclip-plugin.md +32 -0
- package/site/docs/permissions.md +5 -5
- package/site/docs/public-api.md +62 -0
- package/site/docs/security.md +2 -2
- package/site/docs/self-hosting.md +43 -0
- package/site/docs/signup.md +3 -3
- package/site/docs/tools.md +47 -0
- package/site/docs/troubleshooting.md +2 -2
- package/site/docs/what-is-papercliped.md +37 -0
- package/site/privacy.md +5 -3
- package/site/terms.md +1 -1
- package/skills/paperclip/SKILL.md +23 -23
- package/dist/demo.js +0 -144
- package/dist/manage/page.js +0 -170
- package/dist/panel/config.js +0 -61
- package/dist/panel/main.js +0 -90
- package/dist/panel/page.js +0 -553
- package/dist/panel/routes.js +0 -284
- package/dist/panel/server.js +0 -37
- package/docs/panel-role.sql +0 -26
package/docs/LAUNCH.md
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
# Launch guide — Papercliped
|
|
1
|
+
# Launch guide — Papercliped v2
|
|
2
2
|
|
|
3
3
|
Everything here costs **$0/month** (Render free, Supabase free, Cloudflare free). Do the steps in order. Nothing in this repo has been run against a live Render, Supabase, claude.ai or ChatGPT yet — treat the first pass as a staging run and finish with the smoke test (§8).
|
|
4
4
|
|
|
5
5
|
Never paste database passwords, `service_role` keys or `BRIDGE_SECRET` into chats, issues or commits. They go only into the host's environment settings.
|
|
6
6
|
|
|
7
7
|
## 1. Supabase (database)
|
|
8
|
-
1. Open your project → **SQL Editor** → paste all of [`docs/supabase-schema.sql`](supabase-schema.sql) → Run. (This is the full schema: accounts, grants, tokens, audit log, community log, node health, privacy,
|
|
9
|
-
2. Create the
|
|
8
|
+
1. Open your project → **SQL Editor** → paste all of [`docs/supabase-schema.sql`](supabase-schema.sql) → Run. (This is the full schema: accounts, grants, tokens, audit log, community log, node health, privacy, and the read-only `panel_*` views used by the separate operator dashboard.)
|
|
9
|
+
2. Create the least-privilege role for the bridge: edit the password, then run [`docs/least-privilege.sql`](least-privilege.sql) (role `bridge_app`). The read-only role for the operator dashboard is set up from that dashboard's own (private) repository.
|
|
10
10
|
3. **Project Settings → Database**: copy the **Transaction pooler** (port 6543) and **Session pooler** (port 5432) host/user details and the **SSL certificate** (CA). Build:
|
|
11
11
|
- `DATABASE_URL` = transaction pooler as `bridge_app`
|
|
12
12
|
- `DATABASE_MIGRATE_URL` = session pooler as the admin (`postgres.<ref>`) role — used only to apply future migrations at start
|
|
13
|
-
- `PANEL_DATABASE_URL` = transaction pooler as `panel_ro`
|
|
14
13
|
4. Free projects pause after about a week of inactivity. The bridge writes a heartbeat every 10 min while awake and the keep-alive in §5 touches `/readyz` (a database query) every 5 min, so it never goes quiet.
|
|
15
14
|
|
|
16
15
|
## 2. Render (the bridge) — Docker web service, no Blueprint
|
|
@@ -23,7 +22,7 @@ Blueprints are not needed (and the paid features are not used). The repo ships a
|
|
|
23
22
|
|---|---|
|
|
24
23
|
| `BRIDGE_OAUTH` | `1` |
|
|
25
24
|
| `BRIDGE_MODE` | `multi` |
|
|
26
|
-
| `BRIDGE_PUBLIC_URL` | `https://papercliped.
|
|
25
|
+
| `BRIDGE_PUBLIC_URL` | `https://mcp.papercliped.co` (the OAuth issuer; see [DOMAINS.md](DOMAINS.md)) |
|
|
27
26
|
| `BRIDGE_SECRET` | output of `openssl rand -hex 32` — **back it up now**; losing it disconnects every user |
|
|
28
27
|
| `DATABASE_URL` | transaction pooler as `bridge_app` (§1) |
|
|
29
28
|
| `DATABASE_MIGRATE_URL` | session pooler, admin role |
|
|
@@ -39,16 +38,44 @@ Blueprints are not needed (and the paid features are not used). The repo ships a
|
|
|
39
38
|
|
|
40
39
|
You can test the image anywhere with Docker: `docker build -t papercliped . && docker run --rm -p 10000:10000 -e PORT=10000 -e BRIDGE_TOKEN=$(openssl rand -hex 32) papercliped`.
|
|
41
40
|
|
|
42
|
-
## 3. Domain: papercliped.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
## 3. Domain: papercliped.co (Cloudflare, free plan)
|
|
42
|
+
The bridge answers at the root of the domain: `https://papercliped.co`.
|
|
43
|
+
|
|
44
|
+
**A. Put the domain on Cloudflare**
|
|
45
|
+
1. Cloudflare dashboard → **Add a domain** → `papercliped.co` → choose the **Free** plan. Let it import any existing DNS records, then delete ones you don't recognise.
|
|
46
|
+
2. Cloudflare shows two nameservers. At the company you bought the domain from, replace the domain's nameservers with those two. Turn off DNSSEC at the registrar first if it is on (re-enable it from Cloudflare later).
|
|
47
|
+
3. Wait until Cloudflare says the domain is **Active** (minutes to a few hours).
|
|
48
|
+
|
|
49
|
+
**B. Connect it to Render**
|
|
50
|
+
1. Render → your service → **Settings → Custom Domains → Add** `papercliped.co` (and `www.papercliped.co` if you want it). Render shows the target, `<service>.onrender.com`.
|
|
51
|
+
2. Cloudflare → `papercliped.co` → **DNS → Records → Add record**:
|
|
52
|
+
- Type `CNAME`, Name `@`, Target `<service>.onrender.com`, **Proxy status: DNS only (grey cloud)**. (Cloudflare allows a CNAME at the root; it flattens it automatically.)
|
|
53
|
+
- Optional: Type `CNAME`, Name `www`, Target `<service>.onrender.com`, DNS only.
|
|
54
|
+
3. Back in Render click **Verify**. Render issues the TLS certificate (a few minutes). Wait until Render shows the certificate as issued and `https://papercliped.co/healthz` answers.
|
|
55
|
+
4. Cloudflare → **SSL/TLS → Overview**: set encryption mode to **Full (strict)**.
|
|
56
|
+
5. Optional, after the certificate is issued: switch the records to **Proxied (orange cloud)**. If you do, set `BRIDGE_PROXY_HOPS=2` on Render (Cloudflare + Render) and re-test sign-in rate limiting. Grey cloud (DNS only) is fine and simpler.
|
|
57
|
+
|
|
58
|
+
**C. Don't let Cloudflare block AI apps**
|
|
59
|
+
Claude's and ChatGPT's servers call the bridge directly, so a challenge page breaks them. If the records are proxied:
|
|
60
|
+
- Leave **Bot Fight Mode** off, and don't use "I'm Under Attack" mode.
|
|
61
|
+
- If you add WAF or challenge rules, skip them for `/mcp`, `/actions/*`, `/openapi.json`, `/register`, `/token`, `/revoke`, `/authorize*`, `/.well-known/*`, `/api/manage/*` and `/healthz`, `/readyz`.
|
|
62
|
+
- Don't put Cloudflare Access in front of the bridge.
|
|
63
|
+
|
|
64
|
+
**D. Contact address (optional, free)**
|
|
65
|
+
Cloudflare → `papercliped.co` → **Email → Email Routing**: create `support@papercliped.co` forwarding to your inbox and verify the destination address (Cloudflare adds the MX, SPF and DKIM records; leave them alone, they sit beside the root CNAME/A record). Email Routing only receives and forwards; replying *as* support@ needs a separate sending service. Add a DMARC record (`_dmarc` TXT) so the domain can't be spoofed. support@ is also the security contact (docs/SECURITY.md). Then set `SITE_CONTACT=support@papercliped.co` on Render.
|
|
66
|
+
|
|
67
|
+
**More hosts (docs., mcp., api., forum.):** see [DOMAINS.md](DOMAINS.md) for the DNS records, Render domains, `PUBLIC_HOSTS` and the OAuth issuer decision.
|
|
68
|
+
|
|
69
|
+
**E. Point the bridge at the new domain**
|
|
70
|
+
1. Render → Environment: `BRIDGE_PUBLIC_URL=https://mcp.papercliped.co` (where AI apps connect; the website stays on `papercliped.co`), then redeploy.
|
|
71
|
+
2. `BRIDGE_PUBLIC_URL` is the OAuth issuer and is baked into every token. Changing it disconnects every app that connected under the old address; those users connect again with `https://mcp.papercliped.co/mcp`. Set it to the final domain before inviting more users.
|
|
72
|
+
3. Keep-alive worker (§5): `deploy/wrangler.toml` already targets `https://papercliped.co/readyz`; redeploy it with `npx wrangler deploy`.
|
|
73
|
+
4. Paperclip plugin: its default bridge URL is `https://papercliped.co` (instances that changed the setting keep their own value).
|
|
47
74
|
|
|
48
75
|
## 4. Check it works
|
|
49
76
|
```bash
|
|
50
|
-
curl https://papercliped.
|
|
51
|
-
curl https://papercliped.
|
|
77
|
+
curl https://papercliped.co/readyz # {"ok":true,...}
|
|
78
|
+
curl https://papercliped.co/.well-known/oauth-authorization-server | head
|
|
52
79
|
```
|
|
53
80
|
|
|
54
81
|
## 5. Keep it awake (Cloudflare Worker, free)
|
|
@@ -59,39 +86,28 @@ cd deploy && npx wrangler login && npx wrangler deploy # edit TARGET_URL in
|
|
|
59
86
|
It runs every 5 minutes (cron `*/5 * * * *`) against `/readyz`. Open the worker's URL once to see the last result. Alternative: a free UptimeRobot HTTP monitor on `/readyz` at a 5-minute interval. Free hosting can still have occasional cold starts — the first request after a platform restart may take about a minute.
|
|
60
87
|
|
|
61
88
|
## 6. Install paths for users
|
|
62
|
-
- **Claude (claude.ai / Desktop / mobile):** Settings → Connectors → *Add custom connector* → `https://papercliped.
|
|
63
|
-
- **Claude Code:** `claude mcp add --transport http papercliped https://papercliped.
|
|
89
|
+
- **Claude (claude.ai / Desktop / mobile):** Settings → Connectors → *Add custom connector* → `https://mcp.papercliped.co/mcp`. Claude opens the Papercliped sign-in page.
|
|
90
|
+
- **Claude Code:** `claude mcp add --transport http papercliped https://mcp.papercliped.co/mcp`, or install the plugin: `/plugin marketplace add OpenSourcx/papercliped` then `/plugin install papercliped@papercliped`.
|
|
64
91
|
- **npm / local stdio:** `npx papercliped` with `PAPERCLIP_API_URL` and `PAPERCLIP_API_KEY` set.
|
|
65
|
-
- **ChatGPT:** custom MCP app with the same `/mcp` URL (where your plan offers it), or import `https://papercliped.
|
|
92
|
+
- **ChatGPT:** custom MCP app with the same `/mcp` URL (where your plan offers it), or import `https://mcp.papercliped.co/openapi.json` as a GPT Action (see [chatgpt.md](chatgpt.md)).
|
|
66
93
|
|
|
67
94
|
First sign-in: pick a username (6–32 characters, at least one number or `.` `#` `_`), connect your Paperclip (public https URL; approve in Paperclip), and **save the secret key shown once**. Later: sign in with username + key, or reconnect through Paperclip approval and keep the same account. Tick *Appear anonymously* to be shown as an alias like `Ann02`.
|
|
68
95
|
|
|
69
|
-
## 7.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
PANEL_DATABASE_URL=<panel_ro URL> PANEL_DATABASE_SSL=verify PANEL_DATABASE_CA=<PEM>
|
|
74
|
-
PANEL_ALLOWED_IPS=<your IP> # strongly recommended
|
|
75
|
-
```
|
|
76
|
-
It refuses to start if `BRIDGE_SECRET` or `DATABASE_URL` is present.
|
|
77
|
-
- **Easiest ($0, no extra hours):** run it on your own machine — `npx papercliped-panel` — and open http://127.0.0.1:3940.
|
|
78
|
-
- **Always-on:** a second free Render service shares the 750 free hours and could suspend the bridge, so don't. Use another free host (Fly.io, Koyeb, a home server) or put it behind Cloudflare Tunnel + Cloudflare Access.
|
|
79
|
-
- Preview with fake data: `PANEL_DEMO=1 PANEL_ADMIN_TOKEN=<24+ chars> npx papercliped-panel`.
|
|
80
|
-
|
|
81
|
-
The panel shows system health (and turns critical if the bridge stops reporting — asleep, crashed or cut off from the database), latency percentiles, bridge-vs-Paperclip time, faults vs. caller errors, user count, flow success rate and the live log:
|
|
82
|
-
`09:25:30 og.kpnwrld - joined cliped` · `… - left cliped` · `… - updated cliped`.
|
|
83
|
-
Anonymous users appear as their alias. Operator commands: `npx papercliped-admin users | delete-user <name> | grants | revoke <id> | revoke-all --yes`.
|
|
96
|
+
## 7. Watching the service
|
|
97
|
+
- **Public stats (no sign-in):** `https://papercliped.co/api/public/v1/status` and `/api/public/v1/stats?window=24h` give aggregate users, connections, sign-in success, request success rate, latency, error mix and load. The website's `/status` page shows them with charts. Nothing identifying is published (see the public API docs).
|
|
98
|
+
- **Operator dashboard:** lives in a separate, access-controlled service (its own private repository), not in this repo. It reads the same telemetry tables (`audit_events`, `user_events`, `node_samples`) and the read-only `panel_*` views, which the bridge keeps writing and the migrations keep providing.
|
|
99
|
+
- **Operator commands** (on a machine with the bridge's environment): `npx papercliped-admin users | delete-user <name> | grants | revoke <id> | revoke-all --yes`.
|
|
84
100
|
|
|
85
101
|
## 8. Smoke test (before announcing)
|
|
86
102
|
1. Add the connector in claude.ai, create an account, connect a real Paperclip, ask "list my agents".
|
|
87
103
|
2. Disconnect, reconnect with the secret key; reconnect again through Paperclip approval (same account).
|
|
88
|
-
3. Toggle anonymity; confirm the
|
|
89
|
-
4. Stop traffic for 20 min; confirm the Cloudflare worker keeps the service answering and
|
|
104
|
+
3. Toggle anonymity; confirm the operator dashboard shows the alias for old and new entries.
|
|
105
|
+
4. Stop traffic for 20 min; confirm the Cloudflare worker keeps the service answering and `/api/public/v1/status` says `ok`.
|
|
90
106
|
|
|
91
107
|
## 9. Marketplaces — submit only after the smoke test passes
|
|
92
108
|
Requirements change; confirm each against the current official page before submitting.
|
|
93
109
|
|
|
94
|
-
**Claude directory (connectors / plugins)** — have ready: public GitHub repo (done), plugin manifest [`.claude-plugin/plugin.json`](../.claude-plugin/plugin.json), the remote MCP URL `https://papercliped.
|
|
110
|
+
**Claude directory (connectors / plugins)** — have ready: public GitHub repo (done), plugin manifest [`.claude-plugin/plugin.json`](../.claude-plugin/plugin.json), the remote MCP URL `https://mcp.papercliped.co/mcp`, OAuth callbacks (`https://claude.ai/api/mcp/auth_callback`, Claude Code's localhost loopback), privacy policy and terms (served by the bridge at `https://papercliped.co/privacy` and `/terms`; set `SITE_CONTACT` to a real contact address), a support contact, tool descriptions with read/write annotations, and a **test account** with a demo Paperclip for reviewers. Submit through Anthropic's connector/plugin submission form linked from the Claude docs. Until listed, users install through the custom-connector or `/plugin marketplace add` paths above.
|
|
95
111
|
|
|
96
112
|
**ChatGPT (apps / GPT Store)** — an MCP app needs a public https MCP endpoint with OAuth (you have it), a privacy policy URL, and domain verification in the OpenAI developer dashboard; a GPT with Actions needs the OpenAPI URL and OAuth settings (see [chatgpt.md](chatgpt.md)). Submission details for the apps directory are still moving — follow OpenAI's current "apps SDK / submit your app" guide.
|
|
97
113
|
|
|
@@ -101,7 +117,7 @@ npm login # account with 2FA
|
|
|
101
117
|
npm run build && npm test
|
|
102
118
|
npm publish --tag beta # first publish of "papercliped"; check the name is free first: npm view papercliped
|
|
103
119
|
```
|
|
104
|
-
Users run `npx papercliped@beta`.
|
|
120
|
+
Users run `npx papercliped@beta`. Pushing a new version to `main` runs [`release.yml`](../.github/workflows/release.yml), which publishes with npm trusted publishing (no token) and creates the GitHub release; one-time setup is in [RELEASING.md](RELEASING.md). If the name `papercliped` is taken, use a scope (`@kpnworld/papercliped`) and change `name` in `package.json`.
|
|
105
121
|
|
|
106
122
|
## 11. GitHub release "v1.0.0-beta.1"
|
|
107
123
|
Releases → **Draft a new release** → tag `v1.0.0-beta.1` (target `main`) → title *Papercliped v1.0.0-beta.1* → paste [`CHANGELOG.md`](../CHANGELOG.md) → tick **Set as a pre-release** → Publish.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Two npm packages ship from one version: `papercliped` (the bridge, repo root) and `papercliped-paperclip-plugin` (`plugin/`). Publishing uses **npm trusted publishing** (OIDC) from `.github/workflows/release.yml`: no npm token is stored in GitHub, and provenance is attached automatically.
|
|
4
|
+
|
|
5
|
+
Requirements: the repository is public, the workflow runs on GitHub-hosted runners, and GitHub Actions is enabled (and billable, if needed) for the `OpenSourcx` organization.
|
|
6
|
+
|
|
7
|
+
## One-time setup (npmjs.com)
|
|
8
|
+
Trusted publishing can only be configured on a package that already exists.
|
|
9
|
+
|
|
10
|
+
**State as of v2.1.0:** both packages exist on npm (`papercliped` 1.1.0-beta.2, `papercliped-paperclip-plugin` 2.0.0), so step 1 is done. `papercliped-paperclip-plugin@2.0.0` predates the move of account management into the plugin and links with the old one-time codes, which the bridge no longer accepts: v2.1.0 replaces it. Do step 2 for the plugin before tagging if you want the workflow to publish it; otherwise publish 2.1.0 by hand and the workflow skips it.
|
|
11
|
+
|
|
12
|
+
1. Publish the first version by hand, with 2FA (the plugin needs Node 24.11 or newer):
|
|
13
|
+
```sh
|
|
14
|
+
npm login
|
|
15
|
+
npm --prefix plugin ci
|
|
16
|
+
cd plugin && npm publish --otp=<code> # builds first (prepublishOnly); publishes papercliped-paperclip-plugin@2.0.0
|
|
17
|
+
```
|
|
18
|
+
If you publish the plugin by hand at 2.0.0, the release run skips it (that version already exists) and publishes only `papercliped`.
|
|
19
|
+
2. For **each** package on npmjs.com: open the package → **Settings** → **Trusted publishing** → **GitHub Actions** and enter:
|
|
20
|
+
- Organization or user: `OpenSourcx`
|
|
21
|
+
- Repository: `papercliped`
|
|
22
|
+
- Workflow filename: `release.yml`
|
|
23
|
+
- Allow `npm publish`
|
|
24
|
+
3. For each package, set **Publishing access** to *Require two-factor authentication and disallow tokens*, then revoke any old automation tokens.
|
|
25
|
+
|
|
26
|
+
`repository.url` in both `package.json` files must stay exactly `git+https://github.com/OpenSourcx/papercliped.git` (npm checks it against the workflow's repository), and `publishConfig` must stay `{ "access": "public" }`.
|
|
27
|
+
|
|
28
|
+
## The website
|
|
29
|
+
The website (`web/`) is **not** in the npm package. The Docker image builds it (a separate stage) and the bridge serves it from `/app/web/dist`. The npm package's `papercliped-bridge` serves its built-in Markdown pages instead, unless you point `WEB_DIST` at a built copy (`npm --prefix web ci && npm --prefix web run build`).
|
|
30
|
+
|
|
31
|
+
## Each release
|
|
32
|
+
Releases are automatic: **every push to `main` whose version isn't released yet becomes a release.** Nobody pushes tags.
|
|
33
|
+
|
|
34
|
+
1. Add what changed under `## Unreleased` in `CHANGELOG.md`, then run `node scripts/version.mjs <major|minor|patch> "<title>"`. It bumps every file listed in `.release.json` (`package.json`, `plugin/package.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `plugin/src/manifest.ts`, the lockfiles), turns `## Unreleased` into `## vX.Y.Z — <title>` and records the date. Which number to bump, and how to write the commit description, is in the release skill (`.agents/skills/release/SKILL.md`).
|
|
35
|
+
2. Push to `main`.
|
|
36
|
+
|
|
37
|
+
On every push to `main`, `.github/workflows/release.yml` reads the version. If the tag `vX.Y.Z` already exists it stops (an ordinary commit). Otherwise it checks that every version string and the changelog section agree, builds and tests the root and the plugin, packs and installs both packages as a user would (`scripts/check-packages.mjs`), publishes `papercliped` and `papercliped-paperclip-plugin` (a version with `-`, like `2.2.0-beta.1`, goes to the `beta` dist-tag, otherwise `latest`), and only then creates the tag `vX.Y.Z` on that exact commit together with the GitHub release, whose notes are the changelog section followed by every commit description since the previous release. A push that doesn't bump the version ships nothing and gets a warning in the run. Each publish step skips a version already on npm, and the tag is made last, so a failed run can be re-run from the Actions tab (or with **Run workflow**) once the cause is fixed.
|
package/docs/SECURITY.md
CHANGED
|
@@ -1,65 +1,82 @@
|
|
|
1
|
-
# Security model (public / multi-tenant deployment)
|
|
2
|
-
|
|
3
|
-
Read this before running the bridge for people other than yourself.
|
|
4
|
-
|
|
5
|
-
## What you are holding
|
|
6
|
-
For every connected user the bridge stores a **Paperclip board API key**. In Paperclip that key is bound to the *user*, not to a company, and carries **all** of that user's board power (verified in Paperclip's source: `board_api_keys` has no scope column, and the challenge's `requestedCompanyId` is only used to attribute activity-log entries, not to restrict the key). The bridge's scopes (read/control/admin) limit what *connected AI clients* may do through the bridge. They do **not** limit what the stored key can do if the bridge itself is compromised.
|
|
7
|
-
|
|
8
|
-
> **Headline risk:** a full compromise of the running bridge (code execution on the Render service *and* access to its environment) exposes every connected user's Paperclip with full board access, until those keys are revoked. Everything below reduces the likelihood or blast radius; none of it removes that risk. Until Paperclip offers scoped/short-lived keys, tell users plainly, encourage a dedicated Paperclip user for the connection, and keep the user base small enough to be comfortable with that.
|
|
9
|
-
|
|
10
|
-
## Trust boundaries
|
|
11
|
-
```
|
|
12
|
-
AI client (claude.ai, ChatGPT) ──OAuth, scoped, opaque tokens──▶ Bridge ──user's board key, https only──▶ User's Paperclip (stranger-controlled)
|
|
13
|
-
│
|
|
14
|
-
└──▶ Postgres (sealed credentials, token hashes)
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Threats and mitigations (each marked with where it is tested)
|
|
18
|
-
| # | Threat | Mitigation | Test |
|
|
19
|
-
| - | --- | --- | --- |
|
|
20
|
-
| 1 | **SSRF**: a user types an internal/metadata address as their "Paperclip" | https-only DNS names, no IP literals (incl. decimal/hex/IPv6/Unicode-dot forms), port allow-list (443), no `.local/.internal/.lan/…`, bridge's own host denied; **the resolved address is validated inside the connection's DNS lookup**, so there's no check-then-connect gap for DNS rebinding; any non-public answer refuses the whole name; no redirects; 20 s / 2 MB caps | `safe-fetch.test.ts`, `multitenant.test.ts` (nothing leaves the bridge for 15 hostile inputs) |
|
|
21
|
-
| 2 | Hostile "Paperclip" returns crafted data (bad ids, `@evil.com` approval link, non-JSON, huge bodies) | every field validated (uuid ids, strict approval-path pattern, token charset/length, bounded expiry); only whitelisted statuses honoured; `local_trusted` refused; body capped | `multitenant.test.ts` (hostile cases) |
|
|
22
|
-
| 3 | Probing the network through the instance step | per-request attempt cap (5), per-IP rate limit, no error detail from resolution/connection leaks to the user | same |
|
|
23
|
-
| 4 | Cross-tenant mix-up (user A's token sent to B's host) | each grant stores its own instance; the client is built per request from that grant only; the instance is re-validated on **every** call (tampered/newly denied hosts are cut off); the raw-API tool takes only a path | `tenant isolation`, `re-validates the stored instance` |
|
|
24
|
-
| 5 | Database leak | credentials sealed in the app with AES-256-GCM (key from `BRIDGE_SECRET`, never in the DB); tokens stored as SHA-256 only; consumed authorization codes are scrubbed; **the database alone yields nothing usable** | `multitenant.test.ts` (Postgres at-rest check) |
|
|
25
|
-
| 6 | Supabase Data API exposes the tables | dedicated `bridge` schema (not exposed by default), RLS on with no policies, grants revoked from `anon`/`authenticated`; app uses a role that can touch only these tables | `store.test.ts` (anon/authenticated denied; least-privilege role) |
|
|
26
|
-
| 7 | OAuth attacks: code interception, replay, CSRF, open redirect, mix-up | PKCE S256 mandatory; single-use 60 s codes (atomic in SQL; replay revokes the grant it created); CSRF token on every form; never redirects to an unregistered `redirect_uri`; allow-listed redirect hosts; `iss` in responses | `oauth.test.ts`, `store.test.ts` (races) |
|
|
27
|
-
| 8 | Token theft | opaque tokens, 1 h access / 30 d refresh; refresh rotation with **reuse detection** (revokes the grant and the Paperclip key); atomic consume so a race can't mint two | `oauth.test.ts`, `store.test.ts` |
|
|
28
|
-
| 9 | Over-privileged AI client / prompt injection into the model | read/control
|
|
29
|
-
| 10 | Stale credentials linger after a user "disconnects" in the client (clients often just forget tokens) | hourly sweep revokes grants idle > 30 days and deletes the credential; `/revoke` honoured; Paperclip key revoked upstream (`revoke-current`). The key's name in the user's Paperclip is `<client> (via bridge, returns to <host>) (board)` so they can see/revoke it themselves | `multitenant.test.ts` (sweep) |
|
|
30
|
-
| 11 | Phishing via a malicious OAuth client claiming to be "Claude" | clients register only allow-listed redirect hosts; the Paperclip approval page shows the real redirect host in the key label; the consent page shows the redirect host and the instance host | `oauth.test.ts` |
|
|
31
|
-
| 12 | Brute force of the instance/consent/token endpoints | per-IP and per-grant limits in the shared store (work across several instances); client IP taken from the trusted-proxy position, not the forgeable left side of `X-Forwarded-For` | `multitenant.test.ts` |
|
|
32
|
-
| 13 | Clickjacking / XSS on consent pages | CSP (`frame-ancestors 'none'`, nonce'd script, restricted `form-action`), `X-Frame-Options`, all dynamic text escaped | `oauth.test.ts` |
|
|
33
|
-
| 14 | Operator error: unsafe public config | the process refuses to start for: http issuer, short secret, `BRIDGE_TOKEN` set in multi mode, static login in multi mode, no database in multi mode | `multitenant.test.ts` |
|
|
34
|
-
| 16 | Telemetry becoming a data leak / a side door | audit rows hold only tool name, outcome, timings, grant id, client name, tenant host, user id (never arguments, results, tokens or credentials); retention 30 days; recorder can't block or fail a request; the
|
|
35
|
-
| 15 | Key compromise / rotation | key ring (`BRIDGE_SECRET_PREVIOUS`), `rotate-keys`, `revoke-all --yes` | `multitenant.test.ts` |
|
|
36
|
-
|
|
37
|
-
## Residual risks (not mitigated here)
|
|
38
|
-
1. **Bridge compromise = access to all connected Paperclips** (headline risk above). Reduce: minimal dependencies, pinned versions, 2FA on Render/GitHub/Supabase, no shell access to the service, alerts on unusual `audit` volume.
|
|
39
|
-
2. **A user's own Paperclip can feed prompt-injected text to the AI client.** The skill instructs the model to treat tool output as data, but that is not a hard boundary. Scopes bound the damage (a read-only connection can't change anything).
|
|
40
|
-
3. **No per-company limits.** A grant reaches every company that user can reach (Paperclip limitation, see above).
|
|
41
|
-
4. **DNS rebinding after connect, TLS interception by a hostile network between Render and a tenant:** TLS verification is on; we do not pin certificates.
|
|
42
|
-
5. **Single database, single region, single secret.** Losing `BRIDGE_SECRET` disconnects everyone (by design: it cannot be recovered).
|
|
43
|
-
6. **Not independently audited.** Treat this as unreviewed code. Before a wide public launch, get an external review of `src/net/safe-fetch.ts`, `src/oauth/*` and the migrations.
|
|
44
|
-
7. DCR clients accumulate (idle ones are pruned after 7 days; capped at 1000).
|
|
45
|
-
8. ChatGPT's real redirect hosts and Claude's directory-review requirements were taken from documentation/secondary sources, not tested.
|
|
46
|
-
|
|
47
|
-
## Operational checklist
|
|
48
|
-
- [ ] `BRIDGE_SECRET` stored in a password manager, separate from the database backups
|
|
49
|
-
- [ ] `DATABASE_SSL=verify` with the Supabase CA (not `require`)
|
|
50
|
-
- [ ] App uses the `bridge_app` role; admin role only in `DATABASE_MIGRATE_URL`
|
|
51
|
-
- [ ] `bridge` schema not in Supabase's exposed schemas; anon-key read of `bridge.grants` fails
|
|
52
|
-
- [ ] 2FA on Render, Supabase, GitHub, npm
|
|
53
|
-
- [ ] Log retention ≤ 30 days; confirm logs contain no tokens (audit lines hold tool, grant id, client, instance host, user id only)
|
|
54
|
-
- [ ] Privacy policy + terms published; support and security contact set
|
|
55
|
-
- [ ] `revoke-all --yes` rehearsed on staging
|
|
56
|
-
- [ ] External security review done (before >~50 users or any directory listing)
|
|
57
|
-
|
|
58
|
-
## Reporting a vulnerability
|
|
59
|
-
Email **
|
|
60
|
-
|
|
61
|
-
## Accounts, secret keys and anonymity (v1.0.0-beta.1)
|
|
62
|
-
|
|
63
|
-
- A user's **secret key** is shown once, stored only as a salted scrypt hash, and works like a password for *their Paperclip connection*: anyone holding it can sign in as them. Users can replace it by reconnecting through Paperclip approval.
|
|
64
|
-
- Usernames appear only in the operator's logs and
|
|
65
|
-
- Account actions are rate-limited per username and per address; failed sign-ins never reveal whether a username exists beyond what registration already shows.
|
|
1
|
+
# Security model (public / multi-tenant deployment)
|
|
2
|
+
|
|
3
|
+
Read this before running the bridge for people other than yourself.
|
|
4
|
+
|
|
5
|
+
## What you are holding
|
|
6
|
+
For every connected user the bridge stores a **Paperclip board API key**. In Paperclip that key is bound to the *user*, not to a company, and carries **all** of that user's board power (verified in Paperclip's source: `board_api_keys` has no scope column, and the challenge's `requestedCompanyId` is only used to attribute activity-log entries, not to restrict the key). The bridge's scopes (read/control/admin) limit what *connected AI clients* may do through the bridge. They do **not** limit what the stored key can do if the bridge itself is compromised.
|
|
7
|
+
|
|
8
|
+
> **Headline risk:** a full compromise of the running bridge (code execution on the Render service *and* access to its environment) exposes every connected user's Paperclip with full board access, until those keys are revoked. Everything below reduces the likelihood or blast radius; none of it removes that risk. Until Paperclip offers scoped/short-lived keys, tell users plainly, encourage a dedicated Paperclip user for the connection, and keep the user base small enough to be comfortable with that.
|
|
9
|
+
|
|
10
|
+
## Trust boundaries
|
|
11
|
+
```
|
|
12
|
+
AI client (claude.ai, ChatGPT) ──OAuth, scoped, opaque tokens──▶ Bridge ──user's board key, https only──▶ User's Paperclip (stranger-controlled)
|
|
13
|
+
│
|
|
14
|
+
└──▶ Postgres (sealed credentials, token hashes)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Threats and mitigations (each marked with where it is tested)
|
|
18
|
+
| # | Threat | Mitigation | Test |
|
|
19
|
+
| - | --- | --- | --- |
|
|
20
|
+
| 1 | **SSRF**: a user types an internal/metadata address as their "Paperclip" | https-only DNS names, no IP literals (incl. decimal/hex/IPv6/Unicode-dot forms), port allow-list (443), no `.local/.internal/.lan/…`, bridge's own host denied; **the resolved address is validated inside the connection's DNS lookup**, so there's no check-then-connect gap for DNS rebinding; any non-public answer refuses the whole name; no redirects; 20 s / 2 MB caps | `safe-fetch.test.ts`, `multitenant.test.ts` (nothing leaves the bridge for 15 hostile inputs) |
|
|
21
|
+
| 2 | Hostile "Paperclip" returns crafted data (bad ids, `@evil.com` approval link, non-JSON, huge bodies) | every field validated (uuid ids, strict approval-path pattern, token charset/length, bounded expiry); only whitelisted statuses honoured; `local_trusted` refused; body capped | `multitenant.test.ts` (hostile cases) |
|
|
22
|
+
| 3 | Probing the network through the instance step | per-request attempt cap (5), per-IP rate limit, no error detail from resolution/connection leaks to the user | same |
|
|
23
|
+
| 4 | Cross-tenant mix-up (user A's token sent to B's host) | each grant stores its own instance; the client is built per request from that grant only; the instance is re-validated on **every** call (tampered/newly denied hosts are cut off); the raw-API tool takes only a path | `tenant isolation`, `re-validates the stored instance` |
|
|
24
|
+
| 5 | Database leak | credentials sealed in the app with AES-256-GCM (key from `BRIDGE_SECRET`, never in the DB); tokens stored as SHA-256 only; consumed authorization codes are scrubbed; **the database alone yields nothing usable** | `multitenant.test.ts` (Postgres at-rest check) |
|
|
25
|
+
| 6 | Supabase Data API exposes the tables | dedicated `bridge` schema (not exposed by default), RLS on with no policies, grants revoked from `anon`/`authenticated`; app uses a role that can touch only these tables | `store.test.ts` (anon/authenticated denied; least-privilege role) |
|
|
26
|
+
| 7 | OAuth attacks: code interception, replay, CSRF, open redirect, mix-up | PKCE S256 mandatory; single-use 60 s codes (atomic in SQL; replay revokes the grant it created); CSRF token on every form; never redirects to an unregistered `redirect_uri`; allow-listed redirect hosts; `iss` in responses | `oauth.test.ts`, `store.test.ts` (races) |
|
|
27
|
+
| 8 | Token theft | opaque tokens, 1 h access / 30 d refresh; refresh rotation with **reuse detection** (revokes the grant and the Paperclip key); atomic consume so a race can't mint two | `oauth.test.ts`, `store.test.ts` |
|
|
28
|
+
| 9 | Over-privileged AI client / prompt injection into the model | read/control enforced by the bridge, read preselected, out-of-scope tools hidden and refused; confirm gates on destructive calls; per-grant call budget (120/min) | `oauth.test.ts`, `multitenant.test.ts` |
|
|
29
|
+
| 10 | Stale credentials linger after a user "disconnects" in the client (clients often just forget tokens) | hourly sweep revokes grants idle > 30 days and deletes the credential; `/revoke` honoured; Paperclip key revoked upstream (`revoke-current`). The key's name in the user's Paperclip is `<client> (via bridge, returns to <host>) (board)` so they can see/revoke it themselves | `multitenant.test.ts` (sweep) |
|
|
30
|
+
| 11 | Phishing via a malicious OAuth client claiming to be "Claude" | clients register only allow-listed redirect hosts; the Paperclip approval page shows the real redirect host in the key label; the consent page shows the redirect host and the instance host | `oauth.test.ts` |
|
|
31
|
+
| 12 | Brute force of the instance/consent/token endpoints | per-IP and per-grant limits in the shared store (work across several instances); client IP taken from the trusted-proxy position, not the forgeable left side of `X-Forwarded-For` | `multitenant.test.ts` |
|
|
32
|
+
| 13 | Clickjacking / XSS on consent pages | CSP (`frame-ancestors 'none'`, nonce'd script, restricted `form-action`), `X-Frame-Options`, all dynamic text escaped | `oauth.test.ts` |
|
|
33
|
+
| 14 | Operator error: unsafe public config | the process refuses to start for: http issuer, short secret, `BRIDGE_TOKEN` set in multi mode, static login in multi mode, no database in multi mode | `multitenant.test.ts` |
|
|
34
|
+
| 16 | Telemetry becoming a data leak / a side door | audit rows hold only tool name, outcome, timings, grant id, client name, tenant host, user id (never arguments, results, tokens or credentials); retention 30 days; recorder can't block or fail a request; the operator dashboard is a SEPARATE, access-controlled service outside this repo (cannot call tools) with a read-only database role limited to safe views (no secret hashes, sealed credentials or tokens); the public stats API returns aggregates only, never identities | `audit.test.ts`, `public-api.test.ts`, `store.test.ts` |
|
|
35
|
+
| 15 | Key compromise / rotation | key ring (`BRIDGE_SECRET_PREVIOUS`), `rotate-keys`, `revoke-all --yes` | `multitenant.test.ts` |
|
|
36
|
+
|
|
37
|
+
## Residual risks (not mitigated here)
|
|
38
|
+
1. **Bridge compromise = access to all connected Paperclips** (headline risk above). Reduce: minimal dependencies, pinned versions, 2FA on Render/GitHub/Supabase, no shell access to the service, alerts on unusual `audit` volume.
|
|
39
|
+
2. **A user's own Paperclip can feed prompt-injected text to the AI client.** The skill instructs the model to treat tool output as data, but that is not a hard boundary. Scopes bound the damage (a read-only connection can't change anything).
|
|
40
|
+
3. **No per-company limits.** A grant reaches every company that user can reach (Paperclip limitation, see above).
|
|
41
|
+
4. **DNS rebinding after connect, TLS interception by a hostile network between Render and a tenant:** TLS verification is on; we do not pin certificates.
|
|
42
|
+
5. **Single database, single region, single secret.** Losing `BRIDGE_SECRET` disconnects everyone (by design: it cannot be recovered).
|
|
43
|
+
6. **Not independently audited.** Treat this as unreviewed code. Before a wide public launch, get an external review of `src/net/safe-fetch.ts`, `src/oauth/*` and the migrations.
|
|
44
|
+
7. DCR clients accumulate (idle ones are pruned after 7 days; capped at 1000).
|
|
45
|
+
8. ChatGPT's real redirect hosts and Claude's directory-review requirements were taken from documentation/secondary sources, not tested.
|
|
46
|
+
|
|
47
|
+
## Operational checklist
|
|
48
|
+
- [ ] `BRIDGE_SECRET` stored in a password manager, separate from the database backups
|
|
49
|
+
- [ ] `DATABASE_SSL=verify` with the Supabase CA (not `require`)
|
|
50
|
+
- [ ] App uses the `bridge_app` role; admin role only in `DATABASE_MIGRATE_URL`
|
|
51
|
+
- [ ] `bridge` schema not in Supabase's exposed schemas; anon-key read of `bridge.grants` fails
|
|
52
|
+
- [ ] 2FA on Render, Supabase, GitHub, npm
|
|
53
|
+
- [ ] Log retention ≤ 30 days; confirm logs contain no tokens (audit lines hold tool, grant id, client, instance host, user id only)
|
|
54
|
+
- [ ] Privacy policy + terms published; support and security contact set
|
|
55
|
+
- [ ] `revoke-all --yes` rehearsed on staging
|
|
56
|
+
- [ ] External security review done (before >~50 users or any directory listing)
|
|
57
|
+
|
|
58
|
+
## Reporting a vulnerability
|
|
59
|
+
Email **support@papercliped.co** with details (put "security" in the subject), or open a private GitHub security advisory on `OpenSourcx/papercliped`; please don't open public issues for security reports. We aim to acknowledge within 3 business days.
|
|
60
|
+
|
|
61
|
+
## Accounts, secret keys and anonymity (v1.0.0-beta.1)
|
|
62
|
+
|
|
63
|
+
- A user's **secret key** is shown once, stored only as a salted scrypt hash, and works like a password for *their Paperclip connection*: anyone holding it can sign in as them. Users can replace it by reconnecting through Paperclip approval.
|
|
64
|
+
- Usernames appear only in the operator's logs and the private operator dashboard. With **anonymous mode** on, logs, grants and audit rows show an alias (`Ann02`) and a masked instance label instead; toggling rewrites history. This is **pseudonymity, not anonymity from the operator**: someone with base-table database access (the bridge role, the Supabase admin) can still map alias → account. A read-only dashboard role over the safe views cannot.
|
|
65
|
+
- Account actions are rate-limited per username and per address; failed sign-ins never reveal whether a username exists beyond what registration already shows.
|
|
66
|
+
|
|
67
|
+
## Account management through the Paperclip plugin
|
|
68
|
+
|
|
69
|
+
- There is no browser page or cookie session any more: the manage API accepts only bearer plugin tokens, which a cross-site page cannot attach without a CORS preflight (never approved), so there is no CSRF surface. `GET /manage` redirects to the plugin docs.
|
|
70
|
+
- **Linking:** the plugin sends username + secret key once to `POST /api/manage/plugin-link/sign-in`, throttled with the same per-address and per-account counters as the sign-in page. The answer is a long-lived token (`pcb_pl_…`, stored hashed on a grant with **no scopes**, 180-day expiry). The key is not stored by the plugin or the bridge (only its hash, as before).
|
|
71
|
+
- A plugin token opens only the manage API for its own account. It can **not** call the MCP or Actions endpoints. Sensitive actions (new secret key, disconnect Paperclip, delete account) need the secret key in that request and share the sign-in throttle; deleting also needs the username typed. A new secret key revokes every plugin link and issues one fresh token to the plugin that asked. Tests: `test/manage.test.ts`.
|
|
72
|
+
- The plugin runs as trusted, same-origin code inside Paperclip and is installed only by the instance admin. Its worker keeps each person's token in plugin state keyed by a hash of the host-verified user id, never returns it to the page, and talks only to an `https` bridge URL. Residual risk: anyone able to read the plugin's state storage (the Paperclip server operator) can use a linked token for the manage API; remove that link from another Paperclip, or make a new secret key, if the Paperclip host is not trusted.
|
|
73
|
+
- The plugin has not been run in a live Paperclip; see `plugin/README.md`.
|
|
74
|
+
|
|
75
|
+
## Planned (v2): managed subdomains
|
|
76
|
+
See `docs/V3-SUBDOMAINS.md` §7a for the hardening list and the threats to add here when it is built.
|
|
77
|
+
|
|
78
|
+
## Public stats API and the operator dashboard (v2)
|
|
79
|
+
|
|
80
|
+
- `GET /api/public/v1/*` publishes **aggregate numbers only**: counts, rates, latency percentiles, fixed error and sign-in failure categories, and tool-name call counts from the public catalogue. It never returns usernames, aliases, account or grant ids, Paperclip hosts, app names, IP addresses, tokens or free-text details, and never breaks numbers down per user or per tenant. Unknown categories are counted as `other` rather than echoed. Tests: `test/public-api.test.ts` seeds recognisable names, hosts and aliases and asserts none appear in any response (memory and Postgres).
|
|
81
|
+
- It is GET-only, CORS-open without credentials or cookies, cached 30 s and rate-limited to 60 requests per minute per address.
|
|
82
|
+
- The **operator dashboard** (per-user and per-tenant views, the community log with usernames) is **not in this repository**. It runs as a separate, access-controlled service with its own credentials and a read-only database role over the `panel_*` views and telemetry tables. `test/store.test.ts` keeps proving that such a role can read nothing sensitive.
|
package/docs/SITEMAP.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Site map: every page and endpoint
|
|
2
|
+
|
|
3
|
+
Current as of v2.0.0 (read from the route code). Items marked **(planned v3)** do not exist yet. `{{URL}}` is the bridge's public address (today `https://papercliped.co`).
|
|
4
|
+
|
|
5
|
+
## 0. Hosts
|
|
6
|
+
One service, routed by host (`PUBLIC_HOSTS`, `src/web/`): `papercliped.co` the website, `docs.papercliped.co` the docs (landing at `/`, pages at `/<page>`), `mcp.`/`api.papercliped.co` the protocol and APIs, `forum.` a redirect, `www.` a redirect to the apex. See `docs/DOMAINS.md`. Below, docs paths are shown as `/docs/<page>`; on the docs host they are `/<page>`.
|
|
7
|
+
|
|
8
|
+
## 1. Public website (React, `web/`; the bridge's Markdown pages in `src/site/` are the fallback)
|
|
9
|
+
| Path | Page |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `/` | Landing page ("Papercliped, not Paperclipped."), live user count |
|
|
12
|
+
| `/status` | Live status page (public API) |
|
|
13
|
+
| `/changelog` | Changelog (and `/changelog.xml` feed) |
|
|
14
|
+
| `/community` | Community |
|
|
15
|
+
| `/brand` | Brand assets |
|
|
16
|
+
| `/kit` | Design kit (not indexed) |
|
|
17
|
+
| `/docs` | Redirects to `/docs/getting-started` |
|
|
18
|
+
| `/docs/getting-started` | Getting started |
|
|
19
|
+
| `/docs/what-is-papercliped` | What is Papercliped? (overview and glossary) |
|
|
20
|
+
| `/docs/self-hosting` | Run it yourself |
|
|
21
|
+
| `/docs/tools` | Tool reference (generated from `src/tools.ts`) |
|
|
22
|
+
| `/docs/manage-api` | Manage API |
|
|
23
|
+
| `/docs/environment` | Environment variables |
|
|
24
|
+
| `/docs/cli` | Command line |
|
|
25
|
+
| `/docs/limits-and-errors` | Limits and errors |
|
|
26
|
+
| `/docs/faq` | FAQ |
|
|
27
|
+
| `/docs/signup` | Create your account |
|
|
28
|
+
| `/docs/connect-your-paperclip` | Set up your Paperclip (public address, tunnels, domains) |
|
|
29
|
+
| `/docs/manage` | Manage connections |
|
|
30
|
+
| `/docs/paperclip-plugin` | Paperclip plugin |
|
|
31
|
+
| `/docs/permissions` | Permissions |
|
|
32
|
+
| `/docs/anonymous-mode` | Anonymous mode |
|
|
33
|
+
| `/docs/public-api` | Public API and status |
|
|
34
|
+
| `/docs/security` | Security |
|
|
35
|
+
| `/docs/other-ai-apps` | Any AI app (MCP) |
|
|
36
|
+
| `/docs/chatgpt` | ChatGPT setup |
|
|
37
|
+
| `/docs/troubleshooting` | Troubleshooting |
|
|
38
|
+
| `/privacy` | Privacy policy |
|
|
39
|
+
| `/terms` | Terms of service |
|
|
40
|
+
| `/favicon.svg`, `/favicon.ico` | Favicon |
|
|
41
|
+
| `/api/public/stats` | JSON: user and connection counts only (legacy; see section 6) |
|
|
42
|
+
| `/domain` **(planned v3)** | Get a public address: guide, own domain, or the paid managed subdomain |
|
|
43
|
+
| `/docs/subdomain-terms` **(planned v3)** | Subdomain Terms (drafted separately) |
|
|
44
|
+
|
|
45
|
+
## 2. Sign-in and consent (OAuth screens, shown inside Claude/ChatGPT flows) — `src/oauth/`
|
|
46
|
+
| Path | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `GET /authorize` | Start: sign in with username and secret key, or connect your Paperclip |
|
|
49
|
+
| `GET /authorize/status` | Polling while the user approves in Paperclip |
|
|
50
|
+
| `POST /authorize/login`, `/connect`, `/instance`, `/approved`, `/username`, `/welcome`, `/continue`, `/decision` | The steps of the screens above (login, enter address, approve, pick username, secret key shown once, consent: Read only / Full control, Appear anonymously) |
|
|
51
|
+
| `/authorize` link "No public address?" **(planned v3)** | Links to `/domain` |
|
|
52
|
+
|
|
53
|
+
## 3. Account management (inside Paperclip, via the plugin) — `src/manage/`
|
|
54
|
+
| Path | Purpose |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `GET /manage` | Old address: redirects to `/docs/paperclip-plugin` |
|
|
57
|
+
| `/api/manage/*` | JSON API used by the Paperclip plugin (bearer token only, no cookies): `plugin-link/sign-in`, `me`, `connections`, `connections/:id`, `privacy`, `plugin-links`, `plugin-links/:id`, `secret/rotate`, `paperclip/disconnect`, `account/delete` (the last three need the secret key again) |
|
|
58
|
+
| Domain section in the plugin **(planned v3)** | Pro members: address, tunnel status, token, port, release |
|
|
59
|
+
|
|
60
|
+
## 4. AI client endpoints
|
|
61
|
+
| Path | Purpose |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `/mcp` | MCP over Streamable HTTP (Claude and other MCP clients) |
|
|
64
|
+
| `/actions/<tool>` | ChatGPT Actions, one per tool |
|
|
65
|
+
| `/openapi.json` | OpenAPI description for GPT Actions |
|
|
66
|
+
| `/register`, `/token`, `/revoke` | OAuth 2.1 dynamic client registration, tokens, revocation |
|
|
67
|
+
| `/.well-known/oauth-authorization-server` | OAuth server metadata |
|
|
68
|
+
| `/.well-known/oauth-protected-resource`, `/.well-known/oauth-protected-resource/mcp` | Protected-resource metadata |
|
|
69
|
+
|
|
70
|
+
## 5. Operations
|
|
71
|
+
| Path | Purpose |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `/healthz` | Liveness |
|
|
74
|
+
| `/readyz` | Readiness (database ping) |
|
|
75
|
+
|
|
76
|
+
## 6. Public stats API (no sign-in, aggregate only) — `src/public-api/`
|
|
77
|
+
| Path | Purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `GET /api/public/v1/info` | Version, endpoints, limits |
|
|
80
|
+
| `GET /api/public/v1/status` | ok / degraded / down |
|
|
81
|
+
| `GET /api/public/v1/stats?window=1h\|24h\|7d` | Users, connections, sign-ins, requests, errors, load |
|
|
82
|
+
| `GET /api/public/v1/series?window=…` | The same over time |
|
|
83
|
+
| `GET /api/public/v1/errors?window=…` | Error mix and sign-in failure reasons |
|
|
84
|
+
| `GET /api/public/v1/repo` | GitHub stars, forks and open issues (cached hourly, fetched by the bridge) |
|
|
85
|
+
| `GET /api/public/v1/openapi.json` | OpenAPI for this API (separate from the Actions document) |
|
|
86
|
+
|
|
87
|
+
The operator dashboard is a separate, access-controlled service and is not part of this repository.
|
|
88
|
+
|
|
89
|
+
## 7. Not web pages
|
|
90
|
+
- **Paperclip plugin** (`plugin/`, runs inside Paperclip): sidebar entry "Papercliped" and page at `/<company>/papercliped`.
|
|
91
|
+
- **Home-server provisioner** **(planned v3)**: no pages; polls the bridge's job queue.
|
|
92
|
+
- **Repo docs** (`docs/`, not served): ARCHITECTURE, SECURITY, LAUNCH, DEPLOY-RENDER, RELEASING, SITEMAP, V3-SUBDOMAINS, oauth, chatgpt, legal templates, SQL files.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# v3 plan: managed subdomains, pricing and legal (paid)
|
|
2
|
+
|
|
3
|
+
Status: **plan, nothing built.** Versions: **v2** is the current free, open-source build (the bridge, the Claude/MCP connector and the Paperclip plugin); **v3** adds this subdomain system together with pricing and the legal documents. Written from the product discussion of 2026-10-06. Items marked **DECIDED** came from the owner; items marked **PROPOSED** are recommendations awaiting a decision; **OPEN** needs an answer. Legal documents (Subdomain Terms, refund and tax wording) are **out of scope here** and will be drafted separately; section 9 lists what they must cover.
|
|
4
|
+
|
|
5
|
+
## 1. Goal
|
|
6
|
+
Users who cannot get a public `https://` address for their Paperclip can pay **$5 once** to get one: `https://<their-username>.<our-domain>`, tunnelled to their local Paperclip. We provision it; they run one command on their Paperclip machine. Using their own domain stays the encouraged path (see `site/docs/connect-your-paperclip.md`).
|
|
7
|
+
|
|
8
|
+
## 2. Decisions (DECIDED)
|
|
9
|
+
- **Price:** $5, one-time, for the subdomain name and its setup. It is not hosting and not ownership.
|
|
10
|
+
- **The name is the user's username.** Usernames cannot be changed, so the subdomain label never changes. We never create domains for users, only a subdomain under our domain.
|
|
11
|
+
- **Base domain may change** (today `papercliped.co`). The registry stores the **label**, not the full address; the base domain is configuration. The label stays registered to the user across any domain change.
|
|
12
|
+
- **Purpose is limited:** the subdomain is an access address for the user's own Paperclip, like a handle on a platform, not a place to run a business's public site.
|
|
13
|
+
- **Open to everyone at launch (not beta-only).** Paying the $5 makes the user a **pro member**: they get everything beta accounts get (the connection manager at `/manage` and the plugin) plus **management of their domain** (see 4a). Beta stays a separate, free opt-in. The manager's gate changes from `beta` to `beta OR pro`; store a `pro_since` timestamp on the account, set by the paid order.
|
|
14
|
+
- **Symbols are remapped, not refused.** The username stays the user's identity; only the hostname is derived from it by replacing the symbols with hyphens (rules in 6.2).
|
|
15
|
+
- **Anonymous mode does not affect the subdomain.** Anonymous only hides a user from the public live log and the operator panel (it shows an alias there). The username is still the username, so anonymous users get the same `<username>.<domain>` as everyone else.
|
|
16
|
+
- **Cloudflare terms (checked by the owner, not re-verified by us):** the service is fine as long as users do not use the subdomains for actual business uses. The Subdomain Terms and the abuse checks in section 7 must enforce this. **For the legal drafting:** decide exactly where "business use" starts. Working line so far: reaching your own Paperclip through the name is allowed even if that Paperclip supports a business; using the name as a public site, API, storefront or brand, or serving anyone else's service, is not.
|
|
17
|
+
- **Entry point:** a link on the connect screen ("No public address? We've got you covered.") to `/domain` on the bridge site. `/domain` offers: (1) buy a domain yourself and follow the guide, (2) use a domain you own and follow the guide, (3) let us do it ($5).
|
|
18
|
+
- **Provisioner runs on the owner's home server**, using Cloudflare.
|
|
19
|
+
- Legal documents are drafted later (not in this doc).
|
|
20
|
+
|
|
21
|
+
## 3. Architecture
|
|
22
|
+
```
|
|
23
|
+
user's browser/Claude ──https──▶ Cloudflare edge ──tunnel──▶ cloudflared on user's machine ──▶ localhost:3100 (Paperclip)
|
|
24
|
+
|
|
25
|
+
bridge (Render) ── job queue (Postgres) ◀── polls (outbound HTTPS only) ── home server ── Cloudflare API
|
|
26
|
+
```
|
|
27
|
+
- Remotely managed Cloudflare Tunnel **per user**. We set the ingress (one hostname → `http://localhost:<port>`) and the DNS record through the Cloudflare API. The user only runs the connector with their tunnel token.
|
|
28
|
+
- **The home server is not in the data path.** If it is down, new orders wait; existing tunnels keep working.
|
|
29
|
+
- **The Cloudflare API token lives only on the home server**, scoped to the one zone and to tunnels. It is never on the hosted bridge. The home server polls the bridge for jobs over outbound HTTPS, so no ports are opened at home.
|
|
30
|
+
- A provider interface (Cloudflare now, a fake for tests) so another backend can replace it.
|
|
31
|
+
|
|
32
|
+
## 4. User flow
|
|
33
|
+
1. Connect screen link → `/domain` → choose "let us do it".
|
|
34
|
+
2. **Register first** (see 6.4): choose a username, get the secret key, no Paperclip needed yet.
|
|
35
|
+
3. Enter the local address (normally `localhost:3100`; only `localhost`/`127.0.0.1` plus a port are accepted, since we configure where the user's own connector forwards).
|
|
36
|
+
4. Accept the Subdomain Terms (acceptance recorded: version + timestamp + account), pay $5 with Stripe Checkout.
|
|
37
|
+
5. A signed Stripe webhook confirms the order → job queued → home server provisions → bridge stores the tunnel and shows one command to run (`cloudflared service install <token>`; exact per-OS steps to be taken from Cloudflare's docs).
|
|
38
|
+
6. Back to signup with the address filled in: Connect your Paperclip → approve in Paperclip.
|
|
39
|
+
7. `/manage` and the plugin show the tunnel's online status.
|
|
40
|
+
|
|
41
|
+
## 4a. Pro members: domain management (in `/manage` and the plugin)
|
|
42
|
+
- See the address (`label + base domain`), the tunnel status (online/offline, last seen) and the "is it Paperclip?" check result.
|
|
43
|
+
- Re-show the connect command and rotate the tunnel token (invalidates the old one; needs the secret key, like other sensitive actions).
|
|
44
|
+
- Change the local port the tunnel forwards to (still limited to `localhost`/`127.0.0.1` plus a port).
|
|
45
|
+
- Release the name (needs the secret key; starts the quarantine; does not refund).
|
|
46
|
+
- The label itself cannot be changed (it is derived from the username).
|
|
47
|
+
- Plugin tokens stay unable to do secret-key actions, so rotate/release remain browser-only.
|
|
48
|
+
|
|
49
|
+
## 4b. Later: slots (about two months after launch)
|
|
50
|
+
Pro members will be able to buy **3 slots for $10**. Each slot is an extra subdomain, so the first one stays "the username" and the extra ones are **user-chosen labels** under the same naming rules, uniqueness and quarantine. **Design now so this is not a rewrite:** do not make `subdomains.account_id` unique (allow many per account, with a `slot` number and a per-account limit), keep the Paperclip-verification and abuse checks per subdomain, and keep pricing in config. Pro members still cannot change the label of an existing name.
|
|
51
|
+
|
|
52
|
+
## 5. Data model (sketch)
|
|
53
|
+
- `accounts.pro_since` (nullable timestamp; set when the paid order completes).
|
|
54
|
+
- `subdomains`: `account_id`, `slot` (1 for the username label; more later), `label` (unique, lowercase), `tunnel_id`, `local_port`, `status` (`pending` | `active` | `suspended` | `released`), `terms_version`, `paid_at`, `order_id`, `created_at`, `released_at`.
|
|
55
|
+
- `subdomain_jobs`: `id`, `kind` (`provision` | `suspend` | `release` | `repoint`), `payload`, `state`, `attempts`, `claimed_by`, `claimed_until`, `result`, timestamps.
|
|
56
|
+
- `base_domains`: `domain`, `active`, `primary` (supports several at once during a change).
|
|
57
|
+
- `label_quarantine`: `label`, `released_at`, `until`.
|
|
58
|
+
- Address shown to users = `label + primary base domain`. The stored Paperclip instance URL for the account is updated centrally on a domain change.
|
|
59
|
+
|
|
60
|
+
## 6. Hard problems and proposed answers
|
|
61
|
+
1. **Anonymous mode (resolved).** It only hides the user from the public live log and counts, so it does not block the subdomain. Two guardrails: (a) never write a user's label or hostname into public events or the live log, so an alias can't be matched to a username (the live log and panel keep showing only the alias); (b) the order screen tells anonymous users that their subdomain is public and shows their username.
|
|
62
|
+
2. **Usernames are not valid hostnames (resolved: remap).** Derivation, applied in this order: lowercase; replace every run of `.` `#` `_` with a single `-` (a run collapses so a label never contains `--`, which is reserved for `xn--`); trim leading/trailing `-` (usernames already start with a letter or digit). Example: `OG.kpnwrld` → `og-kpnwrld`, `river#2026` → `river-2026`. The derived label is stored in `subdomains.label` and never changes. Different usernames can derive the same label (`og_dev7`, `og.dev7`, `og-dev7`), so uniqueness is enforced **on the label** at claim time, first come first served. A user whose label is already held cannot buy the managed subdomain (their username cannot change): show this **before** payment, charge nothing, and point them to the own-domain guide. Also add a reserved-label list (www, api, mail, admin and similar) on top of the existing username reserved list in `src/accounts/username.ts`.
|
|
63
|
+
3. **Released names can be re-taken.** Deleting an account currently frees its username. **PROPOSED:** quarantine a released label for about 12 months, and delete its DNS record and tunnel on release (prevents subdomain takeover).
|
|
64
|
+
4. **Signup order.** Today an account exists only after the user approves in their Paperclip (`src/oauth/provider.ts`, account creation at the pending-approval step), which a user with no public address cannot do. A **register-first path** is needed: username + secret key without a Paperclip link, then pay, then connect. This touches the account model and the recovery-by-approval path, and is the largest piece of v3.
|
|
65
|
+
5. **Changing the base domain** is not free for users: their Paperclip must accept the new hostname (a setting on their machine). Keep both hostnames live for a long overlap; keep renewing the old domain for as long as users depend on it. If the domain lapses, every user breaks at once.
|
|
66
|
+
6. **Shared registrable domain.** The main domain is now `papercliped.co`, which also serves the bridge, the docs and `/manage`. Putting users at `<name>.papercliped.co` shares the domain's reputation with the bridge, and a user's site could try to set cookies for `.papercliped.co`. Mitigations that already hold: the manager and OAuth cookies are host-only (no `Domain=` attribute), so subdomains never receive them. **OPEN:** use `papercliped.co` for user names (simplest, one level so the free certificate should cover it, **unverified**) or a separate domain just for user names (safer isolation); also reserve labels that collide with our own hosts (`www`, `api`, `app`, `docs`, `status`, `mail`, `panel`).
|
|
67
|
+
|
|
68
|
+
## 7. Abuse and operations
|
|
69
|
+
- **One service per name:** ingress is set by us (one hostname, one local port); users cannot reconfigure it.
|
|
70
|
+
- **Verify the address and port really are a Paperclip (required, at three points):**
|
|
71
|
+
1. **Before activation:** the order's job only marks the name active after the tunnel is up and `GET https://<label>.<domain>/api/health` returns the Paperclip shape (`status`, `deploymentMode`, `deploymentExposure`). Until then the name is `pending`, and unverified names are cleaned up after a short timeout.
|
|
72
|
+
2. **When the user connects:** the existing "Connect your Paperclip" step (the user approving the request inside their own Paperclip) already proves the address is a Paperclip they can sign in to. Bind the subdomain to the Paperclip user and instance id from that step; if the instance later reports a different identity, suspend and review.
|
|
73
|
+
3. **Continuously:** a scheduled check (for example every few hours, with jitter) repeats the health probe. Failing or non-Paperclip responses get a warning, then suspension; the result is shown to the user in `/manage`.
|
|
74
|
+
- **Port and routing rules:** the ingress target is only `localhost`/`127.0.0.1` plus one port set by us; users cannot add rules. Consider limiting what the public hostname forwards (for example only the Paperclip API and its UI paths) once Paperclip's required paths are confirmed.
|
|
75
|
+
- **Probe safety:** the probes go through the same SSRF-hardened fetch as the rest of the bridge, with short timeouts and a response size cap.
|
|
76
|
+
- **Suspension** is one switch (job `suspend`): remove ingress/DNS, keep the label reserved.
|
|
77
|
+
- Rate-limit orders and provisioning; reconcile Cloudflare state against the database on a schedule (orphaned tunnels/records are deleted).
|
|
78
|
+
- Refund automatically if provisioning fails.
|
|
79
|
+
- Idle reclaim: **PROPOSED** only after about 12 months with the tunnel offline, with notices.
|
|
80
|
+
|
|
81
|
+
## 7a. Security hardening to do with v3
|
|
82
|
+
- Home-server worker: outbound-only polling with a signed per-worker credential; jobs are idempotent and carry no secrets other than what the provider needs; the Cloudflare token is least-privilege (one zone, tunnels), stored only on the home server, rotated on a schedule.
|
|
83
|
+
- Webhooks: verify the Stripe signature, reject replays, process each order once.
|
|
84
|
+
- Tunnel tokens: shown only to the owner, re-showing or rotating needs the secret key, never written to logs or the public events.
|
|
85
|
+
- Label rules: reserved list, impersonation/brand blocklist, quarantine, DNS and tunnel deleted on release; reconcile Cloudflare against the database on a schedule.
|
|
86
|
+
- Public events and live log never contain labels or hostnames (anonymous users included).
|
|
87
|
+
- Rate limits on order, claim, probe and token endpoints; audit every provisioning action.
|
|
88
|
+
- Extend `docs/SECURITY.md` and its threat table for: provisioner compromise, Cloudflare token theft, label squatting/takeover, abuse of the tunnel for non-Paperclip traffic, payment fraud and refunds.
|
|
89
|
+
|
|
90
|
+
## 8. Payments
|
|
91
|
+
Stripe Checkout (no card data on our side); orders confirmed only by a verified webhook signature; idempotent on order id; the label is held for ~15 minutes during checkout so two people cannot pay for one name. Tax and refund handling to be decided with the legal drafting.
|
|
92
|
+
|
|
93
|
+
## 9. For the later legal drafting (Subdomain Terms)
|
|
94
|
+
Cover: purpose limit (access address for your own Paperclip; internal use that supports a business is fine, using the name as the public face of a business or hosting other services is not); the name is a revocable licence tied to the account, not property, not transferable, one per account; our rights to suspend/reclaim for abuse or breach and to change base domains with notice; no uptime promise (depends on Cloudflare and the user's machine); refunds only if provisioning fails; conduct and impersonation/trademark rules; what metadata we keep (hostname, tunnel status, not traffic content); acceptance recorded at checkout. Needs a lawyer's review before taking payments, including sales tax.
|
|
95
|
+
|
|
96
|
+
## 10. Open questions
|
|
97
|
+
1. 12-month quarantine and idle-reclaim periods?
|
|
98
|
+
2. Which domain (dedicated, one level)?
|
|
99
|
+
3. If a name is suspended for abuse or reclaimed, does the account keep pro? (Proposal: pro ends only if the name is suspended for breaking the terms; a released name keeps pro status.)
|
|
100
|
+
4. Still to verify with Cloudflare's current docs: tunnels per account, DNS records per zone, certificate coverage for subdomains, per-OS service install steps.
|
|
101
|
+
|
|
102
|
+
## 11. Phases
|
|
103
|
+
1. **Provisioning core:** migration, provider interface + fake, job queue, home-server worker, claim/release/suspend, label rules.
|
|
104
|
+
2. **Register-first account path** and the Stripe order flow.
|
|
105
|
+
3. **Connect UX:** `/domain` page, the link on the connect screen, tunnel status in `/manage` and the plugin, docs.
|
|
106
|
+
4. **Hardening:** health check, suspension, reconcile, quarantine, domain-change tooling.
|