@neofaceid/web-sdk 1.43.0 → 1.44.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.
package/dist/index.d.ts CHANGED
@@ -645,9 +645,41 @@ export declare interface DetectionResult {
645
645
  details?: any;
646
646
  }
647
647
 
648
+ /** Desfecho da inscrição de aparelho. */
649
+ export declare type DeviceEnrollmentOutcome = {
650
+ status: 'enrolled';
651
+ deviceLabel: string | null;
652
+ enrolledAt?: string;
653
+ } | {
654
+ status: 'expired';
655
+ } | {
656
+ status: 'cancelled';
657
+ };
658
+
648
659
  /** Token de inscrição de aparelho, devolvido por {@link requestDeviceEnrollmentToken}. */
649
660
  export declare interface DeviceEnrollmentToken {
650
- /** JWT RS256 de uso único. Vira o conteúdo do QR. */
661
+ /**
662
+ * Identificador curto, no formato `XXXX-XXXX`, de um alfabeto sem os pares
663
+ * que se confundem (`0/O`, `1/I/L`, `U/V`).
664
+ *
665
+ * **É isto que o QR deve carregar**, não o {@link token}. O JWT inteiro vira
666
+ * uma grade densa que exige tela grande e câmera boa, e deixa o segredo
667
+ * óptico — exposto em qualquer foto da tela. O handle fotografado vale muito
668
+ * menos: não carrega `person_id`, `consumer_id` nem assinatura, e morre na
669
+ * primeira troca. Como é legível, também serve para ditar por telefone ou
670
+ * digitar à mão quando a câmera falha.
671
+ */
672
+ handle: string;
673
+ /**
674
+ * Identificador da solicitação. É a chave do canal SSE que avisa quando o
675
+ * aparelho for inscrito: `/api/v1/devices/enrollment/{jti}/events/`.
676
+ */
677
+ jti: string;
678
+ /**
679
+ * JWT RS256 de uso único. Continua vindo por compatibilidade, mas **não deve
680
+ * ir para o QR** — quem resolve o handle é o Push, contra o core, por canal
681
+ * interno.
682
+ */
651
683
  token: string;
652
684
  /** Validade em segundos (o core emite 300). */
653
685
  expiresIn: number;
@@ -1603,7 +1635,7 @@ export declare const registerPersonWithoutFace: (personData: {
1603
1635
  /**
1604
1636
  * Data de lançamento da versão atual
1605
1637
  */
1606
- export declare const RELEASE_DATE = "2026-09-15";
1638
+ export declare const RELEASE_DATE = "2026-09-16";
1607
1639
 
1608
1640
  declare interface RequestChallengeWithSessionParams {
1609
1641
  applicationToken: string;
@@ -1908,6 +1940,42 @@ export declare function startBiometricRegistration(personData: {
1908
1940
  onCannotGesture?: () => void;
1909
1941
  }): void;
1910
1942
 
1943
+ /**
1944
+ * Abre o fluxo de inscrição de aparelho: mostra o código, aguarda o celular
1945
+ * lê-lo e confirma o vínculo.
1946
+ *
1947
+ * Exige uma pessoa autenticada — o core pede o Bearer do DONOR junto do
1948
+ * `X-App-Token`, e por isso a inscrição só existe no autosserviço.
1949
+ *
1950
+ * O QR carrega o **handle curto**, nunca o JWT: um código denso exige tela
1951
+ * grande e câmera boa, e o token inteiro ficaria exposto em qualquer foto da
1952
+ * tela. Quem resolve o handle é a plataforma Push, contra o core, por canal
1953
+ * interno — o navegador não participa dessa troca.
1954
+ *
1955
+ * @example
1956
+ * ```ts
1957
+ * const r = await startDeviceEnrollment({
1958
+ * userAccessToken: sessao.accessToken,
1959
+ * applicationToken: TOKEN,
1960
+ * });
1961
+ * if (r.status === 'enrolled') {
1962
+ * // r.deviceLabel pode ser null enquanto o Push não envia o rótulo
1963
+ * }
1964
+ * ```
1965
+ */
1966
+ export declare function startDeviceEnrollment(options: StartDeviceEnrollmentOptions): Promise<DeviceEnrollmentOutcome>;
1967
+
1968
+ export declare interface StartDeviceEnrollmentOptions {
1969
+ /**
1970
+ * Access token do DONOR autenticado. A inscrição só existe no autosserviço,
1971
+ * com a pessoa logada — o core exige este Bearer **junto** do `X-App-Token`.
1972
+ */
1973
+ userAccessToken: string;
1974
+ applicationToken: string;
1975
+ /** Nome do integrador (fallback: `init({ appName })`). */
1976
+ appName?: string;
1977
+ }
1978
+
1911
1979
  /**
1912
1980
  * Função auxiliar para iniciar a captura de documento
1913
1981
  */
@@ -2040,7 +2108,7 @@ export declare const validateToken: (applicationToken: string) => Promise<boolea
2040
2108
  * MINOR: Incrementado quando adicionamos funcionalidades mantendo compatibilidade
2041
2109
  * PATCH: Incrementado quando corrigimos bugs mantendo compatibilidade
2042
2110
  */
2043
- export declare const VERSION = "1.43.0";
2111
+ export declare const VERSION = "1.44.0";
2044
2112
 
2045
2113
  /**
2046
2114
  * Executa `fn(sessionId)`. Se o servidor devolver 410 (sessão consumida/expirada),
@@ -5678,7 +5678,6 @@ const loginWithEmail = async (email2, password, applicationToken) => {
5678
5678
  }
5679
5679
  };
5680
5680
  const requestDeviceEnrollmentToken = async (userAccessToken, applicationToken) => {
5681
- var _a2, _b, _c;
5682
5681
  if (!userAccessToken) {
5683
5682
  throw new NeoFaceError(
5684
5683
  "Inscrição de aparelho exige um usuário autenticado (access token ausente).",
@@ -5697,23 +5696,36 @@ const requestDeviceEnrollmentToken = async (userAccessToken, applicationToken) =
5697
5696
  signal: controller.signal
5698
5697
  });
5699
5698
  if (!response.ok) {
5700
- if (response.status === 401 || response.status === 403) {
5699
+ if (response.status === 401) {
5700
+ throw new NeoFaceError(
5701
+ "Sua sessão expirou. Entre novamente para inscrever o aparelho.",
5702
+ ErrorType.INVALID_TOKEN
5703
+ );
5704
+ }
5705
+ if (response.status === 403) {
5701
5706
  throw new NeoFaceError(
5702
- "Não foi possível inscrever o aparelho: sessão expirada ou conta sem permissão. Faça login novamente.",
5707
+ "Esta conta não pode inscrever aparelhos. Procure o suporte.",
5703
5708
  ErrorType.INVALID_TOKEN
5704
5709
  );
5705
5710
  }
5706
5711
  throw new NeoFaceError(`API Error ${response.status}`, ErrorType.API_ERROR);
5707
5712
  }
5708
5713
  const data = await response.json();
5709
- const token = data.token ?? ((_a2 = data.data) == null ? void 0 : _a2.token);
5710
- if (!token) {
5711
- throw new NeoFaceError("Resposta sem token de inscrição", ErrorType.API_ERROR);
5714
+ const body = data.data ?? data;
5715
+ const token = body.token;
5716
+ const handle = body.handle;
5717
+ if (!token || !handle) {
5718
+ throw new NeoFaceError(
5719
+ "Resposta de inscrição incompleta (token ou handle ausente)",
5720
+ ErrorType.API_ERROR
5721
+ );
5712
5722
  }
5713
5723
  return {
5724
+ handle,
5725
+ jti: body.jti ?? "",
5714
5726
  token,
5715
- expiresIn: data.expires_in ?? ((_b = data.data) == null ? void 0 : _b.expires_in) ?? 300,
5716
- purpose: data.purpose ?? ((_c = data.data) == null ? void 0 : _c.purpose) ?? "device-enrollment"
5727
+ expiresIn: body.expires_in ?? 300,
5728
+ purpose: body.purpose ?? "device-enrollment"
5717
5729
  };
5718
5730
  } catch (error) {
5719
5731
  if (error instanceof NeoFaceError) throw error;
@@ -8886,8 +8898,8 @@ function BiometricRegistrationModal({
8886
8898
  /* @__PURE__ */ jsx("canvas", { ref: canvasRef })
8887
8899
  ] });
8888
8900
  }
