From e4b1764fb13c4b0037d26462f073836f81c7b8a4 Mon Sep 17 00:00:00 2001 From: Crouton Alpha audit Date: Thu, 5 Nov 2026 07:54:07 +0200 Subject: [PATCH 1/4] docs: add MIT licence --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f39e076 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Buttercode Systems + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 2540f95a03e0c191b4679f1c93be8c3e1b7304cf Mon Sep 17 00:00:00 2001 From: Crouton Alpha audit Date: Thu, 5 Nov 2026 12:03:27 +0200 Subject: [PATCH 2/4] feat: Supabase Postgres storage backend (free-tier durable replacement for suspended Vercel Blob) --- server/store.js | 104 ++++++++++++++++++++++++++++++++--- server/supabaseStore.test.js | 68 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 server/supabaseStore.test.js diff --git a/server/store.js b/server/store.js index 24404ca..5ad0df6 100644 --- a/server/store.js +++ b/server/store.js @@ -5,7 +5,90 @@ const path = require('path'); const { put, get, del, list } = require('@vercel/blob'); const ROOT = process.env.DATA_DIR || path.join(__dirname, '..', 'data'); -const durable = () => Boolean(process.env.BLOB_STORE_ID || process.env.BLOB_READ_WRITE_TOKEN); +const blobConfigured = () => Boolean(process.env.BLOB_STORE_ID || process.env.BLOB_READ_WRITE_TOKEN); +// Supabase (Postgres) is the preferred durable backend when configured: it is +// free-tier, private by default, and the revision UUID gives us the same +// optimistic-concurrency etag semantics Blob conditional writes provided. +const SUPABASE_TABLE = () => process.env.SUPABASE_TABLE || 'contractcheck_records'; +const supabaseConfigured = () => Boolean(process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY); +const durable = () => supabaseConfigured() || blobConfigured(); + +async function sbRequest(suffix, options = {}) { + const res = await fetch(`${process.env.SUPABASE_URL.replace(/\/$/, '')}/rest/v1/${SUPABASE_TABLE()}${suffix}`, { + ...options, + headers: { + apikey: process.env.SUPABASE_SERVICE_ROLE_KEY, + Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`, + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + }); + const text = await res.text(); + let body = null; + try { body = text ? JSON.parse(text) : null; } catch { body = text; } + return { status: res.status, body }; +} + +async function sbRead(name) { + const { status, body } = await sbRequest(`?select=value,revision&name=eq.${encodeURIComponent(name)}`); + if (status === 404) return null; + if (status !== 200) throw new Error(`Supabase read failed (${status}): ${JSON.stringify(body).slice(0, 200)}`); + const row = Array.isArray(body) ? body[0] : null; + return row ? { value: row.value, etag: row.revision } : null; +} + +async function sbWrite(name, value, etag, create) { + if (create) { + const { status, body } = await sbRequest('', { + method: 'POST', + headers: { Prefer: 'return=representation' }, + body: JSON.stringify({ name, value }), + }); + if (status === 409) throw new Error('already exists'); + if (status !== 201 && status !== 200) throw new Error(`Supabase insert failed (${status}): ${JSON.stringify(body).slice(0, 200)}`); + return body; + } + if (etag) { + const { status, body } = await sbRequest(`?name=eq.${encodeURIComponent(name)}&revision=eq.${encodeURIComponent(etag)}`, { + method: 'PATCH', + headers: { Prefer: 'return=representation' }, + body: JSON.stringify({ value }), + }); + if (status !== 200) throw new Error(`Supabase update failed (${status}): ${JSON.stringify(body).slice(0, 200)}`); + if (!Array.isArray(body) || body.length === 0) throw Object.assign(new Error('ETag mismatch'), { code: 'PRECONDITION_FAILED' }); + return body; + } + const { status, body } = await sbRequest('', { + method: 'POST', + headers: { Prefer: 'resolution=merge-duplicates,return=representation' }, + body: JSON.stringify({ name, value }), + }); + if (status !== 201 && status !== 200) throw new Error(`Supabase upsert failed (${status}): ${JSON.stringify(body).slice(0, 200)}`); + return body; +} + +async function sbDel(name, etag) { + let suffix = `?name=eq.${encodeURIComponent(name)}`; + if (etag) suffix += `&revision=eq.${encodeURIComponent(etag)}`; + const { status } = await sbRequest(suffix, { method: 'DELETE' }); + if (status !== 200 && status !== 204) throw new Error(`Supabase delete failed (${status})`); +} + +async function sbList({ prefix = '', cursor, limit = 100 } = {}) { + const offset = cursor ? Number(cursor) : 0; + const { status, body } = await sbRequest(`?select=name,value&name=like.${encodeURIComponent(prefix + '*')}&order=name&offset=${offset}&limit=${limit}`); + if (status !== 200) throw new Error(`Supabase list failed (${status})`); + const rows = Array.isArray(body) ? body : []; + return { + blobs: rows.map(r => ({ + pathname: r.name, + url: r.name, + uploadedAt: Number.isFinite(r.value?.at) ? new Date(r.value.at).toISOString() : new Date(0).toISOString(), + })), + hasMore: rows.length === limit, + cursor: rows.length === limit ? String(offset + limit) : undefined, + }; +} const digest = value => crypto.createHash('sha256').update(String(value)).digest('hex'); const key = (kind, id) => `contractcheck/${kind}/${digest(id)}.json`; @@ -14,6 +97,7 @@ function ensure() { } async function readObject(name) { + if (supabaseConfigured()) return sbRead(name); if (durable()) { const r = await get(name, { access: 'private', useCache: false }); if (!r) return null; @@ -32,6 +116,7 @@ async function readObject(name) { } async function writeObject(name, value, etag, create = false) { + if (supabaseConfigured()) return sbWrite(name, value, etag, create); if (durable()) { return put(name, JSON.stringify(value), { access: 'private', @@ -63,7 +148,8 @@ async function getAnalysis(id) { const r = await readObject(name); if (!r) return null; if (r.value.at < Date.now() - 48 * 3600 * 1000) { - if (durable()) await del(name, { ifMatch: r.etag }); + if (supabaseConfigured()) await sbDel(name, r.etag); + else if (durable()) await del(name, { ifMatch: r.etag }); else fs.rmSync(path.join(ROOT, digest(name) + '.json'), { force: true }); return null; } @@ -171,8 +257,11 @@ async function cleanupExpired() { } let cursor; do { - const batch = await list({ prefix: 'contractcheck/analyses/', cursor, limit: 100 }); - for (const b of batch.blobs) if (new Date(b.uploadedAt).getTime() < cut) await del(b.url); + const batch = supabaseConfigured() ? await sbList({ prefix: 'contractcheck/analyses/', cursor, limit: 100 }) : await list({ prefix: 'contractcheck/analyses/', cursor, limit: 100 }); + for (const b of batch.blobs) if (new Date(b.uploadedAt).getTime() < cut) { + if (supabaseConfigured()) await sbDel(b.pathname); + else await del(b.url); + } cursor = batch.hasMore ? batch.cursor : undefined; } while (cursor); } @@ -193,7 +282,8 @@ async function probeStorage() { if (!written || written.value?.at !== payload.at) { return { durable: true, writable: false, error: 'A storage write completed but the record could not be read back.' }; } - await del(name, { ifMatch: written.etag }); + if (supabaseConfigured()) await sbDel(name, written.etag); + else await del(name, { ifMatch: written.etag }); return { durable: true, writable: true, error: null }; } catch (error) { return { durable: true, writable: false, error: String((error && error.message) || error) }; @@ -204,7 +294,7 @@ async function legacyInventory() { const counts = { grants: 0, activeAnalyses: 0, expiredAnalyses: 0, other: 0, unreadable: 0, legacyGrantPaths: [] }; let cursor; do { - const batch = await list({ prefix: 'contractcheck/', cursor, limit: 1000 }); + const batch = supabaseConfigured() ? await sbList({ prefix: 'contractcheck/', cursor, limit: 1000 }) : await list({ prefix: 'contractcheck/', cursor, limit: 1000 }); for (const b of batch.blobs) { const grant = /^contractcheck\/grants\/[a-f0-9]{64}\.json$/.test(b.pathname); const analysis = /^contractcheck\/analyses\/[a-f0-9]{64}\.json$/.test(b.pathname); @@ -213,7 +303,7 @@ async function legacyInventory() { else if (analysis) counts.expiredAnalyses++; else counts.other++; if (grant || (analysis && new Date(b.uploadedAt).getTime() >= Date.now() - 48 * 3600000)) { - try { const r = await get(b.pathname, { access: 'private', useCache: false }); if (!r) counts.unreadable++; } + try { const r = await readObject(b.pathname); if (!r) counts.unreadable++; } catch { counts.unreadable++; } } } diff --git a/server/supabaseStore.test.js b/server/supabaseStore.test.js new file mode 100644 index 0000000..8a961bc --- /dev/null +++ b/server/supabaseStore.test.js @@ -0,0 +1,68 @@ +const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'); + +// Load the real adapter with @vercel/blob stubbed (unused in Supabase mode) +// and a fake PostgREST fetch. Env is per-instance so suites stay isolated. +function loadStore(){ + const rows=new Map();let seq=0; + const fetch=async(url,options={})=>{ + const u=new URL(url),method=options.method||'GET',params=u.searchParams; + const json=(status,body)=>({status,text:async()=>JSON.stringify(body)}); + const eq=v=>v&&v.startsWith('eq.')?decodeURIComponent(v.slice(3)):v; + const name=eq(params.get('name')); + if(method==='GET'){ + let out=[...rows.values()]; + if(params.get('name')?.startsWith('like.')){const prefix=decodeURIComponent(params.get('name').slice(5)).replace(/\*$/,'');out=out.filter(r=>r.name.startsWith(prefix));} + else if(name) out=out.filter(r=>r.name===name); + const offset=Number(params.get('offset')||0),limit=Number(params.get('limit')||100); + return json(200,out.sort((a,b)=>a.name.localeCompare(b.name)).slice(offset,offset+limit)); + } + if(method==='POST'){ + const body=JSON.parse(options.body),prefer=options.headers?.Prefer||''; + if(rows.has(body.name)&&!prefer.includes('merge-duplicates')) return json(409,{message:'duplicate key value violates unique constraint'}); + const row={name:body.name,value:body.value,revision:'rev-'+(++seq)};rows.set(body.name,row); + return json(201,[row]); + } + if(method==='PATCH'){ + const row=rows.get(name),rev=eq(params.get('revision')); + if(!row||(rev&&row.revision!==rev)) return json(200,[]); + row.value=JSON.parse(options.body).value;row.revision='rev-'+(++seq); + return json(200,[row]); + } + if(method==='DELETE'){ + const row=rows.get(name),rev=eq(params.get('revision')); + if(row&&(!rev||row.revision===rev)) rows.delete(name); + return json(204,null); + } + return json(405,{}); + }; + const module={exports:{}}; + const env={SUPABASE_URL:'https://example.supabase.co',SUPABASE_SERVICE_ROLE_KEY:'test-key'}; + vm.runInNewContext(fs.readFileSync(path.join(__dirname,'store.js'),'utf8'),{ + module,__dirname,process:{env},console,fetch,URL,Response,TextEncoder, + require(name){return name==='@vercel/blob'?{}:require(name);}, + }); + return {store:module.exports,rows}; +} + +test('supabase backend: durable and probe round-trip cleanly',async()=>{ + const {store,rows}=loadStore(); + assert.equal(store.durable(),true); + const probe=await store.probeStorage(); + assert.equal(probe.writable,true,probe.error||''); + assert.equal(rows.size,0,'probe row must be deleted'); +}); + +test('supabase backend: analysis save/read and create-only conflict',async()=>{ + const {store}=loadStore(); + await store.saveAnalysis('a1',{_id:'a1',title:'Lease'}); + assert.equal((await store.getAnalysis('a1')).full.title,'Lease'); + await assert.rejects(()=>store.saveAnalysis('a1',{_id:'a1'}),/already exists/); +}); + +test('supabase backend: grant credit spends exactly once',async()=>{ + const {store}=loadStore(); + await store.saveGrant('CC-TEST-1234567890',{email:'a@example.com',product:'single',credits:1}); + assert.equal((await store.useCredit('CC-TEST-1234567890','a1')).remaining,0); + assert.equal(await store.useCredit('CC-TEST-1234567890','a2'),null,'overspend must be refused'); + assert.equal((await store.useCredit('CC-TEST-1234567890','a1')).remaining,0,'replay of unlocked analysis stays free'); +}); From 4adfbfa8da52e283acd2ae47291af8439afda6a7 Mon Sep 17 00:00:00 2001 From: Crouton Alpha audit Date: Thu, 5 Nov 2026 12:23:55 +0200 Subject: [PATCH 3/4] test: scrub SUPABASE_* env in suites (Vercel build injects production env; durable() flipped true and broke assertions) --- server/deepAudit.test.js | 2 +- server/deployment.test.js | 2 +- server/extraction.test.js | 2 +- server/grantReplay.test.js | 2 +- server/inputSafety.test.js | 2 +- server/localRetention.test.js | 2 +- server/optionalAi.test.js | 2 +- server/storageOutage.test.js | 2 +- server/storageProbe.test.js | 2 +- server/uiJourney.test.js | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/deepAudit.test.js b/server/deepAudit.test.js index 809b8c4..9456f67 100644 --- a/server/deepAudit.test.js +++ b/server/deepAudit.test.js @@ -1,5 +1,5 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'); -for(const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN'])delete process.env[key]; +for(const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN','SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE'])delete process.env[key]; process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-deep-')); process.env.GROQ_API_KEY='test-only';process.env.PAYSTACK_SECRET_KEY='test-only'; const app=require('./index'),store=require('./store'); diff --git a/server/deployment.test.js b/server/deployment.test.js index abd0f87..6506122 100644 --- a/server/deployment.test.js +++ b/server/deployment.test.js @@ -5,7 +5,7 @@ const os=require('node:os'); const path=require('node:path'); // Build environments may expose production storage. Tests must use only their // temporary local ledger, even when invoked by the Vercel build gate. -for (const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN']) delete process.env[key]; +for (const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN','SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE']) delete process.env[key]; process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'contractcheck-tests-')); process.env.PAYSTACK_SECRET_KEY='test-only'; const store=require('./store'); diff --git a/server/extraction.test.js b/server/extraction.test.js index 79109ca..d4fcc21 100644 --- a/server/extraction.test.js +++ b/server/extraction.test.js @@ -1,5 +1,5 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'); -for (const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN']) delete process.env[key]; +for (const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN','SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE']) delete process.env[key]; process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-extraction-')); const app=require('./index'); test('vector graphics and scanned text share the PDF renderer canvas constructors',async()=>{ diff --git a/server/grantReplay.test.js b/server/grantReplay.test.js index 0e6ab76..ad2cd3c 100644 --- a/server/grantReplay.test.js +++ b/server/grantReplay.test.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -for (const key of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN']) delete process.env[key]; +for (const key of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_TABLE']) delete process.env[key]; process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-replay-')); const store = require('./store'); test('receipt replays and concurrent recovery cannot refill spent report credits', async () => { diff --git a/server/inputSafety.test.js b/server/inputSafety.test.js index 3384679..e4e74fe 100644 --- a/server/inputSafety.test.js +++ b/server/inputSafety.test.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -for (const name of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN']) delete process.env[name]; +for (const name of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_TABLE']) delete process.env[name]; process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-input-safety-')); const app = require('./index'); diff --git a/server/localRetention.test.js b/server/localRetention.test.js index 6888bf9..84f5896 100644 --- a/server/localRetention.test.js +++ b/server/localRetention.test.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -for (const key of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN']) delete process.env[key]; +for (const key of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_TABLE']) delete process.env[key]; process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-retention-')); const store = require('./store'); diff --git a/server/optionalAi.test.js b/server/optionalAi.test.js index 3ec625b..5e49f43 100644 --- a/server/optionalAi.test.js +++ b/server/optionalAi.test.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -for (const key of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN']) delete process.env[key]; +for (const key of ['VERCEL', 'BLOB_STORE_ID', 'BLOB_READ_WRITE_TOKEN', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_TABLE']) delete process.env[key]; process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-ai-')); process.env.AI_ENRICHMENT_ENABLED = 'true'; process.env.OPENROUTER_API_KEY = 'test-only'; diff --git a/server/storageOutage.test.js b/server/storageOutage.test.js index 1c604d2..b3fcbf9 100644 --- a/server/storageOutage.test.js +++ b/server/storageOutage.test.js @@ -1,5 +1,5 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'); -for(const k of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN']) delete process.env[k]; +for(const k of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN','SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE']) delete process.env[k]; process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-outage-')); const app=require('./index'),store=require('./store'); test('storage suspension returns retryable 503 for uploaded and pasted contracts without leaking provider details',async()=>{ diff --git a/server/storageProbe.test.js b/server/storageProbe.test.js index 71c9b6a..7566bd8 100644 --- a/server/storageProbe.test.js +++ b/server/storageProbe.test.js @@ -1,5 +1,5 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'); -for (const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN']) delete process.env[key]; +for (const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN','SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE']) delete process.env[key]; process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-storage-probe-')); const app=require('./index'); const store=require('./store'); diff --git a/server/uiJourney.test.js b/server/uiJourney.test.js index 86590aa..c33770a 100644 --- a/server/uiJourney.test.js +++ b/server/uiJourney.test.js @@ -51,7 +51,7 @@ test('an abandoned upload failure cannot appear on another page',async()=>{ }); test('payment journey requires server approval and recovers a lost response without another credit',async()=>{ - const os=require('node:os');for(const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN'])delete process.env[key]; + const os=require('node:os');for(const key of ['VERCEL','BLOB_STORE_ID','BLOB_READ_WRITE_TOKEN','SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE'])delete process.env[key]; process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-ui-payment-'));process.env.PAYSTACK_SECRET_KEY='test-only'; const store=require('./store'),app=require('./index'),realFetch=global.fetch,wasDurable=store.durable; const server=app.listen(0);await new Promise(resolve=>server.once('listening',resolve));const base='http://127.0.0.1:'+server.address().port; From 2ad6b07f6f6fa9fcf209baef6c0b6674d4b28c7a Mon Sep 17 00:00:00 2001 From: Crouton Alpha audit Date: Thu, 5 Nov 2026 12:26:39 +0200 Subject: [PATCH 4/4] test: scrub SUPABASE env in aiReadiness/optionalAiFailures (fetch-mocking suites; Supabase branch uses fetch) --- server/aiReadiness.test.js | 1 + server/optionalAiFailures.test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/server/aiReadiness.test.js b/server/aiReadiness.test.js index ba15e45..367ad9f 100644 --- a/server/aiReadiness.test.js +++ b/server/aiReadiness.test.js @@ -1,5 +1,6 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'); process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-ai-readiness-')); +for(const key of ['SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE']) delete process.env[key]; process.env.AI_ENRICHMENT_ENABLED='true';process.env.GROQ_API_KEY='test-only';process.env.TINYFISH_API_KEY='test-only'; const app=require('./index'); test('an enabled flag without the requested provider keeps UI disabled and forced consent requests local',async()=>{ diff --git a/server/optionalAiFailures.test.js b/server/optionalAiFailures.test.js index e488588..8740128 100644 --- a/server/optionalAiFailures.test.js +++ b/server/optionalAiFailures.test.js @@ -1,5 +1,6 @@ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'); process.env.DATA_DIR=fs.mkdtempSync(path.join(os.tmpdir(),'cc-ai-failure-'));process.env.AI_ENRICHMENT_ENABLED='true';process.env.OPENROUTER_API_KEY='test-only';process.env.GROQ_API_KEY='test-only';process.env.TINYFISH_API_KEY='test-only'; +for(const key of ['SUPABASE_URL','SUPABASE_SERVICE_ROLE_KEY','SUPABASE_TABLE']) delete process.env[key]; const app=require('./index'); test('provider rejection, malformed and truncated responses keep the rules report without extra providers or content logs',async()=>{ const realFetch=global.fetch,realWarn=console.warn,calls=[],warnings=[];let mode='http';