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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, join, normalize, sep } from "node:path";
|
|
3
|
+
import { SITE_CSP } from "./csp.js";
|
|
4
|
+
import { roleOf } from "./hosts.js";
|
|
5
|
+
/** Paths the bridge itself serves on every host; the website never answers them. */
|
|
6
|
+
const PROTOCOL = /^\/(mcp|actions\/|openapi\.json$|register$|authorize|token$|revoke$|\.well-known\/|api\/|manage$|healthz$|readyz$)/;
|
|
7
|
+
const TYPES = {
|
|
8
|
+
".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json",
|
|
9
|
+
".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon", ".woff2": "font/woff2", ".xml": "application/xml; charset=utf-8",
|
|
10
|
+
".txt": "text/plain; charset=utf-8", ".md": "text/plain; charset=utf-8", ".webmanifest": "application/manifest+json",
|
|
11
|
+
};
|
|
12
|
+
const HEADERS = { "X-Content-Type-Options": "nosniff", "Referrer-Policy": "strict-origin-when-cross-origin", "X-Frame-Options": "DENY" };
|
|
13
|
+
/**
|
|
14
|
+
* Serves the React website and does the host routing:
|
|
15
|
+
* marketing host → the site; /docs/* moves to the docs host when there is one
|
|
16
|
+
* docs host → the docs landing at / and each page at /<page>; old /docs/* and /topics/* links redirect there;
|
|
17
|
+
* other pages go to the marketing host
|
|
18
|
+
* api host → protocol endpoints only (handled elsewhere); pages redirect to the marketing host
|
|
19
|
+
* forum host → redirects to the forum
|
|
20
|
+
* Hosts not in PUBLIC_HOSTS (and a bridge with no PUBLIC_HOSTS) get the whole site, as before.
|
|
21
|
+
*/
|
|
22
|
+
export class WebAppRoutes {
|
|
23
|
+
o;
|
|
24
|
+
index;
|
|
25
|
+
routes;
|
|
26
|
+
/** Doc page slugs ("permissions", "faq"…), from the /docs/<slug> entries in routes.json. */
|
|
27
|
+
docSlugs;
|
|
28
|
+
constructor(o) {
|
|
29
|
+
this.o = o;
|
|
30
|
+
this.index = readFileSync(join(o.dir, "index.html"), "utf8");
|
|
31
|
+
this.routes = new Set(JSON.parse(readFileSync(join(o.dir, "routes.json"), "utf8")));
|
|
32
|
+
this.docSlugs = new Set([...this.routes].filter((r) => r.startsWith("/docs/")).map((r) => r.slice("/docs/".length)));
|
|
33
|
+
}
|
|
34
|
+
/** Finds web/dist next to the package, or WEB_DIST. Null when the website isn't built (then the bridge's own pages are used). */
|
|
35
|
+
static locate(env, fallback) {
|
|
36
|
+
const dir = env.WEB_DIST?.trim() || fallback;
|
|
37
|
+
return existsSync(join(dir, "index.html")) && existsSync(join(dir, "routes.json")) ? dir : null;
|
|
38
|
+
}
|
|
39
|
+
canonical(role) {
|
|
40
|
+
return this.o.hosts[role][0];
|
|
41
|
+
}
|
|
42
|
+
redirect(res, to, permanent = true) {
|
|
43
|
+
res.writeHead(permanent ? 301 : 302, { Location: to, "Cache-Control": permanent ? "public, max-age=3600" : "no-store", ...HEADERS });
|
|
44
|
+
res.end();
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/** The page shell, with the host map for the app's links (see web/src/lib/hosts.ts). */
|
|
48
|
+
shell(res, status, head) {
|
|
49
|
+
const map = JSON.stringify({ marketing: this.canonical("marketing"), docs: this.canonical("docs") }).replace(/"/g, """);
|
|
50
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache", "Content-Security-Policy": SITE_CSP, ...HEADERS });
|
|
51
|
+
res.end(head ? undefined : this.index.replace('<meta name="pcl-hosts" content="{}" />', `<meta name="pcl-hosts" content="${map}" />`));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
file(res, path, head) {
|
|
55
|
+
let rel;
|
|
56
|
+
try {
|
|
57
|
+
rel = normalize(decodeURIComponent(path)).replace(/^([/\\])+/, "");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (!rel || rel.split(/[/\\]/).includes("..") || rel === "index.html" || rel === "routes.json")
|
|
63
|
+
return false;
|
|
64
|
+
const full = join(this.o.dir, rel);
|
|
65
|
+
if (!full.startsWith(this.o.dir.endsWith(sep) ? this.o.dir : this.o.dir + sep))
|
|
66
|
+
return false;
|
|
67
|
+
let st;
|
|
68
|
+
try {
|
|
69
|
+
st = statSync(full);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!st.isFile())
|
|
75
|
+
return false;
|
|
76
|
+
const immutable = rel.startsWith("assets" + sep) || rel.startsWith("assets/") || rel.startsWith("fonts");
|
|
77
|
+
res.writeHead(200, {
|
|
78
|
+
"Content-Type": TYPES[extname(full).toLowerCase()] ?? "application/octet-stream",
|
|
79
|
+
"Content-Length": String(st.size),
|
|
80
|
+
"Cache-Control": immutable ? "public, max-age=31536000, immutable" : "public, max-age=3600",
|
|
81
|
+
...(rel.endsWith(".html") ? { "Content-Security-Policy": SITE_CSP } : {}),
|
|
82
|
+
...(immutable && rel.startsWith("fonts") ? { "Access-Control-Allow-Origin": "*" } : {}),
|
|
83
|
+
...HEADERS,
|
|
84
|
+
});
|
|
85
|
+
res.end(head ? undefined : readFileSync(full));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
async handle(req, res, url) {
|
|
89
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
90
|
+
return false;
|
|
91
|
+
const head = req.method === "HEAD";
|
|
92
|
+
const raw = url.pathname;
|
|
93
|
+
const path = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
|
|
94
|
+
if (PROTOCOL.test(path))
|
|
95
|
+
return false;
|
|
96
|
+
const host = String(req.headers.host ?? "").toLowerCase();
|
|
97
|
+
const role = roleOf(this.o.hosts, host);
|
|
98
|
+
const q = url.search;
|
|
99
|
+
const mk = this.canonical("marketing");
|
|
100
|
+
const docs = this.canonical("docs");
|
|
101
|
+
const isDocs = /^\/(docs|topics)(\/|$)/.test(path);
|
|
102
|
+
const docsRest = isDocs ? path.replace(/^\/(docs|topics)\/?/, "") : "";
|
|
103
|
+
if (role === "forum")
|
|
104
|
+
return this.redirect(res, this.o.forumUrl || (mk ? `https://${mk}/community` : "/"), false);
|
|
105
|
+
if (role === "api")
|
|
106
|
+
return mk ? this.redirect(res, `https://${mk}${path === "/" ? "/" : path}${q}`, false) : false;
|
|
107
|
+
if (role === "marketing" && mk && host !== mk)
|
|
108
|
+
return this.redirect(res, `https://${mk}${raw}${q}`); // www → apex
|
|
109
|
+
if (role === "docs") {
|
|
110
|
+
if (isDocs)
|
|
111
|
+
return this.redirect(res, `/${docsRest}${q}`); // old /docs/x and /topics/x links
|
|
112
|
+
if (path === "/" || this.docSlugs.has(path.slice(1)))
|
|
113
|
+
return this.shell(res, 200, head);
|
|
114
|
+
if (this.routes.has(path) && mk)
|
|
115
|
+
return this.redirect(res, `https://${mk}${path}${q}`);
|
|
116
|
+
}
|
|
117
|
+
else if (isDocs) {
|
|
118
|
+
// Marketing (or unlisted) host: docs move to the docs host when there is one. /topics is an old spelling.
|
|
119
|
+
if (docs)
|
|
120
|
+
return this.redirect(res, `https://${docs}/${docsRest}${q}`);
|
|
121
|
+
if (/^\/topics(\/|$)/.test(path))
|
|
122
|
+
return this.redirect(res, `/docs${docsRest ? `/${docsRest}` : ""}${q}`);
|
|
123
|
+
}
|
|
124
|
+
if (path !== "/" && this.file(res, raw, head))
|
|
125
|
+
return true;
|
|
126
|
+
if (this.routes.has(path))
|
|
127
|
+
return this.shell(res, 200, head);
|
|
128
|
+
if (extname(path))
|
|
129
|
+
return false; // a missing file: let the bridge answer 404
|
|
130
|
+
return this.shell(res, 404, head); // the app shows its not-found page
|
|
131
|
+
}
|
|
132
|
+
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,73 +1,74 @@
|
|
|
1
|
-
# Architecture, auth and distribution plan
|
|
2
|
-
|
|
3
|
-
Status: research + recommendation (Oct 2026). Sources: Paperclip source/docs (`paperclipai/paperclip`, read at depth‑1 clone) and Anthropic's connector/plugin docs. Items marked **[unverified]** are inferences or come from secondary sources and need testing before we rely on them.
|
|
4
|
-
|
|
5
|
-
## 1. Verdict: integrate, don't rebuild
|
|
6
|
-
|
|
7
|
-
Keep Paperclip as the system of record and build *around* it. Rebuilding is not justified:
|
|
8
|
-
|
|
9
|
-
- Paperclip is a large, active codebase (8k+ files, ~295 DB migrations, adapters for ~10 agent runtimes, governance, budgets, approvals, MCP gateway, plugin SDK). Our goals — control, sync, reports, API use — are all reachable through its existing REST API. Nothing we need requires different core semantics.
|
|
10
|
-
- A fork forfeits upstream fixes and the Paperclip community's agents/templates (ClipHub). A rebuild would cost far more than the integration gaps below.
|
|
11
|
-
- The real gaps are **auth scoping** and **transport/OAuth**, and both can be solved in a front layer or contributed upstream.
|
|
12
|
-
|
|
13
|
-
## 2. What Paperclip gives us (relevant facts)
|
|
14
|
-
|
|
15
|
-
| Area | Fact | Consequence |
|
|
16
|
-
| --- | --- | --- |
|
|
17
|
-
| API | REST under `/api`; company-scoped; JSON; errors `{error}` | Our bridge is a thin client — no DB access, no business logic |
|
|
18
|
-
| Auth modes | `local_trusted` (no login, loopback) · `authenticated/private` · `authenticated/public` (Better Auth sessions) | Bridge must work with no token (local) and with a token (everything else) |
|
|
19
|
-
| Operator creds | **Board API keys** (`board_api_keys`: hashed, `expiresAt`, `revokedAt`, `lastUsedAt`, bound to a *user*). **No scope or company column** | A board key = that user's full board power. Never hand one to a public third party |
|
|
20
|
-
| Login for tools | CLI challenge flow: client creates a challenge → user approves at `approvalUrl` in Paperclip's UI → client polls and receives a board token | Reusable as our "Sign in with Paperclip" step (no passwords touch our code) |
|
|
21
|
-
| Agent creds | Agent keys / run JWTs; pause, resume, approve etc. are **board‑only** | Operator control must use board creds (documented in README) |
|
|
22
|
-
| OAuth | Paperclip is an OAuth **client** (to upstream MCP servers). It is **not** an authorization server | claude.ai / ChatGPT connectors can't authenticate to Paperclip directly |
|
|
23
|
-
| MCP | Official `@paperclipai/mcp-server` is a stdio wrapper for use inside agent runs. "Gateway mode" is agent→upstream, with profiles/policies/audit. No operator‑facing remote `/mcp` | Our bridge is the missing operator-side endpoint |
|
|
24
|
-
| Plugins | Real plugin runtime: manifest + worker + UI; capability‑gated worker APIs; `apiRoutes` mounted only at `/api/plugins/:id/api/*`; plugins are trusted code; capability additions → `upgrade_pending` until an operator approves; install by npm/local path/image catalog (`paperclipai plugin install`). **No public plugin marketplace** (ClipHub = company templates only) | A native plugin is possible but distribution is operator‑installed, not store‑based |
|
|
25
|
-
|
|
26
|
-
## 3. How public plugins actually ship (Claude & ChatGPT)
|
|
27
|
-
|
|
28
|
-
**Claude** (docs.claude.com connector + plugin docs):
|
|
29
|
-
- **Plugin bundle** (skills, commands, local MCP server like ours): public GitHub repo with `.claude-plugin/plugin.json`, README, LICENSE → `claude plugin validate` → submit at `claude.ai/directory/manage` ("Plugin bundle") → automated validation + security scan, sometimes human review → publish; later versions follow the tracked branch/tag. Data‑handling questions at submit time. Org can also share privately via a git marketplace (`/plugin marketplace add owner/repo`) — works today.
|
|
30
|
-
- **Remote MCP connector** (what claude.ai web/mobile/Desktop use): must be HTTPS. Auth = OAuth 2.0 (DCR or Client‑ID‑Metadata‑Document, PKCE S256, `401` + `WWW-Authenticate: Bearer resource_metadata=…`, RFC 9728 protected‑resource metadata, RFC 8414 AS metadata, form‑encoded `/token`, refresh rotation, 10 s endpoint latency), or none, or a **static header credential (beta, limited orgs)**. Callbacks: `https://claude.ai/api/mcp/auth_callback` and a port‑agnostic loopback for Claude Code. Every tool needs `title` + `readOnlyHint`/`destructiveHint` (we already set these). Reviewers need a populated test account. Custom (unlisted) connectors can be added by URL by any user/org without review.
|
|
31
|
-
- Plugins that reference a remote server should submit that server as a connector too.
|
|
32
|
-
|
|
33
|
-
**ChatGPT** **[unverified: from secondary sources; I could not fetch OpenAI's docs]**:
|
|
34
|
-
- *Apps SDK / MCP apps*: streamable‑HTTP MCP + OAuth 2.1 (DCR, PKCE); public submission needs a verified OpenAI org, public ToS + privacy policy, support contact, demo video, test account and test cases.
|
|
35
|
-
- *Custom GPT Actions*: OpenAPI + API‑key or OAuth; shareable via link or GPT Store. This is what we ship today (bearer key).
|
|
36
|
-
|
|
37
|
-
## 4. Auth design for public use
|
|
38
|
-
|
|
39
|
-
Problem: board keys are all‑powerful, and the connector UIs want OAuth. Solution: put an **OAuth 2.1 authorization server inside the bridge** that fronts Paperclip.
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
claude.ai / ChatGPT ──OAuth 2.1 + PKCE──▶ bridge (AS + resource server) ──board token──▶ Paperclip /api
|
|
43
|
-
│
|
|
44
|
-
/authorize ──▶ create Paperclip CLI-auth challenge ──▶ user approves in Paperclip UI
|
|
45
|
-
(bridge receives board token, stores it encrypted, mints its OWN scoped token)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
- **Sign‑in:** reuse Paperclip's challenge/approve flow, so users authenticate to *their* Paperclip, not to us.
|
|
49
|
-
- **Scopes enforced by the bridge** (Paperclip can't): `paperclip:read` (reports, sync, lists) · `paperclip:control` (pause/resume/wake, issues, comments, goals
|
|
50
|
-
- **Tokens:** short‑lived access JWT (audience = bridge URL), rotating refresh tokens, per‑user revocation (and revoke the Paperclip key on disconnect via `auth revoke-current`). Tokens bound to one company if the user picks one at consent.
|
|
51
|
-
- **Registration:** support CIMD first (no client sprawl), DCR as fallback.
|
|
52
|
-
- **Endpoints to add:** `/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`, `/register`, `/authorize`, `/token`, `/revoke`; `401` challenge on `/mcp`.
|
|
53
|
-
- **Multi‑tenancy:** every Paperclip instance has its own URL. Options: (a) *self‑hosted bridge per operator* (their own domain) — add as a **custom connector by URL**, no directory needed; (b) *one hosted multi‑tenant bridge* where the user supplies their instance URL at consent (needs SSRF‑safe URL validation + Claude "URL pattern"/custom‑connection mode, which takes longer to review); (c) *Paperclip Cloud* — a fixed URL pattern per stack, the cleanest directory story but needs Paperclip's cooperation.
|
|
54
|
-
- Keep `BRIDGE_TOKEN` bearer mode for Claude Code headers, ChatGPT Actions, and the `static_headers` beta.
|
|
55
|
-
|
|
56
|
-
## 5. Phased plan
|
|
57
|
-
|
|
58
|
-
| Phase | Deliver | Ship channel | Status |
|
|
59
|
-
| --- | --- | --- | --- |
|
|
60
|
-
| 1 | stdio MCP + Claude Code plugin + Actions bridge, read‑only mode, confirm gates | git marketplace now; npm publish; submit **Plugin bundle** to Anthropic's directory | **Done** (needs live-instance test, then submit) |
|
|
61
|
-
| 2 | OAuth 2.1 AS in the bridge (§4), scopes, token store, audit log | custom connector by URL on claude.ai; ChatGPT app (dev mode → submission) | **Done** (self-hosted; DCR only; see `docs/oauth.md`). Untested against real claude.ai/ChatGPT clients |
|
|
62
|
-
| 2b | **Multi-tenant public service**: per-user instance URL with SSRF-safe egress, Postgres store, key ring/rotation, idle revocation, Render blueprint, threat model | hosted bridge | **Built, untested live** (see SECURITY.md) |
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
# Architecture, auth and distribution plan
|
|
2
|
+
|
|
3
|
+
Status: research + recommendation (Oct 2026). Sources: Paperclip source/docs (`paperclipai/paperclip`, read at depth‑1 clone) and Anthropic's connector/plugin docs. Items marked **[unverified]** are inferences or come from secondary sources and need testing before we rely on them.
|
|
4
|
+
|
|
5
|
+
## 1. Verdict: integrate, don't rebuild
|
|
6
|
+
|
|
7
|
+
Keep Paperclip as the system of record and build *around* it. Rebuilding is not justified:
|
|
8
|
+
|
|
9
|
+
- Paperclip is a large, active codebase (8k+ files, ~295 DB migrations, adapters for ~10 agent runtimes, governance, budgets, approvals, MCP gateway, plugin SDK). Our goals — control, sync, reports, API use — are all reachable through its existing REST API. Nothing we need requires different core semantics.
|
|
10
|
+
- A fork forfeits upstream fixes and the Paperclip community's agents/templates (ClipHub). A rebuild would cost far more than the integration gaps below.
|
|
11
|
+
- The real gaps are **auth scoping** and **transport/OAuth**, and both can be solved in a front layer or contributed upstream.
|
|
12
|
+
|
|
13
|
+
## 2. What Paperclip gives us (relevant facts)
|
|
14
|
+
|
|
15
|
+
| Area | Fact | Consequence |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| API | REST under `/api`; company-scoped; JSON; errors `{error}` | Our bridge is a thin client — no DB access, no business logic |
|
|
18
|
+
| Auth modes | `local_trusted` (no login, loopback) · `authenticated/private` · `authenticated/public` (Better Auth sessions) | Bridge must work with no token (local) and with a token (everything else) |
|
|
19
|
+
| Operator creds | **Board API keys** (`board_api_keys`: hashed, `expiresAt`, `revokedAt`, `lastUsedAt`, bound to a *user*). **No scope or company column** | A board key = that user's full board power. Never hand one to a public third party |
|
|
20
|
+
| Login for tools | CLI challenge flow: client creates a challenge → user approves at `approvalUrl` in Paperclip's UI → client polls and receives a board token | Reusable as our "Sign in with Paperclip" step (no passwords touch our code) |
|
|
21
|
+
| Agent creds | Agent keys / run JWTs; pause, resume, approve etc. are **board‑only** | Operator control must use board creds (documented in README) |
|
|
22
|
+
| OAuth | Paperclip is an OAuth **client** (to upstream MCP servers). It is **not** an authorization server | claude.ai / ChatGPT connectors can't authenticate to Paperclip directly |
|
|
23
|
+
| MCP | Official `@paperclipai/mcp-server` is a stdio wrapper for use inside agent runs. "Gateway mode" is agent→upstream, with profiles/policies/audit. No operator‑facing remote `/mcp` | Our bridge is the missing operator-side endpoint |
|
|
24
|
+
| Plugins | Real plugin runtime: manifest + worker + UI; capability‑gated worker APIs; `apiRoutes` mounted only at `/api/plugins/:id/api/*`; plugins are trusted code; capability additions → `upgrade_pending` until an operator approves; install by npm/local path/image catalog (`paperclipai plugin install`). **No public plugin marketplace** (ClipHub = company templates only) | A native plugin is possible but distribution is operator‑installed, not store‑based |
|
|
25
|
+
|
|
26
|
+
## 3. How public plugins actually ship (Claude & ChatGPT)
|
|
27
|
+
|
|
28
|
+
**Claude** (docs.claude.com connector + plugin docs):
|
|
29
|
+
- **Plugin bundle** (skills, commands, local MCP server like ours): public GitHub repo with `.claude-plugin/plugin.json`, README, LICENSE → `claude plugin validate` → submit at `claude.ai/directory/manage` ("Plugin bundle") → automated validation + security scan, sometimes human review → publish; later versions follow the tracked branch/tag. Data‑handling questions at submit time. Org can also share privately via a git marketplace (`/plugin marketplace add owner/repo`) — works today.
|
|
30
|
+
- **Remote MCP connector** (what claude.ai web/mobile/Desktop use): must be HTTPS. Auth = OAuth 2.0 (DCR or Client‑ID‑Metadata‑Document, PKCE S256, `401` + `WWW-Authenticate: Bearer resource_metadata=…`, RFC 9728 protected‑resource metadata, RFC 8414 AS metadata, form‑encoded `/token`, refresh rotation, 10 s endpoint latency), or none, or a **static header credential (beta, limited orgs)**. Callbacks: `https://claude.ai/api/mcp/auth_callback` and a port‑agnostic loopback for Claude Code. Every tool needs `title` + `readOnlyHint`/`destructiveHint` (we already set these). Reviewers need a populated test account. Custom (unlisted) connectors can be added by URL by any user/org without review.
|
|
31
|
+
- Plugins that reference a remote server should submit that server as a connector too.
|
|
32
|
+
|
|
33
|
+
**ChatGPT** **[unverified: from secondary sources; I could not fetch OpenAI's docs]**:
|
|
34
|
+
- *Apps SDK / MCP apps*: streamable‑HTTP MCP + OAuth 2.1 (DCR, PKCE); public submission needs a verified OpenAI org, public ToS + privacy policy, support contact, demo video, test account and test cases.
|
|
35
|
+
- *Custom GPT Actions*: OpenAPI + API‑key or OAuth; shareable via link or GPT Store. This is what we ship today (bearer key).
|
|
36
|
+
|
|
37
|
+
## 4. Auth design for public use
|
|
38
|
+
|
|
39
|
+
Problem: board keys are all‑powerful, and the connector UIs want OAuth. Solution: put an **OAuth 2.1 authorization server inside the bridge** that fronts Paperclip.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
claude.ai / ChatGPT ──OAuth 2.1 + PKCE──▶ bridge (AS + resource server) ──board token──▶ Paperclip /api
|
|
43
|
+
│
|
|
44
|
+
/authorize ──▶ create Paperclip CLI-auth challenge ──▶ user approves in Paperclip UI
|
|
45
|
+
(bridge receives board token, stores it encrypted, mints its OWN scoped token)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **Sign‑in:** reuse Paperclip's challenge/approve flow, so users authenticate to *their* Paperclip, not to us.
|
|
49
|
+
- **Scopes enforced by the bridge** (Paperclip can't): `paperclip:read` (reports, sync, lists) · `paperclip:control` (every tool: pause/resume/wake/terminate, issues, comments, goals, approvals, budgets, raw non‑GET). Default consent = read. The bridge already classifies tools `read|write|destructive`; map those to scopes.
|
|
50
|
+
- **Tokens:** short‑lived access JWT (audience = bridge URL), rotating refresh tokens, per‑user revocation (and revoke the Paperclip key on disconnect via `auth revoke-current`). Tokens bound to one company if the user picks one at consent.
|
|
51
|
+
- **Registration:** support CIMD first (no client sprawl), DCR as fallback.
|
|
52
|
+
- **Endpoints to add:** `/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`, `/register`, `/authorize`, `/token`, `/revoke`; `401` challenge on `/mcp`.
|
|
53
|
+
- **Multi‑tenancy:** every Paperclip instance has its own URL. Options: (a) *self‑hosted bridge per operator* (their own domain) — add as a **custom connector by URL**, no directory needed; (b) *one hosted multi‑tenant bridge* where the user supplies their instance URL at consent (needs SSRF‑safe URL validation + Claude "URL pattern"/custom‑connection mode, which takes longer to review); (c) *Paperclip Cloud* — a fixed URL pattern per stack, the cleanest directory story but needs Paperclip's cooperation.
|
|
54
|
+
- Keep `BRIDGE_TOKEN` bearer mode for Claude Code headers, ChatGPT Actions, and the `static_headers` beta.
|
|
55
|
+
|
|
56
|
+
## 5. Phased plan
|
|
57
|
+
|
|
58
|
+
| Phase | Deliver | Ship channel | Status |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| 1 | stdio MCP + Claude Code plugin + Actions bridge, read‑only mode, confirm gates | git marketplace now; npm publish; submit **Plugin bundle** to Anthropic's directory | **Done** (needs live-instance test, then submit) |
|
|
61
|
+
| 2 | OAuth 2.1 AS in the bridge (§4), scopes, token store, audit log | custom connector by URL on claude.ai; ChatGPT app (dev mode → submission) | **Done** (self-hosted; DCR only; see `docs/oauth.md`). Untested against real claude.ai/ChatGPT clients |
|
|
62
|
+
| 2b | **Multi-tenant public service**: per-user instance URL with SSRF-safe egress, Postgres store, key ring/rotation, idle revocation, Render blueprint, threat model | hosted bridge | **Built, untested live** (see SECURITY.md) |
|
|
63
|
+
| 3a | **Paperclip plugin** (connection manager inside Paperclip, `plugin/`) | `paperclipai plugin install` from npm | **Built (v1.2.0-beta.1), untested in a live Paperclip** |
|
|
64
|
+
| 3 | Upstream: (i) PR/issue for **scoped board API keys**; (ii) optional Paperclip‑native plugin hosting the MCP endpoint behind Paperclip's own session auth | `paperclipai plugin install`; image catalog (`distribution/catalog.json`) | Later |
|
|
65
|
+
|
|
66
|
+
Phase 3(ii) caveat **[unverified]**: plugin routes live only under `/api/plugins/:id/api/*` and can't claim root paths, while OAuth discovery wants `/.well-known/*` at the origin root (Claude lets the `401` point `resource_metadata` anywhere, but AS metadata must be at the issuer's well‑known path). A reverse‑proxy rule or a core change would be needed — test before committing to it.
|
|
67
|
+
|
|
68
|
+
## 6. Risks and open questions
|
|
69
|
+
|
|
70
|
+
1. **Never run against a live Paperclip yet.** Do that first, read‑only, then submit.
|
|
71
|
+
2. A hosted bridge holds users' board credentials: needs encryption at rest, a security review, a privacy policy and incident process. Prefer self‑hosting until then.
|
|
72
|
+
3. Prompt injection: issue/comment text from agents flows into the chat model. The skill tells Claude to treat it as data; consider output‑length and tag‑stripping limits.
|
|
73
|
+
4. Paperclip's API is alpha and changes fast; pin tested versions and add a contract test against a real instance in CI.
|
|
74
|
+
5. Directory policies and review criteria change; re‑read before each submission.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Community: Discord, Discussions and socials
|
|
2
|
+
|
|
3
|
+
A ready-to-follow setup for the Papercliped Discord, plus where everything else lives. Links show up on the website only once they're set (`web/src/config/community.ts`, or `VITE_DISCORD_URL`, `VITE_X_URL`, `VITE_DISCUSSIONS_URL`, `VITE_FORUM_URL` at build time). Nothing here needs a bot.
|
|
4
|
+
|
|
5
|
+
## 1. Create the server (about 20 minutes)
|
|
6
|
+
|
|
7
|
+
1. Discord → **+ → Create My Own → For a club or community**. Name: **Papercliped**. Icon: `web/public/brand/mascot-dark.svg` exported to PNG (or the 512px social image).
|
|
8
|
+
2. **Server Settings → Enable Community.** This turns on rules screening, a rules channel and an announcements channel.
|
|
9
|
+
3. **Safety Setup:** verification level **Medium** (verified email, on Discord 5+ minutes); explicit media filter **all members**; turn on **AutoMod** with "Block commonly flagged words", "Block mention spam" (limit 5) and a custom rule that blocks messages matching `pcs_`, `pcb_` and `sk-` (secret keys and tokens), with the reply "Never post secret keys. Make a new one in the Papercliped plugin."
|
|
10
|
+
4. **Invite link:** never expires, no use limit. Put it in `VITE_DISCORD_URL` (Render env for the website build) and tell a maintainer to add it to `.github/ISSUE_TEMPLATE/config.yml`.
|
|
11
|
+
|
|
12
|
+
## 2. Channels
|
|
13
|
+
|
|
14
|
+
| Category | Channel | Who can post | What for |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| **Start here** | `#rules` | Maintainers | Server rules (below); Community "rules screening" uses it |
|
|
17
|
+
| | `#welcome` | Maintainers | What Papercliped is, links to docs and install steps |
|
|
18
|
+
| | `#announcements` | Maintainers | Releases (automatic, below) and news; members can follow it into their own servers |
|
|
19
|
+
| | `#get-contributor` | Everyone | Post a link to your merged PR to get the Contributor role |
|
|
20
|
+
| **Community** | `#general` | Everyone | Chat |
|
|
21
|
+
| | `#help` (Forum channel) | Everyone | One post per question; tags: Claude, ChatGPT, Codex, Plugin, Self-hosting, Solved |
|
|
22
|
+
| | `#ideas` (Forum channel) | Everyone | Feature ideas; good ones become GitHub issues |
|
|
23
|
+
| | `#show-and-tell` | Everyone | What you built with your agents; the only place for self-promotion |
|
|
24
|
+
| **Build** | `#contributors` | Contributors and up | Coordinating work, reviews, design questions |
|
|
25
|
+
| | `#releases-feed` | Bot (webhook) | Every GitHub release, automatically |
|
|
26
|
+
| | `#maintainers` (private) | Maintainers | Moderation and security coordination; never discuss vulnerability details anywhere else |
|
|
27
|
+
|
|
28
|
+
## 3. Roles
|
|
29
|
+
|
|
30
|
+
| Role | Colour | Given to | Permissions beyond default |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| **Maintainer** | Cream `#e9e4b0` | People with merge and release rights on GitHub | Manage messages, timeout members, post in Start here, see `#maintainers` |
|
|
33
|
+
| **Triager** | Blue | People with GitHub triage access | Manage threads in `#help`/`#ideas`, mark Solved |
|
|
34
|
+
| **Contributor** | Green | Anyone with a merged pull request | Post in `#contributors` |
|
|
35
|
+
| **Member** | default | Everyone who accepts the rules | Post in Community |
|
|
36
|
+
|
|
37
|
+
Keep the list of Maintainers on Discord identical to GitHub's. Remove roles when people step back.
|
|
38
|
+
|
|
39
|
+
## 4. Rules (paste into `#rules`)
|
|
40
|
+
|
|
41
|
+
1. Be kind. Follow the [code of conduct](https://github.com/OpenSourcx/papercliped/blob/main/CODE_OF_CONDUCT.md).
|
|
42
|
+
2. **Never post secret keys, tokens or Paperclip addresses with credentials.** If you do by accident, make a new key in the Papercliped plugin straight away and tell a maintainer.
|
|
43
|
+
3. Security problems go to support@papercliped.co or GitHub's private reporting, never a channel.
|
|
44
|
+
4. One question per `#help` post, with what you tried and the error you saw.
|
|
45
|
+
5. No spam or DMs to people you don't know. Self-promotion only in `#show-and-tell`.
|
|
46
|
+
6. Stay on topic: Papercliped, Paperclip and running AI agents.
|
|
47
|
+
7. Maintainers' moderation decisions are final; appeal by email to support@papercliped.co.
|
|
48
|
+
|
|
49
|
+
## 5. Welcome message (paste into `#welcome`)
|
|
50
|
+
|
|
51
|
+
> **Welcome to Papercliped!** Papercliped connects Claude, ChatGPT, Codex and any MCP app to your Paperclip.
|
|
52
|
+
>
|
|
53
|
+
> • Install it: https://github.com/OpenSourcx/papercliped#add-it-to-your-ai-app
|
|
54
|
+
> • Docs: https://docs.papercliped.co
|
|
55
|
+
> • Stuck? Open a post in #help. Ideas go in #ideas.
|
|
56
|
+
> • Want to contribute? Read https://github.com/OpenSourcx/papercliped/blob/main/CONTRIBUTING.md, then grab a `good first issue`.
|
|
57
|
+
>
|
|
58
|
+
> Never post your secret key or tokens.
|
|
59
|
+
|
|
60
|
+
## 6. Onboarding
|
|
61
|
+
|
|
62
|
+
**Server Settings → Onboarding:** default channels `#welcome`, `#general`, `#help`, `#announcements`. One question, "What do you use Papercliped with?" (Claude / ChatGPT / Codex / Plugin in Paperclip / Self-hosting). Each answer is just a tag for now; it helps us see where people come from.
|
|
63
|
+
|
|
64
|
+
## 7. Releases into Discord automatically
|
|
65
|
+
|
|
66
|
+
Every push to `main` becomes a GitHub release (see [RELEASING.md](RELEASING.md)). To post them:
|
|
67
|
+
|
|
68
|
+
1. In `#releases-feed`: **Edit Channel → Integrations → Webhooks → New Webhook**, copy its URL.
|
|
69
|
+
2. GitHub → repository **Settings → Webhooks → Add webhook**. Payload URL: the Discord URL **with `/github` added at the end**. Content type `application/json`. **Let me select individual events → Releases** only.
|
|
70
|
+
3. Optionally do the same for `#announcements`, so major and minor releases are visible there; patches stay in the feed.
|
|
71
|
+
|
|
72
|
+
## 8. Becoming a contributor (how roles are given)
|
|
73
|
+
|
|
74
|
+
1. Someone's pull request is merged.
|
|
75
|
+
2. They post the link in `#get-contributor`.
|
|
76
|
+
3. A maintainer checks it and gives the Contributor role (and adds them to the release thanks).
|
|
77
|
+
|
|
78
|
+
Triager and Maintainer are by invitation, decided by the maintainers together; see "From first PR to maintainer" in [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
79
|
+
|
|
80
|
+
## GitHub Discussions (the forum)
|
|
81
|
+
|
|
82
|
+
Turn on **Discussions** in the repository settings with the categories Q&A, Ideas, Show and tell and Announcements. Then set `FORUM_URL=https://github.com/OpenSourcx/papercliped/discussions` on the bridge (Render), so `forum.papercliped.co` redirects there, and `VITE_DISCUSSIONS_URL` / `VITE_FORUM_URL` for the website. Pin a "Start here" discussion that links the README install steps.
|
|
83
|
+
|
|
84
|
+
## X (social)
|
|
85
|
+
|
|
86
|
+
Create the account, set `VITE_X_URL`, and post major and minor releases from the changelog feed (`https://papercliped.co/changelog.xml`).
|
package/docs/DEPLOY-RENDER.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Deploying on Render
|
|
2
|
-
|
|
3
|
-
The deployment guide now lives in **[LAUNCH.md](LAUNCH.md)** (Supabase → Render free → Cloudflare domain → keep-alive → panel → marketplaces → npm).
|
|
1
|
+
# Deploying on Render
|
|
2
|
+
|
|
3
|
+
The deployment guide now lives in **[LAUNCH.md](LAUNCH.md)** (Supabase → Render free → Cloudflare domain → keep-alive → panel → marketplaces → npm).
|
package/docs/DOMAINS.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Domains and hosts (papercliped.co)
|
|
2
|
+
|
|
3
|
+
One bridge service answers every host; it routes by the `Host` header. Nothing here needs a second server.
|
|
4
|
+
|
|
5
|
+
| Host | What it serves |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `papercliped.co` | The website (landing, status, changelog, community, brand, privacy, terms) and every API |
|
|
8
|
+
| `www.papercliped.co` | Redirects to `papercliped.co` (same path) |
|
|
9
|
+
| `docs.papercliped.co` | The docs: the landing at `/` and each page at `/<page>` (e.g. `/permissions`); old `/docs/...` and `/topics/...` links redirect there |
|
|
10
|
+
| `mcp.papercliped.co` | **Where AI apps connect** (`/mcp`, the OAuth issuer), ChatGPT Actions and `/openapi.json`; web pages redirect to `papercliped.co` |
|
|
11
|
+
| `api.papercliped.co` | `/api/public/v1/*` and `/openapi.json`; web pages redirect to `papercliped.co` |
|
|
12
|
+
| `forum.papercliped.co` | Redirects to the forum (`FORUM_URL`; GitHub Discussions to start, see `docs/COMMUNITY.md`) |
|
|
13
|
+
|
|
14
|
+
Every host also answers the protocol and API paths (`/mcp`, `/authorize`, `/token`, `/register`, `/revoke`, `/.well-known/*`, `/actions/*`, `/openapi.json`, `/api/*`, `/healthz`, `/readyz`), so nothing breaks if an app or person uses another host for them.
|
|
15
|
+
|
|
16
|
+
## 1. Cloudflare DNS
|
|
17
|
+
In Cloudflare → `papercliped.co` → **DNS → Records**, add (Render shows the exact target, `<service>.onrender.com`):
|
|
18
|
+
|
|
19
|
+
| Type | Name | Target | Proxy |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| CNAME | `@` | `<service>.onrender.com` | DNS only |
|
|
22
|
+
| CNAME | `www` | `<service>.onrender.com` | DNS only |
|
|
23
|
+
| CNAME | `docs` | `<service>.onrender.com` | DNS only |
|
|
24
|
+
| CNAME | `mcp` | `<service>.onrender.com` | DNS only |
|
|
25
|
+
| CNAME | `api` | `<service>.onrender.com` | DNS only |
|
|
26
|
+
| CNAME | `forum` | `<service>.onrender.com` | DNS only |
|
|
27
|
+
|
|
28
|
+
Start with **DNS only (grey cloud)** so Render can issue certificates. SSL/TLS mode: **Full (strict)**. If you later turn on proxying (orange cloud), set `BRIDGE_PROXY_HOPS=2` and keep challenges off the API paths (`docs/LAUNCH.md` §3C).
|
|
29
|
+
|
|
30
|
+
## 2. Render
|
|
31
|
+
Render → the service → **Settings → Custom Domains → Add** each of: `papercliped.co`, `www.papercliped.co`, `docs.papercliped.co`, `mcp.papercliped.co`, `api.papercliped.co`, `forum.papercliped.co`. Click **Verify** on each and wait for the certificates.
|
|
32
|
+
|
|
33
|
+
## 3. Environment (Render → Environment)
|
|
34
|
+
```
|
|
35
|
+
PUBLIC_HOSTS=marketing=papercliped.co,www.papercliped.co;docs=docs.papercliped.co;api=mcp.papercliped.co,api.papercliped.co;forum=forum.papercliped.co
|
|
36
|
+
FORUM_URL=https://github.com/OpenSourcx/papercliped/discussions
|
|
37
|
+
```
|
|
38
|
+
The first `marketing` host is the canonical one. Hosts you don't list (like `<service>.onrender.com`) still serve the whole site, so the service stays reachable while DNS changes. The Docker image builds the website and the bridge finds it at `/app/web/dist` (override with `WEB_DIST`).
|
|
39
|
+
|
|
40
|
+
## 4. The OAuth issuer: mcp.papercliped.co (decided)
|
|
41
|
+
`BRIDGE_PUBLIC_URL` is the OAuth issuer **and** the resource AI apps connect to, so an app's connector URL must be `<BRIDGE_PUBLIC_URL>/mcp`. Papercliped uses:
|
|
42
|
+
```
|
|
43
|
+
BRIDGE_PUBLIC_URL=https://mcp.papercliped.co
|
|
44
|
+
```
|
|
45
|
+
Connector URL: **`https://mcp.papercliped.co/mcp`** (ChatGPT Actions: `https://mcp.papercliped.co/openapi.json`). The website and docs stay on `papercliped.co`; the docs show the connector URL through the `{{MCP}}` placeholder.
|
|
46
|
+
|
|
47
|
+
**Changing `BRIDGE_PUBLIC_URL` invalidates every existing token**, so everyone connected must connect again (at the time of the switch, only the owner). After switching:
|
|
48
|
+
- Claude: Settings → Connectors → remove the old connector, add `https://mcp.papercliped.co/mcp`. Claude Code: `claude mcp remove papercliped` then `claude mcp add --transport http papercliped https://mcp.papercliped.co/mcp`.
|
|
49
|
+
- ChatGPT: re-add the connector, or re-import `https://mcp.papercliped.co/openapi.json` into the GPT.
|
|
50
|
+
- The Paperclip plugin needs nothing: it talks to `/api/manage` and `/api/public`, which work on every host (its default bridge URL stays `https://papercliped.co`).
|
|
51
|
+
|
|
52
|
+
## 5. Check
|
|
53
|
+
```
|
|
54
|
+
curl -sI https://www.papercliped.co/status # 301 → https://papercliped.co/status
|
|
55
|
+
curl -sI https://docs.papercliped.co/ # 200 (the docs landing)
|
|
56
|
+
curl -sI https://papercliped.co/docs/permissions # 301 → https://docs.papercliped.co/permissions
|
|
57
|
+
curl -s https://api.papercliped.co/api/public/v1/status
|
|
58
|
+
curl -s https://mcp.papercliped.co/.well-known/oauth-authorization-server | head -c 200 # issuer: https://mcp.papercliped.co
|
|
59
|
+
curl -sI https://forum.papercliped.co/ # 302 → the forum
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On **Windows PowerShell**, `curl` is a different command: use `curl.exe` (built into Windows 10 and 11), and drop `| head -c 200`:
|
|
63
|
+
```powershell
|
|
64
|
+
curl.exe -sI https://www.papercliped.co/status
|
|
65
|
+
curl.exe -sI https://docs.papercliped.co/
|
|
66
|
+
curl.exe -sI https://papercliped.co/docs/permissions
|
|
67
|
+
curl.exe -s https://api.papercliped.co/api/public/v1/status
|
|
68
|
+
curl.exe -s https://mcp.papercliped.co/.well-known/oauth-authorization-server
|
|
69
|
+
curl.exe -sI https://forum.papercliped.co/
|
|
70
|
+
```
|
|
71
|
+
"Could not resolve host" means the DNS record is missing or still spreading; a certificate error means Render hasn't verified that domain yet; a `200` where a `301` is expected means `PUBLIC_HOSTS` isn't set (or the service hasn't redeployed).
|
|
72
|
+
|
|
73
|
+
## Rollback
|
|
74
|
+
Remove `PUBLIC_HOSTS` and redeploy: every host serves the whole site again, with docs at `/docs`. To go back to the old Markdown pages entirely, set `WEB_DIST` to a folder that doesn't exist. DNS records and Render custom domains can stay.
|