@cerefox/memory 0.7.1 → 0.8.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 (48) hide show
  1. package/README.md +62 -25
  2. package/dist/bin/cerefox.js +1163 -344
  3. package/dist/frontend/assets/{index-HNlMcvli.js → index-CAp2_lFX.js} +2 -2
  4. package/dist/frontend/assets/index-CAp2_lFX.js.map +1 -0
  5. package/dist/frontend/index.html +1 -1
  6. package/dist/server-assets/_shared/ef-meta/index.ts +97 -0
  7. package/dist/server-assets/_shared/embeddings/index.ts +175 -0
  8. package/dist/server-assets/_shared/mcp-tools/_chunker.ts +187 -0
  9. package/dist/server-assets/_shared/mcp-tools/_projects.ts +121 -0
  10. package/dist/server-assets/_shared/mcp-tools/_utils.ts +73 -0
  11. package/dist/server-assets/_shared/mcp-tools/audit-log.ts +95 -0
  12. package/dist/server-assets/_shared/mcp-tools/get-document.ts +73 -0
  13. package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +26 -0
  14. package/dist/server-assets/_shared/mcp-tools/get-help.ts +90 -0
  15. package/dist/server-assets/_shared/mcp-tools/index.ts +67 -0
  16. package/dist/server-assets/_shared/mcp-tools/ingest.ts +315 -0
  17. package/dist/server-assets/_shared/mcp-tools/list-metadata-keys.ts +55 -0
  18. package/dist/server-assets/_shared/mcp-tools/list-projects.ts +59 -0
  19. package/dist/server-assets/_shared/mcp-tools/list-versions.ts +72 -0
  20. package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +154 -0
  21. package/dist/server-assets/_shared/mcp-tools/search.ts +193 -0
  22. package/dist/server-assets/_shared/mcp-tools/set-document-projects.ts +163 -0
  23. package/dist/server-assets/_shared/mcp-tools/types.ts +92 -0
  24. package/dist/server-assets/db/migrations/0003_add_document_versions.sql +91 -0
  25. package/dist/server-assets/db/migrations/0004_add_audit_log_review_status_archived.sql +71 -0
  26. package/dist/server-assets/db/migrations/0005_metadata_search.sql +628 -0
  27. package/dist/server-assets/db/migrations/0006_usage_log.sql +255 -0
  28. package/dist/server-assets/db/migrations/0007_usage_log_requestor.sql +178 -0
  29. package/dist/server-assets/db/migrations/0008_soft_delete.sql +130 -0
  30. package/dist/server-assets/db/migrations/0009_audit_log_restore_operation.sql +20 -0
  31. package/dist/server-assets/db/migrations/0010_requestor_enforcement_config.sql +12 -0
  32. package/dist/server-assets/db/migrations/0011_title_boosting.sql +48 -0
  33. package/dist/server-assets/db/rpcs.sql +1723 -0
  34. package/dist/server-assets/db/schema.sql +380 -0
  35. package/dist/server-assets/supabase/functions/cerefox-get-audit-log/index.ts +117 -0
  36. package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +138 -0
  37. package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +819 -0
  38. package/dist/server-assets/supabase/functions/cerefox-list-projects/index.ts +96 -0
  39. package/dist/server-assets/supabase/functions/cerefox-list-versions/index.ts +113 -0
  40. package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +294 -0
  41. package/dist/server-assets/supabase/functions/cerefox-mcp/shared.ts +42 -0
  42. package/dist/server-assets/supabase/functions/cerefox-metadata/index.ts +99 -0
  43. package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +146 -0
  44. package/dist/server-assets/supabase/functions/cerefox-search/index.ts +382 -0
  45. package/docs/guides/connect-agents.md +58 -3
  46. package/docs/guides/migration-v0.5.md +50 -0
  47. package/package.json +3 -2
  48. package/dist/frontend/assets/index-HNlMcvli.js.map +0 -1
