mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
- /fotos/encontre + POST /api/fotos/selfie: só o vetor 128-d, sem imagem, sem score e sem nomes de terceiros; fail-closed pela flag photoAlbum.selfieSearchEnabled (default false) e pelos Consents por chave estável (busca-selfie-fotos/busca-selfie-indice, não seedados — a superfície nasce fechada até o aval jurídico/DPIA). - Índice A/C do gate (PR #1370): só quem aderiu é indexado; descriptors do acervo são transitórios. Collection faceSubject (admin-only, sem vínculo com Contact), vínculos matchedPhotos e marker faces.checkedKey no archivePhoto (modelo + enrolledAt). - Engine @vladmandic/face-api nos dois lados: browser lazy com modelos same-origin (copy-face-vision-assets no prebuild/predev/Docker) e CLI node-wasm sem tfjs-node. CLIs ops-only faces:enroll / faces:index --apply|--verify com guardas C231/C232. - Opt-out self-service por reconhecimento (leave-index) + canal de remoção do C233; rate limit próprio (namespace selfie:<hash>) e bounded body extraído do beacon. - UI portada classe-a-classe do design estendido (paridade certificada, tier primário); e2e frontendFotosSelfie (manifest+curado+projeto, engine stubada) e canário do engine real; runbook §C234, contrato em AGENTS-public.md e changelog. - Migration aditiva add_face_subject (SQL revisado); hard-stops aprovados na sessão.
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
checkContentEventRateLimit,
|
|
contentEventClientKey,
|
|
} from '@/utilities/content/contentEventRateLimit'
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
describe('contentEventClientKey', () => {
|
|
it('reads the tunnel header first, then the first forwarded hop, then the real IP', () => {
|
|
const keyOf = (headers: Record<string, string>) => contentEventClientKey(new Headers(headers))
|
|
|
|
const tunnelKey = keyOf({ 'cf-connecting-ip': '203.0.113.9' })
|
|
expect(tunnelKey).toMatch(/^[a-f0-9]{64}$/)
|
|
|
|
// The same client behind the same header always hashes to the same key.
|
|
expect(keyOf({ 'cf-connecting-ip': '203.0.113.9' })).toBe(tunnelKey)
|
|
expect(keyOf({ 'cf-connecting-ip': '203.0.113.10' })).not.toBe(tunnelKey)
|
|
|
|
expect(keyOf({ 'x-forwarded-for': '198.51.100.4, 10.0.0.1' })).toMatch(/^[a-f0-9]{64}$/)
|
|
expect(keyOf({ 'x-real-ip': '192.0.2.7' })).toMatch(/^[a-f0-9]{64}$/)
|
|
|
|
// No IP header at all: no key, never a shared bucket.
|
|
expect(keyOf({})).toBeNull()
|
|
expect(keyOf({ 'x-forwarded-for': ' ' })).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('checkContentEventRateLimit', () => {
|
|
it('fails open without a key and never throttles an unkeyed client', () => {
|
|
for (let attempt = 0; attempt < 500; attempt += 1) {
|
|
expect(checkContentEventRateLimit(null)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('throttles the 121st event of one client inside the window and leaves others alone', () => {
|
|
const key = 'unit-window-client'
|
|
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
expect(checkContentEventRateLimit(key)).toBe(true)
|
|
}
|
|
expect(checkContentEventRateLimit(key)).toBe(false)
|
|
expect(checkContentEventRateLimit(`${key}-other`)).toBe(true)
|
|
})
|
|
|
|
it('resets when the window expires', () => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2026-09-23T12:00:00.000Z'))
|
|
|
|
const key = 'unit-reset-client'
|
|
for (let attempt = 0; attempt < 120; attempt += 1) checkContentEventRateLimit(key)
|
|
expect(checkContentEventRateLimit(key)).toBe(false)
|
|
|
|
vi.setSystemTime(new Date('2026-09-23T12:10:00.001Z'))
|
|
expect(checkContentEventRateLimit(key)).toBe(true)
|
|
})
|
|
|
|
it('honours a stricter caller budget on the same registry (C234 selfie search)', () => {
|
|
const key = 'unit-strict-budget-client'
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
expect(checkContentEventRateLimit(key, 20)).toBe(true)
|
|
}
|
|
expect(checkContentEventRateLimit(key, 20)).toBe(false)
|
|
|
|
// Another client still has its own window and the default budget is intact.
|
|
expect(checkContentEventRateLimit(`${key}-other`, 20)).toBe(true)
|
|
expect(checkContentEventRateLimit('unit-default-budget-client')).toBe(true)
|
|
})
|
|
})
|