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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +14 -11
  2. package/.claude-plugin/plugin.json +17 -8
  3. package/.mcp.json +8 -14
  4. package/CHANGELOG.md +64 -1
  5. package/LICENSE +21 -21
  6. package/README.md +80 -80
  7. package/commands/paperclip-report.md +5 -5
  8. package/commands/paperclip-status.md +4 -4
  9. package/commands/paperclip-sync.md +5 -5
  10. package/dist/accounts/alias.js +1 -1
  11. package/dist/accounts/username.js +1 -1
  12. package/dist/cli.js +11 -11
  13. package/dist/http.js +17 -3
  14. package/dist/manage/routes.js +60 -82
  15. package/dist/oauth/pages.js +28 -18
  16. package/dist/oauth/pg-store.js +0 -13
  17. package/dist/oauth/provider.js +27 -63
  18. package/dist/oauth/scopes.js +19 -16
  19. package/dist/openapi.js +19 -2
  20. package/dist/public-api/health.js +46 -0
  21. package/dist/public-api/openapi.js +51 -0
  22. package/dist/public-api/routes.js +251 -0
  23. package/dist/public-api/types.js +14 -0
  24. package/dist/server.js +4 -0
  25. package/dist/site/landing.js +3 -3
  26. package/dist/site/markdown.js +2 -1
  27. package/dist/site/site.js +16 -4
  28. package/dist/telemetry/recorder.js +1 -1
  29. package/dist/tools.js +48 -2
  30. package/dist/web/csp.js +16 -0
  31. package/dist/web/hosts.js +23 -0
  32. package/dist/web/routes.js +132 -0
  33. package/docs/ARCHITECTURE.md +74 -73
  34. package/docs/COMMUNITY.md +86 -0
  35. package/docs/DEPLOY-RENDER.md +3 -3
  36. package/docs/DOMAINS.md +74 -0
  37. package/docs/LAUNCH.md +50 -34
  38. package/docs/RELEASING.md +37 -0
  39. package/docs/SECURITY.md +82 -65
  40. package/docs/SITEMAP.md +92 -0
  41. package/docs/V3-SUBDOMAINS.md +106 -0
  42. package/docs/chatgpt.md +20 -20
  43. package/docs/least-privilege.sql +23 -23
  44. package/docs/legal/PRIVACY.md +37 -37
  45. package/docs/legal/TERMS.md +11 -11
  46. package/docs/oauth.md +61 -62
  47. package/docs/supabase-schema.sql +8 -1
  48. package/migrations/001_init.sql +84 -84
  49. package/migrations/002_audit.sql +41 -41
  50. package/migrations/003_accounts.sql +66 -66
  51. package/migrations/004_privacy_panel.sql +57 -57
  52. package/migrations/006_two_levels.sql +5 -0
  53. package/package.json +10 -14
  54. package/site/docs/chatgpt.md +1 -1
  55. package/site/docs/cli.md +27 -0
  56. package/site/docs/connect-your-paperclip.md +120 -0
  57. package/site/docs/environment.md +64 -0
  58. package/site/docs/faq.md +31 -0
  59. package/site/docs/getting-started.md +11 -5
  60. package/site/docs/limits-and-errors.md +32 -0
  61. package/site/docs/manage-api.md +33 -0
  62. package/site/docs/manage.md +12 -21
  63. package/site/docs/other-ai-apps.md +28 -0
  64. package/site/docs/paperclip-plugin.md +32 -0
  65. package/site/docs/permissions.md +5 -5
  66. package/site/docs/public-api.md +62 -0
  67. package/site/docs/security.md +2 -2
  68. package/site/docs/self-hosting.md +43 -0
  69. package/site/docs/signup.md +3 -3
  70. package/site/docs/tools.md +47 -0
  71. package/site/docs/troubleshooting.md +2 -2
  72. package/site/docs/what-is-papercliped.md +37 -0
  73. package/site/privacy.md +5 -3
  74. package/site/terms.md +1 -1
  75. package/skills/paperclip/SKILL.md +23 -23
  76. package/dist/demo.js +0 -144
  77. package/dist/manage/page.js +0 -170
  78. package/dist/panel/config.js +0 -61
  79. package/dist/panel/main.js +0 -90
  80. package/dist/panel/page.js +0 -553
  81. package/dist/panel/routes.js +0 -284
  82. package/dist/panel/server.js +0 -37
  83. package/docs/panel-role.sql +0 -26
