Files
zpan/server/lib/constant-time.ts
Jasper Van 7048ecd417 fix+refactor: audit bugs (license cache, timing-safe token) + architecture cleanup (#430)
* fix(security): constant-time compare for internal API token

The internal telemetry endpoint compared the Bearer token with `!==`, a
timing side-channel. Reuse a shared constantTimeEqual helper (extracted from
download-tokens.ts so both call sites share one implementation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(licensing): drop cached entitlement once the certificate expires

loadEntitlement cached the verified summary for 60s keyed only on wall-clock
age, so a certificate expiring mid-window kept granting Pro/Business features
until the cache lapsed. On a cache hit, also verify nowSeconds is still before
certificateExpiresAt and licenseValidUntil; otherwise re-verify (which yields
null for the expired cert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(ui): dedupe currency and size formatters into @/lib/format

Add formatCurrency (locale-aware, cents→currency) and replace four identical
local formatMoney copies in the store components. Replace the duplicated
formatFileSize/formatTrackSize and the verbatim users-list formatDate with the
shared formatSize/formatDate. The null-handling formatDate variants are left
alone — they intentionally render '—' for null, which the shared helper does
not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(server): dedupe MIME→ext into lib/mime-utils

Three services each hand-rolled a MIME_TO_EXT map + lookup. Consolidate into
lib/mime-utils.mimeToExt (the superset of all keys, 'bin' fallback); each
endpoint keeps its own allow-list and uses the shared lookup for naming.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(server): central domain-error → HTTP mapper

Routes hand-rolled the same StorageQuotaExceededError→422 and
NameConflictError→409 (with NAME_CONFLICT body) mappings — 6 sites in
objects.ts, 4 in webdav.ts, 1 in ihost.ts, plus the WebDavPathError mapping.
Add lib/http-errors.mapDomainError returning { status, message, json } and use
it in each catch (JSON routes render json, WebDAV renders text). Removes the
per-route instanceof ladders and the duplicated conflictBody helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(quota): route getEffectiveQuota through the batch aggregation

getEffectiveQuota (single org, ~8 queries) and getEffectiveQuotasByOrg (batch,
2 queries + in-memory aggregation) reimplemented the same EffectiveQuota
assembly. Have the single path call the batch path with one id and return its
entry; delete the now-unused per-resource SQL helpers (activeExtraEntitlement
Bytes/Names/Where). One aggregation path, fewer queries per call, -75 lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:21:16 -04:00

14 lines
449 B
TypeScript

/**
* Length-aware constant-time string comparison. Avoids leaking how many
* leading characters match via response timing — use for comparing secrets
* (tokens, signatures) supplied by a caller.
*/
export function constantTimeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false
let result = 0
for (let i = 0; i < a.length; i += 1) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return result === 0
}