Files
OmniRoute/src/lib/sync/tokens.ts
Diego Rodrigues de Sa e Souza 20b35c4d20 security: sanitize error messages in API routes (CodeQL js/stack-trace-exposure)
Integrated into release/v3.8.0
2026-05-12 23:23:07 -03:00

108 lines
3.4 KiB
TypeScript

import { createHash, randomBytes } from "crypto";
import {
createSyncTokenRecord,
getSyncTokenByHash,
listSyncTokens,
revokeSyncToken,
touchSyncTokenLastUsed,
type SyncTokenRecord,
} from "@/lib/db/syncTokens";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
function normalizeToken(rawToken: string | null | undefined) {
if (typeof rawToken !== "string") return null;
const token = rawToken.trim();
return token.length > 0 ? token : null;
}
export function hashSyncToken(rawToken: string) {
// CodeQL: Intentionally SHA-256, NOT password hashing. Sync tokens are
// high-entropy random values (osync_ + 32 random bytes) — not user passwords.
// codeql[js/insufficient-password-hash]
return createHash("sha256").update(rawToken).digest("hex"); // nosemgrep: insufficient-password-hash
}
export function generatePlaintextSyncToken() {
return `osync_${randomBytes(32).toString("base64url")}`;
}
export async function issueSyncToken(params: { name: string; syncApiKeyId?: string | null }) {
const plaintextToken = generatePlaintextSyncToken();
const record = await createSyncTokenRecord({
name: params.name,
tokenHash: hashSyncToken(plaintextToken),
syncApiKeyId: params.syncApiKeyId || null,
});
return {
token: plaintextToken,
record,
};
}
export async function validateSyncToken(rawToken: string | null | undefined) {
const token = normalizeToken(rawToken);
if (!token) return null;
const record = await getSyncTokenByHash(hashSyncToken(token));
if (!record || record.revokedAt) return null;
return record;
}
export async function markSyncTokenUsed(record: SyncTokenRecord) {
await touchSyncTokenLastUsed(record.id);
}
export async function listSyncTokenSummaries() {
const records = await listSyncTokens();
return records.map((record) => ({
id: record.id,
name: record.name,
syncApiKeyId: record.syncApiKeyId,
revokedAt: record.revokedAt,
lastUsedAt: record.lastUsedAt,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
}));
}
export async function revokeSyncTokenById(id: string) {
const revoked = await revokeSyncToken(id);
if (!revoked) return null;
return {
id: revoked.id,
name: revoked.name,
syncApiKeyId: revoked.syncApiKeyId,
revokedAt: revoked.revokedAt,
lastUsedAt: revoked.lastUsedAt,
createdAt: revoked.createdAt,
updatedAt: revoked.updatedAt,
};
}
export async function resolveSyncApiKeyIdFromManagementRequest(request: Request) {
const authHeader = request.headers.get("authorization") || request.headers.get("Authorization");
if (typeof authHeader !== "string") return null;
const trimmedHeader = authHeader.trim();
if (!trimmedHeader.toLowerCase().startsWith("bearer ")) return null;
const apiKey = trimmedHeader.slice(7).trim();
if (!apiKey) return null;
const metadata = await getApiKeyMetadata(apiKey);
return metadata?.id || null;
}
export function getSyncTokenFromRequest(request: Request) {
const explicitHeader = request.headers.get("x-sync-token");
if (typeof explicitHeader === "string" && explicitHeader.trim().length > 0) {
return explicitHeader.trim();
}
const authHeader = request.headers.get("authorization") || request.headers.get("Authorization");
if (typeof authHeader !== "string") return null;
const trimmedHeader = authHeader.trim();
if (!trimmedHeader.toLowerCase().startsWith("bearer ")) return null;
return trimmedHeader.slice(7).trim() || null;
}