@cerefox/memory 0.11.1 → 1.0.0-beta.1

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 (39) hide show
  1. package/dist/bin/cerefox.js +1077 -834
  2. package/dist/frontend/assets/index-CCkg5PXt.js +125 -0
  3. package/dist/frontend/assets/index-CCkg5PXt.js.map +1 -0
  4. package/dist/frontend/index.html +1 -1
  5. package/dist/server-assets/_shared/ef-auth/index.ts +134 -0
  6. package/dist/server-assets/_shared/ef-meta/index.ts +1 -1
  7. package/dist/server-assets/_shared/embeddings/index.ts +42 -2
  8. package/dist/server-assets/_shared/ingest/chunker.ts +210 -0
  9. package/dist/server-assets/_shared/ingest/index.ts +32 -0
  10. package/dist/server-assets/_shared/ingest/pipeline-helpers.ts +135 -0
  11. package/dist/server-assets/_shared/mcp-auth/index.ts +352 -0
  12. package/dist/server-assets/_shared/mcp-tools/_chunker.ts +16 -170
  13. package/dist/server-assets/_shared/mcp-tools/ingest.ts +13 -4
  14. package/dist/server-assets/db/migrations/0012_content_format.sql +20 -0
  15. package/dist/server-assets/db/rpcs.sql +76 -8
  16. package/dist/server-assets/db/schema.sql +7 -1
  17. package/dist/server-assets/supabase/functions/cerefox-get-audit-log/index.ts +8 -0
  18. package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +8 -0
  19. package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +22 -171
  20. package/dist/server-assets/supabase/functions/cerefox-list-projects/index.ts +8 -0
  21. package/dist/server-assets/supabase/functions/cerefox-list-versions/index.ts +8 -0
  22. package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +54 -0
  23. package/dist/server-assets/supabase/functions/cerefox-mcp/oauth.ts +121 -0
  24. package/dist/server-assets/supabase/functions/cerefox-metadata/index.ts +8 -0
  25. package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +8 -0
  26. package/dist/server-assets/supabase/functions/cerefox-search/index.ts +11 -1
  27. package/docs/guides/access-paths.md +81 -30
  28. package/docs/guides/cli.md +29 -0
  29. package/docs/guides/configuration.md +4 -1
  30. package/docs/guides/connect-agents.md +98 -54
  31. package/docs/guides/content-format.md +55 -0
  32. package/docs/guides/migration-1.0.md +87 -0
  33. package/docs/guides/ops-scripts.md +1 -1
  34. package/docs/guides/quickstart.md +20 -0
  35. package/docs/guides/setup-supabase.md +154 -13
  36. package/docs/guides/upgrading.md +4 -3
  37. package/package.json +1 -1
  38. package/dist/frontend/assets/index-ojNhWSxm.js +0 -125
  39. package/dist/frontend/assets/index-ojNhWSxm.js.map +0 -1
