import test from 'node:test'; import assert from 'node:assert/strict'; import {base32,totp,matchingStep,newAuthConfig,signSession,validSession,sessionCookie,safeReturn,authConfig,isLoginRedirect} from '../src/lib/auth'; import {proxy} from '../src/proxy'; import {NextRequest} from 'next/server'; import {mkdtempSync,writeFileSync,rmSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; test('TOTP matches RFC 6238 SHA-1 test vectors',()=>{ const secret=base32(Buffer.from('12345678901234567890')); for(const [time,expected] of [[59,'94287082'],[1111111109,'07081804'],[1111111111,'14050471'],[1234567890,'89005924'],[2000000000,'69279037'],[20000000000,'65353130']] as const) assert.equal(totp(secret,Math.floor(time/30),8),expected); assert.equal(matchingStep(secret,totp(secret,100),3000000),100); assert.equal(matchingStep(secret,totp(secret,99),3000000),99); assert.equal(matchingStep(secret,totp(secret,98),3000000),null); assert.equal(matchingStep(secret,'123',3000000),null); }); test('JWT rejects tampering, expiry, wrong keys and malformed values',()=>{ const config=newAuthConfig(),token=signSession(config,1000); assert.equal(validSession(token,config,1000),true); assert.equal(validSession(token,config,1000+604800),false); assert.equal(validSession(token,config,900),false); assert.equal(validSession(token,newAuthConfig(),1000),false); assert.equal(validSession(token+'x',config,1000),false); assert.equal(validSession('eyJhbGciOiJub25lIn0.e30.',config,1000),false); assert.equal(validSession(undefined,config),false); }); test('cookies are HTTP-only and secure by default; redirects stay local',()=>{ assert.match(sessionCookie('test'),/HttpOnly; SameSite=Lax; Max-Age=604800; Secure$/); assert.match(sessionCookie('',true),/Max-Age=0/); for(const path of ['https://evil.test','//evil.test','/\\evil.test','/login?next=/','/\nLocation: bad']) assert.equal(safeReturn(path),'/'); assert.equal(safeReturn('/explore/123?q=test'),'/explore/123?q=test'); }); test('proxy exposes creative entry pages while protecting APIs, data and Studio documents; fails closed',()=>{ const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE; try{ const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config)); for(const path of ['/library/import','/skills/example.png','/_next/data/build/library/import.json','/studio/layouts/ref-01']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path); for(const path of ['/api/upload','/studio/api/export','/studio/api/layouts','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path); for(const path of ['/','/_next/data/build/index.json','/llms.txt','/index.md','/agent','/agent/index.md','/api/agent-index','/api/guides','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path); const creativeEntries=['/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api','/studio/_next/static/chunk.js']; for(const path of creativeEntries){ for(const method of ['GET','HEAD'])assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method})).status,200,`${method} ${path}`); assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method:'POST'})).status,401,`POST ${path} must not bypass authentication`); } const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'}; assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://wiki.test'}})).status,200); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{cookie:'wiki_account='+ 'a'.repeat(64)}})).status,401); assert.equal(proxy(new NextRequest('http://wiki.test/guide/welcome',{method:'POST'})).status,401); for(const path of ['/api/guides','/api/agent-index']) assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method:'POST'})).status,401,path); writeFileSync(process.env.AUTH_CONFIG_FILE,'{}'); assert.throws(()=>authConfig()); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503); delete process.env.AUTH_CONFIG_FILE; assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503,'Missing admin configuration fails closed'); }finally{if(old===undefined)delete process.env.AUTH_CONFIG_FILE;else process.env.AUTH_CONFIG_FILE=old;rmSync(temp,{recursive:true,force:true});} }); test('login redirects recognize browser pages and Next.js data destinations',()=>{ const base='http://wiki.test'; for(const target of ['/login?next=%2F','http://wiki.test/login','/_next/data/build-123/login.json?next=%2F']) assert.equal(isLoginRedirect(target,base),true,target); for(const target of [null,'','https://evil.test/login','//evil.test/login','/api/login.json','/_next/data/build-123/index.json']) assert.equal(isLoginRedirect(target,base),false,String(target)); });