import http from 'node:http';
import { randomUUID } from 'node:crypto';
const port = Number.parseInt(process.env.BITFUN_FEEDBACK_MOCK_PORT ?? '38971', 10);
const enrollments = new Map();
const refreshTokens = new Map();
const createRequests = new Map();
const replyRequests = new Map();
const records = new Map();
let nextFault = null;
const server = http.createServer(async (request, response) => {
const requestId = normalizeRequestId(header(request, 'x-request-id'));
const url = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
logRequestStage(requestStage(request.method, url.pathname), requestId);
if (request.method === 'GET' && url.pathname === '/health') {
return send(response, 200, { status: 'ok' }, requestId);
}
if (request.method === 'POST' && url.pathname === '/__mock/control') {
const body = await readJson(request);
nextFault = typeof body.fault === 'string' ? body.fault : null;
if (Number.isInteger(body.seed_inbox) && body.seed_inbox > 0) {
seedInbox(Math.min(body.seed_inbox, 120));
}
if (body.admin_reply && typeof body.admin_reply.feedback_id === 'string') {
addAdminReply(body.admin_reply.feedback_id, body.admin_reply.content ?? 'Mock support reply');
}
if (body.seed_messages && typeof body.seed_messages.feedback_id === 'string') {
seedMessages(body.seed_messages.feedback_id, Math.min(body.seed_messages.count ?? 0, 250));
}
return send(response, 200, { next_fault: nextFault, records: records.size }, requestId);
}
const fault = takeFault();
if (fault === 'timeout') {
setTimeout(() => sendError(response, 500, 'INTERNAL_ERROR', requestId), 25_000);
return;
}
if (fault === '403') return sendError(response, 403, 'SCOPE_INSUFFICIENT', requestId);
if (fault === '429') return sendError(response, 429, 'RATE_LIMITED', requestId, { 'Retry-After': '30' });
if (fault === '5xx') return sendError(response, 503, 'INTERNAL_ERROR', requestId);
if (fault === '401') return sendError(response, 401, 'ACCESS_TOKEN_INVALID', requestId);
if (fault === 'cursor_invalid') return sendError(response, 400, 'CURSOR_INVALID', requestId);
if (fault === 'capability_invalid') return sendError(response, 403, 'CAPABILITY_INVALID', requestId);
if (request.method === 'POST' && url.pathname === '/auth/v1/anonymous/enroll') {
const body = await readJson(request);
const idempotencyKey = requireUuidHeader(request, response, requestId);
if (!idempotencyKey) return;
if (typeof body.key !== 'string' || body.key.length === 0) {
return sendError(response, 400, 'ENROLL_KEY_REQUIRED', requestId);
}
const identity = `${body.key}:${idempotencyKey}`;
const existing = enrollments.get(identity);
if (existing) return send(response, 201, existing, requestId, { 'Idempotency-Replayed': 'true' });
const created = tokenPair(randomUUID());
enrollments.set(identity, created);
refreshTokens.set(created.refresh_token, created.anonymous_id);
return send(response, 201, created, requestId, { 'Idempotency-Replayed': 'false' });
}
if (request.method === 'POST' && url.pathname === '/auth/v1/anonymous/token') {
const body = await readJson(request);
const anonymousId = refreshTokens.get(body.refresh_token);
if (!anonymousId) return sendError(response, 401, 'REFRESH_TOKEN_INVALID', requestId);
refreshTokens.delete(body.refresh_token);
const refreshed = tokenPair(anonymousId);
refreshTokens.set(refreshed.refresh_token, anonymousId);
return send(response, 200, refreshed, requestId);
}
if (request.method === 'POST' && url.pathname === '/support/v1/feedback') {
if (!validBearer(request)) return sendError(response, 401, 'ACCESS_TOKEN_INVALID', requestId);
const body = await readJson(request);
const idempotencyKey = requireUuidHeader(request, response, requestId);
if (!idempotencyKey) return;
const fingerprint = JSON.stringify(body);
const existing = createRequests.get(idempotencyKey);
if (existing && existing.fingerprint !== fingerprint) {
return sendError(response, 409, 'FEEDBACK_IDEMPOTENT_CONFLICT', requestId);
}
if (existing) {
return send(response, 201, { ...existing.result, idempotency_replayed: true }, requestId, {
'Idempotency-Replayed': 'true',
});
}
if (!['runtime_error', 'feature_request', 'usage_question', 'other'].includes(body.category)) {
return sendError(response, 400, 'CATEGORY_INVALID', requestId);
}
if (typeof body.content !== 'string' || body.content.trim().length === 0) {
return sendError(response, 400, 'CONTENT_EMPTY', requestId);
}
if (Array.from(body.content.trim()).length > 2_000) {
return sendError(response, 400, 'CONTENT_TOO_LONG', requestId);
}
const feedbackId = randomUUID();
const createdAt = new Date().toISOString();
const result = {
feedback_id: feedbackId,
capability_token: randomUUID(),
status: 'submitted',
inbox_cursor: new Date().toISOString(),
schema_version: '1.0.0',
};
if (fault === 'capability_missing') delete result.capability_token;
createRequests.set(idempotencyKey, { fingerprint, result });
records.set(feedbackId, {
...body,
...result,
has_new_reply: false,
created_at: createdAt,
updated_at: createdAt,
read_cursor: createdAt,
messages: [{
message_id: randomUUID(),
sender_type: 'user',
content: body.content.trim(),
content_deleted: false,
created_at: createdAt,
}],
});
return send(response, 201, result, requestId, { 'Idempotency-Replayed': 'false' });
}
if (request.method === 'GET' && url.pathname === '/support/v1/feedback/inbox') {
if (!validBearer(request)) return sendError(response, 401, 'ACCESS_TOKEN_INVALID', requestId);
const limit = parseLimit(url.searchParams.get('limit'), 20, 100);
if (limit === null) return sendError(response, 400, 'PAGE_SIZE_INVALID', requestId);
const offset = decodeCursor(url.searchParams.get('cursor'));
if (offset === null) return sendError(response, 400, 'CURSOR_INVALID', requestId);
const ordered = [...records.values()].sort((left, right) =>
right.created_at.localeCompare(left.created_at) || right.feedback_id.localeCompare(left.feedback_id));
const page = ordered.slice(offset, offset + limit);
const nextOffset = offset + page.length;
return send(response, 200, {
items: page.map(record => ({
feedback_id: record.feedback_id,
category: record.category,
status: record.status,
has_new_reply: record.has_new_reply,
created_at: record.created_at,
updated_at: record.updated_at,
})),
cursor: encodeCursor(nextOffset),
has_more: nextOffset < ordered.length,
}, requestId);
}
const messagesMatch = /^\/support\/v1\/feedback\/([^/]+)\/messages$/.exec(url.pathname);
if (request.method === 'POST' && messagesMatch) {
const record = authorizeRecord(request, response, requestId, messagesMatch[1]);
if (!record) return;
const idempotencyKey = requireUuidHeader(request, response, requestId);
if (!idempotencyKey) return;
const body = await readJson(request);
const fingerprint = `${record.feedback_id}:${JSON.stringify(body)}`;
const existing = replyRequests.get(idempotencyKey);
if (existing && existing.fingerprint !== fingerprint) {
return sendError(response, 409, 'IDEMPOTENCY_CONFLICT', requestId);
}
if (existing) {
return send(response, 201, existing.result, requestId, {
'Idempotency-Replayed': 'true',
});
}
if (record.status === 'resolved') {
return sendError(response, 409, 'FEEDBACK_ALREADY_RESOLVED', requestId);
}
if (typeof body.content !== 'string' || body.content.trim().length === 0) {
return sendError(response, 400, 'CONTENT_EMPTY', requestId);
}
if (Array.from(body.content.trim()).length > 2_000) {
return sendError(response, 400, 'CONTENT_TOO_LONG', requestId);
}
const previousTime = Date.parse(record.messages.at(-1)?.created_at ?? record.created_at);
const createdAt = new Date(Math.max(Date.now(), previousTime + 1)).toISOString();
const message = {
message_id: randomUUID(),
sender_type: 'user',
content: body.content.trim(),
content_deleted: false,
created_at: createdAt,
};
record.messages.push(message);
record.status = 'in_progress';
record.has_new_reply = false;
record.updated_at = createdAt;
const result = {
message_id: message.message_id,
sender_type: message.sender_type,
created_at: message.created_at,
feedback_status: record.status,
};
replyRequests.set(idempotencyKey, { fingerprint, result });
return send(response, 201, result, requestId, { 'Idempotency-Replayed': 'false' });
}
if (request.method === 'GET' && messagesMatch) {
const record = authorizeRecord(request, response, requestId, messagesMatch[1]);
if (!record) return;
const limit = parseLimit(url.searchParams.get('limit'), 50, 200);
if (limit === null) return sendError(response, 400, 'PAGE_SIZE_INVALID', requestId);
const offset = decodeCursor(url.searchParams.get('cursor'), 'messages');
if (offset === null) return sendError(response, 400, 'CURSOR_INVALID', requestId);
const page = record.messages.slice(offset, offset + limit);
const nextOffset = offset + page.length;
return send(response, 200, {
feedback_id: record.feedback_id,
messages: page,
cursor: encodeCursor(nextOffset, 'messages'),
has_more: nextOffset < record.messages.length,
}, requestId);
}
const ackMatch = /^\/support\/v1\/feedback\/([^/]+)\/ack$/.exec(url.pathname);
if (request.method === 'POST' && ackMatch) {
const record = authorizeRecord(request, response, requestId, ackMatch[1]);
if (!record) return;
const body = await readJson(request);
const requested = Date.parse(body.read_cursor);
if (!Number.isFinite(requested)) return sendError(response, 400, 'READ_CURSOR_INVALID', requestId);
const latest = record.messages.at(-1)?.created_at ?? record.created_at;
const effective = new Date(Math.max(
Date.parse(record.read_cursor ?? record.created_at),
Math.min(requested, Date.parse(latest)),
)).toISOString();
record.read_cursor = effective;
if (record.status === 'waiting_user' && Date.parse(effective) >= Date.parse(latest)) {
record.status = 'in_progress';
}
record.has_new_reply = record.messages.some(message =>
message.sender_type === 'admin' && Date.parse(message.created_at) > Date.parse(effective));
return send(response, 200, {
feedback_id: record.feedback_id,
read_cursor: effective,
feedback_status: record.status,
}, requestId);
}
return sendError(response, 404, 'NOT_FOUND', requestId);
});
server.listen(port, '127.0.0.1', () => {
process.stdout.write(`Feedback mock listening at http://127.0.0.1:${port}\n`);
});
function tokenPair(anonymousId) {
return {
anonymous_id: anonymousId,
access_token: `access-${randomUUID()}`,
refresh_token: `refresh-${randomUUID()}`,
expires_in: 3_600,
refresh_expires_in: 2_592_000,
scope: 'feedback:write,feedback:read',
schema_version: '1.0.0',
};
}
function seedInbox(count) {
for (let index = 0; index < count; index += 1) {
const feedbackId = randomUUID();
const createdAt = new Date(Date.now() - index * 60_000).toISOString();
records.set(feedbackId, {
feedback_id: feedbackId,
category: ['runtime_error', 'feature_request', 'usage_question', 'other'][index % 4],
status: ['submitted', 'in_progress', 'waiting_user', 'resolved'][index % 4],
has_new_reply: index % 3 === 0,
created_at: createdAt,
updated_at: createdAt,
});
}
}
function parseLimit(value, fallback, maximum) {
if (value === null) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed >= 1 && parsed <= maximum ? parsed : null;
}
function encodeCursor(offset, kind = 'inbox') {
return Buffer.from(`${kind}:${offset}`, 'utf8').toString('base64url');
}
function decodeCursor(cursor, kind = 'inbox') {
if (cursor === null) return 0;
try {
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
const match = new RegExp(`^${kind}:(\\d+)$`).exec(decoded);
return match ? Number.parseInt(match[1], 10) : null;
} catch {
return null;
}
}
function authorizeRecord(request, response, requestId, feedbackId) {
if (!validBearer(request)) {
sendError(response, 401, 'ACCESS_TOKEN_INVALID', requestId);
return null;
}
const record = records.get(feedbackId);
if (!record) {
sendError(response, 404, 'FEEDBACK_NOT_FOUND', requestId);
return null;
}
if (!record.capability_token
|| header(request, 'x-feedback-capability') !== record.capability_token) {
sendError(response, 403, 'CAPABILITY_INVALID', requestId);
return null;
}
return record;
}
function addAdminReply(feedbackId, content) {
const record = records.get(feedbackId);
if (!record || !Array.isArray(record.messages)) return;
const createdAt = new Date(Date.now() + record.messages.length).toISOString();
record.messages.push({
message_id: randomUUID(),
sender_type: 'admin',
content: String(content),
content_deleted: false,
created_at: createdAt,
});
record.status = 'waiting_user';
record.has_new_reply = true;
record.updated_at = createdAt;
}
function seedMessages(feedbackId, count) {
const record = records.get(feedbackId);
if (!record || !Array.isArray(record.messages) || count <= 0) return;
for (let index = 0; index < count; index += 1) {
const createdAt = new Date(Date.parse(record.created_at) + (index + 1) * 1_000).toISOString();
record.messages.push({
message_id: randomUUID(),
sender_type: index % 2 === 0 ? 'admin' : 'user',
content: `Mock message ${index + 1}`,
content_deleted: false,
created_at: createdAt,
});
}
record.status = 'waiting_user';
record.has_new_reply = true;
record.updated_at = record.messages.at(-1).created_at;
}
function takeFault() {
const fault = nextFault;
nextFault = null;
return fault;
}
function validBearer(request) {
return /^Bearer access-/.test(header(request, 'authorization') ?? '');
}
function requireUuidHeader(request, response, requestId) {
const value = header(request, 'idempotency-key');
if (!value || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
sendError(response, 409, 'IDEMPOTENCY_KEY_INVALID', requestId);
return null;
}
return value;
}
function header(request, name) {
const value = request.headers[name];
return Array.isArray(value) ? value[0] : value;
}
function normalizeRequestId(value) {
return typeof value === 'string' && /^[a-zA-Z0-9._:-]{1,128}$/.test(value)
? value
: randomUUID();
}
function requestStage(method, pathname) {
if (method === 'GET' && pathname === '/health') return 'health';
if (method === 'POST' && pathname === '/__mock/control') return 'control';
if (method === 'POST' && pathname === '/auth/v1/anonymous/enroll') return 'enroll';
if (method === 'POST' && pathname === '/auth/v1/anonymous/token') return 'refresh';
if (method === 'POST' && pathname === '/support/v1/feedback') return 'create';
if (method === 'GET' && pathname === '/support/v1/feedback/inbox') return 'inbox';
if (/^\/support\/v1\/feedback\/[^/]+\/messages$/.test(pathname)) {
return method === 'GET' ? 'message_history' : method === 'POST' ? 'reply' : 'unknown';
}
if (method === 'POST' && /^\/support\/v1\/feedback\/[^/]+\/ack$/.test(pathname)) {
return 'acknowledge';
}
return 'unknown';
}
function logRequestStage(stage, requestId) {
process.stdout.write(`${JSON.stringify({ stage, requestId })}\n`);
}
async function readJson(request) {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
if (chunks.length === 0) return {};
try {
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
} catch {
return {};
}
}
function sendError(response, status, code, requestId, headers = {}) {
send(response, status, {
error_code: code,
error_message: 'Mock diagnostic text must never be shown directly by the client.',
request_id: requestId,
}, requestId, headers);
}
function send(response, status, body, requestId, headers = {}) {
response.writeHead(status, {
'Cache-Control': 'no-store',
'Content-Type': 'application/json',
'X-Request-ID': requestId,
...headers,
});
response.end(JSON.stringify(body));
}