@@ -0,0 +1,121 @@
1
+ /**
2
+ * OAuth 2.1 protected-resource glue for cerefox-mcp (design §3–6).
3
+ *
4
+ * Pure auth logic lives in `_shared/mcp-auth/`; this file is the HTTP/Deno layer:
5
+ * it builds the authenticator from Function secrets, serves the RFC 9728 metadata
6
+ * document, and formats the 401 challenge. It is imported only by the EF.
7
+ */
8
+
9
+ import { CORS_HEADERS } from "./shared.ts";
10
+ import {
11
+ type AuthResult,
12
+ createMcpAuthenticator,
13
+ type McpAuthenticator,
14
+ } from "../../../_shared/mcp-auth/index.ts";
15
+
16
+ /** Stable public path of this function (used to build absolute metadata URLs). */
17
+ export const FUNCTION_PATH = "/functions/v1/cerefox-mcp";
18
+
19
+ /** The protected-resource metadata route, matched as a suffix of the request path. */
20
+ const PRS_SUFFIX = "/.well-known/oauth-protected-resource";
21
+
22
+ /**
23
+ * Origin of the deployed project, e.g. `https://<ref>.supabase.co`, from the
24
+ * platform-injected `SUPABASE_URL` — NOT from request headers.
25
+ *
26
+ * SECURITY (design §6): the token issuer and JWKS URL are derived from this origin.
27
+ * Deriving it from client-influenced headers (`x-forwarded-host`/`-proto`) would let
28
+ * a caller point token validation at an attacker-controlled JWKS and forge a token
29
+ * that passes (JWKS-poisoning auth bypass). `SUPABASE_URL` is set by the platform for
30
+ * every Edge Function and is not client-controllable (every other Cerefox EF already
31
+ * depends on it). It is also the exact public https origin Anthropic requires for the
32
+ * RFC 9728 `resource` identifier — so this both hardens auth and removes the earlier
33
+ * `http://` (internal-proxy) scheme workaround.
34
+ */
35
+ function projectOrigin(): string {
36
+ return (Deno.env.get("SUPABASE_URL") ?? "").replace(/\/+$/, "");
37
+ }
38
+
39
+ /** Absolute URL of this MCP server (the `resource` identifier — must match exactly). */
40
+ export function resourceUrl(): string {
41
+ return `${projectOrigin()}${FUNCTION_PATH}`;
42
+ }
43
+
44
+ /** The Supabase auth server issuer for this project. */
45
+ export function issuerUrl(): string {
46
+ return `${projectOrigin()}/auth/v1`;
47
+ }
48
+
49
+ /** True when the request targets the (public) protected-resource metadata route. */
50
+ export function isProtectedResourceMetadata(req: Request): boolean {
51
+ const path = new URL(req.url).pathname;
52
+ // Accept both the plain suffix and the RFC 9728 path-insertion form
53
+ // (…/oauth-protected-resource/functions/v1/cerefox-mcp).
54
+ return path.includes(PRS_SUFFIX);
55
+ }
56
+
57
+ /** RFC 9728 Protected Resource Metadata document. */
58
+ export function protectedResourceMetadata(): Response {
59
+ const body = {
60
+ resource: resourceUrl(),
61
+ // Anthropic reads authorization_servers[0]; Supabase serves valid AS metadata here.
62
+ authorization_servers: [issuerUrl()],
63
+ bearer_methods_supported: ["header"],
64
+ scopes_supported: ["openid", "email"],
65
+ resource_name: "Cerefox",
66
+ };
67
+ return new Response(JSON.stringify(body), {
68
+ status: 200,
69
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
70
+ });
71
+ }
72
+
73
+ /**
74
+ * 401 with an explicit `resource_metadata` pointer (RFC 9728 §5.1), so clients
75
+ * discover our metadata route rather than probing the domain root.
76
+ */
77
+ export function unauthorizedChallenge(result: AuthResult): Response {
78
+ const metadataUrl = `${resourceUrl()}${PRS_SUFFIX}`;
79
+ const invalid = result.ok === false && result.reason !== "no_token";
80
+ const params = [`resource_metadata="${metadataUrl}"`];
81
+ if (invalid) params.push(`error="invalid_token"`);
82
+ return new Response(JSON.stringify({ error: "unauthorized" }), {
83
+ status: 401,
84
+ headers: {
85
+ ...CORS_HEADERS,
86
+ "Content-Type": "application/json",
87
+ "WWW-Authenticate": `Bearer ${params.join(", ")}`,
88
+ },
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Build the authenticator from Function secrets + the injected SUPABASE_URL.
94
+ *
95
+ * Secrets (set via `supabase secrets set`):
96
+ * CEREFOX_OAUTH_OWNER_ID — pinned owner user id. The OAuth path fails CLOSED
97
+ * when this is unset (design §6 / Finding 3), unless
98
+ * CEREFOX_OAUTH_ALLOW_ANY_USER="true" is set.
99
+ * CEREFOX_OAUTH_ALLOW_ANY_USER — explicit opt-out of the owner pin (multi-user /
100
+ * sign-ups-disabled setups). Default off.
101
+ *
102
+ * The non-OAuth static path is the **Cerefox access token** (`CEREFOX_ACCESS_TOKENS`,
103
+ * iter-28E) — the same credential the primitive EFs accept — checked in the handler
104
+ * via `_shared/ef-auth` (see index.ts), NOT here. The legacy `CEREFOX_MCP_STATIC_BEARER`
105
+ * (anon JWT) is retired: `staticBearer` is left unset so this authenticator handles
106
+ * OAuth only.
107
+ */
108
+ export function buildAuthenticator(): McpAuthenticator {
109
+ const issuer = issuerUrl();
110
+ const ownerUserId = Deno.env.get("CEREFOX_OAUTH_OWNER_ID") ?? null;
111
+ const allowAnyUser = Deno.env.get("CEREFOX_OAUTH_ALLOW_ANY_USER") === "true";
112
+ return createMcpAuthenticator({
113
+ issuer,
114
+ jwksUri: `${issuer}/.well-known/jwks.json`,
115
+ expectedAudience: "authenticated",
116
+ ownerUserId,
117
+ allowAnyUser,
118
+ staticBearer: null,
119
+ allowedAlgs: ["ES256", "RS256"],
120
+ });
121
+ }
@@ -1,6 +1,7 @@
1
1
  import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
2
  import { createClient } from "jsr:@supabase/supabase-js@2";
3
3
  import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
4
+ import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
4
5
 
5
6
  /**
6
7
  * cerefox-metadata — Supabase Edge Function
@@ -29,6 +30,13 @@ Deno.serve(async (req: Request): Promise<Response> => {
29
30
  return new Response(null, { status: 200, headers: CORS_HEADERS });
30
31
  }
31
32
 
33
+ const authFail = efAuthGate(
34
+ req.headers.get("Authorization"),
35
+ Deno.env.get("CEREFOX_ACCESS_TOKENS"),
36
+ { ...CORS_HEADERS, "Content-Type": "application/json" },
37
+ );
38
+ if (authFail) return authFail;
39
+
32
40
  if (isVersionRequest(req)) {
33
41
  return versionResponse("cerefox-metadata", { ...CORS_HEADERS, "Content-Type": "application/json" });
34
42
  }
@@ -1,6 +1,7 @@
1
1
  import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
2
  import { createClient } from "jsr:@supabase/supabase-js@2";
3
3
  import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
4
+ import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
4
5
 
5
6
  /**
6
7
  * cerefox-metadata-search -- Supabase Edge Function
@@ -43,6 +44,13 @@ Deno.serve(async (req: Request): Promise<Response> => {
43
44
  return new Response(null, { status: 200, headers: CORS_HEADERS });
44
45
  }
45
46
 
47
+ const authFail = efAuthGate(
48
+ req.headers.get("Authorization"),
49
+ Deno.env.get("CEREFOX_ACCESS_TOKENS"),
50
+ { ...CORS_HEADERS, "Content-Type": "application/json" },
51
+ );
52
+ if (authFail) return authFail;
53
+
46
54
  if (isVersionRequest(req)) {
47
55
  return versionResponse("cerefox-metadata-search", { ...CORS_HEADERS, "Content-Type": "application/json" });
48
56
  }
@@ -1,6 +1,8 @@
1
1
  import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
2
  import { createClient } from "jsr:@supabase/supabase-js@2";
3
3
  import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
4
+ import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
5
+ import { capEmbeddingInput } from "../../../_shared/embeddings/index.ts";
4
6
 
5
7
  /**
6
8
  * cerefox-search — Supabase Edge Function
@@ -68,6 +70,7 @@ const EMBEDDING_INITIAL_BACKOFF_MS = 500; // 500ms, 1s, 2s exponential backoff
68
70
 
69
71
  async function getEmbedding(text: string, apiKey: string): Promise<number[]> {
70
72
  let lastError: Error | null = null;
73
+ const input = capEmbeddingInput(text); // iter-28D Phase 0: cap (queries are short; safety)
71
74
 
72
75
  for (let attempt = 0; attempt < EMBEDDING_MAX_RETRIES; attempt++) {
73
76
  try {
@@ -79,7 +82,7 @@ async function getEmbedding(text: string, apiKey: string): Promise<number[]> {
79
82
  },
80
83
  body: JSON.stringify({
81
84
  model: OPENAI_MODEL,
82
- input: text,
85
+ input,
83
86
  dimensions: EMBEDDING_DIMENSIONS,
84
87
  }),
85
88
  });
@@ -182,6 +185,13 @@ Deno.serve(async (req: Request) => {
182
185
  });
183
186
  }
184
187
 
188
+ const authFail = efAuthGate(
189
+ req.headers.get("Authorization"),
190
+ Deno.env.get("CEREFOX_ACCESS_TOKENS"),
191
+ headers,
192
+ );
193
+ if (authFail) return authFail;
194
+
185
195
  if (isVersionRequest(req)) {
186
196
  return versionResponse("cerefox-search", headers);
187
197
  }
@@ -3,27 +3,31 @@
3
3
  Cerefox is built in three distinct layers. Understanding them tells you which credentials to
4
4
  configure, what can reach the database, and which path is right for your integration.
5
5
 
6
- > **A note on Supabase keys (2026):** Cerefox needs two API keys for two different transport
7
- > layers. Layer 1 (Edge Functions) uses the **legacy anon JWT** as a Bearer token — the new
8
- > `sb_publishable_…` key is rejected by the Edge Function gateway. Layer 2 (web + CLI REST)
9
- > uses the new **secret key** (`sb_secret_…`) or the legacy `service_role` JWT — either
10
- > works. See [`setup-supabase.md` → Supabase API keys (2026)](setup-supabase.md#supabase-api-keys-2026)
11
- > for the full picture and why this asymmetry exists.
6
+ > **A note on credentials (2026):** Cerefox uses two different credentials for two transport
7
+ > layers. Layer 1 (Edge Functions) uses the **Cerefox access token** (`cfx_pat_…`) as a Bearer
8
+ > token — a random, Cerefox-managed secret validated in-function (the legacy Supabase anon JWT
9
+ > is retired for all Edge Function paths as of iter-28E). Layer 2 (web + CLI REST) uses the new
10
+ > **secret key** (`sb_secret_…`) or the legacy `service_role` JWT — either works. See
11
+ > [`setup-supabase.md` → Supabase API keys (2026)](setup-supabase.md#supabase-api-keys-2026)
12
+ > for the full picture.
12
13
 
13
14
  ---
14
15
 
15
16
  ## Layer 1 — AI Agents via Edge Functions (HTTPS)
16
17
 
17
18
  This is the primary integration layer for AI clients. Nine Supabase Edge Functions are
18
- deployed on the Supabase platform and are reachable over HTTPS with nothing more than the
19
- **legacy anon JWT** (a public-facing JWT, `eyJ…`). The Supabase gateway validates the key
20
- before any request reaches a function; individual functions then use the service-role key
21
- internally to call Postgres RPCs. Your anon key is never elevated to database-level access.
22
-
23
- > ⚠️ Use the **legacy anon JWT** here, not the new `sb_publishable_…` key. The Edge
24
- > Function gateway rejects non-JWT keys with `UNAUTHORIZED_INVALID_JWT_FORMAT`. See
25
- > [`setup-supabase.md` → Supabase API keys (2026)](setup-supabase.md#supabase-api-keys-2026)
26
- > for why.
19
+ deployed on the Supabase platform and are reachable over HTTPS with the **Cerefox access
20
+ token** (`cfx_pat_…`) as a Bearer token. Each function is deployed `--no-verify-jwt` and
21
+ **authenticates the token in-function** (constant-time compare against the accepted set); it
22
+ then uses the service-role key internally to call Postgres RPCs. The token grants Edge
23
+ Function access only — it is never elevated to database-level access, and the service-role key
24
+ never leaves the server.
25
+
26
+ > ⚠️ The credential here is the **Cerefox access token**, not a Supabase key. Generate it with
27
+ > `cerefox token generate` (it sets the `CEREFOX_ACCESS_TOKENS` Function secret and writes
28
+ > `CEREFOX_ACCESS_TOKEN` to your local `.env`). The legacy Supabase anon JWT is retired for
29
+ > Edge Function auth as of iter-28E. See
30
+ > [`setup-supabase.md` → Step 7](setup-supabase.md#step-7--oauth-for-cloud-agents-claudeai--mobile-optional).
27
31
 
28
32
  ### The nine Edge Functions
29
33
 
@@ -37,7 +41,7 @@ internally to call Postgres RPCs. Your anon key is never elevated to database-le
37
41
  | `cerefox-get-audit-log` | Query audit log entries with filters |
38
42
  | `cerefox-metadata-search` | Query documents by metadata key-value criteria without text search |
39
43
  | `cerefox-list-projects` | List all projects with names, IDs, and descriptions |
40
- | `cerefox-mcp` | Streamable HTTP MCP adapter — calls Postgres RPCs directly |
44
+ | `cerefox-mcp` | Streamable HTTP MCP adapter — calls Postgres RPCs directly. **Also** an OAuth 2.1 protected resource for cloud/mobile Claude (optional — see "the OAuth variant" below) |
41
45
 
42
46
  ### How clients connect
43
47
 
@@ -46,10 +50,10 @@ the MCP Streamable HTTP protocol and calls the Postgres RPCs directly (no intern
46
50
  to the primitive Edge Functions). The client only ever talks to one URL.
47
51
 
48
52
  ```
49
- MCP client (anon key)
53
+ MCP client (Cerefox token)
50
54
  │
51
55
  ▼
52
- cerefox-mcp
56
+ cerefox-mcp (in-function token check)
53
57
  │
54
58
  ▼ (service-role key, internal)
55
59
  Postgres RPCs
@@ -60,15 +64,46 @@ using an OpenAPI schema. `cerefox-mcp` is not involved (ChatGPT does not support
60
64
  Streamable HTTP MCP protocol).
61
65
 
62
66
  **curl / scripts / custom HTTP clients** can also call the primitives directly using the
63
- same anon key as a Bearer token.
67
+ same Cerefox token as a Bearer token.
64
68
 
65
69
  ### Credentials needed
66
70
 
67
71
  - `CEREFOX_SUPABASE_URL` — your Supabase project URL
68
- - **Legacy anon JWT** — found in your Supabase dashboard under **Project Settings → API Keys → Legacy → anon**. (Do not use the new `sb_publishable_…` key — gateway constraint.)
72
+ - **Cerefox access token** (`cfx_pat_…`) — generate it with `cerefox token generate`. It is a
73
+ random, Cerefox-managed secret (not a Supabase key), validated in-function on every Edge
74
+ Function call.
69
75
 
70
76
  See `docs/guides/connect-agents.md` for step-by-step setup per client.
71
77
 
78
+ ### The OAuth variant of `cerefox-mcp` — cloud & mobile Claude (optional)
79
+
80
+ claude.ai web and the Claude mobile app **cannot** send a static Bearer token — a custom
81
+ connector there requires **OAuth**. As an optional feature (iter-28A), `cerefox-mcp` is
82
+ therefore *also* an OAuth 2.1 protected resource, so those clients get the same full
83
+ tool surface. Nothing else in Layer 1 changes, and you can ignore this entirely if you
84
+ don't use cloud/mobile Claude.
85
+
86
+ How it differs from the static-token path:
87
+
88
+ - **Two accepted credentials.** `cerefox-mcp` authenticates each request itself
89
+ (`_shared/mcp-auth/`), accepting **either** a valid OAuth 2.1 access token (verified
90
+ against the project **JWKS**, with the token's `sub` pinned to `CEREFOX_OAUTH_OWNER_ID`)
91
+ **or** the static **Cerefox access token** (constant-time compare, the same token the
92
+ static-Bearer clients use). All nine data Edge Functions are deployed `--no-verify-jwt` and
93
+ do their token check in-function; `cerefox-mcp` additionally supports the OAuth arm.
94
+ - **Supabase is the authorization server.** The OAuth flow uses Supabase's native OAuth 2.1
95
+ Server. The one piece that must serve HTML — the **consent page** — is a free **Cloudflare
96
+ Worker** (`cloudflare/cerefox-consent/`), because a Supabase Edge Function can't serve
97
+ `text/html` on the default `*.supabase.co` domain.
98
+ - **The owner pin is the authorization boundary.** `CEREFOX_OAUTH_OWNER_ID` (the owner
99
+ user's UUID) is a server-side value — never entered into any client — and only tokens
100
+ whose `sub` matches it are accepted.
101
+
102
+ Config: `CEREFOX_OAUTH_OWNER_ID` (owner pin), a pre-registered OAuth App using
103
+ **`client_secret_post`**, and the Cloudflare Worker (public project URL + publishable key
104
+ baked in). Setup: [`setup-supabase.md` → Step 7](setup-supabase.md#step-7--oauth-for-cloud-agents-claudeai--mobile-optional).
105
+ Design: [`docs/specs/oauth-mcp-server-design.md`](../specs/oauth-mcp-server-design.md).
106
+
72
107
  ---
73
108
 
74
109
  ## Layer 2 — Web UI and CLI via Supabase REST
@@ -177,10 +212,11 @@ single container with an internally-held token.
177
212
 
178
213
  | Caller | Transport | Auth credential | Typical use |
179
214
  |---|---|---|---|
180
- | Claude Code / Cursor | HTTPS → `cerefox-mcp` | Legacy anon JWT | Daily AI assistant access |
181
- | Claude Desktop | HTTPS → `cerefox-mcp` (via `supergateway`) | Legacy anon JWT | Daily AI assistant access |
182
- | ChatGPT Custom GPT | HTTPS → primitive Edge Functions | Legacy anon JWT | AI assistant via GPT Actions |
183
- | curl / HTTP scripts | HTTPS → primitive Edge Functions | Legacy anon JWT | Ad-hoc queries, automation |
215
+ | Claude Code / Cursor | HTTPS → `cerefox-mcp` | Cerefox access token (`cfx_pat_…`) | Advanced/fallback; prefer local MCP for daily use |
216
+ | Claude Desktop | HTTPS → `cerefox-mcp` (via `supergateway`) | Cerefox access token (`cfx_pat_…`) | Advanced/fallback; prefer local MCP for daily use |
217
+ | **Cloud Claude (claude.ai web + mobile)** | HTTPS → `cerefox-mcp` over **OAuth 2.1** | Owner-pinned OAuth access token (JWKS-verified) | Optional; memory in the browser + on the phone |
218
+ | ChatGPT Custom GPT | HTTPS → primitive Edge Functions | Cerefox access token (`cfx_pat_…`) | AI assistant via GPT Actions |
219
+ | curl / HTTP scripts | HTTPS → primitive Edge Functions | Cerefox access token (`cfx_pat_…`) | Ad-hoc queries, automation |
184
220
  | Web UI (`cerefox web`) | Supabase REST API | Secret key (or legacy service_role) | Web UI backend (TS Hono) |
185
221
  | `cerefox` CLI (human) | Supabase REST API | Secret key (or legacy service_role) | Ingestion, search, reindex, backup |
186
222
  | Local coding agent via `cerefox` CLI | Supabase REST API | Secret key (or legacy service_role) | User-authorised agent (Claude Code, Codex CLI, opencode, OpenClaw, Hermes, …) acting on user's behalf via Bash tool |
@@ -188,11 +224,26 @@ single container with an internally-held token.
188
224
 
189
225
  ### Key security principle
190
226
 
191
- The (legacy) anon JWT is safe to share with AI agents and client applications — it can
192
- only call the operations exposed by the Edge Functions, and the Supabase gateway
193
- rate-limits and validates it. The secret key / `service_role` JWT and the database
194
- password must never be embedded in client-facing configuration or committed to the
195
- repository.
227
+ The **Cerefox access token** (`cfx_pat_…`) is the **Edge Function credential**. It authenticates
228
+ to the Edge Functions (which run business logic with the service-role key internally); each
229
+ function validates it in-function and Supabase rate-limits the request. **It is not RLS-scoped**
230
+ — any holder can call the full EF tool surface (read *and* write). So treat it as a shared
231
+ secret for *trusted* agents/clients — keep it in local configs, but do **not** publish it on a
232
+ public web page. Unlike the retired legacy anon JWT (which was revoke-only on ES256-migrated
233
+ projects), the Cerefox token is **rotatable** with zero downtime via `cerefox token rotate`.
234
+
235
+ > **Schema 0.7.0 hardening:** the `cerefox_*` RPCs are `SECURITY DEFINER` and previously
236
+ > had broader-than-intended `EXECUTE` grants. They now grant `EXECUTE` only to
237
+ > `service_role` (which every legitimate caller uses) — revoked from `anon`/`authenticated`/
238
+ > `PUBLIC`. A security hardening; run `cerefox server deploy` to apply. See
239
+ > [`docs/specs/security-model.md`](../specs/security-model.md).
240
+
241
+ The **publishable** key (`sb_publishable_…`) is genuinely public-safe: the EF gateway
242
+ rejects it and (post-0.7.0) it cannot call the RPCs either, so it grants no KB access — it
243
+ only reaches Supabase Auth. That's why the OAuth consent page embeds it, not the anon JWT.
244
+
245
+ The secret key / `service_role` JWT and the database password must never be embedded in
246
+ client-facing configuration or committed to the repository.
196
247
 
197
248
  ---
198
249
 
@@ -233,7 +284,7 @@ So the access model is:
233
284
  is therefore always possible until the human explicitly chooses purge.
234
285
 
235
286
  A `cerefox purge-doc` CLI command, a `cerefox_purge_document` MCP tool, or a
236
- `/documents/{id}/purge` HTTP endpoint accessible via the anon JWT would each break this
287
+ `/documents/{id}/purge` HTTP endpoint accessible via the Cerefox token would each break this
237
288
  property. **Do not add them without a governance design that replaces the human-in-the-
238
289
  loop step with an equivalent guard** (e.g. a "purge approval queue" the web UI must
239
290
  clear before the operation actually runs).
@@ -539,6 +539,34 @@ Detects fresh vs. existing databases: a fresh DB gets schema + RPCs + migration
539
539
 
540
540
  ---
541
541
 
542
+ ### `cerefox token generate` / `cerefox token rotate` / `cerefox token list`
543
+
544
+ **Purpose**: manage the **Cerefox access token** (`cfx_pat_…`) — the Bearer credential that
545
+ callers present to the Edge Functions (remote MCP, GPT Actions, direct HTTP). It replaces the
546
+ retired legacy anon JWT (iter-28E). The token is validated in-function; the accepted set lives
547
+ in the `CEREFOX_ACCESS_TOKENS` Supabase Function secret, and the value this machine presents is
548
+ `CEREFOX_ACCESS_TOKEN` in local `.env`.
549
+
550
+ **Synopsis**:
551
+ ```
552
+ cerefox token generate # mint a token, set it on Supabase, write it to local .env
553
+ cerefox token rotate # add a new token (accepted set becomes [new, old]) — zero downtime
554
+ cerefox token rotate --finalize # drop the old token once every client is on the new one
555
+ cerefox token list # show masked fingerprints of the accepted set (never the value)
556
+ ```
557
+
558
+ - `generate` prints the token **once** (it's a secret — reprints are impossible; lose it → `rotate`),
559
+ sets the `CEREFOX_ACCESS_TOKENS` Function secret, and upserts `CEREFOX_ACCESS_TOKEN` into your
560
+ local `.env` (backs the file up; warns if `.env` isn't gitignored; `--no-env` skips the write).
561
+ - `rotate` widens the accepted set to `[new, old]` so clients cut over with no downtime;
562
+ `rotate --finalize` removes the old token afterward.
563
+ - Paste the token into each client per [`connect-agents.md`](connect-agents.md): the Custom GPT's
564
+ Actions → Authentication (Bearer), or the remote-MCP client header.
565
+ - Run `token generate` **before** a token-gated `cerefox server deploy` — deploying token-gated
566
+ Edge Functions with no token set locks every caller out.
567
+
568
+ ---
569
+
542
570
  ### `cerefox config list` / `cerefox config get` / `cerefox config set`
543
571
 
544
572
  **Purpose**: read/write runtime config in `cerefox_config` (e.g. `usage_tracking_enabled`, `require_requestor_identity`).
@@ -607,6 +635,7 @@ These flat commands handle install, configuration, and health. Run any with `--h
607
635
  | `cerefox doctor` | Diagnose the install (credentials, DB reachability, schema version). |
608
636
  | `cerefox status` | Show connection + schema status. |
609
637
  | `cerefox configure-agent --tool <client>` | Write MCP client config (`claude-code`, `claude-desktop`, `cursor`, `codex`, `gemini`). |
638
+ | `cerefox token generate` / `rotate` / `list` | Manage the Cerefox access token (`cfx_pat_…`) — the Edge Function Bearer credential (remote MCP, GPT Actions, curl). See the [`cerefox token`](#cerefox-token-generate--cerefox-token-rotate--cerefox-token-list) section above. |
610
639
  | `cerefox self-update` | Update the installed `@cerefox/memory` package. |
611
640
  | `cerefox completion` | Emit a shell completion script. |
612
641
  | `cerefox backup create` / `cerefox backup restore` | File-system backup / restore of the knowledge base (see [`ops-scripts.md`](ops-scripts.md)). |
@@ -39,11 +39,14 @@ Full rule documented in [`docs/specs/polish-and-distribution-design.md` §7](../
39
39
  |----------|---------|----------|-------------|
40
40
  | `CEREFOX_SUPABASE_URL` | `""` | For app | Supabase project URL. Found in: Project Settings → API → Project URL |
41
41
  | `CEREFOX_SUPABASE_KEY` | `""` | For app | New **secret key** (`sb_secret_…`) from Project Settings → API Keys → Secret key. Legacy `service_role` JWT also works. **Keep secret.** See [`setup-supabase.md` → Supabase API keys (2026)](setup-supabase.md#supabase-api-keys-2026). |
42
- | `CEREFOX_SUPABASE_ANON_KEY` | `""` | For Edge Functions / e2e | **Legacy anon JWT** (`eyJ…`), under "Legacy" in Project Settings → API Keys. Used as Bearer token for Edge Function / MCP / GPT Action calls. The new `sb_publishable_…` key fails at the Edge Function gateway and cannot replace this. See [`setup-supabase.md`](setup-supabase.md#supabase-api-keys-2026). |
42
+ | `CEREFOX_ACCESS_TOKEN` | `""` | For Edge Functions / e2e | **Cerefox access token** (`cfx_pat_…`), the single token this machine presents. Written to local `.env` by `cerefox token generate`. Used as the `Authorization: Bearer …` credential for remote MCP / GPT Actions / direct Edge Function calls, and read by `cerefox doctor` + the live e2e tests. Rotatable via `cerefox token rotate`. |
43
+ | `CEREFOX_ACCESS_TOKENS` | `""` | Server-side (Function secret) | The **server-side accepted set** — a comma-separated list of Cerefox tokens, set as a Supabase **Function secret** (not local `.env`). A request is accepted if its Bearer matches any token in the set, enabling zero-downtime rotation. Managed by `cerefox token generate` / `rotate`. |
44
+ | `CEREFOX_SUPABASE_ANON_KEY` | `""` | *(deprecated / unused)* | Formerly the legacy anon JWT Bearer for Edge Function calls. **Retired in iter-28E** — Edge Functions now authenticate the Cerefox access token in-function. Retained only so an old `.env` still parses; no longer read. |
43
45
  | `CEREFOX_DATABASE_URL` | `""` | For scripts | Direct Postgres URL for deployment scripts. **Use the Session Pooler** (port `5432`) — Transaction Pooler (`6543`) does not support DDL. Username must include the project-ref suffix (`postgres.<project-ref>`). Append `?sslmode=require`. See [`setup-supabase.md` → Connection pooling (2026)](setup-supabase.md#connection-pooling-2026). |
44
46
 
45
47
  **When each is needed:**
46
48
  - `CEREFOX_SUPABASE_URL` + `CEREFOX_SUPABASE_KEY` — used by the CLI, web UI, and local MCP server (ingestion, search) via the Supabase Data API
49
+ - `CEREFOX_ACCESS_TOKEN` — used to call the Edge Functions (remote MCP, GPT Actions, direct HTTP) and by `cerefox doctor` / the live e2e tests; `CEREFOX_ACCESS_TOKENS` is its server-side counterpart (the accepted set). Generate both with `cerefox token generate`.
47
50
  - `CEREFOX_DATABASE_URL` — used only for schema deploys (`cerefox server deploy`, or the contributor scripts `bun scripts/db_*.ts`) via a direct Postgres connection
48
51
 
49
52
  ---