@beltec/face-gate 0.1.0 → 0.1.1
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.
- package/README.md +218 -218
- package/dist/index.cjs +46 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +46 -9
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +61 -61
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/types.ts","../package.json","../src/server/index.ts"],"sourcesContent":["/** 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","/**\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"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/types.ts","../package.json","../src/server/index.ts"],"sourcesContent":["/** Caixa do rosto dominante, em frações da imagem (0 a 1). */\r\nexport type FaceBox = { xmin: number; ymin: number; xmax: number; ymax: number };\r\n\r\n/** Tokens de uma análise (unidade de processamento do Face Gate: megapixels × peso de cada etapa). */\r\nexport type TokenUsage = {\r\n tokens: number;\r\n /** `validate` conta em dobro com melhoria (valida antes e depois); `enhance` = custo da melhoria. */\r\n breakdown: { decode: number; validate: number; clean: number; enhance?: number; avatar?: number };\r\n /** Saldo da janela do plano (ex.: 5 h a partir do 1º uso). */\r\n window?: { used: number; limit: number; hours: number; started_on: string | null; resets_at: string | null };\r\n /** Saldo do teto mensal (renova no aniversário do contrato). */\r\n month?: { used: number; limit: number; cycle_start: string; cycle_end: string };\r\n};\r\n\r\n/**\r\n * Melhoria de imagem (serviço \"validate_enhance\"):\r\n * - \"classic\": luz, cor, contraste, ruído e nitidez leve; determinística, não altera o rosto;\r\n * - \"gfpgan\": a clássica + restauração do rosto com IA (recria detalhes; avisa `face_restored_ai`).\r\n */\r\nexport type EnhanceMethod = \"classic\" | \"gfpgan\";\r\n\r\n/** Resumo de uma validação dentro de `enhancement` (antes = foto original, depois = melhorada). */\r\nexport type ValidationSummary = { accepted: boolean; score: number | null; hard_fails: string[] };\r\n\r\nexport type Enhancement = {\r\n method: EnhanceMethod;\r\n /** Etapas aplicadas: white_balance, exposure, local_contrast, denoise, sharpen, face_restoration. */\r\n steps: string[];\r\n /** Dispositivo do GFPGAN (null na clássica). */\r\n device: \"cpu\" | \"gpu\" | null;\r\n before: ValidationSummary;\r\n after: ValidationSummary;\r\n /** Falhas da original que a melhoria não corrige (desfoque, olhos fechados…): continuam reprovando. */\r\n carried_over: string[];\r\n /** Foto melhorada (JPEG em data URL). */\r\n enhanced_image: string;\r\n};\r\n\r\n/** Formato do avatar (serviço \"avatar\"): redondo (fora do círculo transparente) ou quadrado. */\r\nexport type AvatarShape = \"square\" | \"round\";\r\n\r\n/**\r\n * Onde a pessoa fica no avatar (só com fundo de cor ou imagem): centro; apoiada na base (o fundo aparece em\r\n * cima); na base à esquerda ou à direita.\r\n */\r\nexport type AvatarLayout = \"center\" | \"bottom\" | \"left\" | \"right\";\r\n\r\n/** Dados do avatar gerado (POST /v1/photos/avatar). */\r\nexport type AvatarInfo = {\r\n shape: AvatarShape;\r\n /** Lado em pixels (64 a 1024). */\r\n size: number;\r\n /** Modelo de remoção de fundo que separou a pessoa. */\r\n model: string;\r\n /** \"transparent\", \"image\" (fundo enviado) ou a cor \"#rrggbb\". */\r\n background: string;\r\n /** % do quadro deixado para o fundo (0 com fundo transparente). */\r\n margin?: number;\r\n layout?: AvatarLayout;\r\n};\r\n\r\n/** Avisos da validação. `face_restored_ai`: o GFPGAN recriou detalhes do rosto. */\r\nexport type ValidationWarning = \"face_restored_ai\" | \"gpu_fallback_cpu\" | (string & {});\r\n\r\n/** Resultado da validação de uma foto (mesmo formato de POST /v1/photos/*). */\r\nexport type ValidationResult = {\r\n accepted: boolean;\r\n score: number;\r\n checks: Record<string, boolean | null>;\r\n /** Critérios que reprovaram (ex.: \"eyes_closed\", \"sunglasses\", \"low_light\"). */\r\n hard_fails?: string[];\r\n reasons?: string[];\r\n warnings?: ValidationWarning[];\r\n error?: string;\r\n dominant_face_bbox?: FaceBox | null;\r\n /** Foto com o fundo removido, recortada no rosto (PNG em data URL), quando pedido `clean`. */\r\n cleaned_image?: string;\r\n rembg_model?: string;\r\n device?: \"cpu\" | \"gpu\";\r\n timings?: { validation_ms?: number };\r\n usage?: TokenUsage;\r\n /** Presente quando a foto passou pela melhoria (`enhance`). */\r\n enhancement?: Enhancement;\r\n /** Avatar (só com a foto aprovada): PNG em data URL só com a pessoa do rosto em foco. */\r\n avatar_image?: string;\r\n avatar?: AvatarInfo;\r\n};\r\n\r\nexport type StreamOptions = {\r\n /** Remover o fundo da foto capturada (serviço \"validate_clean\" contratado). */\r\n clean?: boolean;\r\n model?: \"u2net\" | \"u2netp\" | \"birefnet-portrait\";\r\n device?: \"cpu\" | \"gpu\";\r\n /** Limiares de validação (mesmos nomes de GET /v1/thresholds). */\r\n thresholds?: Record<string, number>;\r\n /**\r\n * Melhorar a foto capturada antes de validar (serviço \"validate_enhance\" contratado). Os quadros ao\r\n * vivo não passam pela melhoria. null/undefined = sem melhoria.\r\n */\r\n enhance?: EnhanceMethod | null;\r\n};\r\n\r\n/** Ticket de uso único (60 s) para o navegador abrir a webcam, emitido pelo SEU servidor. */\r\nexport type StreamTicket = { ticket: string; url: string; expiresIn?: number };\r\n\r\n/** Há quanto tempo os quadros seguidos estão aprovados e parados, e o alvo para capturar. */\r\nexport type Stable = { ms: number; needed: number };\r\n\r\nexport type FaceGateErrorCode =\r\n | \"unauthorized\" // 401/4401: chave, token ou ticket inválido/expirado\r\n | \"forbidden\" // 403/4403: serviço não contratado, chave bloqueada, fora do contrato ou IP não liberado\r\n | \"token_month_limit\" // 402/4402: teto mensal de tokens atingido\r\n | \"token_window_limit\" // 429/4429: janela de tokens esgotada (ou muitas requisições)\r\n | \"bad_request\" // 400/4400\r\n | \"camera_denied\"\r\n | \"camera_unavailable\"\r\n | \"network\"\r\n | \"server\";\r\n\r\nexport class FaceGateError extends Error {\r\n constructor(\r\n public code: FaceGateErrorCode,\r\n message: string,\r\n public status?: number,\r\n /** Quando o limite renova (ISO), se a API informou. */\r\n public resetsAt?: string,\r\n ) {\r\n super(message);\r\n this.name = \"FaceGateError\";\r\n }\r\n}\r\n","{\r\n \"name\": \"@beltec/face-gate\",\r\n \"version\": \"0.1.1\",\r\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).\",\r\n \"license\": \"UNLICENSED\",\r\n \"author\": \"BelTec · grupo Belfort\",\r\n \"homepage\": \"https://face-gate.beltec.ai\",\r\n \"keywords\": [\"face-gate\", \"beltec\", \"webcam\", \"face\", \"validation\", \"react\", \"nextjs\", \"controle de acesso\"],\r\n \"type\": \"module\",\r\n \"sideEffects\": [\"*.css\"],\r\n \"main\": \"./dist/index.cjs\",\r\n \"module\": \"./dist/index.js\",\r\n \"types\": \"./dist/index.d.ts\",\r\n \"exports\": {\r\n \".\": {\r\n \"types\": \"./dist/index.d.ts\",\r\n \"import\": \"./dist/index.js\",\r\n \"require\": \"./dist/index.cjs\"\r\n },\r\n \"./server\": {\r\n \"types\": \"./dist/server.d.ts\",\r\n \"import\": \"./dist/server.js\",\r\n \"require\": \"./dist/server.cjs\"\r\n },\r\n \"./styles.css\": \"./dist/styles.css\",\r\n \"./package.json\": \"./package.json\"\r\n },\r\n \"files\": [\"dist\", \"README.md\"],\r\n \"engines\": {\r\n \"node\": \">=18.17\"\r\n },\r\n \"scripts\": {\r\n \"build\": \"tsup && node -e \\\"require('node:fs').copyFileSync('src/react/styles.css','dist/styles.css')\\\"\",\r\n \"dev\": \"tsup --watch\",\r\n \"typecheck\": \"tsc --noEmit\",\r\n \"test\": \"vitest run\",\r\n \"prepublishOnly\": \"npm run typecheck && npm test && npm run build\"\r\n },\r\n \"peerDependencies\": {\r\n \"react\": \">=18\",\r\n \"react-dom\": \">=18\"\r\n },\r\n \"peerDependenciesMeta\": {\r\n \"react\": { \"optional\": true },\r\n \"react-dom\": { \"optional\": true }\r\n },\r\n \"devDependencies\": {\r\n \"@types/node\": \"^20.14.0\",\r\n \"@types/react\": \"^19.0.0\",\r\n \"@types/react-dom\": \"^19.0.0\",\r\n \"jsdom\": \"^25.0.1\",\r\n \"react\": \"^19.0.0\",\r\n \"react-dom\": \"^19.0.0\",\r\n \"tsup\": \"^8.3.5\",\r\n \"typescript\": \"^5.6.3\",\r\n \"vitest\": \"^2.1.8\"\r\n },\r\n \"publishConfig\": {\r\n \"access\": \"public\"\r\n }\r\n}\r\n","/**\r\n * @beltec/face-gate/server — uso SÓ no servidor (Node 18+, Next.js Route Handlers, Express…).\r\n *\r\n * Guarda o client_secret da chave e conversa com a API do Face Gate:\r\n * - `createStreamTicket()`: ticket de uso único (60 s) para o navegador abrir a webcam (<FaceGate>);\r\n * - `validate()`: valida um arquivo de foto (opcionalmente melhora a imagem e/ou remove o fundo);\r\n * - `avatar()`: valida e, se aprovada, devolve o avatar (só a pessoa, quadrado ou redondo, sobre o fundo pedido);\r\n * - `ticketHandler()`: Route Handler pronto para o Next (POST /api/face-gate/ticket).\r\n * O secret e o access token nunca vão ao navegador; o navegador só recebe o ticket.\r\n */\r\nimport {\r\n FaceGateError,\r\n type AvatarLayout,\r\n type AvatarShape,\r\n type FaceGateErrorCode,\r\n type StreamOptions,\r\n type StreamTicket,\r\n type ValidationResult,\r\n} from \"../core/types\";\r\nimport { version } from \"../../package.json\";\r\n\r\nexport * from \"../core/types\";\r\n\r\nexport const DEFAULT_BASE_URL = \"https://face-gate-api.beltec.ai\";\r\n\r\n/**\r\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\r\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\r\n * package.json no build.\r\n */\r\nexport const SDK_USER_AGENT = (() => {\r\n const proc = (globalThis as { process?: { versions?: { node?: string }; platform?: string } }).process;\r\n const runtime = proc?.versions?.node ? `node ${proc.versions.node}; ${proc.platform ?? \"?\"}` : \"edge\";\r\n return `face-gate-sdk/${version} (${runtime})`;\r\n})();\r\n\r\nexport type FaceGateServerOptions = {\r\n /** key_id da chave criada no portal do Face Gate. */\r\n clientId: string;\r\n clientSecret: string;\r\n /** URL da API (padrão: produção). */\r\n baseUrl?: string;\r\n /** fetch alternativo (testes, proxies). */\r\n fetch?: typeof fetch;\r\n};\r\n\r\nexport type ValidateOptions = StreamOptions & {\r\n /** Nome do arquivo enviado (o tipo é deduzido do conteúdo). */\r\n filename?: string;\r\n};\r\n\r\ntype ImageInput = Blob | ArrayBuffer | Uint8Array;\r\n\r\nexport type AvatarOptions = {\r\n /**\r\n * Id de um template de avatar salvo no portal (Avatares): formato, tamanho, fundo e enquadramento prontos, sem\r\n * reenviar a imagem de fundo. As outras opções passadas aqui têm prioridade; `background`/`backgroundColor`\r\n * trocam o fundo do template.\r\n */\r\n template?: number;\r\n /** \"round\" (padrão; fora do círculo transparente) ou \"square\". */\r\n shape?: AvatarShape;\r\n /** Lado em pixels, de 64 a 1024 (padrão 512). */\r\n size?: number;\r\n /** Fundo em cor sólida, \"#RRGGBB\". Sem fundo nenhum, o PNG sai transparente. */\r\n backgroundColor?: string;\r\n /** Imagem de fundo (ajustada para preencher o quadro); tem prioridade sobre `backgroundColor`. */\r\n background?: ImageInput;\r\n /**\r\n * Só com `background` (imagem): enquadramento do fundo. `x`/`y` de 0 a 100 escolhem qual parte da imagem\r\n * aparece (50/50 = centro, padrão; como o object-position do CSS); `zoom` de 100 a 300 % amplia além do\r\n * preenchimento.\r\n */\r\n backgroundPosition?: { x?: number; y?: number; zoom?: number };\r\n /**\r\n * Só com fundo (`backgroundColor` ou `background`): % do quadro deixado para o fundo em volta da pessoa,\r\n * de 0 (cabeça e ombros justos, padrão) a 60. Com fundo transparente é ignorado.\r\n */\r\n margin?: number;\r\n /** Só com fundo: onde a pessoa fica — \"center\" (padrão), \"bottom\", \"left\" ou \"right\". */\r\n layout?: AvatarLayout;\r\n /** Modelo de remoção de fundo que separa a pessoa (padrão do servidor no avatar: BiRefNet). */\r\n model?: StreamOptions[\"model\"];\r\n device?: StreamOptions[\"device\"];\r\n thresholds?: Record<string, number>;\r\n filename?: string;\r\n};\r\n\r\nconst toBlob = (image: ImageInput) => (image instanceof Blob ? image : new Blob([image as BlobPart]));\r\n\r\ntype Json = Record<string, unknown>;\r\n\r\nconst STATUS_CODE: Record<number, FaceGateErrorCode> = {\r\n 400: \"bad_request\",\r\n 401: \"unauthorized\",\r\n 402: \"token_month_limit\",\r\n 403: \"forbidden\",\r\n 413: \"bad_request\",\r\n 429: \"token_window_limit\",\r\n};\r\n\r\nasync function toError(res: Response): Promise<FaceGateError> {\r\n const body = (await res.json().catch(() => null)) as Json | null;\r\n const detail = typeof body?.detail === \"string\" ? body.detail : `Erro ${res.status} na API do Face Gate`;\r\n const resets = (body?.resets_at ?? body?.resets_on) as string | undefined;\r\n return new FaceGateError(STATUS_CODE[res.status] ?? \"server\", detail, res.status, resets);\r\n}\r\n\r\nexport function createFaceGate(options: FaceGateServerOptions) {\r\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\r\n const doFetch = options.fetch ?? fetch;\r\n if (!options.clientId || !options.clientSecret) {\r\n throw new FaceGateError(\"unauthorized\", \"Informe clientId e clientSecret da chave do Face Gate\");\r\n }\r\n let cached: { token: string; expires: number } | null = null;\r\n\r\n async function call(path: string, init: RequestInit): Promise<Response> {\r\n try {\r\n const headers = { \"User-Agent\": SDK_USER_AGENT, ...(init.headers as Record<string, string> | undefined) };\r\n return await doFetch(baseUrl + path, { ...init, headers, cache: \"no-store\" });\r\n } catch (e) {\r\n throw new FaceGateError(\"network\", `Não foi possível conectar ao Face Gate (${(e as Error).message})`);\r\n }\r\n }\r\n\r\n /** Access token da chave (15 min), reaproveitado até 30 s antes de expirar. */\r\n async function accessToken(force = false): Promise<string> {\r\n if (!force && cached && cached.expires > Date.now() + 30_000) return cached.token;\r\n const body = new URLSearchParams({ grant_type: \"client_credentials\", client_id: options.clientId,\r\n client_secret: options.clientSecret });\r\n const res = await call(\"/v1/auth/token\", {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body,\r\n });\r\n if (!res.ok) throw await toError(res);\r\n const j = (await res.json()) as { access_token: string; expires_in: number };\r\n cached = { token: j.access_token, expires: Date.now() + j.expires_in * 1000 };\r\n return cached.token;\r\n }\r\n\r\n /** Chamada autenticada; um 401 (token revogado/expirado) renova o token e tenta uma vez de novo. */\r\n async function authed(path: string, init: RequestInit): Promise<Response> {\r\n let res = await call(path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${await accessToken()}` } });\r\n if (res.status === 401) {\r\n res = await call(path, { ...init, headers: { ...init.headers, Authorization: `Bearer ${await accessToken(true)}` } });\r\n }\r\n return res;\r\n }\r\n\r\n /** Ticket para o navegador (uso único, 60 s). A whitelist de IP vale aqui, no IP do seu servidor. */\r\n async function createStreamTicket(): Promise<StreamTicket> {\r\n const res = await authed(\"/v1/stream-ticket\", { method: \"POST\" });\r\n if (!res.ok) throw await toError(res);\r\n const j = (await res.json()) as { ticket: string; expires_in: number };\r\n return { ticket: j.ticket, url: baseUrl.replace(/^http/, \"ws\") + \"/v1/stream\", expiresIn: j.expires_in };\r\n }\r\n\r\n /**\r\n * Valida uma foto (JPG, PNG ou WebP até 10 MB). Com `clean`, devolve também `cleaned_image`.\r\n * Com `enhance` (\"classic\" | \"gfpgan\"), melhora a foto e valida a versão melhorada\r\n * (POST /v1/photos/validate-and-enhance): o resultado traz `enhancement` com antes/depois.\r\n */\r\n async function validate(image: ImageInput, opts: ValidateOptions = {}): Promise<ValidationResult> {\r\n const form = new FormData();\r\n form.append(\"file\", toBlob(image), opts.filename ?? \"foto.jpg\");\r\n for (const [k, v] of Object.entries(opts.thresholds ?? {})) form.append(k, String(v));\r\n if (opts.enhance) {\r\n form.append(\"enhance\", opts.enhance);\r\n if (opts.clean) form.append(\"clean\", \"true\");\r\n }\r\n if (opts.clean || opts.enhance) {\r\n if (opts.clean && opts.model) form.append(\"model\", opts.model);\r\n if (opts.device) form.append(\"device\", opts.device);\r\n }\r\n const path = opts.enhance ? \"/v1/photos/validate-and-enhance\"\r\n : opts.clean ? \"/v1/photos/validate-and-clean\" : \"/v1/photos/validate\";\r\n const res = await authed(path, { method: \"POST\", body: form });\r\n // 422 = erro ao processar a imagem: a API devolve o resultado com `error`\r\n if (!res.ok && res.status !== 422) throw await toError(res);\r\n return (await res.json()) as ValidationResult;\r\n }\r\n\r\n /**\r\n * Avatar (serviço \"avatar\" contratado, POST /v1/photos/avatar): valida a foto e, se aprovada, devolve em\r\n * `avatar_image` (PNG, data URL) só a pessoa do rosto em foco — fundo e outros elementos/pessoas removidos —,\r\n * recortada centrada no rosto, sobre `background` (imagem), `backgroundColor` ou transparente.\r\n * Com a foto reprovada não há avatar: `accepted` = false e os motivos em `hard_fails`.\r\n */\r\n async function avatar(image: ImageInput, opts: AvatarOptions = {}): Promise<ValidationResult> {\r\n const form = new FormData();\r\n form.append(\"file\", toBlob(image), opts.filename ?? \"foto.jpg\");\r\n if (opts.template != null) form.append(\"template\", String(opts.template));\r\n // com template, o formato vem dele (só vai no form se for pedido explicitamente)\r\n if (opts.shape || opts.template == null) form.append(\"shape\", opts.shape ?? \"round\");\r\n if (opts.size != null) form.append(\"size\", String(opts.size));\r\n if (opts.background) {\r\n form.append(\"background\", toBlob(opts.background), \"fundo.jpg\");\r\n const pos = opts.backgroundPosition;\r\n if (pos?.x != null) form.append(\"background_x\", String(pos.x));\r\n if (pos?.y != null) form.append(\"background_y\", String(pos.y));\r\n if (pos?.zoom != null) form.append(\"background_zoom\", String(pos.zoom));\r\n } else if (opts.backgroundColor) form.append(\"background_color\", opts.backgroundColor);\r\n if (opts.background || opts.backgroundColor || opts.template != null) {\r\n // margem e layout só existem com fundo (transparente = recorte justo); o template pode ter fundo\r\n if (opts.margin != null) form.append(\"margin\", String(opts.margin));\r\n if (opts.layout) form.append(\"layout\", opts.layout);\r\n }\r\n if (opts.model) form.append(\"model\", opts.model);\r\n if (opts.device) form.append(\"device\", opts.device);\r\n for (const [k, v] of Object.entries(opts.thresholds ?? {})) form.append(k, String(v));\r\n const res = await authed(\"/v1/photos/avatar\", { method: \"POST\", body: form });\r\n if (!res.ok && res.status !== 422) throw await toError(res);\r\n return (await res.json()) as ValidationResult;\r\n }\r\n\r\n /**\r\n * Route Handler para o App Router do Next.js:\r\n * // app/api/face-gate/ticket/route.ts\r\n * export const POST = faceGate.ticketHandler({ authorize: async (req) => !!(await getSession()) });\r\n * `authorize` decide quem pode abrir a câmera (ex.: só usuários logados no SEU sistema).\r\n */\r\n function ticketHandler(handlerOptions: { authorize?: (req: Request) => boolean | Promise<boolean> } = {}) {\r\n return async (req: Request): Promise<Response> => {\r\n if (handlerOptions.authorize && !(await handlerOptions.authorize(req))) {\r\n return Response.json({ detail: \"Não autorizado\" }, { status: 401 });\r\n }\r\n try {\r\n return Response.json(await createStreamTicket(), { headers: { \"Cache-Control\": \"no-store\" } });\r\n } catch (e) {\r\n const err = e as FaceGateError;\r\n return Response.json({ detail: err.message, code: err.code, resets_at: err.resetsAt },\r\n { status: err.status && err.status >= 400 ? err.status : 502 });\r\n }\r\n };\r\n }\r\n\r\n return { accessToken, createStreamTicket, validate, avatar, ticketHandler };\r\n}\r\n\r\nexport type FaceGateServer = ReturnType<typeof createFaceGate>;\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@beltec/face-gate",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"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).",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"author": "BelTec · grupo Belfort",
|
|
7
|
-
"homepage": "https://face-gate.beltec.ai",
|
|
8
|
-
"keywords": ["face-gate", "beltec", "webcam", "face", "validation", "react", "nextjs", "controle de acesso"],
|
|
9
|
-
"type": "module",
|
|
10
|
-
"sideEffects": ["*.css"],
|
|
11
|
-
"main": "./dist/index.cjs",
|
|
12
|
-
"module": "./dist/index.js",
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"exports": {
|
|
15
|
-
".": {
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"import": "./dist/index.js",
|
|
18
|
-
"require": "./dist/index.cjs"
|
|
19
|
-
},
|
|
20
|
-
"./server": {
|
|
21
|
-
"types": "./dist/server.d.ts",
|
|
22
|
-
"import": "./dist/server.js",
|
|
23
|
-
"require": "./dist/server.cjs"
|
|
24
|
-
},
|
|
25
|
-
"./styles.css": "./dist/styles.css",
|
|
26
|
-
"./package.json": "./package.json"
|
|
27
|
-
},
|
|
28
|
-
"files": ["dist", "README.md"],
|
|
29
|
-
"engines": {
|
|
30
|
-
"node": ">=18.17"
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build": "tsup && node -e \"require('node:fs').copyFileSync('src/react/styles.css','dist/styles.css')\"",
|
|
34
|
-
"dev": "tsup --watch",
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
38
|
-
},
|
|
39
|
-
"peerDependencies": {
|
|
40
|
-
"react": ">=18",
|
|
41
|
-
"react-dom": ">=18"
|
|
42
|
-
},
|
|
43
|
-
"peerDependenciesMeta": {
|
|
44
|
-
"react": { "optional": true },
|
|
45
|
-
"react-dom": { "optional": true }
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "^20.14.0",
|
|
49
|
-
"@types/react": "^19.0.0",
|
|
50
|
-
"@types/react-dom": "^19.0.0",
|
|
51
|
-
"jsdom": "^25.0.1",
|
|
52
|
-
"react": "^19.0.0",
|
|
53
|
-
"react-dom": "^19.0.0",
|
|
54
|
-
"tsup": "^8.3.5",
|
|
55
|
-
"typescript": "^5.6.3",
|
|
56
|
-
"vitest": "^2.1.8"
|
|
57
|
-
},
|
|
58
|
-
"publishConfig": {
|
|
59
|
-
"access": "public"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@beltec/face-gate",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"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).",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"author": "BelTec · grupo Belfort",
|
|
7
|
+
"homepage": "https://face-gate.beltec.ai",
|
|
8
|
+
"keywords": ["face-gate", "beltec", "webcam", "face", "validation", "react", "nextjs", "controle de acesso"],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": ["*.css"],
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"types": "./dist/server.d.ts",
|
|
22
|
+
"import": "./dist/server.js",
|
|
23
|
+
"require": "./dist/server.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./styles.css": "./dist/styles.css",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": ["dist", "README.md"],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.17"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup && node -e \"require('node:fs').copyFileSync('src/react/styles.css','dist/styles.css')\"",
|
|
34
|
+
"dev": "tsup --watch",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=18",
|
|
41
|
+
"react-dom": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": { "optional": true },
|
|
45
|
+
"react-dom": { "optional": true }
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.14.0",
|
|
49
|
+
"@types/react": "^19.0.0",
|
|
50
|
+
"@types/react-dom": "^19.0.0",
|
|
51
|
+
"jsdom": "^25.0.1",
|
|
52
|
+
"react": "^19.0.0",
|
|
53
|
+
"react-dom": "^19.0.0",
|
|
54
|
+
"tsup": "^8.3.5",
|
|
55
|
+
"typescript": "^5.6.3",
|
|
56
|
+
"vitest": "^2.1.8"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|