@beltec/face-gate 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/FaceGate.tsx","../src/core/types.ts","../src/core/stream.ts","../src/react/Emblem.tsx","../src/react/framing.ts","../src/react/labels.ts"],"sourcesContent":["import { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { openFaceGateStream, type FaceGateStream } from \"../core/stream\";\nimport { FaceGateError, type FaceBox, type Stable, type StreamOptions, type StreamTicket, type ValidationResult } from \"../core/types\";\nimport { Emblem } from \"./Emblem\";\nimport {\n FULL,\n WHOLE,\n aimCrop,\n areaFromPixels,\n baseCrop,\n boxStyle,\n cropTransform,\n ease,\n fitCrop,\n framingThresholds,\n isDark,\n moved,\n type Area,\n type Crop,\n} from \"./framing\";\nimport { DEFAULT_LABELS, describeFails, type FaceGateLabels } from \"./labels\";\n\nexport type FaceGateCapture = {\n /**\n * Foto final: sem fundo (PNG) quando `clean`; senão a melhorada (JPEG) quando `enhance`; senão o quadro\n * capturado (JPEG). Data URL ou blob URL.\n */\n image: string;\n /** O quadro capturado, como enviado à API. */\n frame: Blob;\n result: ValidationResult;\n};\n\nexport type FaceGateProps = StreamOptions & {\n /** Pede ao SEU servidor um ticket de uso único (ex.: fetch(\"/api/face-gate/ticket\", { method: \"POST\" })). */\n getTicket: () => Promise<StreamTicket>;\n /** Foto capturada e validada (aprovada ou reprovada na validação final). */\n onCapture?: (capture: FaceGateCapture) => void;\n /** Cada quadro analisado ao vivo. */\n onLive?: (result: ValidationResult) => void;\n onError?: (error: FaceGateError) => void;\n /** Liga a câmera ao montar (padrão: espera o clique em \"Ligar câmera\"). */\n autoStart?: boolean;\n /** Abertura animada do escudo ao ligar a câmera (padrão: true; desligada com prefers-reduced-motion). */\n intro?: boolean;\n /** Marca d'água do escudo no canto do vídeo (padrão: true). */\n watermark?: boolean;\n /**\n * Enquadramento automático (padrão: true): a imagem aproxima e acompanha o rosto, mantendo-o no centro; a foto\n * capturada é esse recorte e só sai com o rosto centralizado (tolerância de 8%). Ignora as tarjas pretas que\n * algumas câmeras acrescentam.\n */\n autoFrame?: boolean;\n /** Prévia espelhada, como um espelho (padrão: true). A foto enviada e a final não são espelhadas. */\n mirror?: boolean;\n labels?: Partial<FaceGateLabels>;\n className?: string;\n style?: CSSProperties;\n};\n\ntype Phase = \"off\" | \"starting\" | \"live\" | \"processing\" | \"done\";\nconst NO_STABLE: Stable = { ms: 0, needed: 0 };\nconst CENTER: FaceBox = { xmin: 0.3, ymin: 0.2, xmax: 0.7, ymax: 0.8 };\nconst INTRO_MS = 3400;\nconst KEEP_FRAMES = 15;\n\n/** Sem rosto por esse tempo: o recorte volta para a imagem inteira (área útil). */\nconst LOST_MS = 1500;\n/** Maior lado do quadro enviado (sem ampliar o recorte além da resolução da câmera). */\nconst SEND_MAX = 800;\n/** Quadros entre medições da área útil (a 1ª é logo no início). */\nconst MEASURE_EVERY = 30;\n/** Câmera em resolução maior para o recorte; se vierem quadros pretos (alguns Android), reabre na simples. */\nconst CAMERA_HD: MediaTrackConstraints = { width: { ideal: 1280 }, height: { ideal: 960 }, facingMode: \"user\" };\nconst CAMERA_SD: MediaTrackConstraints = { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: \"user\" };\nconst DARK_FRAMES = 3;\n\n/**\n * Webcam com validação em tempo real do Face Gate: uma conexão WebSocket por sessão, mira que segue o\n * rosto, captura automática só depois de ~0,8 s aprovado e parado, scanner sobre a foto até a resposta.\n * Importe também \"@beltec/face-gate/styles.css\".\n */\nexport function FaceGate({\n getTicket, onCapture, onLive, onError, autoStart = false, intro = true, watermark = true, labels, className,\n style, clean = false, model, device, thresholds, enhance = null, autoFrame = true, mirror = true,\n}: FaceGateProps) {\n const t = { ...DEFAULT_LABELS, ...labels };\n const videoRef = useRef<HTMLVideoElement>(null);\n const mediaRef = useRef<MediaStream | null>(null);\n const streamRef = useRef<FaceGateStream | null>(null);\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const framesRef = useRef(new Map<number, { blob: Blob; crop: Crop; result?: ValidationResult }>());\n // enquadramento: recorte atual (suavizado; o mesmo da prévia e do envio), alvo e área útil da imagem\n const viewRef = useRef<HTMLDivElement>(null);\n const cropRef = useRef<Crop>(FULL);\n const targetRef = useRef<Crop>(FULL);\n const areaRef = useRef<Area>(WHOLE);\n const measuredRef = useRef(0);\n const areaCanvasRef = useRef<HTMLCanvasElement | null>(null);\n const lostSinceRef = useRef(0);\n const darkRef = useRef({ count: 0, checked: 0, fellBack: false });\n const capturedRef = useRef<{ blob: Blob; url: string } | null>(null);\n const gateRef = useRef({ camera: false, socket: false, intro: false });\n const hadFaceRef = useRef(false);\n const introTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const [phase, setPhase] = useState<Phase>(\"off\");\n const [showIntro, setShowIntro] = useState(false);\n const [live, setLive] = useState<ValidationResult | null>(null);\n const [stable, setStable] = useState<Stable>(NO_STABLE);\n const [detectKey, setDetectKey] = useState(0);\n const [frozen, setFrozen] = useState<{ url: string; box: FaceBox } | null>(null);\n const [final, setFinal] = useState<{ url: string; result: ValidationResult } | null>(null);\n const [error, setError] = useState<string | null>(null);\n // proporção real da câmera (no celular ela vem em pé, 3:4): o quadro acompanha\n const [ratio, setRatio] = useState(4 / 3);\n\n // Valores mais recentes em ref: a sessão não reinicia a cada render. Com o enquadramento, o centro é exigido.\n const th = autoFrame ? framingThresholds(thresholds) : thresholds;\n const cfg = useRef({ getTicket, onCapture, onLive, onError, options: { clean, model, device, thresholds: th, enhance } });\n useEffect(() => {\n cfg.current = { getTicket, onCapture, onLive, onError, options: { clean, model, device, thresholds: th, enhance } };\n });\n useEffect(() => {\n streamRef.current?.configure({ clean, model, device, thresholds: th, enhance });\n // th é derivado de thresholds/autoFrame\n }, [clean, model, device, thresholds, autoFrame, enhance]);\n\n // Recorte em movimento suave na prévia enquanto a câmera está ao vivo (direto no DOM, sem re-render).\n useEffect(() => {\n if (phase !== \"live\" || !autoFrame) return;\n let id = 0;\n const tick = () => {\n cropRef.current = ease(cropRef.current, targetRef.current);\n if (viewRef.current) viewRef.current.style.transform = cropTransform(cropRef.current, mirror);\n id = requestAnimationFrame(tick);\n };\n id = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(id);\n }, [phase, autoFrame, mirror]);\n\n function release() {\n if (introTimer.current) clearTimeout(introTimer.current);\n streamRef.current?.close();\n streamRef.current = null;\n mediaRef.current?.getTracks().forEach((tr) => tr.stop());\n mediaRef.current = null;\n }\n useEffect(() => release, []);\n\n function fail(err: FaceGateError) {\n release();\n setShowIntro(false);\n setPhase(\"off\");\n setError(err.message);\n cfg.current.onError?.(err);\n }\n\n /** Mede a área útil (sem tarjas pretas) numa miniatura do quadro inteiro; recorte e alvo ficam dentro dela. */\n function measureArea(v: HTMLVideoElement) {\n if (measuredRef.current++ % MEASURE_EVERY !== 0) return;\n const W = 64;\n const H = Math.max(8, Math.round((W * v.videoHeight) / v.videoWidth));\n const c = (areaCanvasRef.current ??= document.createElement(\"canvas\"));\n c.width = W;\n c.height = H;\n const ctx = c.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n ctx.drawImage(v, 0, 0, W, H);\n const next = areaFromPixels(ctx.getImageData(0, 0, W, H).data, W, H);\n const prev = areaRef.current;\n if (Math.abs(next.x - prev.x) + Math.abs(next.y - prev.y) + Math.abs(next.w - prev.w) + Math.abs(next.h - prev.h) < 0.02) return;\n areaRef.current = next;\n const into = (cr: Crop) => fitCrop(cr.s >= 0.999 ? baseCrop(next) : cr, next);\n cropRef.current = into(cropRef.current);\n targetRef.current = into(targetRef.current);\n }\n\n /** Nos primeiros quadros: preto seguido = câmera que não entrega imagem em HD; reabre em SD, uma vez. */\n function checkDark(ctx: CanvasRenderingContext2D, w: number, h: number): boolean {\n const d = darkRef.current;\n if (d.fellBack || d.checked >= DARK_FRAMES * 2) return false;\n d.checked += 1;\n d.count = isDark(ctx.getImageData(0, 0, w, h).data) ? d.count + 1 : 0;\n if (d.count < DARK_FRAMES) return false;\n d.fellBack = true;\n void reopenCamera();\n return true;\n }\n\n async function reopenCamera() {\n try {\n const media = await navigator.mediaDevices.getUserMedia({ video: CAMERA_SD });\n mediaRef.current?.getTracks().forEach((tr) => tr.stop());\n mediaRef.current = media;\n if (videoRef.current) {\n videoRef.current.srcObject = media;\n await videoRef.current.play().catch(() => {});\n }\n } catch {\n // segue com a câmera que já está aberta\n }\n requestAnimationFrame(sendFrame);\n }\n\n function aim(seq: number, box: FaceBox | null | undefined) {\n if (!autoFrame) return;\n const now = performance.now();\n if (!box) {\n lostSinceRef.current ||= now;\n if (now - lostSinceRef.current > LOST_MS) targetRef.current = baseCrop(areaRef.current);\n return;\n }\n lostSinceRef.current = 0;\n const used = framesRef.current.get(seq)?.crop ?? cropRef.current;\n const next = aimCrop(box, used, areaRef.current);\n if (moved(next, targetRef.current)) targetRef.current = next;\n }\n\n function sendFrame() {\n const v = videoRef.current;\n const s = streamRef.current;\n if (!v || !v.videoWidth || !s) return;\n if (autoFrame) measureArea(v);\n const crop = autoFrame ? cropRef.current : FULL;\n const sw = crop.s * v.videoWidth;\n const sh = crop.s * v.videoHeight;\n const k = Math.min(1, SEND_MAX / Math.max(sw, sh));\n const c = (canvasRef.current ??= document.createElement(\"canvas\"));\n c.width = Math.round(sw * k);\n c.height = Math.round(sh * k);\n const ctx = c.getContext(\"2d\", { willReadFrequently: true });\n ctx?.drawImage(v, crop.x * v.videoWidth, crop.y * v.videoHeight, sw, sh, 0, 0, c.width, c.height);\n if (autoFrame && ctx && checkDark(ctx, c.width, c.height)) return; // reabrindo a câmera em SD\n c.toBlob(\n (blob) => {\n const cur = streamRef.current;\n if (!blob || !cur) return;\n const seq = cur.sendFrame(blob);\n if (seq == null) return;\n const frames = framesRef.current;\n frames.set(seq, { blob, crop });\n for (const k of frames.keys()) if (k <= seq - KEEP_FRAMES) frames.delete(k);\n },\n \"image/jpeg\",\n 0.85,\n );\n }\n\n function goLiveIfReady() {\n const g = gateRef.current;\n if (!g.camera || !g.socket || !g.intro || !streamRef.current) return;\n setShowIntro(false);\n setPhase(\"live\");\n sendFrame();\n }\n\n async function start() {\n setError(null);\n if (typeof window === \"undefined\") return;\n if (!window.isSecureContext) return fail(new FaceGateError(\"camera_unavailable\", t.insecure));\n if (!navigator.mediaDevices?.getUserMedia) return fail(new FaceGateError(\"camera_unavailable\", t.noCamera));\n release();\n const reduceMotion = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n const playIntro = intro && !reduceMotion;\n gateRef.current = { camera: false, socket: false, intro: !playIntro };\n framesRef.current.clear();\n hadFaceRef.current = false;\n cropRef.current = targetRef.current = FULL;\n areaRef.current = WHOLE;\n measuredRef.current = 0;\n lostSinceRef.current = 0;\n darkRef.current = { count: 0, checked: 0, fellBack: false };\n if (viewRef.current) viewRef.current.style.transform = cropTransform(FULL, mirror);\n setLive(null);\n setStable(NO_STABLE);\n setFrozen(null);\n setFinal(null);\n setShowIntro(playIntro);\n setPhase(\"starting\");\n if (playIntro) {\n introTimer.current = setTimeout(() => {\n gateRef.current.intro = true;\n goLiveIfReady();\n }, INTRO_MS);\n }\n\n const [cam, tk] = await Promise.allSettled([\n navigator.mediaDevices.getUserMedia({ video: autoFrame ? CAMERA_HD : CAMERA_SD }),\n cfg.current.getTicket(),\n ]);\n if (cam.status === \"rejected\") {\n const name = (cam.reason as DOMException)?.name;\n const msg = name === \"NotAllowedError\" ? t.denied : name === \"NotFoundError\" ? t.notFound : name === \"NotReadableError\" ? t.busy : String((cam.reason as Error)?.message ?? cam.reason);\n return fail(new FaceGateError(name === \"NotAllowedError\" ? \"camera_denied\" : \"camera_unavailable\", msg));\n }\n if (tk.status === \"rejected\") {\n cam.value.getTracks().forEach((tr) => tr.stop());\n const e = tk.reason;\n return fail(e instanceof FaceGateError ? e : new FaceGateError(\"network\", (e as Error)?.message ?? \"Falha ao obter o ticket\"));\n }\n\n mediaRef.current = cam.value;\n if (videoRef.current) {\n videoRef.current.srcObject = cam.value;\n void videoRef.current.play().catch(() => {});\n }\n streamRef.current = openFaceGateStream(tk.value.url, tk.value.ticket, cfg.current.options, {\n onReady: () => {\n gateRef.current.socket = true;\n goLiveIfReady();\n },\n onFrame: (seq, r, st) => {\n const f = framesRef.current.get(seq);\n if (f) f.result = r;\n aim(seq, r.dominant_face_bbox);\n const hasFace = Boolean(r.dominant_face_bbox);\n if (hasFace && !hadFaceRef.current) setDetectKey((k) => k + 1);\n hadFaceRef.current = hasFace;\n setLive(r);\n setStable(st);\n cfg.current.onLive?.(r);\n requestAnimationFrame(sendFrame); // a API limita o ritmo e só captura quando estável\n },\n onCaptured: (seq) => {\n const frames = framesRef.current;\n const pick = frames.get(seq) ?? frames.get(Math.max(...frames.keys()));\n mediaRef.current?.getTracks().forEach((tr) => tr.stop());\n mediaRef.current = null;\n if (pick) {\n const url = URL.createObjectURL(pick.blob);\n capturedRef.current = { blob: pick.blob, url };\n setFrozen({ url, box: pick.result?.dominant_face_bbox ?? CENTER });\n }\n setPhase(\"processing\");\n },\n onFinal: (_seq, r) => {\n streamRef.current?.close();\n streamRef.current = null;\n const cap = capturedRef.current;\n const image = r.cleaned_image ?? r.enhancement?.enhanced_image ?? cap?.url ?? \"\";\n setFinal({ url: image, result: r });\n setPhase(\"done\");\n if (cap) cfg.current.onCapture?.({ image, frame: cap.blob, result: r });\n },\n onError: fail,\n });\n }\n\n function stop() {\n release();\n setShowIntro(false);\n setPhase(\"off\");\n }\n\n useEffect(() => {\n if (autoStart) void start();\n // só ao montar\n }, []);\n\n const cameraOn = phase === \"starting\" || phase === \"live\";\n const box = phase === \"live\" ? (live?.dominant_face_bbox ?? null) : null;\n const ok = Boolean(live?.accepted);\n const holding = phase === \"live\" && ok && stable.ms > 0 && stable.needed > 0;\n const progress = holding ? Math.min(stable.ms / stable.needed, 1) : 0;\n const status =\n phase === \"starting\" ? t.connecting\n : !live ? t.searching\n : live.accepted ? (holding ? t.hold : t.allGood)\n : !live.dominant_face_bbox ? t.searching\n : `${t.fix}: ${describeFails(live.hard_fails)}`;\n\n return (\n <div className={[\"fg-root\", className].filter(Boolean).join(\" \")} style={style}>\n {/* proporção da câmera, sem passar de ~70% da altura da tela; geometria no estilo (não depende do CSS) */}\n <div className=\"fg-stage\" style={{ aspectRatio: String(ratio), width: `min(100%, calc(70dvh * ${ratio}))`, marginInline: \"auto\" }}>\n {/* camada com o recorte e o espelho: o <video> fica sem transformação (em alguns Android ele fica preto) */}\n <div ref={viewRef} hidden={!cameraOn}\n style={{ position: \"absolute\", inset: 0, transformOrigin: \"0 0\", transform: cropTransform(FULL, mirror) }}>\n <video ref={videoRef} autoPlay playsInline muted className=\"fg-video\"\n onLoadedData={(e) => {\n const v = e.currentTarget;\n if (v.videoWidth && v.videoHeight) setRatio(v.videoWidth / v.videoHeight);\n gateRef.current.camera = true;\n goLiveIfReady();\n }} />\n </div>\n\n {phase === \"live\" && !box && (\n <div className=\"fg-search\" aria-hidden=\"true\"\n style={{ top: \"15%\", left: \"50%\", width: \"auto\", height: \"68%\", aspectRatio: \"3 / 4\", maxWidth: \"80%\", translate: \"-50% 0\" }}>\n <span className=\"fg-search-sweep\" />\n </div>\n )}\n {box && (\n <div key={detectKey} className=\"fg-track\" data-state={ok ? \"ok\" : \"warn\"} style={boxStyle(box, mirror)} aria-hidden=\"true\">\n <span className=\"fg-track-line\" />\n {holding && <span className=\"fg-track-hold\" style={{ transform: `scaleX(${progress})` }} />}\n </div>\n )}\n\n {phase === \"processing\" && frozen && (\n <>\n <img src={frozen.url} alt=\"\" className=\"fg-video\" style={mirror ? { transform: \"scaleX(-1)\" } : undefined} />\n <div className=\"fg-scan\" style={boxStyle(frozen.box, mirror)} aria-hidden=\"true\">\n <span className=\"fg-scan-line\" />\n </div>\n </>\n )}\n\n {phase === \"done\" && final && (\n <div className=\"fg-result\">\n <img src={final.url} alt={final.result.accepted ? t.approved : t.rejected} className=\"fg-result-img\" />\n {final.result.enhancement && (\n <span\n className=\"fg-badge\"\n data-ai={final.result.warnings?.includes(\"face_restored_ai\") ? \"true\" : undefined}\n title={final.result.warnings?.includes(\"face_restored_ai\") ? t.enhancedAi : undefined}\n >\n {t.enhanced}\n {final.result.warnings?.includes(\"face_restored_ai\") && <small>{t.enhancedAi}</small>}\n </span>\n )}\n </div>\n )}\n\n {watermark && phase !== \"off\" && <Emblem className=\"fg-watermark\" />}\n\n {showIntro && (\n <div className=\"fg-intro\" aria-hidden=\"true\">\n <Emblem animated className=\"fg-intro-emblem\" />\n </div>\n )}\n\n {phase === \"off\" && (\n <div className=\"fg-idle\">\n <Emblem className=\"fg-idle-emblem\" />\n <strong>{t.cameraOff}</strong>\n <span>{error ?? t.cameraHint}</span>\n </div>\n )}\n\n {(cameraOn || phase === \"processing\") && (\n <div className=\"fg-status\" role=\"status\">\n <span className={`fg-dot ${phase === \"processing\" ? \"fg-dot-busy\" : ok ? \"fg-dot-ok\" : live ? \"fg-dot-warn\" : \"\"}`} />\n <span>{phase === \"processing\" ? (clean ? t.analyzingClean : enhance ? t.analyzingEnhance : t.analyzing) : status}</span>\n </div>\n )}\n {phase === \"done\" && final && (\n <div className=\"fg-status\" role=\"status\">\n <span className={`fg-dot ${final.result.accepted ? \"fg-dot-ok\" : \"fg-dot-warn\"}`} />\n <span>\n {final.result.accepted ? t.approved : `${t.rejected}: ${describeFails(final.result.hard_fails ?? final.result.reasons)}`}\n </span>\n </div>\n )}\n </div>\n\n <div className=\"fg-actions\">\n {phase === \"done\" || (phase === \"off\" && final) ? (\n <button type=\"button\" className=\"fg-button\" onClick={start}>{t.retry}</button>\n ) : (\n <button type=\"button\" className={`fg-button ${cameraOn ? \"fg-button-outline\" : \"\"}`}\n disabled={phase === \"processing\"} onClick={cameraOn ? stop : start}>\n {cameraOn ? t.stop : t.start}\n </button>\n )}\n </div>\n </div>\n );\n}\n","/** Caixa do rosto dominante, em frações da imagem (0 a 1). */\nexport type FaceBox = { xmin: number; ymin: number; xmax: number; ymax: number };\n\n/** Tokens de uma análise (unidade de processamento do Face Gate: megapixels × peso de cada etapa). */\nexport type TokenUsage = {\n tokens: number;\n /** `validate` conta em dobro com melhoria (valida antes e depois); `enhance` = custo da melhoria. */\n breakdown: { decode: number; validate: number; clean: number; enhance?: number; avatar?: number };\n /** Saldo da janela do plano (ex.: 5 h a partir do 1º uso). */\n window?: { used: number; limit: number; hours: number; started_on: string | null; resets_at: string | null };\n /** Saldo do teto mensal (renova no aniversário do contrato). */\n month?: { used: number; limit: number; cycle_start: string; cycle_end: string };\n};\n\n/**\n * Melhoria de imagem (serviço \"validate_enhance\"):\n * - \"classic\": luz, cor, contraste, ruído e nitidez leve; determinística, não altera o rosto;\n * - \"gfpgan\": a clássica + restauração do rosto com IA (recria detalhes; avisa `face_restored_ai`).\n */\nexport type EnhanceMethod = \"classic\" | \"gfpgan\";\n\n/** Resumo de uma validação dentro de `enhancement` (antes = foto original, depois = melhorada). */\nexport type ValidationSummary = { accepted: boolean; score: number | null; hard_fails: string[] };\n\nexport type Enhancement = {\n method: EnhanceMethod;\n /** Etapas aplicadas: white_balance, exposure, local_contrast, denoise, sharpen, face_restoration. */\n steps: string[];\n /** Dispositivo do GFPGAN (null na clássica). */\n device: \"cpu\" | \"gpu\" | null;\n before: ValidationSummary;\n after: ValidationSummary;\n /** Falhas da original que a melhoria não corrige (desfoque, olhos fechados…): continuam reprovando. */\n carried_over: string[];\n /** Foto melhorada (JPEG em data URL). */\n enhanced_image: string;\n};\n\n/** Formato do avatar (serviço \"avatar\"): redondo (fora do círculo transparente) ou quadrado. */\nexport type AvatarShape = \"square\" | \"round\";\n\n/**\n * Onde a pessoa fica no avatar (só com fundo de cor ou imagem): centro; apoiada na base (o fundo aparece em\n * cima); na base à esquerda ou à direita.\n */\nexport type AvatarLayout = \"center\" | \"bottom\" | \"left\" | \"right\";\n\n/** Dados do avatar gerado (POST /v1/photos/avatar). */\nexport type AvatarInfo = {\n shape: AvatarShape;\n /** Lado em pixels (64 a 1024). */\n size: number;\n /** Modelo de remoção de fundo que separou a pessoa. */\n model: string;\n /** \"transparent\", \"image\" (fundo enviado) ou a cor \"#rrggbb\". */\n background: string;\n /** % do quadro deixado para o fundo (0 com fundo transparente). */\n margin?: number;\n layout?: AvatarLayout;\n};\n\n/** Avisos da validação. `face_restored_ai`: o GFPGAN recriou detalhes do rosto. */\nexport type ValidationWarning = \"face_restored_ai\" | \"gpu_fallback_cpu\" | (string & {});\n\n/** Resultado da validação de uma foto (mesmo formato de POST /v1/photos/*). */\nexport type ValidationResult = {\n accepted: boolean;\n score: number;\n checks: Record<string, boolean | null>;\n /** Critérios que reprovaram (ex.: \"eyes_closed\", \"sunglasses\", \"low_light\"). */\n hard_fails?: string[];\n reasons?: string[];\n warnings?: ValidationWarning[];\n error?: string;\n dominant_face_bbox?: FaceBox | null;\n /** Foto com o fundo removido, recortada no rosto (PNG em data URL), quando pedido `clean`. */\n cleaned_image?: string;\n rembg_model?: string;\n device?: \"cpu\" | \"gpu\";\n timings?: { validation_ms?: number };\n usage?: TokenUsage;\n /** Presente quando a foto passou pela melhoria (`enhance`). */\n enhancement?: Enhancement;\n /** Avatar (só com a foto aprovada): PNG em data URL só com a pessoa do rosto em foco. */\n avatar_image?: string;\n avatar?: AvatarInfo;\n};\n\nexport type StreamOptions = {\n /** Remover o fundo da foto capturada (serviço \"validate_clean\" contratado). */\n clean?: boolean;\n model?: \"u2net\" | \"u2netp\" | \"birefnet-portrait\";\n device?: \"cpu\" | \"gpu\";\n /** Limiares de validação (mesmos nomes de GET /v1/thresholds). */\n thresholds?: Record<string, number>;\n /**\n * Melhorar a foto capturada antes de validar (serviço \"validate_enhance\" contratado). Os quadros ao\n * vivo não passam pela melhoria. null/undefined = sem melhoria.\n */\n enhance?: EnhanceMethod | null;\n};\n\n/** Ticket de uso único (60 s) para o navegador abrir a webcam, emitido pelo SEU servidor. */\nexport type StreamTicket = { ticket: string; url: string; expiresIn?: number };\n\n/** Há quanto tempo os quadros seguidos estão aprovados e parados, e o alvo para capturar. */\nexport type Stable = { ms: number; needed: number };\n\nexport type FaceGateErrorCode =\n | \"unauthorized\" // 401/4401: chave, token ou ticket inválido/expirado\n | \"forbidden\" // 403/4403: serviço não contratado, chave bloqueada, fora do contrato ou IP não liberado\n | \"token_month_limit\" // 402/4402: teto mensal de tokens atingido\n | \"token_window_limit\" // 429/4429: janela de tokens esgotada (ou muitas requisições)\n | \"bad_request\" // 400/4400\n | \"camera_denied\"\n | \"camera_unavailable\"\n | \"network\"\n | \"server\";\n\nexport class FaceGateError extends Error {\n constructor(\n public code: FaceGateErrorCode,\n message: string,\n public status?: number,\n /** Quando o limite renova (ISO), se a API informou. */\n public resetsAt?: string,\n ) {\n super(message);\n this.name = \"FaceGateError\";\n }\n}\n","import { FaceGateError, type FaceGateErrorCode, type Stable, type StreamOptions, type ValidationResult } from \"./types\";\n\n/**\n * Cliente do WebSocket /v1/stream do Face Gate (uma conexão por sessão de câmera).\n *\n * O navegador manda quadros JPEG (mensagens binárias, um por vez: o próximo sai quando chega o resultado\n * do anterior) e recebe o resultado de cada um. A API só captura depois de ~0,8 s de quadros aprovados e\n * parados (`stable`), avisa `captured` com o seq do melhor quadro e devolve a validação final.\n */\nexport type StreamHandlers = {\n onReady?: () => void;\n onFrame?: (seq: number, result: ValidationResult, stable: Stable) => void;\n onCaptured?: (seq: number) => void;\n onFinal?: (seq: number, result: ValidationResult) => void;\n /** Conexão encerrada sem ter sido pedida (`close()`). */\n onError?: (error: FaceGateError) => void;\n};\n\nconst CLOSE: Record<number, [FaceGateErrorCode, string]> = {\n 4400: [\"bad_request\", \"A API recusou a mensagem da câmera.\"],\n 4401: [\"unauthorized\", \"Sessão da câmera expirada. Tente de novo.\"],\n 4402: [\"token_month_limit\", \"Limite mensal de tokens atingido.\"],\n 4403: [\"forbidden\", \"Acesso negado para esta validação.\"],\n 4408: [\"bad_request\", \"A câmera ficou ligada tempo demais.\"],\n 4429: [\"token_window_limit\", \"Limite de uso atingido. Aguarde e tente de novo.\"],\n 1011: [\"server\", \"Erro interno na API.\"],\n};\n\nexport type FaceGateStream = {\n /** Envia o quadro e devolve o seq dele (1, 2, 3… igual ao servidor); null se a conexão não está aberta. */\n sendFrame: (frame: Blob) => number | null;\n configure: (options: StreamOptions) => void;\n /** Recomeça depois de uma captura, na mesma conexão. */\n reset: () => void;\n close: () => void;\n};\n\ntype WebSocketLike = new (url: string) => WebSocket;\n\nexport function openFaceGateStream(\n url: string,\n ticket: string,\n options: StreamOptions,\n handlers: StreamHandlers,\n WS: WebSocketLike = WebSocket,\n): FaceGateStream {\n const ws = new WS(url);\n ws.binaryType = \"arraybuffer\";\n let closedByUs = false;\n let finished = false;\n let seq = 0;\n\n ws.onopen = () => ws.send(JSON.stringify({ type: \"start\", token: ticket, ...options }));\n ws.onmessage = (e: MessageEvent) => {\n if (typeof e.data !== \"string\") return;\n const m = JSON.parse(e.data);\n if (m.type === \"ready\") handlers.onReady?.();\n else if (m.type === \"frame\") handlers.onFrame?.(m.seq, m.result, m.stable ?? { ms: 0, needed: 0 });\n else if (m.type === \"captured\") handlers.onCaptured?.(m.seq);\n else if (m.type === \"final\") {\n finished = true;\n handlers.onFinal?.(m.seq, m.result);\n }\n };\n ws.onclose = (e: CloseEvent) => {\n if (closedByUs || finished) return;\n const [code, message] = CLOSE[e.code] ?? [\"network\", e.reason || \"Conexão com a API perdida.\"];\n handlers.onError?.(new FaceGateError(code, e.reason || message, e.code));\n };\n\n const open = () => ws.readyState === 1;\n return {\n sendFrame: (frame) => {\n if (!open()) return null;\n ws.send(frame);\n return ++seq;\n },\n configure: (o) => {\n if (open()) ws.send(JSON.stringify({ type: \"config\", ...o }));\n },\n reset: () => {\n finished = false;\n if (open()) ws.send(JSON.stringify({ type: \"reset\" }));\n },\n close: () => {\n closedByUs = true;\n if (ws.readyState <= 1) ws.close(1000);\n },\n };\n}\n","import { useId } from \"react\";\r\n\r\n/**\r\n * Escudo BelTec (Face Gate, uma solução BelTec): a faixa do escudo Belfort liga três pontos — a lente\r\n * (a caixa que vê), a Central que entende e as pessoas que agem. Com `animated`, se desenha como na\r\n * abertura da câmera do app (CSS em styles.css, classes .fg-emblem-*).\r\n */\r\nexport function Emblem({ animated = false, className }: { animated?: boolean; className?: string }) {\r\n const id = useId().replace(/:/g, \"\");\r\n const shield = \"M4 4.5Q4 4 4.5 4H43.5Q44 4 44 4.5V30C44 42 35 52 24 52C13 52 4 42 4 30Z\";\r\n return (\r\n <svg\r\n viewBox=\"0 0 48 56\"\r\n fill=\"none\"\r\n aria-hidden=\"true\"\r\n className={[className, animated ? \"fg-emblem-animated\" : \"\"].filter(Boolean).join(\" \")}\r\n >\r\n <defs>\r\n <linearGradient id={`${id}g`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\r\n <stop offset=\"0\" stopColor=\"#e3c35c\" />\r\n <stop offset=\"0.55\" stopColor=\"#c9a43e\" />\r\n <stop offset=\"1\" stopColor=\"#a8852a\" />\r\n </linearGradient>\r\n <clipPath id={`${id}c`}>\r\n <path d={shield} />\r\n </clipPath>\r\n </defs>\r\n <path className=\"fg-emblem-shield\" d={shield} fill={`url(#${id}g)`} />\r\n <g clipPath={`url(#${id}c)`}>\r\n <path className=\"fg-emblem-band\" d=\"M-2 41.2L50 10.8\" stroke=\"#24282d\" strokeWidth=\"9.5\" pathLength={1} />\r\n </g>\r\n <path className=\"fg-emblem-line\" d=\"M10.5 33.9L37.5 18.1\" stroke=\"#c9a43e\" strokeWidth=\"0.9\" pathLength={1} />\r\n <circle className=\"fg-emblem-dot\" cx=\"10.5\" cy=\"33.9\" r=\"1.6\" fill=\"#c9a43e\" />\r\n <circle className=\"fg-emblem-dot fg-emblem-dot-2\" cx=\"37.5\" cy=\"18.1\" r=\"1.6\" fill=\"#c9a43e\" />\r\n <g className=\"fg-emblem-lens\">\r\n <circle cx=\"24\" cy=\"26\" r=\"7.4\" fill=\"#24282d\" />\r\n <circle cx=\"24\" cy=\"26\" r=\"5.3\" stroke=\"#c9a43e\" strokeWidth=\"1.6\" />\r\n <circle cx=\"24\" cy=\"26\" r=\"2.6\" fill=\"#15181b\" stroke=\"#c9a43e\" strokeWidth=\"0.8\" />\r\n <circle cx=\"23\" cy=\"25\" r=\"0.8\" fill=\"#f1dc93\" />\r\n </g>\r\n </svg>\r\n );\r\n}\r\n","// Enquadramento automático da webcam (como o \"Enquadramento Centralizado\" do iPad): um recorte da imagem da\n// câmera, na mesma proporção, que acompanha o rosto e o mantém no centro. A prévia mostra o recorte e é ele que\n// vai para a validação, então a foto já sai centralizada. Funções puras (testáveis) + a medição da área útil.\n\nimport type { FaceBox } from \"../core/types\";\n\n/** Recorte: canto (`x`, `y`) e lado (`s`) em fração da imagem; `s` = 1 é a imagem inteira. */\nexport type Crop = { x: number; y: number; s: number };\n/** Área útil da imagem (fração do quadro): sem as tarjas pretas que algumas câmeras/drivers acrescentam. */\nexport type Area = { x: number; y: number; w: number; h: number };\n\nexport const FULL: Crop = { x: 0, y: 0, s: 1 };\nexport const WHOLE: Area = { x: 0, y: 0, w: 1, h: 1 };\n/** O rosto mira ~40% da altura do quadro (o analyzer pede área mínima de 6%). */\nexport const FACE_H_TARGET = 0.4;\n/** Zoom de até 2x (a câmera abre em 1280×960 para o recorte não perder nitidez). */\nexport const MIN_CROP = 0.5;\n/** Só muda o alvo se ele andar mais que isso: parado, sem tremor (a janela estável da captura conta). */\nexport const RETARGET = 0.03;\n/** Na webcam, captura só com o rosto realmente no centro: tolerância de 8% (o padrão do analyzer é 22%). */\nexport const CENTER_TOLERANCE = 0.08;\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);\n\n/** Maior recorte (proporção do quadro) dentro da área útil, centralizado nela: o quadro \"sem rosto\". */\nexport function baseCrop(a: Area): Crop {\n const s = Math.min(a.w, a.h);\n return { s, x: a.x + (a.w - s) / 2, y: a.y + (a.h - s) / 2 };\n}\n\n/** Prende o recorte dentro da área útil (tamanho e posição). */\nexport function fitCrop(c: Crop, a: Area): Crop {\n const s = Math.min(c.s, a.w, a.h);\n return { s, x: clamp(c.x, a.x, a.x + a.w - s), y: clamp(c.y, a.y, a.y + a.h - s) };\n}\n\n/**\n * Alvo do recorte para o rosto `box`, em coordenadas do recorte `used` (o do quadro analisado): centraliza o\n * rosto e o deixa com ~40% da altura, sem sair da área útil.\n */\nexport function aimCrop(box: FaceBox, used: Crop, a: Area): Crop {\n const cx = used.x + ((box.xmin + box.xmax) / 2) * used.s;\n const cy = used.y + ((box.ymin + box.ymax) / 2) * used.s;\n const maxS = Math.min(a.w, a.h);\n const s = clamp(((box.ymax - box.ymin) * used.s) / FACE_H_TARGET, Math.min(MIN_CROP, maxS), maxS);\n return fitCrop({ s, x: cx - s / 2, y: cy - s / 2 }, a);\n}\n\n/** Andou o bastante para valer trocar o alvo? */\nexport const moved = (a: Crop, b: Crop) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.s - b.s) > RETARGET;\n\n/** Um passo do movimento suave do recorte atual `c` até o alvo `t`. */\nexport const ease = (c: Crop, t: Crop, k = 0.12): Crop => ({\n x: c.x + (t.x - c.x) * k,\n y: c.y + (t.y - c.y) * k,\n s: c.s + (t.s - c.s) * k,\n});\n\n/**\n * CSS da prévia do recorte, para uma camada com `transform-origin: 0 0` (no estilo, não em classe) e o vídeo\n * ocupando-a inteira. `mirror`: como um espelho (a pessoa se orienta melhor); o quadro enviado não é espelhado.\n */\nexport function cropTransform(c: Crop, mirror = true): string {\n const k = 1 / c.s;\n const move = `translate(${-c.x * 100}%, ${-c.y * 100}%)`;\n return mirror ? `translateX(100%) scale(${-k}, ${k}) ${move}` : `scale(${k}) ${move}`;\n}\n\n/** Caixa do rosto na prévia (espelhada, se for o caso), em % da camada. */\nexport function boxStyle(b: FaceBox, mirror: boolean) {\n const left = mirror ? 1 - b.xmax : b.xmin;\n return {\n left: `${left * 100}%`,\n top: `${b.ymin * 100}%`,\n width: `${(b.xmax - b.xmin) * 100}%`,\n height: `${(b.ymax - b.ymin) * 100}%`,\n };\n}\n\n/**\n * Área útil numa miniatura RGBA (`px`, `w` × `h`): colunas/linhas das bordas sem nenhum ponto claro são tarja.\n * Ambiente escuro (menos de 40% útil) devolve a imagem inteira.\n */\nexport function areaFromPixels(px: Uint8ClampedArray, w: number, h: number): Area {\n const lit = (x: number, y: number) => {\n const i = (y * w + x) * 4;\n return Math.max(px[i] ?? 0, px[i + 1] ?? 0, px[i + 2] ?? 0) > 24;\n };\n const colLit = (x: number) => Array.from({ length: h }, (_, y) => lit(x, y)).some(Boolean);\n const rowLit = (y: number) => Array.from({ length: w }, (_, x) => lit(x, y)).some(Boolean);\n let x0 = 0;\n let x1 = w - 1;\n let y0 = 0;\n let y1 = h - 1;\n while (x0 < x1 && !colLit(x0)) x0++;\n while (x1 > x0 && !colLit(x1)) x1--;\n while (y0 < y1 && !rowLit(y0)) y0++;\n while (y1 > y0 && !rowLit(y1)) y1--;\n if (x0 === 0 && x1 === w - 1 && y0 === 0 && y1 === h - 1) return WHOLE;\n // 1 px de margem da miniatura: a borda da tarja costuma ter um degradê\n const area = { x: (x0 + 1) / w, y: (y0 + 1) / h, w: Math.max((x1 - x0 - 1) / w, 0), h: Math.max((y1 - y0 - 1) / h, 0) };\n return area.w < 0.4 || area.h < 0.4 ? WHOLE : area;\n}\n\n/** Imagem quase toda preta (média < 8 de 255)? Câmera que não entrega imagem na resolução pedida. */\nexport function isDark(px: Uint8ClampedArray): boolean {\n let sum = 0;\n let n = 0;\n for (let i = 0; i < px.length; i += 4 * 97) {\n sum += (px[i] ?? 0) + (px[i + 1] ?? 0) + (px[i + 2] ?? 0);\n n += 3;\n }\n return n > 0 && sum / n < 8;\n}\n\n/** Limiares da webcam: centro com tolerância de 8% (ou a que o integrador pediu, se for mais rígida). */\nexport function framingThresholds(th: Record<string, number> | undefined): Record<string, number> {\n const t = th ?? {};\n return {\n ...t,\n CENTER_TOL_X: Math.min(t.CENTER_TOL_X ?? CENTER_TOLERANCE, CENTER_TOLERANCE),\n CENTER_TOL_Y: Math.min(t.CENTER_TOL_Y ?? CENTER_TOLERANCE, CENTER_TOLERANCE),\n };\n}\n","/** Textos do componente (pt-BR). Passe `labels` no <FaceGate> para trocar qualquer um. */\r\nexport const DEFAULT_LABELS = {\r\n start: \"Ligar câmera\",\r\n stop: \"Desligar câmera\",\r\n retry: \"Nova captura\",\r\n connecting: \"Conectando…\",\r\n searching: \"Procurando rosto…\",\r\n hold: \"Fique parado…\",\r\n allGood: \"Tudo certo, segure a posição…\",\r\n fix: \"Corrigir\",\r\n analyzing: \"Analisando o rosto…\",\r\n analyzingClean: \"Analisando o rosto e removendo o fundo…\",\r\n analyzingEnhance: \"Melhorando a imagem e analisando o rosto…\",\r\n enhanced: \"Imagem melhorada\",\r\n enhancedAi: \"Rosto restaurado com IA: confira se a foto continua fiel à pessoa.\",\r\n approved: \"Foto aprovada\",\r\n rejected: \"Foto reprovada\",\r\n cameraOff: \"Câmera desligada\",\r\n cameraHint: \"A foto é capturada sozinha quando todos os critérios passam.\",\r\n insecure: \"A câmera exige HTTPS (ou localhost).\",\r\n noCamera: \"Este navegador não permite acesso à câmera.\",\r\n denied: \"Permissão de câmera negada. Libere o acesso na barra do navegador.\",\r\n notFound: \"Nenhuma câmera encontrada.\",\r\n busy: \"A câmera está em uso por outro aplicativo. Feche-o e tente de novo.\",\r\n};\r\n\r\nexport type FaceGateLabels = typeof DEFAULT_LABELS;\r\n\r\n/** Critério reprovado (hard_fails da API) → orientação curta para quem está na câmera. */\r\nexport const FAIL_LABELS: Record<string, string> = {\r\n no_face: \"enquadre o rosto\",\r\n multiple_faces: \"só uma pessoa na imagem\",\r\n face_too_small: \"aproxime-se da câmera\",\r\n face_off_center: \"centralize o rosto\",\r\n eyes_closed: \"abra os olhos\",\r\n not_looking_at_camera: \"olhe para a câmera\",\r\n head_too_tilted: \"endireite a cabeça\",\r\n image_blurry: \"fique parado (imagem tremida)\",\r\n poor_lighting: \"melhore a luz no rosto\",\r\n overexposed: \"luz forte demais no rosto\",\r\n glasses_sunglasses: \"retire os óculos escuros\",\r\n helmet_detected: \"retire o capacete\",\r\n covered_face: \"descubra o rosto\",\r\n smile_detected: \"expressão neutra\",\r\n};\r\n\r\nexport function describeFails(codes: string[] = []): string {\r\n return codes.map((c) => FAIL_LABELS[c] ?? c).join(\", \");\r\n}\r\n"],"mappings":"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","names":["jsx","jsxs","box","k"]}
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server/index.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
24
+ FaceGateError: () => FaceGateError,
25
+ SDK_USER_AGENT: () => SDK_USER_AGENT,
26
+ createFaceGate: () => createFaceGate
27
+ });
28
+ module.exports = __toCommonJS(server_exports);
29
+
30
+ // src/core/types.ts
31
+ var FaceGateError = class extends Error {
32
+ constructor(code, message, status, resetsAt) {
33
+ super(message);
34
+ this.code = code;
35
+ this.status = status;
36
+ this.resetsAt = resetsAt;
37
+ this.name = "FaceGateError";
38
+ }
39
+ };
40
+
41
+ // package.json
42
+ var version = "0.1.0";
43
+
44
+ // src/server/index.ts
45
+ var DEFAULT_BASE_URL = "https://face-gate-api.beltec.ai";
46
+ var SDK_USER_AGENT = (() => {
47
+ const proc = globalThis.process;
48
+ const runtime = proc?.versions?.node ? `node ${proc.versions.node}; ${proc.platform ?? "?"}` : "edge";
49
+ return `face-gate-sdk/${version} (${runtime})`;
50
+ })();
51
+ var toBlob = (image) => image instanceof Blob ? image : new Blob([image]);
52
+ var STATUS_CODE = {
53
+ 400: "bad_request",
54
+ 401: "unauthorized",
55
+ 402: "token_month_limit",
56
+ 403: "forbidden",
57
+ 413: "bad_request",
58
+ 429: "token_window_limit"
59
+ };
60
+ async function toError(res) {
61
+ const body = await res.json().catch(() => null);
62
+ const detail = typeof body?.detail === "string" ? body.detail : `Erro ${res.status} na API do Face Gate`;
63
+ const resets = body?.resets_at ?? body?.resets_on;
64
+ return new FaceGateError(STATUS_CODE[res.status] ?? "server", detail, res.status, resets);
65
+ }
66
+ function createFaceGate(options) {
67
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
68
+ const doFetch = options.fetch ?? fetch;
69
+ if (!options.clientId || !options.clientSecret) {
70
+ throw new FaceGateError("unauthorized", "Informe clientId e clientSecret da chave do Face Gate");
71
+ }
72
+ let cached = null;
73
+ async function call(path, init) {
74
+ try {
75
+ const headers = { "User-Agent": SDK_USER_AGENT, ...init.headers };
76
+ return await doFetch(baseUrl + path, { ...init, headers, cache: "no-store" });
77
+ } catch (e) {
78
+ throw new FaceGateError("network", `N\xE3o foi poss\xEDvel conectar ao Face Gate (${e.message})`);
79
+ }
80
+ }
81
+ async function accessToken(force = false) {
82
+ if (!force && cached && cached.expires > Date.now() + 3e4) return cached.token;
83
+ const body = new URLSearchParams({
84
+ grant_type: "client_credentials",
85
+ client_id: options.clientId,
86
+ client_secret: options.clientSecret
87
+ });
88
+ const res = await call("/v1/auth/token", {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
91
+ body
92
+ });
93
+ if (!res.ok) throw await toError(res);
94
+ const j = await res.json();
95
+ cached = { token: j.access_token, expires: Date.now() + j.expires_in * 1e3 };
96
+ return cached.token;
97
+ }
98
+ async function authed(path, init) {
99
+ let res = await call(path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${await accessToken()}` } });
100
+ if (res.status === 401) {
101
+ res = await call(path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${await accessToken(true)}` } });
102
+ }
103
+ return res;
104
+ }
105
+ async function createStreamTicket() {
106
+ const res = await authed("/v1/stream-ticket", { method: "POST" });
107
+ if (!res.ok) throw await toError(res);
108
+ const j = await res.json();
109
+ return { ticket: j.ticket, url: baseUrl.replace(/^http/, "ws") + "/v1/stream", expiresIn: j.expires_in };
110
+ }
111
+ async function validate(image, opts = {}) {
112
+ const form = new FormData();
113
+ form.append("file", toBlob(image), opts.filename ?? "foto.jpg");
114
+ for (const [k, v] of Object.entries(opts.thresholds ?? {})) form.append(k, String(v));
115
+ if (opts.enhance) {
116
+ form.append("enhance", opts.enhance);
117
+ if (opts.clean) form.append("clean", "true");
118
+ }
119
+ if (opts.clean || opts.enhance) {
120
+ if (opts.clean && opts.model) form.append("model", opts.model);
121
+ if (opts.device) form.append("device", opts.device);
122
+ }
123
+ const path = opts.enhance ? "/v1/photos/validate-and-enhance" : opts.clean ? "/v1/photos/validate-and-clean" : "/v1/photos/validate";
124
+ const res = await authed(path, { method: "POST", body: form });
125
+ if (!res.ok && res.status !== 422) throw await toError(res);
126
+ return await res.json();
127
+ }
128
+ async function avatar(image, opts = {}) {
129
+ const form = new FormData();
130
+ form.append("file", toBlob(image), opts.filename ?? "foto.jpg");
131
+ if (opts.template != null) form.append("template", String(opts.template));
132
+ if (opts.shape || opts.template == null) form.append("shape", opts.shape ?? "round");
133
+ if (opts.size != null) form.append("size", String(opts.size));
134
+ if (opts.background) {
135
+ form.append("background", toBlob(opts.background), "fundo.jpg");
136
+ const pos = opts.backgroundPosition;
137
+ if (pos?.x != null) form.append("background_x", String(pos.x));
138
+ if (pos?.y != null) form.append("background_y", String(pos.y));
139
+ if (pos?.zoom != null) form.append("background_zoom", String(pos.zoom));
140
+ } else if (opts.backgroundColor) form.append("background_color", opts.backgroundColor);
141
+ if (opts.background || opts.backgroundColor || opts.template != null) {
142
+ if (opts.margin != null) form.append("margin", String(opts.margin));
143
+ if (opts.layout) form.append("layout", opts.layout);
144
+ }
145
+ if (opts.model) form.append("model", opts.model);
146
+ if (opts.device) form.append("device", opts.device);
147
+ for (const [k, v] of Object.entries(opts.thresholds ?? {})) form.append(k, String(v));
148
+ const res = await authed("/v1/photos/avatar", { method: "POST", body: form });
149
+ if (!res.ok && res.status !== 422) throw await toError(res);
150
+ return await res.json();
151
+ }
152
+ function ticketHandler(handlerOptions = {}) {
153
+ return async (req) => {
154
+ if (handlerOptions.authorize && !await handlerOptions.authorize(req)) {
155
+ return Response.json({ detail: "N\xE3o autorizado" }, { status: 401 });
156
+ }
157
+ try {
158
+ return Response.json(await createStreamTicket(), { headers: { "Cache-Control": "no-store" } });
159
+ } catch (e) {
160
+ const err = e;
161
+ return Response.json(
162
+ { detail: err.message, code: err.code, resets_at: err.resetsAt },
163
+ { status: err.status && err.status >= 400 ? err.status : 502 }
164
+ );
165
+ }
166
+ };
167
+ }
168
+ return { accessToken, createStreamTicket, validate, avatar, ticketHandler };
169
+ }
170
+ // Annotate the CommonJS export names for ESM import in node:
171
+ 0 && (module.exports = {
172
+ DEFAULT_BASE_URL,
173
+ FaceGateError,
174
+ SDK_USER_AGENT,
175
+ createFaceGate
176
+ });
177
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/index.ts","../src/core/types.ts","../package.json"],"sourcesContent":["/**\n * @beltec/face-gate/server — uso SÓ no servidor (Node 18+, Next.js Route Handlers, Express…).\n *\n * Guarda o client_secret da chave e conversa com a API do Face Gate:\n * - `createStreamTicket()`: ticket de uso único (60 s) para o navegador abrir a webcam (<FaceGate>);\n * - `validate()`: valida um arquivo de foto (opcionalmente melhora a imagem e/ou remove o fundo);\n * - `avatar()`: valida e, se aprovada, devolve o avatar (só a pessoa, quadrado ou redondo, sobre o fundo pedido);\n * - `ticketHandler()`: Route Handler pronto para o Next (POST /api/face-gate/ticket).\n * O secret e o access token nunca vão ao navegador; o navegador só recebe o ticket.\n */\nimport {\n FaceGateError,\n type AvatarLayout,\n type AvatarShape,\n type FaceGateErrorCode,\n type StreamOptions,\n type StreamTicket,\n type ValidationResult,\n} from \"../core/types\";\nimport { version } from \"../../package.json\";\n\nexport * from \"../core/types\";\n\nexport const DEFAULT_BASE_URL = \"https://face-gate-api.beltec.ai\";\n\n/**\n * User-Agent das chamadas do SDK: \"face-gate-sdk/0.1.0 (node 22.11.0; linux)\". O Face Gate mostra \"SDK Face Gate\n * 0.1.0\" no portal (ex.: nas tentativas de IP não liberado), em vez do fetch genérico do Node. A versão vem do\n * package.json no build.\n */\nexport const SDK_USER_AGENT = (() => {\n const proc = (globalThis as { process?: { versions?: { node?: string }; platform?: string } }).process;\n const runtime = proc?.versions?.node ? `node ${proc.versions.node}; ${proc.platform ?? \"?\"}` : \"edge\";\n return `face-gate-sdk/${version} (${runtime})`;\n})();\n\nexport type FaceGateServerOptions = {\n /** key_id da chave criada no portal do Face Gate. */\n clientId: string;\n clientSecret: string;\n /** URL da API (padrão: produção). */\n baseUrl?: string;\n /** fetch alternativo (testes, proxies). */\n fetch?: typeof fetch;\n};\n\nexport type ValidateOptions = StreamOptions & {\n /** Nome do arquivo enviado (o tipo é deduzido do conteúdo). */\n filename?: string;\n};\n\ntype ImageInput = Blob | ArrayBuffer | Uint8Array;\n\nexport type AvatarOptions = {\n /**\n * Id de um template de avatar salvo no portal (Avatares): formato, tamanho, fundo e enquadramento prontos, sem\n * reenviar a imagem de fundo. As outras opções passadas aqui têm prioridade; `background`/`backgroundColor`\n * trocam o fundo do template.\n */\n template?: number;\n /** \"round\" (padrão; fora do círculo transparente) ou \"square\". */\n shape?: AvatarShape;\n /** Lado em pixels, de 64 a 1024 (padrão 512). */\n size?: number;\n /** Fundo em cor sólida, \"#RRGGBB\". Sem fundo nenhum, o PNG sai transparente. */\n backgroundColor?: string;\n /** Imagem de fundo (ajustada para preencher o quadro); tem prioridade sobre `backgroundColor`. */\n background?: ImageInput;\n /**\n * Só com `background` (imagem): enquadramento do fundo. `x`/`y` de 0 a 100 escolhem qual parte da imagem\n * aparece (50/50 = centro, padrão; como o object-position do CSS); `zoom` de 100 a 300 % amplia além do\n * preenchimento.\n */\n backgroundPosition?: { x?: number; y?: number; zoom?: number };\n /**\n * Só com fundo (`backgroundColor` ou `background`): % do quadro deixado para o fundo em volta da pessoa,\n * de 0 (cabeça e ombros justos, padrão) a 60. Com fundo transparente é ignorado.\n */\n margin?: number;\n /** Só com fundo: onde a pessoa fica — \"center\" (padrão), \"bottom\", \"left\" ou \"right\". */\n layout?: AvatarLayout;\n /** Modelo de remoção de fundo que separa a pessoa (padrão do servidor no avatar: BiRefNet). */\n model?: StreamOptions[\"model\"];\n device?: StreamOptions[\"device\"];\n thresholds?: Record<string, number>;\n filename?: string;\n};\n\nconst toBlob = (image: ImageInput) => (image instanceof Blob ? image : new Blob([image as BlobPart]));\n\ntype Json = Record<string, unknown>;\n\nconst STATUS_CODE: Record<number, FaceGateErrorCode> = {\n 400: \"bad_request\",\n 401: \"unauthorized\",\n 402: \"token_month_limit\",\n 403: \"forbidden\",\n 413: \"bad_request\",\n 429: \"token_window_limit\",\n};\n\nasync function toError(res: Response): Promise<FaceGateError> {\n const body = (await res.json().catch(() => null)) as Json | null;\n const detail = typeof body?.detail === \"string\" ? body.detail : `Erro ${res.status} na API do Face Gate`;\n const resets = (body?.resets_at ?? body?.resets_on) as string | undefined;\n return new FaceGateError(STATUS_CODE[res.status] ?? \"server\", detail, res.status, resets);\n}\n\nexport function createFaceGate(options: FaceGateServerOptions) {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? fetch;\n if (!options.clientId || !options.clientSecret) {\n throw new FaceGateError(\"unauthorized\", \"Informe clientId e clientSecret da chave do Face Gate\");\n }\n let cached: { token: string; expires: number } | null = null;\n\n async function call(path: string, init: RequestInit): Promise<Response> {\n try {\n const headers = { \"User-Agent\": SDK_USER_AGENT, ...(init.headers as Record<string, string> | undefined) };\n return await doFetch(baseUrl + path, { ...init, headers, cache: \"no-store\" });\n } catch (e) {\n throw new FaceGateError(\"network\", `Não foi possível conectar ao Face Gate (${(e as Error).message})`);\n }\n }\n\n /** Access token da chave (15 min), reaproveitado até 30 s antes de expirar. */\n async function accessToken(force = false): Promise<string> {\n if (!force && cached && cached.expires > Date.now() + 30_000) return cached.token;\n const body = new URLSearchParams({ grant_type: \"client_credentials\", client_id: options.clientId,\n client_secret: options.clientSecret });\n const res = await call(\"/v1/auth/token\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n });\n if (!res.ok) throw await toError(res);\n const j = (await res.json()) as { access_token: string; expires_in: number };\n cached = { token: j.access_token, expires: Date.now() + j.expires_in * 1000 };\n return cached.token;\n }\n\n /** Chamada autenticada; um 401 (token revogado/expirado) renova o token e tenta uma vez de novo. */\n async function authed(path: string, init: RequestInit): Promise<Response> {\n let res = await call(path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${await accessToken()}` } });\n if (res.status === 401) {\n res = await call(path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${await accessToken(true)}` } });\n }\n return res;\n }\n\n /** Ticket para o navegador (uso único, 60 s). A whitelist de IP vale aqui, no IP do seu servidor. */\n async function createStreamTicket(): Promise<StreamTicket> {\n const res = await authed(\"/v1/stream-ticket\", { method: \"POST\" });\n if (!res.ok) throw await toError(res);\n const j = (await res.json()) as { ticket: string; expires_in: number };\n return { ticket: j.ticket, url: baseUrl.replace(/^http/, \"ws\") + \"/v1/stream\", expiresIn: j.expires_in };\n }\n\n /**\n * Valida uma foto (JPG, PNG ou WebP até 10 MB). Com `clean`, devolve também `cleaned_image`.\n * Com `enhance` (\"classic\" | \"gfpgan\"), melhora a foto e valida a versão melhorada\n * (POST /v1/photos/validate-and-enhance): o resultado traz `enhancement` com antes/depois.\n */\n async function validate(image: ImageInput, opts: ValidateOptions = {}): Promise<ValidationResult> {\n const form = new FormData();\n form.append(\"file\", toBlob(image), opts.filename ?? \"foto.jpg\");\n for (const [k, v] of Object.entries(opts.thresholds ?? {})) form.append(k, String(v));\n if (opts.enhance) {\n form.append(\"enhance\", opts.enhance);\n if (opts.clean) form.append(\"clean\", \"true\");\n }\n if (opts.clean || opts.enhance) {\n if (opts.clean && opts.model) form.append(\"model\", opts.model);\n if (opts.device) form.append(\"device\", opts.device);\n }\n const path = opts.enhance ? \"/v1/photos/validate-and-enhance\"\n : opts.clean ? \"/v1/photos/validate-and-clean\" : \"/v1/photos/validate\";\n const res = await authed(path, { method: \"POST\", body: form });\n // 422 = erro ao processar a imagem: a API devolve o resultado com `error`\n if (!res.ok && res.status !== 422) throw await toError(res);\n return (await res.json()) as ValidationResult;\n }\n\n /**\n * Avatar (serviço \"avatar\" contratado, POST /v1/photos/avatar): valida a foto e, se aprovada, devolve em\n * `avatar_image` (PNG, data URL) só a pessoa do rosto em foco — fundo e outros elementos/pessoas removidos —,\n * recortada centrada no rosto, sobre `background` (imagem), `backgroundColor` ou transparente.\n * Com a foto reprovada não há avatar: `accepted` = false e os motivos em `hard_fails`.\n */\n async function avatar(image: ImageInput, opts: AvatarOptions = {}): Promise<ValidationResult> {\n const form = new FormData();\n form.append(\"file\", toBlob(image), opts.filename ?? \"foto.jpg\");\n if (opts.template != null) form.append(\"template\", String(opts.template));\n // com template, o formato vem dele (só vai no form se for pedido explicitamente)\n if (opts.shape || opts.template == null) form.append(\"shape\", opts.shape ?? \"round\");\n if (opts.size != null) form.append(\"size\", String(opts.size));\n if (opts.background) {\n form.append(\"background\", toBlob(opts.background), \"fundo.jpg\");\n const pos = opts.backgroundPosition;\n if (pos?.x != null) form.append(\"background_x\", String(pos.x));\n if (pos?.y != null) form.append(\"background_y\", String(pos.y));\n if (pos?.zoom != null) form.append(\"background_zoom\", String(pos.zoom));\n } else if (opts.backgroundColor) form.append(\"background_color\", opts.backgroundColor);\n if (opts.background || opts.backgroundColor || opts.template != null) {\n // margem e layout só existem com fundo (transparente = recorte justo); o template pode ter fundo\n if (opts.margin != null) form.append(\"margin\", String(opts.margin));\n if (opts.layout) form.append(\"layout\", opts.layout);\n }\n if (opts.model) form.append(\"model\", opts.model);\n if (opts.device) form.append(\"device\", opts.device);\n for (const [k, v] of Object.entries(opts.thresholds ?? {})) form.append(k, String(v));\n const res = await authed(\"/v1/photos/avatar\", { method: \"POST\", body: form });\n if (!res.ok && res.status !== 422) throw await toError(res);\n return (await res.json()) as ValidationResult;\n }\n\n /**\n * Route Handler para o App Router do Next.js:\n * // app/api/face-gate/ticket/route.ts\n * export const POST = faceGate.ticketHandler({ authorize: async (req) => !!(await getSession()) });\n * `authorize` decide quem pode abrir a câmera (ex.: só usuários logados no SEU sistema).\n */\n function ticketHandler(handlerOptions: { authorize?: (req: Request) => boolean | Promise<boolean> } = {}) {\n return async (req: Request): Promise<Response> => {\n if (handlerOptions.authorize && !(await handlerOptions.authorize(req))) {\n return Response.json({ detail: \"Não autorizado\" }, { status: 401 });\n }\n try {\n return Response.json(await createStreamTicket(), { headers: { \"Cache-Control\": \"no-store\" } });\n } catch (e) {\n const err = e as FaceGateError;\n return Response.json({ detail: err.message, code: err.code, resets_at: err.resetsAt },\n { status: err.status && err.status >= 400 ? err.status : 502 });\n }\n };\n }\n\n return { accessToken, createStreamTicket, validate, avatar, ticketHandler };\n}\n\nexport type FaceGateServer = ReturnType<typeof createFaceGate>;\n","/** Caixa do rosto dominante, em frações da imagem (0 a 1). */\nexport type FaceBox = { xmin: number; ymin: number; xmax: number; ymax: number };\n\n/** Tokens de uma análise (unidade de processamento do Face Gate: megapixels × peso de cada etapa). */\nexport type TokenUsage = {\n tokens: number;\n /** `validate` conta em dobro com melhoria (valida antes e depois); `enhance` = custo da melhoria. */\n breakdown: { decode: number; validate: number; clean: number; enhance?: number; avatar?: number };\n /** Saldo da janela do plano (ex.: 5 h a partir do 1º uso). */\n window?: { used: number; limit: number; hours: number; started_on: string | null; resets_at: string | null };\n /** Saldo do teto mensal (renova no aniversário do contrato). */\n month?: { used: number; limit: number; cycle_start: string; cycle_end: string };\n};\n\n/**\n * Melhoria de imagem (serviço \"validate_enhance\"):\n * - \"classic\": luz, cor, contraste, ruído e nitidez leve; determinística, não altera o rosto;\n * - \"gfpgan\": a clássica + restauração do rosto com IA (recria detalhes; avisa `face_restored_ai`).\n */\nexport type EnhanceMethod = \"classic\" | \"gfpgan\";\n\n/** Resumo de uma validação dentro de `enhancement` (antes = foto original, depois = melhorada). */\nexport type ValidationSummary = { accepted: boolean; score: number | null; hard_fails: string[] };\n\nexport type Enhancement = {\n method: EnhanceMethod;\n /** Etapas aplicadas: white_balance, exposure, local_contrast, denoise, sharpen, face_restoration. */\n steps: string[];\n /** Dispositivo do GFPGAN (null na clássica). */\n device: \"cpu\" | \"gpu\" | null;\n before: ValidationSummary;\n after: ValidationSummary;\n /** Falhas da original que a melhoria não corrige (desfoque, olhos fechados…): continuam reprovando. */\n carried_over: string[];\n /** Foto melhorada (JPEG em data URL). */\n enhanced_image: string;\n};\n\n/** Formato do avatar (serviço \"avatar\"): redondo (fora do círculo transparente) ou quadrado. */\nexport type AvatarShape = \"square\" | \"round\";\n\n/**\n * Onde a pessoa fica no avatar (só com fundo de cor ou imagem): centro; apoiada na base (o fundo aparece em\n * cima); na base à esquerda ou à direita.\n */\nexport type AvatarLayout = \"center\" | \"bottom\" | \"left\" | \"right\";\n\n/** Dados do avatar gerado (POST /v1/photos/avatar). */\nexport type AvatarInfo = {\n shape: AvatarShape;\n /** Lado em pixels (64 a 1024). */\n size: number;\n /** Modelo de remoção de fundo que separou a pessoa. */\n model: string;\n /** \"transparent\", \"image\" (fundo enviado) ou a cor \"#rrggbb\". */\n background: string;\n /** % do quadro deixado para o fundo (0 com fundo transparente). */\n margin?: number;\n layout?: AvatarLayout;\n};\n\n/** Avisos da validação. `face_restored_ai`: o GFPGAN recriou detalhes do rosto. */\nexport type ValidationWarning = \"face_restored_ai\" | \"gpu_fallback_cpu\" | (string & {});\n\n/** Resultado da validação de uma foto (mesmo formato de POST /v1/photos/*). */\nexport type ValidationResult = {\n accepted: boolean;\n score: number;\n checks: Record<string, boolean | null>;\n /** Critérios que reprovaram (ex.: \"eyes_closed\", \"sunglasses\", \"low_light\"). */\n hard_fails?: string[];\n reasons?: string[];\n warnings?: ValidationWarning[];\n error?: string;\n dominant_face_bbox?: FaceBox | null;\n /** Foto com o fundo removido, recortada no rosto (PNG em data URL), quando pedido `clean`. */\n cleaned_image?: string;\n rembg_model?: string;\n device?: \"cpu\" | \"gpu\";\n timings?: { validation_ms?: number };\n usage?: TokenUsage;\n /** Presente quando a foto passou pela melhoria (`enhance`). */\n enhancement?: Enhancement;\n /** Avatar (só com a foto aprovada): PNG em data URL só com a pessoa do rosto em foco. */\n avatar_image?: string;\n avatar?: AvatarInfo;\n};\n\nexport type StreamOptions = {\n /** Remover o fundo da foto capturada (serviço \"validate_clean\" contratado). */\n clean?: boolean;\n model?: \"u2net\" | \"u2netp\" | \"birefnet-portrait\";\n device?: \"cpu\" | \"gpu\";\n /** Limiares de validação (mesmos nomes de GET /v1/thresholds). */\n thresholds?: Record<string, number>;\n /**\n * Melhorar a foto capturada antes de validar (serviço \"validate_enhance\" contratado). Os quadros ao\n * vivo não passam pela melhoria. null/undefined = sem melhoria.\n */\n enhance?: EnhanceMethod | null;\n};\n\n/** Ticket de uso único (60 s) para o navegador abrir a webcam, emitido pelo SEU servidor. */\nexport type StreamTicket = { ticket: string; url: string; expiresIn?: number };\n\n/** Há quanto tempo os quadros seguidos estão aprovados e parados, e o alvo para capturar. */\nexport type Stable = { ms: number; needed: number };\n\nexport type FaceGateErrorCode =\n | \"unauthorized\" // 401/4401: chave, token ou ticket inválido/expirado\n | \"forbidden\" // 403/4403: serviço não contratado, chave bloqueada, fora do contrato ou IP não liberado\n | \"token_month_limit\" // 402/4402: teto mensal de tokens atingido\n | \"token_window_limit\" // 429/4429: janela de tokens esgotada (ou muitas requisições)\n | \"bad_request\" // 400/4400\n | \"camera_denied\"\n | \"camera_unavailable\"\n | \"network\"\n | \"server\";\n\nexport class FaceGateError extends Error {\n constructor(\n public code: FaceGateErrorCode,\n message: string,\n public status?: number,\n /** Quando o limite renova (ISO), se a API informou. */\n public resetsAt?: string,\n ) {\n super(message);\n this.name = \"FaceGateError\";\n }\n}\n","{\n \"name\": \"@beltec/face-gate\",\n \"version\": \"0.1.0\",\n \"description\": \"Face Gate, uma solução BelTec: validação de fotos de rosto para controle de acesso — componente React de webcam em tempo real e helper de servidor (Node/Next).\",\n \"license\": \"UNLICENSED\",\n \"author\": \"BelTec · grupo Belfort\",\n \"homepage\": \"https://face-gate.beltec.ai\",\n \"keywords\": [\"face-gate\", \"beltec\", \"webcam\", \"face\", \"validation\", \"react\", \"nextjs\", \"controle de acesso\"],\n \"type\": \"module\",\n \"sideEffects\": [\"*.css\"],\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"./server\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\",\n \"require\": \"./dist/server.cjs\"\n },\n \"./styles.css\": \"./dist/styles.css\",\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\"dist\", \"README.md\"],\n \"engines\": {\n \"node\": \">=18.17\"\n },\n \"scripts\": {\n \"build\": \"tsup && node -e \\\"require('node:fs').copyFileSync('src/react/styles.css','dist/styles.css')\\\"\",\n \"dev\": \"tsup --watch\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"prepublishOnly\": \"npm run typecheck && npm test && npm run build\"\n },\n \"peerDependencies\": {\n \"react\": \">=18\",\n \"react-dom\": \">=18\"\n },\n \"peerDependenciesMeta\": {\n \"react\": { \"optional\": true },\n \"react-dom\": { \"optional\": true }\n },\n \"devDependencies\": {\n \"@types/node\": \"^20.14.0\",\n \"@types/react\": \"^19.0.0\",\n \"@types/react-dom\": \"^19.0.0\",\n \"jsdom\": \"^25.0.1\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.6.3\",\n \"vitest\": \"^2.1.8\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,223 @@
1
+ /** Caixa do rosto dominante, em frações da imagem (0 a 1). */
2
+ type FaceBox = {
3
+ xmin: number;
4
+ ymin: number;
5
+ xmax: number;
6
+ ymax: number;
7
+ };
8
+ /** Tokens de uma análise (unidade de processamento do Face Gate: megapixels × peso de cada etapa). */
9
+ type TokenUsage = {
10
+ tokens: number;
11
+ /** `validate` conta em dobro com melhoria (valida antes e depois); `enhance` = custo da melhoria. */
12
+ breakdown: {
13
+ decode: number;
14
+ validate: number;
15
+ clean: number;
16
+ enhance?: number;
17
+ avatar?: number;
18
+ };
19
+ /** Saldo da janela do plano (ex.: 5 h a partir do 1º uso). */
20
+ window?: {
21
+ used: number;
22
+ limit: number;
23
+ hours: number;
24
+ started_on: string | null;
25
+ resets_at: string | null;
26
+ };
27
+ /** Saldo do teto mensal (renova no aniversário do contrato). */
28
+ month?: {
29
+ used: number;
30
+ limit: number;
31
+ cycle_start: string;
32
+ cycle_end: string;
33
+ };
34
+ };
35
+ /**
36
+ * Melhoria de imagem (serviço "validate_enhance"):
37
+ * - "classic": luz, cor, contraste, ruído e nitidez leve; determinística, não altera o rosto;
38
+ * - "gfpgan": a clássica + restauração do rosto com IA (recria detalhes; avisa `face_restored_ai`).
39
+ */
40
+ type EnhanceMethod = "classic" | "gfpgan";
41
+ /** Resumo de uma validação dentro de `enhancement` (antes = foto original, depois = melhorada). */
42
+ type ValidationSummary = {
43
+ accepted: boolean;
44
+ score: number | null;
45
+ hard_fails: string[];
46
+ };
47
+ type Enhancement = {
48
+ method: EnhanceMethod;
49
+ /** Etapas aplicadas: white_balance, exposure, local_contrast, denoise, sharpen, face_restoration. */
50
+ steps: string[];
51
+ /** Dispositivo do GFPGAN (null na clássica). */
52
+ device: "cpu" | "gpu" | null;
53
+ before: ValidationSummary;
54
+ after: ValidationSummary;
55
+ /** Falhas da original que a melhoria não corrige (desfoque, olhos fechados…): continuam reprovando. */
56
+ carried_over: string[];
57
+ /** Foto melhorada (JPEG em data URL). */
58
+ enhanced_image: string;
59
+ };
60
+ /** Formato do avatar (serviço "avatar"): redondo (fora do círculo transparente) ou quadrado. */
61
+ type AvatarShape = "square" | "round";
62
+ /**
63
+ * Onde a pessoa fica no avatar (só com fundo de cor ou imagem): centro; apoiada na base (o fundo aparece em
64
+ * cima); na base à esquerda ou à direita.
65
+ */
66
+ type AvatarLayout = "center" | "bottom" | "left" | "right";
67
+ /** Dados do avatar gerado (POST /v1/photos/avatar). */
68
+ type AvatarInfo = {
69
+ shape: AvatarShape;
70
+ /** Lado em pixels (64 a 1024). */
71
+ size: number;
72
+ /** Modelo de remoção de fundo que separou a pessoa. */
73
+ model: string;
74
+ /** "transparent", "image" (fundo enviado) ou a cor "#rrggbb". */
75
+ background: string;
76
+ /** % do quadro deixado para o fundo (0 com fundo transparente). */
77
+ margin?: number;
78
+ layout?: AvatarLayout;
79
+ };
80
+ /** Avisos da validação. `face_restored_ai`: o GFPGAN recriou detalhes do rosto. */
81
+ type ValidationWarning = "face_restored_ai" | "gpu_fallback_cpu" | (string & {});
82
+ /** Resultado da validação de uma foto (mesmo formato de POST /v1/photos/*). */
83
+ type ValidationResult = {
84
+ accepted: boolean;
85
+ score: number;
86
+ checks: Record<string, boolean | null>;
87
+ /** Critérios que reprovaram (ex.: "eyes_closed", "sunglasses", "low_light"). */
88
+ hard_fails?: string[];
89
+ reasons?: string[];
90
+ warnings?: ValidationWarning[];
91
+ error?: string;
92
+ dominant_face_bbox?: FaceBox | null;
93
+ /** Foto com o fundo removido, recortada no rosto (PNG em data URL), quando pedido `clean`. */
94
+ cleaned_image?: string;
95
+ rembg_model?: string;
96
+ device?: "cpu" | "gpu";
97
+ timings?: {
98
+ validation_ms?: number;
99
+ };
100
+ usage?: TokenUsage;
101
+ /** Presente quando a foto passou pela melhoria (`enhance`). */
102
+ enhancement?: Enhancement;
103
+ /** Avatar (só com a foto aprovada): PNG em data URL só com a pessoa do rosto em foco. */
104
+ avatar_image?: string;
105
+ avatar?: AvatarInfo;
106
+ };
107
+ type StreamOptions = {
108
+ /** Remover o fundo da foto capturada (serviço "validate_clean" contratado). */
109
+ clean?: boolean;
110
+ model?: "u2net" | "u2netp" | "birefnet-portrait";
111
+ device?: "cpu" | "gpu";
112
+ /** Limiares de validação (mesmos nomes de GET /v1/thresholds). */
113
+ thresholds?: Record<string, number>;
114
+ /**
115
+ * Melhorar a foto capturada antes de validar (serviço "validate_enhance" contratado). Os quadros ao
116
+ * vivo não passam pela melhoria. null/undefined = sem melhoria.
117
+ */
118
+ enhance?: EnhanceMethod | null;
119
+ };
120
+ /** Ticket de uso único (60 s) para o navegador abrir a webcam, emitido pelo SEU servidor. */
121
+ type StreamTicket = {
122
+ ticket: string;
123
+ url: string;
124
+ expiresIn?: number;
125
+ };
126
+ /** Há quanto tempo os quadros seguidos estão aprovados e parados, e o alvo para capturar. */
127
+ type Stable = {
128
+ ms: number;
129
+ needed: number;
130
+ };
131
+ type FaceGateErrorCode = "unauthorized" | "forbidden" | "token_month_limit" | "token_window_limit" | "bad_request" | "camera_denied" | "camera_unavailable" | "network" | "server";
132
+ declare class FaceGateError extends Error {
133
+ code: FaceGateErrorCode;
134
+ status?: number | undefined;
135
+ /** Quando o limite renova (ISO), se a API informou. */
136
+ resetsAt?: string | undefined;
137
+ constructor(code: FaceGateErrorCode, message: string, status?: number | undefined,
138
+ /** Quando o limite renova (ISO), se a API informou. */
139
+ resetsAt?: string | undefined);
140
+ }
141
+
142
+ /**
143
+ * @beltec/face-gate/server — uso SÓ no servidor (Node 18+, Next.js Route Handlers, Express…).
144
+ *
145
+ * Guarda o client_secret da chave e conversa com a API do Face Gate:
146
+ * - `createStreamTicket()`: ticket de uso único (60 s) para o navegador abrir a webcam (<FaceGate>);
147
+ * - `validate()`: valida um arquivo de foto (opcionalmente melhora a imagem e/ou remove o fundo);
148
+ * - `avatar()`: valida e, se aprovada, devolve o avatar (só a pessoa, quadrado ou redondo, sobre o fundo pedido);
149
+ * - `ticketHandler()`: Route Handler pronto para o Next (POST /api/face-gate/ticket).
150
+ * O secret e o access token nunca vão ao navegador; o navegador só recebe o ticket.
151
+ */
152
+
153
+ declare const DEFAULT_BASE_URL = "https://face-gate-api.beltec.ai";
154
+ /**
155
+ * User-Agent das chamadas do SDK: "face-gate-sdk/0.1.0 (node 22.11.0; linux)". O Face Gate mostra "SDK Face Gate
156
+ * 0.1.0" no portal (ex.: nas tentativas de IP não liberado), em vez do fetch genérico do Node. A versão vem do
157
+ * package.json no build.
158
+ */
159
+ declare const SDK_USER_AGENT: string;
160
+ type FaceGateServerOptions = {
161
+ /** key_id da chave criada no portal do Face Gate. */
162
+ clientId: string;
163
+ clientSecret: string;
164
+ /** URL da API (padrão: produção). */
165
+ baseUrl?: string;
166
+ /** fetch alternativo (testes, proxies). */
167
+ fetch?: typeof fetch;
168
+ };
169
+ type ValidateOptions = StreamOptions & {
170
+ /** Nome do arquivo enviado (o tipo é deduzido do conteúdo). */
171
+ filename?: string;
172
+ };
173
+ type ImageInput = Blob | ArrayBuffer | Uint8Array;
174
+ type AvatarOptions = {
175
+ /**
176
+ * Id de um template de avatar salvo no portal (Avatares): formato, tamanho, fundo e enquadramento prontos, sem
177
+ * reenviar a imagem de fundo. As outras opções passadas aqui têm prioridade; `background`/`backgroundColor`
178
+ * trocam o fundo do template.
179
+ */
180
+ template?: number;
181
+ /** "round" (padrão; fora do círculo transparente) ou "square". */
182
+ shape?: AvatarShape;
183
+ /** Lado em pixels, de 64 a 1024 (padrão 512). */
184
+ size?: number;
185
+ /** Fundo em cor sólida, "#RRGGBB". Sem fundo nenhum, o PNG sai transparente. */
186
+ backgroundColor?: string;
187
+ /** Imagem de fundo (ajustada para preencher o quadro); tem prioridade sobre `backgroundColor`. */
188
+ background?: ImageInput;
189
+ /**
190
+ * Só com `background` (imagem): enquadramento do fundo. `x`/`y` de 0 a 100 escolhem qual parte da imagem
191
+ * aparece (50/50 = centro, padrão; como o object-position do CSS); `zoom` de 100 a 300 % amplia além do
192
+ * preenchimento.
193
+ */
194
+ backgroundPosition?: {
195
+ x?: number;
196
+ y?: number;
197
+ zoom?: number;
198
+ };
199
+ /**
200
+ * Só com fundo (`backgroundColor` ou `background`): % do quadro deixado para o fundo em volta da pessoa,
201
+ * de 0 (cabeça e ombros justos, padrão) a 60. Com fundo transparente é ignorado.
202
+ */
203
+ margin?: number;
204
+ /** Só com fundo: onde a pessoa fica — "center" (padrão), "bottom", "left" ou "right". */
205
+ layout?: AvatarLayout;
206
+ /** Modelo de remoção de fundo que separa a pessoa (padrão do servidor no avatar: BiRefNet). */
207
+ model?: StreamOptions["model"];
208
+ device?: StreamOptions["device"];
209
+ thresholds?: Record<string, number>;
210
+ filename?: string;
211
+ };
212
+ declare function createFaceGate(options: FaceGateServerOptions): {
213
+ accessToken: (force?: boolean) => Promise<string>;
214
+ createStreamTicket: () => Promise<StreamTicket>;
215
+ validate: (image: ImageInput, opts?: ValidateOptions) => Promise<ValidationResult>;
216
+ avatar: (image: ImageInput, opts?: AvatarOptions) => Promise<ValidationResult>;
217
+ ticketHandler: (handlerOptions?: {
218
+ authorize?: (req: Request) => boolean | Promise<boolean>;
219
+ }) => (req: Request) => Promise<Response>;
220
+ };
221
+ type FaceGateServer = ReturnType<typeof createFaceGate>;
222
+
223
+ export { type AvatarInfo, type AvatarLayout, type AvatarOptions, type AvatarShape, DEFAULT_BASE_URL, type EnhanceMethod, type Enhancement, type FaceBox, FaceGateError, type FaceGateErrorCode, type FaceGateServer, type FaceGateServerOptions, SDK_USER_AGENT, type Stable, type StreamOptions, type StreamTicket, type TokenUsage, type ValidateOptions, type ValidationResult, type ValidationSummary, type ValidationWarning, createFaceGate };