@@ -0,0 +1,380 @@
1
+ -- Cerefox Database Schema
2
+ -- Run via: python scripts/db_deploy.py
3
+ -- Or manually in Supabase SQL Editor.
4
+ --
5
+ -- Requires extensions: vector (pgvector), uuid-ossp
6
+ -- These are enabled at the top of db_deploy.py before this file is applied.
7
+ --
8
+ -- @version: 0.3.1
9
+ -- The `@version` marker above is read by the schema-version-mismatch banner
10
+ -- (see /api/v1/schema-version). Bump it whenever schema.sql or rpcs.sql
11
+ -- changes in a way that requires `python scripts/db_deploy.py` to be re-run.
12
+ -- The deployed value is exposed via the `cerefox_schema_version()` RPC at
13
+ -- the bottom of rpcs.sql.
14
+
15
+ -- ── Projects ──────────────────────────────────────────────────────────────────
16
+ -- Lightweight user-defined categories. No predefined taxonomy.
17
+ -- A document can belong to many projects (see cerefox_document_projects).
18
+
19
+ CREATE TABLE IF NOT EXISTS cerefox_projects (
20
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
21
+ name TEXT NOT NULL,
22
+ description TEXT,
23
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
24
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
25
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
26
+
27
+ CONSTRAINT cerefox_projects_name_unique UNIQUE (name)
28
+ );
29
+
30
+ -- ── Documents ─────────────────────────────────────────────────────────────────
31
+ -- One row per ingested document (markdown file, paste, agent write-back, etc.)
32
+ -- Project assignment lives in the cerefox_document_projects junction table.
33
+ -- Full content lives exclusively in cerefox_chunks — there is no content column here.
34
+
35
+ CREATE TABLE IF NOT EXISTS cerefox_documents (
36
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
37
+ title TEXT NOT NULL,
38
+ source TEXT NOT NULL DEFAULT 'manual',
39
+ -- source values: 'file' | 'paste' | 'agent' | 'url' | 'manual'
40
+ source_path TEXT,
41
+ -- SHA-256 of raw markdown content; used for deduplication
42
+ content_hash TEXT NOT NULL,
43
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
44
+ chunk_count INT NOT NULL DEFAULT 0,
45
+ total_chars INT NOT NULL DEFAULT 0,
46
+ -- review_status: human governance flag. 'approved' = validated by human,
47
+ -- 'pending_review' = modified by agent, not yet reviewed.
48
+ -- Content is searchable in both states.
49
+ review_status TEXT NOT NULL DEFAULT 'approved',
50
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
51
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
52
+ -- Soft delete: NULL = active, timestamp = deleted (recoverable).
53
+ -- Search indexes exclude soft-deleted docs. Purge does the real CASCADE DELETE.
54
+ deleted_at TIMESTAMPTZ DEFAULT NULL,
55
+
56
+ CONSTRAINT cerefox_documents_hash_unique UNIQUE (content_hash),
57
+ CONSTRAINT cerefox_documents_review_status_check CHECK (review_status IN ('approved', 'pending_review'))
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_documents_deleted_at
61
+ ON cerefox_documents (deleted_at) WHERE deleted_at IS NOT NULL;
62
+
63
+ -- ── Document versions ──────────────────────────────────────────────────────────
64
+ -- One row per archived version of a document. Created automatically before each
65
+ -- content update (accidental-deletion protection).
66
+ --
67
+ -- Versions do NOT store a content snapshot — full content is reconstructed from
68
+ -- cerefox_chunks WHERE version_id = <this version's id>.
69
+ --
70
+ -- version_number is sequential per document (1, 2, 3 …), unique per document.
71
+ -- Cascade delete: deleting a document removes all its versions, which cascades
72
+ -- further to any chunks archived under those versions.
73
+
74
+ CREATE TABLE IF NOT EXISTS cerefox_document_versions (
75
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
76
+ document_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
77
+ version_number INT NOT NULL,
78
+ source TEXT NOT NULL DEFAULT 'manual',
79
+ -- Stats snapshotted at archive time (chunk_count and total_chars of the archived content)
80
+ chunk_count INT NOT NULL DEFAULT 0,
81
+ total_chars INT NOT NULL DEFAULT 0,
82
+ -- archived: when true, this version is protected from retention cleanup.
83
+ -- Set via the version archival API. Default: false (eligible for cleanup).
84
+ archived BOOLEAN NOT NULL DEFAULT FALSE,
85
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
86
+
87
+ CONSTRAINT cerefox_document_versions_doc_num_unique UNIQUE (document_id, version_number)
88
+ );
89
+
90
+ -- ── Audit log ────────────────────────────────────────────────────────────────
91
+ -- Immutable, append-only record of all write operations against the knowledge base.
92
+ -- No UPDATE or DELETE allowed (enforced by RLS policy with no update/delete grants).
93
+ -- Audit entries persist regardless of version cleanup.
94
+ --
95
+ -- document_id is nullable to support logging operations on deleted documents
96
+ -- (the audit entry survives the document deletion).
97
+ -- version_id links to the version snapshot created by the operation (if any).
98
+
99
+ CREATE TABLE IF NOT EXISTS cerefox_audit_log (
100
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
101
+ document_id UUID REFERENCES cerefox_documents(id) ON DELETE SET NULL,
102
+ version_id UUID REFERENCES cerefox_document_versions(id) ON DELETE SET NULL,
103
+ operation TEXT NOT NULL,
104
+ -- operation values: 'create', 'update-content', 'update-metadata', 'delete',
105
+ -- 'status-change', 'archive', 'unarchive'
106
+ author TEXT NOT NULL DEFAULT 'unknown',
107
+ -- author: human username, agent name/model, or 'system' for automated actions
108
+ author_type TEXT NOT NULL DEFAULT 'user',
109
+ -- author_type: 'user' (human via web UI/CLI) or 'agent' (AI via MCP/Edge Function)
110
+ size_before INT,
111
+ size_after INT,
112
+ description TEXT NOT NULL DEFAULT '',
113
+ -- description: free-text explaining what changed and why.
114
+ -- Auto-generated for system actions (approval, archival, retention cleanup).
115
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
116
+
117
+ CONSTRAINT cerefox_audit_log_operation_check CHECK (
118
+ operation IN ('create', 'update-content', 'update-metadata', 'delete',
119
+ 'status-change', 'archive', 'unarchive', 'restore')
120
+ ),
121
+ CONSTRAINT cerefox_audit_log_author_type_check CHECK (author_type IN ('user', 'agent'))
122
+ );
123
+
124
+ -- ── Document ↔ Project junction ───────────────────────────────────────────────
125
+ -- Many-to-many: one document can belong to zero or more projects.
126
+
127
+ CREATE TABLE IF NOT EXISTS cerefox_document_projects (
128
+ document_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
129
+ project_id UUID NOT NULL REFERENCES cerefox_projects(id) ON DELETE CASCADE,
130
+ PRIMARY KEY (document_id, project_id)
131
+ );
132
+
133
+ -- ── Chunks ────────────────────────────────────────────────────────────────────
134
+ -- One row per chunk of a document. Embeddings and FTS live here.
135
+ --
136
+ -- version_id discriminates between current and archived chunks:
137
+ -- NULL → current version (searchable, indexed by HNSW and GIN)
138
+ -- non-NULL → archived under that version (excluded from search indexes,
139
+ -- lazily deleted with their parent version row)
140
+ --
141
+ -- When a document is updated, all current chunks (version_id IS NULL) are
142
+ -- archived by setting version_id to the new version row's UUID. New chunks are
143
+ -- then inserted with version_id = NULL.
144
+
145
+ CREATE TABLE IF NOT EXISTS cerefox_chunks (
146
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
147
+ document_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
148
+ -- version_id: NULL = current; non-NULL = archived under this version
149
+ version_id UUID REFERENCES cerefox_document_versions(id) ON DELETE CASCADE,
150
+ chunk_index INT NOT NULL,
151
+ -- heading_path: the heading hierarchy at this chunk, e.g. ARRAY['Overview', 'Architecture']
152
+ heading_path TEXT[] NOT NULL DEFAULT '{}',
153
+ heading_level INT,
154
+ -- title: the deepest heading at this chunk (for display and FTS boosting)
155
+ title TEXT,
156
+ content TEXT NOT NULL,
157
+ char_count INT NOT NULL,
158
+
159
+ -- Primary embedding: always computed, cloud API (default: OpenAI text-embedding-3-small)
160
+ embedding_primary VECTOR(768) NOT NULL,
161
+ -- Upgrade embedding: optional, alternative model (Fireworks, Vertex, etc.)
162
+ embedding_upgrade VECTOR(768),
163
+
164
+ -- Track which model produced each embedding
165
+ embedder_primary TEXT NOT NULL DEFAULT 'text-embedding-3-small',
166
+ embedder_upgrade TEXT,
167
+
168
+ -- Full-text search vector: document title (A) + chunk heading (A) + body content (B).
169
+ -- Set explicitly at ingestion time by cerefox_ingest_document RPC (Option B: computed in
170
+ -- the RPC using p_title, which is already a parameter). Not GENERATED because a generated
171
+ -- column cannot reference cerefox_documents.title (cross-table reference forbidden).
172
+ -- Updated by cerefox_update_chunk_fts RPC when document title changes without content change.
173
+ fts TSVECTOR,
174
+
175
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
176
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
177
+ -- NOTE: no UNIQUE (document_id, chunk_index) table constraint here.
178
+ -- Uniqueness of current chunks is enforced by the partial index below.
179
+ );
180
+
181
+ -- ── Migration tracking ────────────────────────────────────────────────────────
182
+ -- Records which migration files have been applied (used by db_migrate.py).
183
+
184
+ CREATE TABLE IF NOT EXISTS cerefox_migrations (
185
+ id SERIAL PRIMARY KEY,
186
+ filename TEXT NOT NULL UNIQUE,
187
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
188
+ );
189
+
190
+ -- ── Indexes ───────────────────────────────────────────────────────────────────
191
+
192
+ -- Full-text search — current chunks only (WHERE version_id IS NULL)
193
+ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_fts
194
+ ON cerefox_chunks USING GIN(fts)
195
+ WHERE version_id IS NULL;
196
+
197
+ -- Vector similarity — primary embedding, current chunks only
198
+ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_emb_primary
199
+ ON cerefox_chunks USING hnsw (embedding_primary vector_cosine_ops)
200
+ WITH (m = 16, ef_construction = 64)
201
+ WHERE version_id IS NULL;
202
+
203
+ -- Vector similarity — upgrade embedding, current chunks only
204
+ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_emb_upgrade
205
+ ON cerefox_chunks USING hnsw (embedding_upgrade vector_cosine_ops)
206
+ WITH (m = 16, ef_construction = 64)
207
+ WHERE version_id IS NULL;
208
+
209
+ -- Partial unique index: enforces (document_id, chunk_index) uniqueness for
210
+ -- current chunks. Archived chunks are excluded and may share chunk_index values
211
+ -- across versions for the same document.
212
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_cerefox_chunks_current_unique
213
+ ON cerefox_chunks(document_id, chunk_index)
214
+ WHERE version_id IS NULL;
215
+
216
+ -- Archived chunk lookup — ordered retrieval of chunks for a specific version
217
+ CREATE INDEX IF NOT EXISTS idx_cerefox_chunks_version
218
+ ON cerefox_chunks(version_id, chunk_index)
219
+ WHERE version_id IS NOT NULL;
220
+
221
+ -- Document metadata (JSONB) — enables fast filtering by tags, source, etc.
222
+ CREATE INDEX IF NOT EXISTS idx_cerefox_docs_metadata
223
+ ON cerefox_documents USING GIN(metadata);
224
+
225
+ -- Documents by content_hash (unique constraint covers equality; this covers range/prefix)
226
+ CREATE INDEX IF NOT EXISTS idx_cerefox_docs_hash
227
+ ON cerefox_documents(content_hash);
228
+
229
+ -- Junction table lookup — find all projects for a document, and vice-versa
230
+ CREATE INDEX IF NOT EXISTS idx_cerefox_document_projects_doc
231
+ ON cerefox_document_projects(document_id);
232
+
233
+ CREATE INDEX IF NOT EXISTS idx_cerefox_document_projects_project
234
+ ON cerefox_document_projects(project_id);
235
+
236
+ -- Document versions lookup -- find all versions for a document, ordered by date
237
+ CREATE INDEX IF NOT EXISTS idx_cerefox_document_versions_doc
238
+ ON cerefox_document_versions(document_id, created_at DESC);
239
+
240
+ -- Audit log indexes -- support temporal, author, and document-scoped queries
241
+ CREATE INDEX IF NOT EXISTS idx_cerefox_audit_log_created
242
+ ON cerefox_audit_log(created_at DESC);
243
+
244
+ CREATE INDEX IF NOT EXISTS idx_cerefox_audit_log_document
245
+ ON cerefox_audit_log(document_id, created_at DESC)
246
+ WHERE document_id IS NOT NULL;
247
+
248
+ CREATE INDEX IF NOT EXISTS idx_cerefox_audit_log_author
249
+ ON cerefox_audit_log(author, created_at DESC);
250
+
251
+ -- Full-text search on audit log descriptions
252
+ CREATE INDEX IF NOT EXISTS idx_cerefox_audit_log_desc_fts
253
+ ON cerefox_audit_log USING GIN(to_tsvector('english', description));
254
+
255
+ -- ── updated_at trigger ────────────────────────────────────────────────────────
256
+
257
+ CREATE OR REPLACE FUNCTION cerefox_set_updated_at()
258
+ RETURNS TRIGGER
259
+ LANGUAGE plpgsql
260
+ SET search_path = public, pg_catalog
261
+ AS $$
262
+ BEGIN
263
+ NEW.updated_at = NOW();
264
+ RETURN NEW;
265
+ END;
266
+ $$;
267
+
268
+ CREATE OR REPLACE TRIGGER trig_cerefox_projects_updated
269
+ BEFORE UPDATE ON cerefox_projects
270
+ FOR EACH ROW EXECUTE FUNCTION cerefox_set_updated_at();
271
+
272
+ CREATE OR REPLACE TRIGGER trig_cerefox_documents_updated
273
+ BEFORE UPDATE ON cerefox_documents
274
+ FOR EACH ROW EXECUTE FUNCTION cerefox_set_updated_at();
275
+
276
+ CREATE OR REPLACE TRIGGER trig_cerefox_chunks_updated
277
+ BEFORE UPDATE ON cerefox_chunks
278
+ FOR EACH ROW EXECUTE FUNCTION cerefox_set_updated_at();
279
+
280
+
281
+ -- ── Live-database migration ───────────────────────────────────────────────────
282
+ -- When applying this schema to an existing database that still has the old
283
+ -- project_id column on cerefox_documents, drop it and migrate existing
284
+ -- document-project associations to the new junction table.
285
+ -- Safe to re-run — wrapped in a DO block that checks column existence.
286
+
287
+ DO $$
288
+ BEGIN
289
+ -- Migrate existing project_id → cerefox_document_projects
290
+ IF EXISTS (
291
+ SELECT 1 FROM information_schema.columns
292
+ WHERE table_name = 'cerefox_documents'
293
+ AND column_name = 'project_id'
294
+ ) THEN
295
+ -- Copy existing single-project associations into the junction table.
296
+ -- Ignore duplicates in case of a partial previous migration.
297
+ INSERT INTO cerefox_document_projects (document_id, project_id)
298
+ SELECT id, project_id
299
+ FROM cerefox_documents
300
+ WHERE project_id IS NOT NULL
301
+ ON CONFLICT DO NOTHING;
302
+
303
+ -- Drop the old FK column and its index.
304
+ ALTER TABLE cerefox_documents DROP COLUMN project_id;
305
+ END IF;
306
+ END;
307
+ $$;
308
+
309
+ -- ── Config table ─────────────────────────────────────────────────────────────
310
+ -- Key-value configuration stored in Postgres. Edge Functions and Python read
311
+ -- this at call time -- no redeploy needed to toggle settings.
312
+ -- Currently used for: usage_tracking_enabled, require_requestor_identity,
313
+ -- requestor_identity_format.
314
+
315
+ CREATE TABLE IF NOT EXISTS cerefox_config (
316
+ key TEXT PRIMARY KEY,
317
+ value TEXT NOT NULL
318
+ );
319
+
320
+ -- Seed default config (idempotent)
321
+ INSERT INTO cerefox_config (key, value)
322
+ VALUES ('usage_tracking_enabled', 'false')
323
+ ON CONFLICT (key) DO NOTHING;
324
+ INSERT INTO cerefox_config (key, value)
325
+ VALUES ('require_requestor_identity', 'false')
326
+ ON CONFLICT (key) DO NOTHING;
327
+ INSERT INTO cerefox_config (key, value)
328
+ VALUES ('requestor_identity_format', '^[a-zA-Z0-9_:.\- ]+$')
329
+ ON CONFLICT (key) DO NOTHING;
330
+
331
+
332
+ -- ── Usage log ────────────────────────────────────────────────────────────────
333
+ -- Tracks read operations across all access paths. Opt-in; controlled by
334
+ -- cerefox_config 'usage_tracking_enabled'. Feeds the analytics page.
335
+
336
+ CREATE TABLE IF NOT EXISTS cerefox_usage_log (
337
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
338
+ logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
339
+ operation TEXT NOT NULL,
340
+ access_path TEXT NOT NULL,
341
+ requestor TEXT,
342
+ document_id UUID REFERENCES cerefox_documents(id) ON DELETE SET NULL,
343
+ project_id UUID REFERENCES cerefox_projects(id) ON DELETE SET NULL,
344
+ query_text TEXT,
345
+ result_count INT,
346
+ extra JSONB DEFAULT '{}'::JSONB
347
+ );
348
+
349
+ CREATE INDEX IF NOT EXISTS idx_usage_log_logged_at
350
+ ON cerefox_usage_log (logged_at DESC);
351
+ CREATE INDEX IF NOT EXISTS idx_usage_log_operation_logged_at
352
+ ON cerefox_usage_log (operation, logged_at DESC);
353
+ CREATE INDEX IF NOT EXISTS idx_usage_log_access_path
354
+ ON cerefox_usage_log (access_path);
355
+ CREATE INDEX IF NOT EXISTS idx_usage_log_requestor
356
+ ON cerefox_usage_log (requestor) WHERE requestor IS NOT NULL;
357
+ CREATE INDEX IF NOT EXISTS idx_usage_log_document_id
358
+ ON cerefox_usage_log (document_id) WHERE document_id IS NOT NULL;
359
+
360
+
361
+ -- ── Row Level Security ────────────────────────────────────────────────────────
362
+ -- Enable RLS on all tables. No permissive policies are added: direct table
363
+ -- access via the anon key (PostgREST) is denied by default.
364
+ --
365
+ -- This does NOT affect the Python application or Edge Functions, which use the
366
+ -- service role key (bypasses RLS unconditionally). All search and write
367
+ -- operations go through SECURITY DEFINER RPCs (rpcs.sql), which run as the
368
+ -- function owner (postgres superuser) and also bypass RLS.
369
+ --
370
+ -- Safe to re-run — ALTER TABLE ... ENABLE ROW LEVEL SECURITY is idempotent.
371
+
372
+ ALTER TABLE cerefox_projects ENABLE ROW LEVEL SECURITY;
373
+ ALTER TABLE cerefox_documents ENABLE ROW LEVEL SECURITY;
374
+ ALTER TABLE cerefox_chunks ENABLE ROW LEVEL SECURITY;
375
+ ALTER TABLE cerefox_document_projects ENABLE ROW LEVEL SECURITY;
376
+ ALTER TABLE cerefox_document_versions ENABLE ROW LEVEL SECURITY;
377
+ ALTER TABLE cerefox_audit_log ENABLE ROW LEVEL SECURITY;
378
+ ALTER TABLE cerefox_migrations ENABLE ROW LEVEL SECURITY;
379
+ ALTER TABLE cerefox_config ENABLE ROW LEVEL SECURITY;
380
+ ALTER TABLE cerefox_usage_log ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,117 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { createClient } from "jsr:@supabase/supabase-js@2";
3
+ import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
4
+
5
+ /**
6
+ * cerefox-get-audit-log -- Supabase Edge Function
7
+ *
8
+ * Returns audit log entries with optional filters. Calls the
9
+ * cerefox_list_audit_entries() RPC which joins cerefox_documents
10
+ * to include doc_title.
11
+ *
12
+ * Called by the cerefox-mcp Edge Function (MCP tool), GPT Actions
13
+ * (direct HTTP POST), or any HTTP client.
14
+ *
15
+ * Request body (JSON):
16
+ * document_id? : string -- filter by document UUID
17
+ * author? : string -- filter by author name
18
+ * operation? : string -- filter by operation type
19
+ * since? : string -- ISO timestamp lower bound
20
+ * until? : string -- ISO timestamp upper bound
21
+ * limit? : number -- max entries (default 50, max 200)
22
+ *
23
+ * Response: Array of audit log entries with doc_title
24
+ */
25
+
26
+ const CORS_HEADERS = {
27
+ "Access-Control-Allow-Origin": "*",
28
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
29
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
30
+ };
31
+
32
+ Deno.serve(async (req: Request): Promise<Response> => {
33
+ // CORS preflight
34
+ if (req.method === "OPTIONS") {
35
+ return new Response(null, { status: 200, headers: CORS_HEADERS });
36
+ }
37
+
38
+ if (isVersionRequest(req)) {
39
+ return versionResponse("cerefox-get-audit-log", { ...CORS_HEADERS, "Content-Type": "application/json" });
40
+ }
41
+
42
+ if (req.method !== "POST") {
43
+ return new Response("Method Not Allowed", {
44
+ status: 405,
45
+ headers: CORS_HEADERS,
46
+ });
47
+ }
48
+
49
+ try {
50
+ const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
51
+ const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
52
+ const supabase = createClient(supabaseUrl, supabaseKey);
53
+
54
+ const body = await req.json().catch(() => ({}));
55
+
56
+ // Configurable requestor enforcement
57
+ const identityField = "requestor";
58
+ const identityValue = body[identityField];
59
+ const { data: reqConfig } = await supabase.rpc("cerefox_get_config", { p_key: "require_requestor_identity" });
60
+ if (reqConfig === "true") {
61
+ if (!identityValue || (typeof identityValue === "string" && identityValue.trim() === "")) {
62
+ return new Response(
63
+ JSON.stringify({ error: `Missing required parameter "${identityField}". Server requires caller identity.` }),
64
+ { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
65
+ );
66
+ }
67
+ const { data: fmtConfig } = await supabase.rpc("cerefox_get_config", { p_key: "requestor_identity_format" });
68
+ if (fmtConfig && typeof fmtConfig === "string" && fmtConfig.trim() !== "") {
69
+ if (!new RegExp(fmtConfig).test(identityValue)) {
70
+ return new Response(
71
+ JSON.stringify({ error: `Invalid "${identityField}" format. Does not match pattern: ${fmtConfig}` }),
72
+ { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
73
+ );
74
+ }
75
+ }
76
+ }
77
+
78
+ const params: Record<string, unknown> = {};
79
+ if (body.document_id) params.p_document_id = body.document_id;
80
+ if (body.author) params.p_author = body.author;
81
+ if (body.operation) params.p_operation = body.operation;
82
+ if (body.since) params.p_since = body.since;
83
+ if (body.until) params.p_until = body.until;
84
+ if (body.limit) params.p_limit = Math.min(Number(body.limit) || 50, 200);
85
+
86
+ const { data, error } = await supabase.rpc(
87
+ "cerefox_list_audit_entries",
88
+ params,
89
+ );
90
+
91
+ if (error) {
92
+ return new Response(JSON.stringify({ error: error.message }), {
93
+ status: 500,
94
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
95
+ });
96
+ }
97
+
98
+ // Fire-and-forget usage logging
99
+ Promise.resolve(supabase.rpc("cerefox_log_usage", {
100
+ p_operation: "get_audit_log",
101
+ p_access_path: "edge-function",
102
+ p_requestor: body.requestor ?? null,
103
+ p_result_count: (data ?? []).length,
104
+ })).catch(() => {});
105
+
106
+ return new Response(JSON.stringify(data ?? []), {
107
+ status: 200,
108
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
109
+ });
110
+ } catch (err) {
111
+ const message = err instanceof Error ? err.message : String(err);
112
+ return new Response(JSON.stringify({ error: message }), {
113
+ status: 500,
114
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
115
+ });
116
+ }
117
+ });
@@ -0,0 +1,138 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { createClient } from "jsr:@supabase/supabase-js@2";
3
+ import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
4
+
5
+ /**
6
+ * cerefox-get-document — Supabase Edge Function
7
+ *
8
+ * Retrieves the full reconstructed content of a document (current or an archived version).
9
+ * Calls the cerefox_get_document RPC via the service-role key (never exposed to callers).
10
+ *
11
+ * Called by:
12
+ * - cerefox-mcp Edge Function (MCP tools/call for cerefox_get_document)
13
+ * - GPT Custom Actions (direct HTTP POST via OpenAPI schema)
14
+ * - Any authenticated HTTP client
15
+ *
16
+ * Request body (JSON):
17
+ * { document_id: string, version_id?: string | null }
18
+ *
19
+ * Response (200):
20
+ * { document_id, doc_title, full_content, chunk_count, total_chars, is_archived, version_id }
21
+ * Response (404):
22
+ * { error: "Document not found" }
23
+ * Response (400):
24
+ * { error: "document_id is required" }
25
+ */
26
+
27
+ const CORS_HEADERS = {
28
+ "Access-Control-Allow-Origin": "*",
29
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
30
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
31
+ };
32
+
33
+ Deno.serve(async (req: Request): Promise<Response> => {
34
+ if (req.method === "OPTIONS") {
35
+ return new Response(null, { status: 200, headers: CORS_HEADERS });
36
+ }
37
+
38
+ if (isVersionRequest(req)) {
39
+ return versionResponse("cerefox-get-document", { ...CORS_HEADERS, "Content-Type": "application/json" });
40
+ }
41
+
42
+ if (req.method !== "POST") {
43
+ return new Response("Method Not Allowed", { status: 405, headers: CORS_HEADERS });
44
+ }
45
+
46
+ try {
47
+ const body = await req.json();
48
+ const document_id = body.document_id as string | undefined;
49
+ const version_id = (body.version_id ?? null) as string | null;
50
+
51
+ if (!document_id) {
52
+ return new Response(JSON.stringify({ error: "document_id is required" }), {
53
+ status: 400,
54
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
55
+ });
56
+ }
57
+
58
+ const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
59
+ const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
60
+ const supabase = createClient(supabaseUrl, supabaseKey);
61
+
62
+ // Configurable requestor enforcement
63
+ const identityField = "requestor";
64
+ const identityValue = body[identityField];
65
+ const { data: reqConfig } = await supabase.rpc("cerefox_get_config", { p_key: "require_requestor_identity" });
66
+ if (reqConfig === "true") {
67
+ if (!identityValue || (typeof identityValue === "string" && identityValue.trim() === "")) {
68
+ return new Response(
69
+ JSON.stringify({ error: `Missing required parameter "${identityField}". Server requires caller identity.` }),
70
+ { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
71
+ );
72
+ }
73
+ const { data: fmtConfig } = await supabase.rpc("cerefox_get_config", { p_key: "requestor_identity_format" });
74
+ if (fmtConfig && typeof fmtConfig === "string" && fmtConfig.trim() !== "") {
75
+ if (!new RegExp(fmtConfig).test(identityValue)) {
76
+ return new Response(
77
+ JSON.stringify({ error: `Invalid "${identityField}" format. Does not match pattern: ${fmtConfig}` }),
78
+ { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
79
+ );
80
+ }
81
+ }
82
+ }
83
+
84
+ const { data, error } = await supabase.rpc("cerefox_get_document", {
85
+ p_document_id: document_id,
86
+ p_version_id: version_id,
87
+ });
88
+
89
+ if (error) {
90
+ return new Response(JSON.stringify({ error: error.message }), {
91
+ status: 500,
92
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
93
+ });
94
+ }
95
+
96
+ const row = data?.[0] as {
97
+ doc_title?: string;
98
+ full_content?: string;
99
+ chunk_count?: number;
100
+ total_chars?: number;
101
+ } | undefined;
102
+
103
+ if (!row) {
104
+ return new Response(JSON.stringify({ error: "Document not found" }), {
105
+ status: 404,
106
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
107
+ });
108
+ }
109
+
110
+ // Fire-and-forget usage logging
111
+ Promise.resolve(supabase.rpc("cerefox_log_usage", {
112
+ p_operation: "get_document",
113
+ p_access_path: "edge-function",
114
+ p_requestor: body.requestor ?? null,
115
+ p_document_id: document_id,
116
+ p_result_count: 1,
117
+ })).catch(() => {});
118
+
119
+ return new Response(
120
+ JSON.stringify({
121
+ document_id,
122
+ doc_title: row.doc_title ?? "Untitled",
123
+ full_content: row.full_content ?? "",
124
+ chunk_count: row.chunk_count ?? 0,
125
+ total_chars: row.total_chars ?? 0,
126
+ is_archived: version_id !== null,
127
+ version_id,
128
+ }),
129
+ { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
130
+ );
131
+ } catch (err) {
132
+ const message = err instanceof Error ? err.message : String(err);
133
+ return new Response(JSON.stringify({ error: message }), {
134
+ status: 500,
135
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
136
+ });
137
+ }
138
+ });