fix(auth): resolve Smirk challenge parameter types

This commit is contained in:
2026-10-21 15:43:01 -04:00
parent 0244a4ab8c
commit 2aec438226
2 changed files with 28 additions and 7 deletions
+13 -7
View File
@@ -10,6 +10,16 @@ const { verifyNip98Event } = require('../utils/nip98');
const AUTH_WINDOW_MS = 60 * 1000;
const AUTH_RATE_MAX_KEYS = 10000;
// Shared with the real PostgreSQL lifecycle check so parameter inference is exercised
// against the migrated schema, rather than accepted only by query mocks.
const challengeInsertQuery = `
INSERT INTO smirk_challenges (challenge, socket_id, user_id)
SELECT $1::varchar(64), $2::varchar(255), id
FROM users
WHERE id = $3 AND socket_id = $2 AND anon_token = $4
RETURNING id
`;
/**
* The host a NIP-98 auth event must be signed for.
*
@@ -260,13 +270,7 @@ router.post('/api/auth/smirk/challenge', challengeRateLimit, asyncHandler(async
// Store a stable user binding as well as the current socket. The user binding is what
// prevents a public Tavern/Match socket id from being used to attach an attacker's key.
const inserted = await db.query(`
INSERT INTO smirk_challenges (challenge, socket_id, user_id)
SELECT $1, $2, id
FROM users
WHERE id = $3 AND socket_id = $2 AND anon_token = $4
RETURNING id
`, [challenge, socketId, session.userId, session.token]);
const inserted = await db.query(challengeInsertQuery, [challenge, socketId, session.userId, session.token]);
if (inserted.rows.length !== 1) throw unauthorized();
// Opportunistic cleanup of expired and long-used challenges.
@@ -470,3 +474,5 @@ router.get('/api/auth/smirk/status', asyncHandler(async (req, res) => {
return router;
};
module.exports.challengeInsertQuery = challengeInsertQuery;
+15
View File
@@ -12,6 +12,7 @@ const path = require('node:path');
const { promisify } = require('node:util');
const { Client } = require('pg');
const { chromium } = require('playwright-core');
const { challengeInsertQuery } = require('../routes/auth');
const execFile = promisify(childProcess.execFile);
const ROOT = path.resolve(__dirname, '../..');
@@ -143,6 +144,7 @@ function classifyStartupFailure(output) {
function classifyHarnessFailure(error, phase) {
const sqlStateCodes = {
'42P08': 'DATABASE_PARAMETER_TYPE_CONFLICT',
'42P01': 'DATABASE_TABLE_UNDEFINED',
'42703': 'DATABASE_COLUMN_UNDEFINED',
'42883': 'DATABASE_FUNCTION_UNDEFINED',
@@ -370,6 +372,19 @@ async function main() {
throw error;
});
// PREPARE checks the route's actual parameter inference without generating a
// challenge, activating Smirk, or passing any authentication values to PostgreSQL.
phase = 'auth-query-compile';
const authDb = new Client({ host: socketDir, port: dbPort, database: dbName, user: dbUser,
connectionTimeoutMillis: 2000, statement_timeout: 5000, lock_timeout: 2000 });
try {
await authDb.connect();
await authDb.query(`PREPARE showcase_smirk_challenge AS ${challengeInsertQuery}`);
await authDb.query('DEALLOCATE showcase_smirk_challenge');
} finally {
await authDb.end();
}
phase = 'browser-setup';
browser = await chromium.launch({ headless: true });
for (const role of ROLES) {