@@ -1,66 +1,66 @@
1
- -- Papercliped accounts: username + secret key, one stored Paperclip connection per account, and the user event log.
2
- -- Nothing here stores a plaintext secret: `secret_hash` is a salted scrypt hash, `sealed_credential` is AES-GCM sealed in the app.
3
-
4
- create table if not exists bridge.accounts (
5
- id text primary key,
6
- username text not null, -- as typed; shown only in the operator's logs
7
- username_key text not null, -- lowercased; unique
8
- secret_hash text not null, -- scrypt$N$r$p$salt$hash of the generated secret key
9
- created_at bigint not null,
10
- last_login_at bigint,
11
- disabled boolean not null default false
12
- );
13
- create unique index if not exists accounts_username_key_idx on bridge.accounts (username_key);
14
-
15
- -- One connection per account. A given Paperclip identity (instance + user id) can belong to only one account.
16
- create table if not exists bridge.account_links (
17
- account_id text primary key references bridge.accounts (id) on delete cascade,
18
- instance_url text not null,
19
- paperclip_user_id text,
20
- sealed_credential text, -- null once dropped (idle expiry / disconnect)
21
- created_at bigint not null,
22
- connected_at bigint not null,
23
- last_used_at bigint not null
24
- );
25
- create unique index if not exists account_links_identity_idx on bridge.account_links (instance_url, paperclip_user_id) where paperclip_user_id is not null;
26
- create index if not exists account_links_idle_idx on bridge.account_links (last_used_at) where sealed_credential is not null;
27
-
28
- -- The community log: joined / left / updated / login / ... (see src/accounts/types.ts). `detail` is a short non-sensitive reason.
29
- create table if not exists bridge.user_events (
30
- id bigint generated always as identity primary key,
31
- ts bigint not null,
32
- account_id text,
33
- username text,
34
- kind text not null,
35
- detail text
36
- );
37
- create index if not exists user_events_ts_idx on bridge.user_events (ts desc);
38
- create index if not exists user_events_kind_idx on bridge.user_events (kind, ts desc);
39
-
40
- -- A row the app rewrites on a timer so a free-tier Supabase project always sees recent write activity.
41
- create table if not exists bridge.heartbeat (
42
- id integer primary key,
43
- at bigint not null
44
- );
45
-
46
- alter table bridge.grants add column if not exists account_id text;
47
- alter table bridge.grants add column if not exists username text;
48
- alter table bridge.audit_events add column if not exists username text;
49
- create index if not exists grants_account_idx on bridge.grants (account_id) where not revoked;
50
- create index if not exists audit_username_idx on bridge.audit_events (username, ts desc) where username is not null;
51
-
52
- alter table bridge.accounts enable row level security;
53
- alter table bridge.account_links enable row level security;
54
- alter table bridge.user_events enable row level security;
55
- alter table bridge.heartbeat enable row level security;
56
- revoke all on bridge.accounts, bridge.account_links, bridge.user_events, bridge.heartbeat from public;
57
-
58
- do $$
59
- declare r text;
60
- begin
61
- foreach r in array array['anon', 'authenticated'] loop
62
- if exists (select 1 from pg_roles where rolname = r) then
63
- execute format('revoke all on bridge.accounts, bridge.account_links, bridge.user_events, bridge.heartbeat from %I', r);
64
- end if;
65
- end loop;
66
- end $$;
1
+ -- Papercliped accounts: username + secret key, one stored Paperclip connection per account, and the user event log.
2
+ -- Nothing here stores a plaintext secret: `secret_hash` is a salted scrypt hash, `sealed_credential` is AES-GCM sealed in the app.
3
+
4
+ create table if not exists bridge.accounts (
5
+ id text primary key,
6
+ username text not null, -- as typed; shown only in the operator's logs
7
+ username_key text not null, -- lowercased; unique
8
+ secret_hash text not null, -- scrypt$N$r$p$salt$hash of the generated secret key
9
+ created_at bigint not null,
10
+ last_login_at bigint,
11
+ disabled boolean not null default false
12
+ );
13
+ create unique index if not exists accounts_username_key_idx on bridge.accounts (username_key);
14
+
15
+ -- One connection per account. A given Paperclip identity (instance + user id) can belong to only one account.
16
+ create table if not exists bridge.account_links (
17
+ account_id text primary key references bridge.accounts (id) on delete cascade,
18
+ instance_url text not null,
19
+ paperclip_user_id text,
20
+ sealed_credential text, -- null once dropped (idle expiry / disconnect)
21
+ created_at bigint not null,
22
+ connected_at bigint not null,
23
+ last_used_at bigint not null
24
+ );
25
+ create unique index if not exists account_links_identity_idx on bridge.account_links (instance_url, paperclip_user_id) where paperclip_user_id is not null;
26
+ create index if not exists account_links_idle_idx on bridge.account_links (last_used_at) where sealed_credential is not null;
27
+
28
+ -- The community log: joined / left / updated / login / ... (see src/accounts/types.ts). `detail` is a short non-sensitive reason.
29
+ create table if not exists bridge.user_events (
30
+ id bigint generated always as identity primary key,
31
+ ts bigint not null,
32
+ account_id text,
33
+ username text,
34
+ kind text not null,
35
+ detail text
36
+ );
37
+ create index if not exists user_events_ts_idx on bridge.user_events (ts desc);
38
+ create index if not exists user_events_kind_idx on bridge.user_events (kind, ts desc);
39
+
40
+ -- A row the app rewrites on a timer so a free-tier Supabase project always sees recent write activity.
41
+ create table if not exists bridge.heartbeat (
42
+ id integer primary key,
43
+ at bigint not null
44
+ );
45
+
46
+ alter table bridge.grants add column if not exists account_id text;
47
+ alter table bridge.grants add column if not exists username text;
48
+ alter table bridge.audit_events add column if not exists username text;
49
+ create index if not exists grants_account_idx on bridge.grants (account_id) where not revoked;
50
+ create index if not exists audit_username_idx on bridge.audit_events (username, ts desc) where username is not null;
51
+
52
+ alter table bridge.accounts enable row level security;
53
+ alter table bridge.account_links enable row level security;
54
+ alter table bridge.user_events enable row level security;
55
+ alter table bridge.heartbeat enable row level security;
56
+ revoke all on bridge.accounts, bridge.account_links, bridge.user_events, bridge.heartbeat from public;
57
+
58
+ do $$
59
+ declare r text;
60
+ begin
61
+ foreach r in array array['anon', 'authenticated'] loop
62
+ if exists (select 1 from pg_roles where rolname = r) then
63
+ execute format('revoke all on bridge.accounts, bridge.account_links, bridge.user_events, bridge.heartbeat from %I', r);
64
+ end if;
65
+ end loop;
66
+ end $$;
@@ -1,57 +1,57 @@
1
- -- Privacy (anonymous display names) and the separate operator panel.
2
- --
3
- -- * Accounts can opt in to appear in logs and the panel as a generated alias ("Ann02") and with an anonymous label instead of
4
- -- their Paperclip's hostname. Events and audit rows store only the DISPLAY name, so the panel's database role never sees the
5
- -- real username of an anonymous account. (The service operator with admin access to the base tables still can; see docs/SECURITY.md.)
6
- -- * The bridge writes periodic health samples; the panel reads them, so it can tell when the bridge is down or asleep.
7
- -- * The panel connects with a read-only role that can see only the views and telemetry tables below — never the credential,
8
- -- the secret hash, or real usernames.
9
-
10
- alter table bridge.accounts add column if not exists anonymous boolean not null default false;
11
- alter table bridge.accounts add column if not exists alias text;
12
- alter table bridge.account_links add column if not exists instance_label text; -- the host, or "anon-xxxxxx" for anonymous accounts
13
- create unique index if not exists accounts_alias_idx on bridge.accounts (lower(alias)) where alias is not null;
14
-
15
- create table if not exists bridge.node_samples (
16
- id bigint generated always as identity primary key,
17
- ts bigint not null,
18
- node text not null,
19
- db_ping_ms real,
20
- loop_lag_p99_ms real,
21
- rss_mb real,
22
- heap_mb real,
23
- uptime_s integer,
24
- version text
25
- );
26
- create index if not exists node_samples_ts_idx on bridge.node_samples (ts desc);
27
- create index if not exists node_samples_node_idx on bridge.node_samples (node, ts desc);
28
- alter table bridge.node_samples enable row level security;
29
- revoke all on bridge.node_samples from public;
30
-
31
- -- Views run with the rights of the role that owns them (the migration role), so the panel role needs no access to the base tables.
32
- create or replace view bridge.panel_accounts as
33
- select id,
34
- case when anonymous and alias is not null then alias else username end as display_name,
35
- (anonymous and alias is not null) as anonymous,
36
- created_at, last_login_at, disabled
37
- from bridge.accounts;
38
-
39
- create or replace view bridge.panel_grants as
40
- select id, client_name, scopes, created_at, last_used_at, revoked, account_id, username as display_name
41
- from bridge.grants;
42
-
43
- create or replace view bridge.panel_links as
44
- select account_id, coalesce(instance_label, 'unknown') as instance_label, connected_at, last_used_at, (sealed_credential is not null) as has_credential
45
- from bridge.account_links;
46
-
47
- revoke all on bridge.panel_accounts, bridge.panel_grants, bridge.panel_links from public;
48
-
49
- do $$
50
- declare r text;
51
- begin
52
- foreach r in array array['anon', 'authenticated'] loop
53
- if exists (select 1 from pg_roles where rolname = r) then
54
- execute format('revoke all on bridge.node_samples, bridge.panel_accounts, bridge.panel_grants, bridge.panel_links from %I', r);
55
- end if;
56
- end loop;
57
- end $$;
1
+ -- Privacy (anonymous display names) and the separate operator panel.
2
+ --
3
+ -- * Accounts can opt in to appear in logs and the panel as a generated alias ("Ann02") and with an anonymous label instead of
4
+ -- their Paperclip's hostname. Events and audit rows store only the DISPLAY name, so the panel's database role never sees the
5
+ -- real username of an anonymous account. (The service operator with admin access to the base tables still can; see docs/SECURITY.md.)
6
+ -- * The bridge writes periodic health samples; the panel reads them, so it can tell when the bridge is down or asleep.
7
+ -- * The panel connects with a read-only role that can see only the views and telemetry tables below — never the credential,
8
+ -- the secret hash, or real usernames.
9
+
10
+ alter table bridge.accounts add column if not exists anonymous boolean not null default false;
11
+ alter table bridge.accounts add column if not exists alias text;
12
+ alter table bridge.account_links add column if not exists instance_label text; -- the host, or "anon-xxxxxx" for anonymous accounts
13
+ create unique index if not exists accounts_alias_idx on bridge.accounts (lower(alias)) where alias is not null;
14
+
15
+ create table if not exists bridge.node_samples (
16
+ id bigint generated always as identity primary key,
17
+ ts bigint not null,
18
+ node text not null,
19
+ db_ping_ms real,
20
+ loop_lag_p99_ms real,
21
+ rss_mb real,
22
+ heap_mb real,
23
+ uptime_s integer,
24
+ version text
25
+ );
26
+ create index if not exists node_samples_ts_idx on bridge.node_samples (ts desc);
27
+ create index if not exists node_samples_node_idx on bridge.node_samples (node, ts desc);
28
+ alter table bridge.node_samples enable row level security;
29
+ revoke all on bridge.node_samples from public;
30
+
31
+ -- Views run with the rights of the role that owns them (the migration role), so the panel role needs no access to the base tables.
32
+ create or replace view bridge.panel_accounts as
33
+ select id,
34
+ case when anonymous and alias is not null then alias else username end as display_name,
35
+ (anonymous and alias is not null) as anonymous,
36
+ created_at, last_login_at, disabled
37
+ from bridge.accounts;
38
+
39
+ create or replace view bridge.panel_grants as
40
+ select id, client_name, scopes, created_at, last_used_at, revoked, account_id, username as display_name
41
+ from bridge.grants;
42
+
43
+ create or replace view bridge.panel_links as
44
+ select account_id, coalesce(instance_label, 'unknown') as instance_label, connected_at, last_used_at, (sealed_credential is not null) as has_credential
45
+ from bridge.account_links;
46
+
47
+ revoke all on bridge.panel_accounts, bridge.panel_grants, bridge.panel_links from public;
48
+
49
+ do $$
50
+ declare r text;
51
+ begin
52
+ foreach r in array array['anon', 'authenticated'] loop
53
+ if exists (select 1 from pg_roles where rolname = r) then
54
+ execute format('revoke all on bridge.node_samples, bridge.panel_accounts, bridge.panel_grants, bridge.panel_links from %I', r);
55
+ end if;
56
+ end loop;
57
+ end $$;
@@ -0,0 +1,5 @@
1
+ -- Two access levels: Read only and Full control. The old third level (paperclip:admin) is folded into Full control,
2
+ -- which now covers every tool. Existing grants keep working; this only tidies the stored scope lists.
3
+ update bridge.grants
4
+ set scopes = array(select distinct s from unnest(array_replace(scopes, 'paperclip:admin', 'paperclip:control')) as s order by s desc)
5
+ where 'paperclip:admin' = any(scopes);
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "papercliped",
3
- "version": "1.1.0-beta.2",
4
- "description": "Papercliped \u2014 connect Claude and ChatGPT to your Paperclip: control agents, sync, report. MCP server + hosted multi-tenant OAuth bridge + separate operator panel.",
3
+ "version": "2.1.0",
4
+ "description": "Papercliped — connect Claude, ChatGPT and any MCP app to your Paperclip: control agents, sync, report. MCP server + hosted multi-tenant OAuth bridge + public stats API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "papercliped": "dist/stdio.js",
9
9
  "papercliped-bridge": "dist/http.js",
