Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "qryptchat-web",
"private": true,
"version": "0.5.2",
"version": "0.5.3",
"type": "module",
"bin": {
"qc": "./bin/qc.js",
Expand Down
11 changes: 7 additions & 4 deletions src/app/api/chat/conversations/[id]/participants/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { supabaseAuthCookieName } from '@/lib/supabase/auth-cookie.js';
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
import { bearerToken } from '@/lib/auth/bearer.js';

// Lazy service role client creation
let supabaseServiceRole = null;
Expand All @@ -30,8 +31,10 @@ async function resolveRouteParams(params) {
async function authenticateUser(request) {
try {
// Get access token from cookies
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
const cookieHeader = request.headers.get('cookie') || '';
if (!bearer && !cookieHeader) {
return { error: 'No cookies found' };
}

Expand All @@ -46,12 +49,12 @@ async function authenticateUser(request) {
);

// Try different cookie sources for the access token
let accessToken = null;
let accessToken = bearer;

console.log('🔐 Available cookies:', Object.keys(cookies));

// Try Supabase-specific cookies first
if (cookies[supabaseAuthCookieName()]) {
if (!accessToken && cookies[supabaseAuthCookieName()]) {
console.log('🔐 Found Supabase auth token cookie');
try {
let tokenData = cookies[supabaseAuthCookieName()];
Expand Down
19 changes: 19 additions & 0 deletions src/app/api/chat/conversations/[id]/participants/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ describe('GET /api/chat/conversations/[id]/participants', () => {
expect(mocks.participantEq).toHaveBeenCalledWith('conversation_id', 'conversation-1');
});

it('accepts a Bearer token with no cookie at all (qc, MCP, passkey sessions)', async () => {
mocks.serviceFrom.mockImplementation((table) => {
if (table === 'users') return createUsersQuery();
if (table === 'conversation_participants') return createParticipantCheckQuery();
throw new Error(`Unexpected table: ${table}`);
});

const { GET } = await import('./route.js');
const response = await GET(
new Request('https://qrypt.chat/api/chat/conversations/conversation-1/participants', {
headers: { authorization: 'Bearer qc-token' }
}),
{ params: Promise.resolve({ id: 'conversation-1' }) }
);

expect(response.status).toBe(200);
expect(mocks.authGetUser).toHaveBeenCalledWith('qc-token');
});

it('preserves equals padding in base64 auth cookies', async () => {
mocks.serviceFrom.mockImplementation((table) => {
if (table === 'users') return createUsersQuery();
Expand Down
9 changes: 6 additions & 3 deletions src/app/api/chat/messages/[id]/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { bearerToken } from '@/lib/auth/bearer.js';

async function resolveRouteParams(params) {
return (await params) || {};
Expand All @@ -24,12 +25,14 @@ async function authenticateUser(request) {
.split(';')
.find(c => c.trim().startsWith('sb-access-token='));

if (!sessionCookie) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
if (!bearer && !sessionCookie) {
return null;
}

const separatorIndex = sessionCookie.indexOf('=');
const token = separatorIndex >= 0 ? sessionCookie.slice(separatorIndex + 1) : '';
const separatorIndex = sessionCookie ? sessionCookie.indexOf('=') : -1;
const token = bearer || (separatorIndex >= 0 ? sessionCookie.slice(separatorIndex + 1) : '');
if (!token) {
return null;
}
Expand Down
9 changes: 6 additions & 3 deletions src/app/api/chat/messages/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NextResponse } from 'next/server';
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
import { createClient } from '@supabase/supabase-js';
import { normalizeMessagePagination } from './pagination.js';
import { bearerToken } from '@/lib/auth/bearer.js';
// Lazy service role client creation
let supabaseServiceRole = null;
function getServiceRoleClient() {
Expand All @@ -30,8 +31,10 @@ async function resolveRouteParams(params) {
async function authenticateUser(request) {
try {
// Parse cookies to get JWT token
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
const cookieHeader = request.headers.get('cookie') || '';
if (!bearer && !cookieHeader) {
console.log('🔐 [API] ❌ No cookies found in request');
return { user: null, error: 'No authentication cookies found' };
}
Expand All @@ -44,7 +47,7 @@ async function authenticateUser(request) {
})
);

const accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];
const accessToken = bearer || cookies['sb-access-token'] || cookies['sb-refresh-token'];
if (!accessToken) {
console.log('🔐 [API] ❌ No Supabase auth tokens found in cookies');
return { user: null, error: 'No authentication tokens found' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { bearerToken } from '@/lib/auth/bearer.js';

// Create regular Supabase client for authentication
const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
Expand All @@ -15,8 +16,10 @@ const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, proces
async function authenticateUser(request) {
try {
// Parse cookies to get JWT token
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
const cookieHeader = request.headers.get('cookie') || '';
if (!bearer && !cookieHeader) {
console.log('🔐 [API] ❌ No cookies found in request');
return { user: null, error: 'No authentication cookies found' };
}
Expand All @@ -29,7 +32,7 @@ async function authenticateUser(request) {
})
);

const accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];
const accessToken = bearer || cookies['sb-access-token'] || cookies['sb-refresh-token'];
if (!accessToken) {
console.log('🔐 [API] ❌ No Supabase auth tokens found in cookies');
return { user: null, error: 'No authentication tokens found' };
Expand Down
9 changes: 6 additions & 3 deletions src/app/api/conversations/[id]/disappearing-messages/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { NextResponse } from 'next/server';
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
import { createClient } from '@supabase/supabase-js';
import { bearerToken } from '@/lib/auth/bearer.js';
// Lazy service role client creation
let supabaseServiceRole = null;
function getServiceRoleClient() {
Expand All @@ -28,8 +29,10 @@ async function resolveRouteParams(params) {
async function authenticateUser(request) {
try {
// Parse cookies to get JWT token
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
const cookieHeader = request.headers.get('cookie') || '';
if (!bearer && !cookieHeader) {
console.log('🔐 [API] ❌ No cookies found in request');
return { user: null, error: 'No authentication cookies found' };
}
Expand All @@ -42,7 +45,7 @@ async function authenticateUser(request) {
})
);

const accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];
const accessToken = bearer || cookies['sb-access-token'] || cookies['sb-refresh-token'];
if (!accessToken) {
console.log('🔐 [API] ❌ No Supabase auth tokens found in cookies');
return { user: null, error: 'No authentication tokens found' };
Expand Down
11 changes: 7 additions & 4 deletions src/app/api/crypto/public-keys/all/route.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { supabaseAuthCookieName } from '@/lib/supabase/auth-cookie.js';
import { bearerToken } from '@/lib/auth/bearer.js';

// Regular (anon) client used ONLY to validate the caller's JWT. Never use the
// service-role key for auth checks.
Expand All @@ -17,8 +18,10 @@ const supabaseClient = createClient(
*/
async function authenticateUser(request) {
try {
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
const cookieHeader = request.headers.get('cookie') || '';
if (!bearer && !cookieHeader) {
return { error: 'No cookies found' };
}

Expand All @@ -31,10 +34,10 @@ async function authenticateUser(request) {
})
);

let accessToken = null;
let accessToken = bearer;

// Supabase-specific auth cookie first
if (cookies[supabaseAuthCookieName()]) {
if (!accessToken && cookies[supabaseAuthCookieName()]) {
try {
let tokenData = cookies[supabaseAuthCookieName()];
if (tokenData.startsWith('base64-')) {
Expand Down
11 changes: 7 additions & 4 deletions src/app/api/crypto/public-keys/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { supabaseAuthCookieName } from '@/lib/supabase/auth-cookie.js';
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
import { bearerToken } from '@/lib/auth/bearer.js';

// Lazy service role client creation
let supabaseServiceRole = null;
Expand Down Expand Up @@ -47,8 +48,10 @@ export function normalizePublicKeyUserIds(value) {
async function authenticateUser(request) {
try {
// Get access token from cookies
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) {
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
const bearer = bearerToken(request);
const cookieHeader = request.headers.get('cookie') || '';
if (!bearer && !cookieHeader) {
return { error: 'No cookies found' };
}

Expand All @@ -63,10 +66,10 @@ async function authenticateUser(request) {
);

// Try different cookie sources for the access token
let accessToken = null;
let accessToken = bearer;

// Try Supabase-specific cookies first
if (cookies[supabaseAuthCookieName()]) {
if (!accessToken && cookies[supabaseAuthCookieName()]) {
try {
let tokenData = cookies[supabaseAuthCookieName()];
if (tokenData.startsWith('base64-')) {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/auth/bearer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* The access token from an `Authorization: Bearer …` header, or null.
*
* qc, the MCP server and API clients authenticate this way, and so do browser
* sessions that never get a cookie (passkey, CoinPay, phone). Routes with
* their own cookie parsing must check this first.
* @param {Request} request
* @returns {string | null}
*/
export function bearerToken(request) {
const header = request?.headers?.get?.('authorization');
if (typeof header !== 'string') return null;
const token = header.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
return token || null;
}
17 changes: 17 additions & 0 deletions src/lib/auth/bearer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { bearerToken } from './bearer.js';

const req = (authorization) => new Request('https://qrypt.chat/x', authorization ? { headers: { authorization } } : {});

describe('bearerToken', () => {
it('reads the token, whatever the scheme casing and spacing', () => {
expect(bearerToken(req('Bearer abc'))).toBe('abc');
expect(bearerToken(req('bearer abc '))).toBe('abc');
});

it('is null without a usable Bearer header', () => {
expect(bearerToken(req())).toBeNull();
expect(bearerToken(req('Basic abc'))).toBeNull();
expect(bearerToken(req('Bearer '))).toBeNull();
});
});
Loading