8889
- const VERSION = "1.43.0";
8890
- const RELEASE_DATE = "2026-09-15";
8901
+ const VERSION = "1.44.0";
8902
+ const RELEASE_DATE = "2026-09-16";
8891
8903
  const CONSENT_TRAIL_STORAGE_KEY = "neoface_consent_trail_v1";
8892
8904
  const CONSENT_TRAIL_MAX_LIMIT = 100;
8893
8905
  async function hashString(inputStr) {
@@ -9599,8 +9611,8 @@ function StatusPill({ tone, children, icon, className }) {
9599
9611
  }
9600
9612
  );
9601
9613
  }
9602
- const STYLE_ID$2 = "neofaceid-awaiting-push-styles";
9603
- const CSS$1 = `
9614
+ const STYLE_ID$3 = "neofaceid-awaiting-push-styles";
9615
+ const CSS$2 = `
9604
9616
  .neofaceid-root .nfid-push {
9605
9617
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
9606
9618
  padding: 8px 8px 4px;
@@ -9651,7 +9663,7 @@ const CSS$1 = `
9651
9663
  .neofaceid-root .nfid-push-pulse { animation: none; }
9652
9664
  }
9653
9665
  `;
9654
- function formatRemaining(seconds) {
9666
+ function formatRemaining$1(seconds) {
9655
9667
  const safe = Math.max(0, seconds);
9656
9668
  const mm = Math.floor(safe / 60);
9657
9669
  const ss = safe % 60;
@@ -9665,7 +9677,7 @@ function AwaitingPushView({
9665
9677
  }) {
9666
9678
  useEffect(() => {
9667
9679
  injectThemeStyles();
9668
- injectScopedStyles(STYLE_ID$2, CSS$1);
9680
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9669
9681
  }, []);
9670
9682
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "status", "aria-live": "polite", children: [
9671
9683
  /* @__PURE__ */ jsx(StatusPill, { tone: "attention", children: "Aguardando confirmação" }),
@@ -9705,7 +9717,7 @@ function AwaitingPushView({
9705
9717
  ] }),
9706
9718
  /* @__PURE__ */ jsxs("p", { className: "nfid-push-countdown", children: [
9707
9719
  "Expira em ",
9708
- formatRemaining(remainingSeconds)
9720
+ formatRemaining$1(remainingSeconds)
9709
9721
  ] }),
9710
9722
  /* @__PURE__ */ jsx("div", { className: "nfid-push-actions", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Cancelar" }) }),
9711
9723
  /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
@@ -9714,7 +9726,7 @@ function AwaitingPushView({
9714
9726
  function PushExpiredView({ onFallback, onCancel }) {
9715
9727
  useEffect(() => {
9716
9728
  injectThemeStyles();
9717
- injectScopedStyles(STYLE_ID$2, CSS$1);
9729
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9718
9730
  }, []);
9719
9731
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9720
9732
  /* @__PURE__ */ jsxs(
@@ -9751,7 +9763,7 @@ function PushCancelledView({
9751
9763
  }) {
9752
9764
  useEffect(() => {
9753
9765
  injectThemeStyles();
9754
- injectScopedStyles(STYLE_ID$2, CSS$1);
9766
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9755
9767
  }, []);
9756
9768
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9757
9769
  /* @__PURE__ */ jsxs(
@@ -9789,7 +9801,7 @@ function PushCancelledView({
9789
9801
  function PushDeniedView({ appName, onCancel }) {
9790
9802
  useEffect(() => {
9791
9803
  injectThemeStyles();
9792
- injectScopedStyles(STYLE_ID$2, CSS$1);
9804
+ injectScopedStyles(STYLE_ID$3, CSS$2);
9793
9805
  }, []);
9794
9806
  return /* @__PURE__ */ jsxs("div", { className: "nfid-push", role: "alert", "aria-live": "assertive", children: [
9795
9807
  /* @__PURE__ */ jsxs(
@@ -9820,7 +9832,7 @@ function PushDeniedView({ appName, onCancel }) {
9820
9832
  /* @__PURE__ */ jsx("div", { className: "nfid-push-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
9821
9833
  ] });
9822
9834
  }
9823
- const CONTAINER_ID = "neofaceid-push-approval-container";
9835
+ const CONTAINER_ID$1 = "neofaceid-push-approval-container";
9824
9836
  function classifyAuthorizationFrame(frame) {
9825
9837
  var _a2, _b, _c;
9826
9838
  switch (frame.status) {
@@ -9976,7 +9988,7 @@ function PushApprovalModal({
9976
9988
  function awaitPushApproval(options) {
9977
9989
  return new Promise((resolve) => {
9978
9990
  const container = document.createElement("div");
9979
- container.id = CONTAINER_ID;
9991
+ container.id = CONTAINER_ID$1;
9980
9992
  document.body.appendChild(container);
9981
9993
  const root = createRoot(container);
9982
9994
  const settle = (outcome) => {
@@ -9987,6 +9999,392 @@ function awaitPushApproval(options) {
9987
9999
  root.render(/* @__PURE__ */ jsx(PushApprovalModal, { ...options, onSettle: settle }));
9988
10000
  });
9989
10001
  }
10002
+ const STYLE_ID$2 = "neofaceid-device-enrollment-styles";
10003
+ const CSS$1 = `
10004
+ .neofaceid-root .nfid-enroll {
10005
+ display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
10006
+ padding: 8px 8px 4px;
10007
+ }
10008
+ .neofaceid-root .nfid-enroll h2 {
10009
+ margin: 6px 0 0; font-size: 20px; font-weight: 700; color: var(--nfid-text); line-height: 1.25;
10010
+ letter-spacing: -0.015em;
10011
+ }
10012
+ .neofaceid-root .nfid-enroll p {
10013
+ margin: 0; font-size: 14px; color: var(--nfid-text-muted); line-height: 1.5;
10014
+ }
10015
+ /* O QR é sempre escuro sobre branco, independente do tema: leitor de código
10016
+ falha com contraste invertido, e a moldura clara faz a zona de silêncio. */
10017
+ .neofaceid-root .nfid-enroll-qr {
10018
+ background: #FFFFFF; padding: 14px; border-radius: 16px; margin-top: 12px;
10019
+ line-height: 0; box-shadow: 0 0 0 1px var(--nfid-line);
10020
+ }
10021
+ .neofaceid-root .nfid-enroll-qr svg { display: block; width: 184px; height: 184px; }
10022
+ .neofaceid-root .nfid-enroll-handle {
10023
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
10024
+ margin-top: 14px; padding: 12px 24px;
10025
+ background: var(--nfid-surface-muted); border-radius: 12px; width: 100%;
10026
+ }
10027
+ .neofaceid-root .nfid-enroll-handle-label {
10028
+ font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
10029
+ color: var(--nfid-text-subtle);
10030
+ }
10031
+ .neofaceid-root .nfid-enroll-handle-value {
10032
+ font-size: 26px; font-weight: 800; line-height: 1.1; letter-spacing: 0.12em;
10033
+ color: var(--nfid-text); font-variant-numeric: tabular-nums;
10034
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
10035
+ }
10036
+ .neofaceid-root .nfid-enroll-countdown {
10037
+ font-size: 13px; color: var(--nfid-text-muted); font-variant-numeric: tabular-nums;
10038
+ margin-top: 10px;
10039
+ }
10040
+ .neofaceid-root .nfid-enroll-actions {
10041
+ display: flex; flex-direction: column; gap: 10px; width: 100%; margin-top: 16px;
10042
+ }
10043
+ .neofaceid-root .nfid-enroll-note {
10044
+ font-size: 12px; line-height: 1.45; color: var(--nfid-text-muted);
10045
+ background: var(--nfid-surface-muted); border-radius: 10px;
10046
+ padding: 10px 14px; margin-top: 12px; text-align: left; width: 100%;
10047
+ }
10048
+ .neofaceid-root .nfid-enroll-icon { width: 56px; height: 56px; }
10049
+ .neofaceid-root .nfid-enroll-footer {
10050
+ display: flex; justify-content: center; margin-top: 16px; padding-top: 12px;
10051
+ border-top: 1px solid var(--nfid-line); width: 100%;
10052
+ }
10053
+ `;
10054
+ function formatRemaining(seconds) {
10055
+ const safe = Math.max(0, seconds);
10056
+ const mm = Math.floor(safe / 60);
10057
+ const ss = safe % 60;
10058
+ return `${mm}:${String(ss).padStart(2, "0")}`;
10059
+ }
10060
+ function QrCode({ value }) {
10061
+ const [path, setPath] = useState(null);
10062
+ useEffect(() => {
10063
+ let cancelled = false;
10064
+ import("./qrcode-DYfGiWu6.js").then((mod) => {
10065
+ if (cancelled) return;
10066
+ const qrcode = mod.default ?? mod;
10067
+ const qr = qrcode(0, "M");
10068
+ qr.addData(value);
10069
+ qr.make();
10070
+ const count = qr.getModuleCount();
10071
+ const parts = [];
10072
+ for (let r = 0; r < count; r += 1) {
10073
+ for (let c = 0; c < count; c += 1) {
10074
+ if (qr.isDark(r, c)) parts.push(`M${c} ${r}h1v1h-1z`);
10075
+ }
10076
+ }
10077
+ setPath({ d: parts.join(""), count });
10078
+ }).catch(() => {
10079
+ if (!cancelled) setPath(null);
10080
+ });
10081
+ return () => {
10082
+ cancelled = true;
10083
+ };
10084
+ }, [value]);
10085
+ if (!path) {
10086
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 21 21", "aria-hidden": "true" });
10087
+ }
10088
+ return /* @__PURE__ */ jsx(
10089
+ "svg",
10090
+ {
10091
+ viewBox: `0 0 ${path.count} ${path.count}`,
10092
+ shapeRendering: "crispEdges",
10093
+ role: "img",
10094
+ "aria-label": `Código para inscrição do aparelho: ${value.split("").join(" ")}`,
10095
+ children: /* @__PURE__ */ jsx("path", { d: path.d, fill: "#000000" })
10096
+ }
10097
+ );
10098
+ }
10099
+ function DeviceEnrollmentView({
10100
+ appName,
10101
+ handle,
10102
+ remainingSeconds,
10103
+ onCancel
10104
+ }) {
10105
+ useEffect(() => {
10106
+ injectThemeStyles();
10107
+ injectScopedStyles(STYLE_ID$2, CSS$1);
10108
+ }, []);
10109
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-enroll", role: "status", "aria-live": "polite", children: [
10110
+ /* @__PURE__ */ jsx(StatusPill, { tone: "attention", children: "Aguardando o aparelho" }),
10111
+ /* @__PURE__ */ jsx("h2", { children: "Vincule seu celular" }),
10112
+ /* @__PURE__ */ jsxs("p", { children: [
10113
+ "Abra o NeoFaceID Push no celular e aponte para este código para vinculá-lo à sua conta no",
10114
+ " ",
10115
+ appName,
10116
+ "."
10117
+ ] }),
10118
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-qr", children: /* @__PURE__ */ jsx(QrCode, { value: handle }) }),
10119
+ /* @__PURE__ */ jsxs("div", { className: "nfid-enroll-handle", children: [
10120
+ /* @__PURE__ */ jsx("span", { className: "nfid-enroll-handle-label", children: "Ou digite este código" }),
10121
+ /* @__PURE__ */ jsx("span", { className: "nfid-enroll-handle-value", children: handle })
10122
+ ] }),
10123
+ /* @__PURE__ */ jsxs("p", { className: "nfid-enroll-countdown", children: [
10124
+ "O código expira em ",
10125
+ formatRemaining(remainingSeconds)
10126
+ ] }),
10127
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-actions", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Cancelar" }) }),
10128
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
10129
+ ] });
10130
+ }
10131
+ function DeviceEnrolledView({ deviceLabel, onDone }) {
10132
+ useEffect(() => {
10133
+ injectThemeStyles();
10134
+ injectScopedStyles(STYLE_ID$2, CSS$1);
10135
+ }, []);
10136
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-enroll", role: "alert", "aria-live": "assertive", children: [
10137
+ /* @__PURE__ */ jsxs(
10138
+ "svg",
10139
+ {
10140
+ className: "nfid-enroll-icon",
10141
+ viewBox: "0 0 24 24",
10142
+ fill: "none",
10143
+ stroke: "currentColor",
10144
+ strokeWidth: "1.75",
10145
+ strokeLinecap: "round",
10146
+ strokeLinejoin: "round",
10147
+ "aria-hidden": "true",
10148
+ style: { color: "#1E9E6A" },
10149
+ children: [
10150
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
10151
+ /* @__PURE__ */ jsx("path", { d: "M8.5 12.5l2.5 2.5 4.5-5" })
10152
+ ]
10153
+ }
10154
+ ),
10155
+ /* @__PURE__ */ jsx("h2", { children: "Aparelho vinculado" }),
10156
+ deviceLabel ? /* @__PURE__ */ jsxs("p", { children: [
10157
+ /* @__PURE__ */ jsx("strong", { children: deviceLabel }),
10158
+ " agora pode confirmar suas operações."
10159
+ ] }) : /* @__PURE__ */ jsx("p", { children: "Um aparelho acabou de ser vinculado à sua conta." }),
10160
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-note", children: deviceLabel ? /* @__PURE__ */ jsx(Fragment, { children: "Se este não é o seu aparelho, procure o suporte agora — alguém pode ter vinculado um celular à sua conta." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
10161
+ /* @__PURE__ */ jsx("strong", { children: "Não conseguimos identificar qual aparelho foi vinculado." }),
10162
+ " Se não foi você quem acabou de fazer isso, procure o suporte agora — alguém pode ter vinculado um celular à sua conta."
10163
+ ] }) }),
10164
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-actions", children: /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onDone, children: "Entendi" }) }),
10165
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
10166
+ ] });
10167
+ }
10168
+ function DeviceEnrollmentExpiredView({
10169
+ onRetry,
10170
+ onCancel
10171
+ }) {
10172
+ useEffect(() => {
10173
+ injectThemeStyles();
10174
+ injectScopedStyles(STYLE_ID$2, CSS$1);
10175
+ }, []);
10176
+ return /* @__PURE__ */ jsxs("div", { className: "nfid-enroll", role: "alert", "aria-live": "assertive", children: [
10177
+ /* @__PURE__ */ jsxs(
10178
+ "svg",
10179
+ {
10180
+ className: "nfid-enroll-icon",
10181
+ viewBox: "0 0 24 24",
10182
+ fill: "none",
10183
+ stroke: "currentColor",
10184
+ strokeWidth: "1.75",
10185
+ strokeLinecap: "round",
10186
+ strokeLinejoin: "round",
10187
+ "aria-hidden": "true",
10188
+ style: { color: "var(--nfid-text-muted)" },
10189
+ children: [
10190
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
10191
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })
10192
+ ]
10193
+ }
10194
+ ),
10195
+ /* @__PURE__ */ jsx("h2", { children: "O código expirou" }),
10196
+ /* @__PURE__ */ jsx("p", { children: "Códigos de vinculação valem poucos minutos, por segurança. Gere um novo para tentar." }),
10197
+ /* @__PURE__ */ jsxs("div", { className: "nfid-enroll-actions", children: [
10198
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: onRetry, children: "Gerar novo código" }),
10199
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onCancel, children: "Agora não" })
10200
+ ] }),
10201
+ /* @__PURE__ */ jsx("div", { className: "nfid-enroll-footer", children: /* @__PURE__ */ jsx(BrandFooter, {}) })
10202
+ ] });
10203
+ }
10204
+ const CONTAINER_ID = "neofaceid-device-enrollment-container";
10205
+ function classifyEnrollmentFrame(frame) {
10206
+ var _a2, _b, _c;
10207
+ switch (frame.status) {
10208
+ case "enrolled":
10209
+ return {
10210
+ kind: "resolve",
10211
+ value: {
10212
+ status: "enrolled",
10213
+ // Vem `null` enquanto a PUSH-133 não sai. Propagamos o nulo em vez de
10214
+ // inventar um rótulo: a tela precisa poder dizer que não identificou.
10215
+ deviceLabel: ((_a2 = frame.data) == null ? void 0 : _a2.device_label) ?? null,
10216
+ enrolledAt: (_b = frame.data) == null ? void 0 : _b.enrolled_at
10217
+ }
10218
+ };
10219
+ case "expired":
10220
+ return { kind: "resolve", value: { status: "expired" } };
10221
+ case "error":
10222
+ case "failed":
10223
+ return {
10224
+ kind: "reject",
10225
+ error: new NeoFaceError(
10226
+ frame.message ?? ((_c = frame.data) == null ? void 0 : _c.message) ?? "Falha ao inscrever o aparelho",
10227
+ ErrorType.API_ERROR
10228
+ )
10229
+ };
10230
+ default:
10231
+ return { kind: "pending" };
10232
+ }
10233
+ }
10234
+ function DeviceEnrollmentModal({
10235
+ userAccessToken,
10236
+ applicationToken,
10237
+ appName,
10238
+ attempt,
10239
+ onSettle,
10240
+ onRetry,
10241
+ onFatal
10242
+ }) {
10243
+ const headerAppName = appName ?? getAppName() ?? "sua aplicação";
10244
+ const [phase, setPhase] = useState({ kind: "loading" });
10245
+ const [remaining, setRemaining] = useState(0);
10246
+ useEffect(() => {
10247
+ const controller = new AbortController();
10248
+ let done = false;
10249
+ (async () => {
10250
+ let issued;
10251
+ try {
10252
+ issued = await requestDeviceEnrollmentToken(userAccessToken, applicationToken);
10253
+ } catch (err) {
10254
+ if (!done) onFatal(err);
10255
+ return;
10256
+ }
10257
+ if (done) return;
10258
+ setPhase({
10259
+ kind: "waiting",
10260
+ handle: issued.handle,
10261
+ jti: issued.jti,
10262
+ expiresIn: issued.expiresIn
10263
+ });
10264
+ setRemaining(issued.expiresIn);
10265
+ const base = getBaseUrl();
10266
+ const channel = `${base}/api/v1/devices/enrollment/${encodeURIComponent(issued.jti)}/events/`;
10267
+ try {
10268
+ const outcome = await consumeSseStream({
10269
+ signal: controller.signal,
10270
+ timeoutMs: (issued.expiresIn + 30) * 1e3,
10271
+ requestTicket: async () => {
10272
+ var _a2;
10273
+ const res = await fetch(`${channel}ticket/`, {
10274
+ method: "POST",
10275
+ headers: { "Content-Type": "application/json", "X-App-Token": applicationToken }
10276
+ });
10277
+ if (!res.ok) {
10278
+ throw new NeoFaceError(
10279
+ `Falha ao abrir o canal de inscrição: ${res.status}`,
10280
+ res.status === 401 || res.status === 403 ? ErrorType.INVALID_TOKEN : ErrorType.NETWORK
10281
+ );
10282
+ }
10283
+ const body = await res.json();
10284
+ const ticket = ((_a2 = body == null ? void 0 : body.data) == null ? void 0 : _a2.ticket) ?? (body == null ? void 0 : body.ticket);
10285
+ if (!ticket) {
10286
+ throw new NeoFaceError("Canal de inscrição sem ticket", ErrorType.API_ERROR);
10287
+ }
10288
+ return ticket;
10289
+ },
10290
+ buildUrl: (ticket) => `${channel}?ticket=${encodeURIComponent(ticket)}`,
10291
+ classify: classifyEnrollmentFrame
10292
+ });
10293
+ if (done) return;
10294
+ if (outcome.status === "enrolled") {
10295
+ setPhase({
10296
+ kind: "enrolled",
10297
+ deviceLabel: outcome.deviceLabel,
10298
+ enrolledAt: outcome.enrolledAt
10299
+ });
10300
+ return;
10301
+ }
10302
+ setPhase({ kind: "expired" });
10303
+ } catch {
10304
+ if (done || controller.signal.aborted) return;
10305
+ setPhase({ kind: "expired" });
10306
+ }
10307
+ })();
10308
+ return () => {
10309
+ done = true;
10310
+ controller.abort();
10311
+ };
10312
+ }, [attempt]);
10313
+ useEffect(() => {
10314
+ if (phase.kind !== "waiting") return void 0;
10315
+ const id = setInterval(() => setRemaining((r) => r > 0 ? r - 1 : 0), 1e3);
10316
+ return () => clearInterval(id);
10317
+ }, [phase.kind]);
10318
+ if (phase.kind === "loading") return null;
10319
+ if (phase.kind === "enrolled") {
10320
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Aparelho vinculado", children: /* @__PURE__ */ jsx(
10321
+ DeviceEnrolledView,
10322
+ {
10323
+ deviceLabel: phase.deviceLabel,
10324
+ onDone: () => onSettle({
10325
+ status: "enrolled",
10326
+ deviceLabel: phase.deviceLabel,
10327
+ enrolledAt: phase.enrolledAt
10328
+ })
10329
+ }
10330
+ ) });
10331
+ }
10332
+ if (phase.kind === "expired") {
10333
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Código expirado", children: /* @__PURE__ */ jsx(
10334
+ DeviceEnrollmentExpiredView,
10335
+ {
10336
+ onRetry,
10337
+ onCancel: () => onSettle({ status: "expired" })
10338
+ }
10339
+ ) });
10340
+ }
10341
+ return /* @__PURE__ */ jsx(Sheet, { open: true, dismissOnOverlay: false, ariaLabel: "Vincular aparelho", children: /* @__PURE__ */ jsx(
10342
+ DeviceEnrollmentView,
10343
+ {
10344
+ appName: headerAppName,
10345
+ handle: phase.handle,
10346
+ remainingSeconds: remaining,
10347
+ onCancel: () => onSettle({ status: "cancelled" })
10348
+ }
10349
+ ) });
10350
+ }
10351
+ function startDeviceEnrollment(options) {
10352
+ return new Promise((resolve, reject) => {
10353
+ const container = document.createElement("div");
10354
+ container.id = CONTAINER_ID;
10355
+ document.body.appendChild(container);
10356
+ const root = createRoot(container);
10357
+ let attempt = 0;
10358
+ const cleanup = () => {
10359
+ root.unmount();
10360
+ if (document.body.contains(container)) document.body.removeChild(container);
10361
+ };
10362
+ const render = () => {
10363
+ root.render(
10364
+ /* @__PURE__ */ jsx(
10365
+ DeviceEnrollmentModal,
10366
+ {
10367
+ ...options,
10368
+ attempt,
10369
+ onSettle: (outcome) => {
10370
+ cleanup();
10371
+ resolve(outcome);
10372
+ },
10373
+ onRetry: () => {
10374
+ attempt += 1;
10375
+ render();
10376
+ },
10377
+ onFatal: (error) => {
10378
+ cleanup();
10379
+ reject(error);
10380
+ }
10381
+ }
10382
+ )
10383
+ );
10384
+ };
10385
+ render();
10386
+ });
10387
+ }
9990
10388
  function evaluateFacePosition(box, videoWidth, videoHeight) {
9991
10389
  if (!box || !videoWidth || !videoHeight) {
9992
10390
  return {
@@ -15503,6 +15901,7 @@ export {
15503
15901
  start,
15504
15902
  startAutoLogin,
15505
15903
  startBiometricRegistration,
15904
+ startDeviceEnrollment,
15506
15905
  startDocumentCapture,
15507
15906
  startFaceLogin,
15508
15907
  startHandLogin,