10
- "papercliped-panel": "dist/panel/main.js",
11
10
  "papercliped-admin": "dist/cli.js"
12
11
  },
13
12
  "files": [
@@ -24,7 +23,7 @@
24
23
  "LICENSE"
25
24
  ],
26
25
  "scripts": {
27
- "build": "tsc",
26
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
28
27
  "typecheck": "tsc --noEmit",
29
28
  "test": "vitest run",
30
29
  "start:stdio": "node dist/stdio.js",
@@ -33,24 +32,21 @@
33
32
  "migrate": "node dist/cli.js migrate",
34
33
  "start": "node dist/http.js",
35
34
  "admin": "node dist/cli.js",
36
- "demo": "node dist/demo.js",
37
35
  "schema:sql": "node dist/cli.js schema-sql > docs/supabase-schema.sql",
38
- "panel": "node dist/panel/main.js",
39
- "start:panel": "node dist/panel/main.js",
40
36
  "prepublishOnly": "npm run build"
41
37
  },
42
38
  "dependencies": {
43
39
  "@modelcontextprotocol/sdk": "^1.17.0",
44
40
  "pg": "^8.23.1",
45
- "undici": "^7.30.0",
46
- "zod": "^3.25.0",
47
- "zod-to-json-schema": "^3.25.2"
41
+ "undici": "^8.11.2",
42
+ "zod": "^4.6.5"
48
43
  },
49
44
  "devDependencies": {
50
- "@types/node": "^22.0.0",
45
+ "@types/node": "^26.6.4",
51
46
  "@types/pg": "^8.23.1",
52
- "typescript": "^5.6.0",
53
- "vitest": "^2.1.0"
47
+ "typescript": "^7.0.2",
48
+ "vite": "^8.3.3",
49
+ "vitest": "^5.0.3"
54
50
  },
55
51
  "engines": {
56
52
  "node": ">=20.18"
@@ -59,7 +55,7 @@
59
55
  "type": "git",
60
56
  "url": "git+https://github.com/OpenSourcx/papercliped.git"
61
57
  },
62
- "homepage": "https://papercliped.kpnsolute.com",
58
+ "homepage": "https://papercliped.co",
63
59
  "keywords": [
64
60
  "paperclip",
65
61
  "mcp",
@@ -3,7 +3,7 @@
3
3
  Requires a ChatGPT plan that can create GPTs with Actions.
4
4
 
5
5
  1. Create a GPT, then **Configure → Create new action**.
6
- 2. **Import from URL:** `{{URL}}/openapi.json`.
6
+ 2. **Import from URL:** `{{MCP}}/openapi.json`.
7
7
  3. **Authentication:** OAuth, using the endpoints listed at `{{URL}}/.well-known/oauth-authorization-server`.
8
8
  4. Add these instructions:
9
9
 
@@ -0,0 +1,27 @@
1
+ # Command line
2
+
3
+ Papercliped installs these commands (`npx -p papercliped@latest <command>`, or from a clone after `npm run build`).
4
+
5
+ | Command | What it runs |
6
+ | --- | --- |
7
+ | `papercliped` | The MCP server over stdio, for one Paperclip (local use) |
8
+ | `papercliped-bridge` | The HTTP bridge (MCP over HTTP, ChatGPT Actions, OAuth, the public site and APIs) |
9
+ | `papercliped-admin` | Operator commands, below |
10
+
11
+ ## papercliped-admin
12
+ Run it with the same environment as the bridge (it reads the same database).
13
+
14
+ | Command | What it does |
15
+ | --- | --- |
16
+ | `migrate` | Apply database migrations |
17
+ | `schema-sql` | Print the full schema as one SQL script (for pasting into Supabase) |
18
+ | `users` | List accounts: name, anonymity, status, last sign-in |
19
+ | `delete-user <username>` | Delete an account and everything tied to it |
20
+ | `grants` | List live connections |
21
+ | `revoke <grant id>` | Disconnect one connection |
22
+ | `revoke-all --yes` | Disconnect every connection |
23
+ | `sweep` | Revoke connections idle longer than `BRIDGE_IDLE_REVOKE_DAYS` |
24
+ | `rotate-keys` | Re-seal stored Paperclip keys with the current `BRIDGE_SECRET` (after rotating it) |
25
+
26
+ ## npm scripts (in a clone)
27
+ `npm test`, `npm run build`, `npm run start:http`, `npm run start:stdio`, `npm run openapi` (writes `openapi.json`), `npm run migrate`, `npm run schema:sql`.
@@ -0,0 +1,120 @@
1
+ # Set up your Paperclip for Papercliped
2
+
3
+ Papercliped talks to **your** Paperclip over the internet, so your Paperclip needs a public address. This guide gets you one, step by step, whether you already own a domain or not.
4
+
5
+ You don't need to open any ports on your router for any option below.
6
+
7
+ ## What Papercliped needs from your Paperclip
8
+ - **A public `https://` address on port 443** that answers over the internet. Private, LAN, `localhost` and plain `http://` addresses are refused on purpose.
9
+ - **Paperclip in authenticated mode and public exposure.** You can check from any computer:
10
+ ```
11
+ curl https://your-address/api/health
12
+ ```
13
+ A healthy answer is JSON that includes `"status":"ok"`, `"deploymentMode":"authenticated"` and `"deploymentExposure":"public"`. (On Windows PowerShell, `curl.exe https://your-address/api/health` works the same way.)
14
+ - **No login wall or bot challenge in front of `/api/*`.** If you put something like Cloudflare Access, "Under Attack" mode or a managed challenge in front of Paperclip, it must skip `/api/*`, or Papercliped will be blocked.
15
+ - **Paperclip must accept the new hostname.** Paperclip can reject hostnames it doesn't know, and public mode needs its public URL set. Paperclip's own settings for this include `PAPERCLIP_PUBLIC_URL` and `PAPERCLIP_ALLOWED_HOSTNAMES`; the exact steps are in Paperclip's deployment docs (search "Paperclip deployment modes" on docs.paperclip.ing). Check them there rather than guessing.
16
+
17
+ Paperclip listens on port `3100` by default, which the examples below use. If yours is different, use your port.
18
+
19
+ ## Path A: you own a domain (recommended)
20
+ This gives you a stable address like `https://paperclip.yourdomain.com` that survives restarts. The tunnel and DNS are free on Cloudflare's free plan. The domain itself isn't: there is no free `.com`. (Cloudflare Registrar sells domains at wholesale cost, so it's one cheap option if you need to buy one.)
21
+
22
+ 1. **Put your domain on Cloudflare.** Create a free Cloudflare account, add your domain, and switch its nameservers to the two Cloudflare gives you (done at the company where you bought the domain). Wait until Cloudflare shows the domain as active.
23
+ 2. **Install `cloudflared`** on the machine that runs Paperclip. Find the installer for Windows, macOS or Linux on Cloudflare's downloads page (search "cloudflared downloads").
24
+ 3. **Log in.** This opens a browser so you can pick your domain:
25
+ ```
26
+ cloudflared tunnel login
27
+ ```
28
+ 4. **Create the tunnel.** Pick any name:
29
+ ```
30
+ cloudflared tunnel create papercliped-paperclip
31
+ ```
32
+ It prints a tunnel ID (a UUID) and saves a credentials file in your `.cloudflared` folder. Keep that file private.
33
+ 5. **Write the config.** In your `.cloudflared` folder (`~/.cloudflared` on macOS/Linux, `%USERPROFILE%\.cloudflared` on Windows) create `config.yml`:
34
+ ```
35
+ tunnel: <TUNNEL-ID>
36
+ credentials-file: <PATH-TO-THE-CREDENTIALS-FILE>.json
37
+ ingress:
38
+ - hostname: paperclip.yourdomain.com
39
+ service: http://localhost:3100
40
+ - service: http_status:404
41
+ ```
42
+ The last rule is required: it answers 404 for anything else.
43
+ 6. **Point the name at the tunnel:**
44
+ ```
45
+ cloudflared tunnel route dns papercliped-paperclip paperclip.yourdomain.com
46
+ ```
47
+ 7. **Run it.** To try it first:
48
+ ```
49
+ cloudflared tunnel run papercliped-paperclip
50
+ ```
51
+ To keep it running (starts with your computer), install it as a service:
52
+ - **macOS:** `cloudflared service install` (runs when you log in) or `sudo cloudflared service install` (runs at boot).
53
+ - **Linux:** `cloudflared service install` (with `sudo` if needed).
54
+ - **Windows:** run `cloudflared.exe service install` from an Administrator PowerShell. A Windows service may not read the config from your user folder; check Cloudflare's "Run as a service on Windows" page for where it expects `config.yml`.
55
+ 8. **Check it** from any computer:
56
+ ```
57
+ curl https://paperclip.yourdomain.com/api/health
58
+ ```
59
+ You should see the JSON described above.
60
+
61
+ **If your computer sleeps or turns off,** the tunnel goes down and your connected AI apps stop working until it's back. Turn off sleep on the machine, or run Paperclip on an always-on computer or server.
62
+
63
+ ## Path B: you don't have a domain yet
64
+ All of these are free. They differ in how stable the address is.
65
+
66
+ | Option | Address | Stable? | Good for |
67
+ | --- | --- | --- | --- |
68
+ | Tailscale Funnel | `https://<machine>.<tailnet>.ts.net` | Yes | Daily use |
69
+ | ngrok free plan | one free static `ngrok-free.app` / `ngrok-free.dev` domain | Yes | Daily use, with a caveat below |
70
+ | Cloudflare quick tunnel | random `*.trycloudflare.com` | **No, changes every restart** | Trying Papercliped only |
71
+
72
+ ### Tailscale Funnel (stable)
73
+ Funnel makes a service on your computer reachable from the public internet at your tailnet's `ts.net` name.
74
+ 1. Create a free Tailscale account and install Tailscale on the machine that runs Paperclip.
75
+ 2. In the Tailscale admin console, turn on **HTTPS certificates** for your tailnet and allow Funnel. (Running the `tailscale funnel` command below walks you through enabling it and updates your tailnet policy for you.)
76
+ 3. Start Funnel for Paperclip's port:
77
+ ```
78
+ tailscale funnel 3100
79
+ ```
80
+ It prints your public address, like `https://my-pc.my-tailnet.ts.net`. Funnel only serves on ports 443, 8443 and 10000; use the default 443 for Papercliped.
81
+ 4. Funnel traffic has bandwidth limits you can't change. That is fine for Papercliped's small requests, but check Tailscale's Funnel docs for the current rules on your plan.
82
+
83
+ ### ngrok free plan (stable address, one catch)
84
+ 1. Create a free ngrok account. Every account gets one free static "dev domain" (for example `your-name.ngrok-free.dev`); see it in the ngrok dashboard under Domains.
85
+ 2. Install ngrok and add your authtoken (the dashboard shows the exact command).
86
+ 3. Run, with your own domain:
87
+ ```
88
+ ngrok http --url=your-name.ngrok-free.dev 3100
89
+ ```
90
+ 4. **The catch:** on the free plan ngrok shows a "visit site" warning page to browsers. Papercliped's requests come from a program, not a browser, but if the connection fails with a strange HTML page, this is the likely cause. Check ngrok's free-plan docs for the current behavior and any header that skips it.
91
+
92
+ ### Cloudflare quick tunnel (trial only)
93
+ No account, no domain:
94
+ ```
95
+ cloudflared tunnel --url http://localhost:3100
96
+ ```
97
+ It prints a random `https://something.trycloudflare.com` address. **The address changes every time you restart it**, and when it changes your Papercliped connection stops working until you connect again with the new address. Quick tunnels are for trying Papercliped for an afternoon, not for daily use.
98
+
99
+ ## Connect to Papercliped
100
+ 1. Make sure `curl https://your-address/api/health` works (see above).
101
+ 2. Go to [Create your account]({{URL}}/docs/signup), add Papercliped as a connector in Claude, and choose **Connect your Paperclip**.
102
+ 3. Enter your public address (just the address, no path), then approve the request in your Paperclip.
103
+
104
+ ## Keep it safe
105
+ - **Expose only Paperclip.** Point the tunnel at Paperclip's port and nothing else on your computer.
106
+ - **Keep Paperclip up to date.**
107
+ - **Use a dedicated Paperclip user for the connection** if you can, so you can see and limit what it does.
108
+ - **You can cut access any time:** revoke the Papercliped key in Paperclip, or disconnect apps in the [Paperclip plugin](/docs/paperclip-plugin).
109
+ - **If the tunnel or your computer is off,** connected apps simply stop working until it's back. Nothing is lost.
110
+
111
+ ## Troubleshooting
112
+ - **"Could not reach your Paperclip":** the tunnel isn't running, the DNS name hasn't spread yet (give a new name a few minutes), or the address isn't on port 443. Test with `curl https://your-address/api/health` from another network (for example, your phone's mobile data).
113
+ - **"Private address" errors:** you entered a LAN address, `localhost`, or `http://`. Use the public `https://` address from your tunnel.
114
+ - **A challenge or login page appears:** exempt `/api/*` from Cloudflare Access, "Under Attack" mode or any managed challenge.
115
+ - **"Host not allowed" or a 4xx from Paperclip:** Paperclip doesn't know the new hostname. Set its public URL and allowed hostnames (see Paperclip's deployment docs, above) and restart it.
116
+ - **Certificate warnings:** use the HTTPS address your tunnel provider gives you, not your own certificate on a raw IP. Tunnels handle certificates for you.
117
+ - **Worked yesterday, not today:** the address changed (quick tunnels do this), the tunnel stopped, or the computer slept.
118
+
119
+ ## Not independently confirmed
120
+ These came from vendor docs summarized through search, not read page by page. Check the vendor's current docs before relying on them: Windows service setup for `cloudflared`, Tailscale's current Funnel limits, ngrok's free-plan warning page behavior, and Paperclip's exact setting names for public URL and allowed hostnames.
@@ -0,0 +1,64 @@
1
+ # Environment variables
2
+
3
+ Every setting the bridge reads, with its default. Only set what you need. Never put secrets in issues or commits.
4
+
5
+ ## Papercliped status tool
6
+ | Variable | Default | Meaning |
7
+ | --- | --- | --- |
8
+ | `PAPERCLIPED_URL` | `https://papercliped.co` | Where the `papercliped_service_status` tool reads the public status API (https only) |
9
+
10
+ ## Paperclip (all modes)
11
+ | Variable | Default | Meaning |
12
+ | --- | --- | --- |
13
+ | `PAPERCLIP_API_URL` | `http://localhost:3100` | Your Paperclip (local and single modes). `/api` is added if missing. |
14
+ | `PAPERCLIP_API_KEY` | none | Board token for local and single modes (not needed in Paperclip's `local_trusted` mode) |
15
+ | `PAPERCLIP_COMPANY_ID` | none | Default company for tools that take one |
16
+ | `PAPERCLIP_READ_ONLY` | off | `1` blocks every change before it reaches Paperclip |
17
+ | `PAPERCLIP_TIMEOUT_MS` | `30000` | Timeout for each Paperclip call |
18
+ | `PAPERCLIP_PUBLIC_URL` | none | Single mode: the Paperclip address users' browsers open to approve sign-in, if different from the API address |
19
+
20
+ ## HTTP bridge
21
+ | Variable | Default | Meaning |
22
+ | --- | --- | --- |
23
+ | `BRIDGE_HOST` | `127.0.0.1` (`0.0.0.0` when `PORT` is set) | Address to listen on |
24
+ | `BRIDGE_PORT` / `PORT` | `3939` | Port |
25
+ | `BRIDGE_PUBLIC_URL` | none | The public https origin. With OAuth it is the issuer: changing it disconnects every app |
26
+ | `BRIDGE_TOKEN` | none | Static bearer token (single-Paperclip setups, ChatGPT Actions). Not allowed in multi mode |
27
+ | `BRIDGE_PROXY_HOPS` | `0` | Trusted reverse proxies in front (for client addresses in rate limits); `1` on Render, `2` behind Cloudflare proxying too |
28
+ | `BRIDGE_AUDIT_RETENTION_DAYS` | `30` | Audit rows older than this are deleted hourly |
29
+ | `BRIDGE_AUDIT_STDERR` | on | Also print each audit event as a JSON line |
30
+ | `BRIDGE_KEEPALIVE` | on | Free tiers: self-ping and database heartbeat every 10 minutes |
31
+ | `PUBLIC_HOSTS` | none | Host roles for one service on several hosts, e.g. `marketing=papercliped.co,www.papercliped.co;docs=docs.papercliped.co;api=mcp.papercliped.co;forum=forum.papercliped.co` (see `docs/DOMAINS.md`) |
32
+ | `FORUM_URL` | the marketing host's `/community` | Where the forum host redirects |
33
+ | `WEB_DIST` | `web/dist` next to the package | The built website; without it the bridge serves its own Markdown pages |
34
+ | `SITE_CONTACT` | the GitHub issues page | Contact shown on the privacy and terms pages |
35
+ | `SITE_EFFECTIVE_DATE` | `2026-10-06` | Date shown on the privacy and terms pages |
36
+
37
+ ## OAuth (claude.ai, ChatGPT apps)
38
+ | Variable | Default | Meaning |
39
+ | --- | --- | --- |
40
+ | `BRIDGE_OAUTH` | off | `1` turns on the built-in OAuth 2.1 server |
41
+ | `BRIDGE_SECRET` | required | At least 32 characters (`openssl rand -hex 32`); seals stored credentials. Back it up |
42
+ | `BRIDGE_SECRET_PREVIOUS` | none | Comma-separated old secrets, for rotation |
43
+ | `BRIDGE_MODE` | `single` | `single` (one Paperclip) or `multi` (each user connects their own) |
44
+ | `BRIDGE_LOGIN` | `paperclip` | `paperclip` (approve in Paperclip) or `static` (operator password = `BRIDGE_TOKEN`; single mode only) |
45
+ | `BRIDGE_DATA_FILE` | none | JSON file for state (single mode, no database) |
46
+ | `BRIDGE_ACCESS_TTL_SEC` | `3600` | Access token lifetime |
47
+ | `BRIDGE_REFRESH_TTL_SEC` | `2592000` (30 days) | Refresh token lifetime |
48
+ | `BRIDGE_IDLE_REVOKE_DAYS` | `30` | Revoke connections unused this long (`0` = never) |
49
+ | `BRIDGE_CALLS_PER_MINUTE` | `120` | Tool calls per minute per connection |
50
+ | `BRIDGE_OAUTH_REDIRECT_HOSTS` | claude.ai, claude.com, chatgpt.com, chat.openai.com, platform.openai.com | Hosts allowed as OAuth redirect targets |
51
+ | `BRIDGE_INSTANCE_ALLOWLIST` | any | Multi mode: only these Paperclip hosts (`*.example.com` allowed) |
52
+ | `BRIDGE_INSTANCE_DENYLIST` | none | Multi mode: never these hosts |
53
+ | `BRIDGE_ALLOWED_PORTS` | `443` | Multi mode: ports a Paperclip address may use |
54
+ | `BRIDGE_ALLOW_EPHEMERAL` | off | Multi mode without a database, for local experiments only |
55
+
56
+ ## Database (Postgres)
57
+ | Variable | Default | Meaning |
58
+ | --- | --- | --- |
59
+ | `DATABASE_URL` | none | Postgres connection (required in multi mode) |
60
+ | `DATABASE_SSL` | `verify` | `verify`, `require` or `off` |
61
+ | `DATABASE_CA` | none | CA certificate (PEM; `\n` for newlines) for `verify` |
62
+ | `DATABASE_POOL_MAX` | `10` | Connection pool size |
63
+ | `BRIDGE_AUTO_MIGRATE` | off | Apply migrations at start |
64
+ | `DATABASE_MIGRATE_URL` | `DATABASE_URL` | A different (admin) connection for migrations |
@@ -0,0 +1,31 @@
1
+ # FAQ
2
+
3
+ ## Is Papercliped free?
4
+ Yes. It's free and open source under the MIT licence. The hosted bridge at papercliped.co runs the same code you can run yourself.
5
+
6
+ ## Is it made by Paperclip?
7
+ No. Papercliped is an independent community project that uses Paperclip's public API.
8
+
9
+ ## What does the AI see?
10
+ Only what it asks for through the tools, within your level. Issue and comment text written by agents can reach the AI; treat it as data, not instructions.
11
+
12
+ ## What do you store?
13
+ Your username, a hash of your secret key, your Paperclip's address, an encrypted Paperclip key, connection records and a 30-day activity log. Details in [Privacy](/privacy).
14
+
15
+ ## Can I use it without a public Paperclip?
16
+ Yes: run it locally with `npx papercliped@latest`. The hosted bridge needs a public https address; see [Set up your Paperclip](/docs/connect-your-paperclip).
17
+
18
+ ## Why does pausing an agent fail with 403?
19
+ Pause, resume, wake and approve need a Paperclip **board** token, and a connection at **Full control**. Check your level in the [Paperclip plugin](/docs/paperclip-plugin).
20
+
21
+ ## How do I stop it?
22
+ Disconnect the app on the manage page, revoke the key in your Paperclip, or delete your account. It stops at once.
23
+
24
+ ## I lost my secret key.
25
+ Choose **Connect your Paperclip** when signing in and approve in Paperclip: you keep your account and can make a new key.
26
+
27
+ ## Does it work with ChatGPT?
28
+ Yes, through ChatGPT's MCP connectors or GPT Actions. See [ChatGPT setup](/docs/chatgpt).
29
+
30
+ ## Where do I report a bug or suggest something?
31
+ On GitHub: [open an issue](https://github.com/OpenSourcx/papercliped/issues), or use the community page on papercliped.co. Security problems go privately to support@papercliped.co.
@@ -3,12 +3,12 @@
3
3
  Papercliped connects Claude (and ChatGPT) to **your** Paperclip. You keep control: you choose how much the AI may do, and you can cut access at any time.
4
4
 
5
5
  ## What you need
6
- - A Paperclip that is reachable over the internet at an `https://` address (for example `https://workforce.example.com`). Private, local and `http://` addresses are refused on purpose.
6
+ - A Paperclip that is reachable over the internet at an `https://` address (for example `https://workforce.example.com`). Private, local and `http://` addresses are refused on purpose. No public address yet? See [Set up your Paperclip](/docs/connect-your-paperclip).
7
7
  - A Claude account that can add connectors, or Claude Code.
8
8
 
9
9
  ## Connect from Claude
10
10
  1. In Claude open **Settings → Connectors → Add custom connector**.
11
- 2. Use the address `{{URL}}/mcp` and click **Connect**.
11
+ 2. Use the address `{{MCP}}/mcp` and click **Connect**.
12
12
  3. On the Papercliped page choose **Connect your Paperclip**, enter your Paperclip's address, and approve the request in your Paperclip.
13
13
  4. Pick a **username** (see below). Papercliped then shows your **secret key once**. Save it in a password manager.
14
14
  5. Choose how much to allow (see [Permissions](/docs/permissions)) and click **Allow**.
@@ -16,18 +16,24 @@ Papercliped connects Claude (and ChatGPT) to **your** Paperclip. You keep contro
16
16
 
17
17
  ## Claude Code
18
18
  ```
19
- claude mcp add --transport http papercliped {{URL}}/mcp
19
+ claude mcp add --transport http papercliped {{MCP}}/mcp
20
20
  ```
21
21
  Or install the plugin: `/plugin marketplace add OpenSourcx/papercliped`, then `/plugin install papercliped@papercliped`.
22
22
 
23
23
  ## Run it yourself (npm)
24
24
  ```
25
- npx papercliped@beta
25
+ npx papercliped@latest
26
26
  ```
27
27
  This starts a local server for one Paperclip, using `PAPERCLIP_API_URL` and `PAPERCLIP_API_KEY`.
28
28
 
29
+ ## Other AI apps
30
+ Any app that supports MCP can use Papercliped. See [Any AI app (MCP)](/docs/other-ai-apps).
31
+
32
+ ## Inside your Paperclip
33
+ The Papercliped plugin for Paperclip is where you manage everything: connected apps, levels, privacy and your account. See [Paperclip plugin](/docs/paperclip-plugin).
34
+
29
35
  ## ChatGPT
30
- Papercliped publishes an OpenAPI description at `{{URL}}/openapi.json` for GPT Actions. See [ChatGPT setup](/docs/chatgpt).
36
+ Papercliped publishes an OpenAPI description at `{{MCP}}/openapi.json` for GPT Actions. See [ChatGPT setup](/docs/chatgpt).
31
37
 
32
38
  See [Create your account](/docs/signup) for every screen in detail.
33
39