@vatts/auth 2.1.3-canary.1.0.2 → 2.1.3-canary.1.0.4

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/LICENSE CHANGED
@@ -1,13 +1,13 @@
1
- Copyright 2026 mfraz
2
-
3
- Licensed under the Apache License, Version 2.0 (the "License");
4
- you may not use this file except in compliance with the License.
5
- You may obtain a copy of the License at
6
-
7
- http://www.apache.org/licenses/LICENSE-2.0
8
-
9
- Unless required by applicable law or agreed to in writing, software
10
- distributed under the License is distributed on an "AS IS" BASIS,
11
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- See the License for the specific language governing permissions and
1
+ Copyright 2026 mfraz
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
13
  limitations under the License.
package/README.md CHANGED
@@ -1,58 +1,58 @@
1
- <div align="center">
2
- <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png">
4
- <img alt="Vatts.js logo" src="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png" width="128">
5
- </picture>
6
- <h1>@vatts/auth</h1>
7
-
8
- [![NPM](https://img.shields.io/npm/v/@vatts/auth.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@vatts/auth)
9
- [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg?style=for-the-badge&labelColor=000000)](../../LICENSE)
10
- [![GitHub](https://img.shields.io/badge/GitHub-mfrazlab/vatts.js-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/mfrazlab/vatts.js)
11
- </div>
12
-
13
- ___
14
-
15
- ## Getting Started
16
-
17
- **@vatts/auth** is the official authentication package for the **Vatts.js** framework, providing secure, flexible, and production-ready authentication with JWT, multiple OAuth providers and session management.
18
-
19
- - Secure by default — robust JWT implementation
20
- - Multiple providers — OAuth, credentials, and custom
21
- - Ready for React and Vue — integrated hooks and components
22
- - Strong typing — 100% TypeScript
23
- - Zero configuration — works out-of-the-box
24
- - Production-ready — CSRF protection, and more
25
-
26
- ___
27
-
28
- ## Documentation
29
-
30
- Visit [https://vatts.mfraz.ovh](https://vatts.mfraz.ovh) for full documentation of Vatts.js and its official packages.
31
-
32
- ___
33
-
34
- ## Community
35
-
36
- Join the Vatts.js community on [GitHub Discussions](https://github.com/mfraz/vatts.js) to ask questions, share ideas, and showcase your projects.
37
-
38
- ___
39
-
40
- ## Security
41
-
42
- Vatts.js uses a security-first hybrid architecture in which Node.js coordinates the application layer and a high-performance Go networking engine manages HTTP connections and transport protocols.
43
- By delegating network handling to Go, the platform achieves strong isolation, predictable request processing, and native support for modern transports like HTTP/3 under SSL — all while maintaining consistent security enforcement across environments, including local and non-TLS setups.
44
-
45
- If you believe you have found a security vulnerability in Vatts.js, we encourage you to **responsibly disclose it and NOT open a public issue**.
46
-
47
- To participate in our vulnerability disclosure program, please email [contact@mfraz.ovh](mailto:contact@mfraz.ovh). We will add you to the program and provide further instructions for submitting your report.
48
-
49
- ___
50
-
51
-
52
- ## License
53
-
54
- Copyright 2026 mfraz
55
-
56
- This project is licensed under the [Apache License 2.0](./LICENSE).
57
-
58
- ___
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png">
4
+ <img alt="Vatts.js logo" src="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png" width="128">
5
+ </picture>
6
+ <h1>@vatts/auth</h1>
7
+
8
+ [![NPM](https://img.shields.io/npm/v/@vatts/auth.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@vatts/auth)
9
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg?style=for-the-badge&labelColor=000000)](../../LICENSE)
10
+ [![GitHub](https://img.shields.io/badge/GitHub-mfrazlab/vatts.js-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/mfrazlab/vatts.js)
11
+ </div>
12
+
13
+ ___
14
+
15
+ ## Getting Started
16
+
17
+ **@vatts/auth** is the official authentication package for the **Vatts.js** framework, providing secure, flexible, and production-ready authentication with JWT, multiple OAuth providers and session management.
18
+
19
+ - Secure by default — robust JWT implementation
20
+ - Multiple providers — OAuth, credentials, and custom
21
+ - Ready for React and Vue — integrated hooks and components
22
+ - Strong typing — 100% TypeScript
23
+ - Zero configuration — works out-of-the-box
24
+ - Production-ready — CSRF protection, and more
25
+
26
+ ___
27
+
28
+ ## Documentation
29
+
30
+ Visit [https://vatts.mfraz.ovh](https://vatts.mfraz.ovh) for full documentation of Vatts.js and its official packages.
31
+
32
+ ___
33
+
34
+ ## Community
35
+
36
+ Join the Vatts.js community on [GitHub Discussions](https://github.com/mfraz/vatts.js) to ask questions, share ideas, and showcase your projects.
37
+
38
+ ___
39
+
40
+ ## Security
41
+
42
+ Vatts.js uses a security-first hybrid architecture in which Node.js coordinates the application layer and a high-performance Go networking engine manages HTTP connections and transport protocols.
43
+ By delegating network handling to Go, the platform achieves strong isolation, predictable request processing, and native support for modern transports like HTTP/3 under SSL — all while maintaining consistent security enforcement across environments, including local and non-TLS setups.
44
+
45
+ If you believe you have found a security vulnerability in Vatts.js, we encourage you to **responsibly disclose it and NOT open a public issue**.
46
+
47
+ To participate in our vulnerability disclosure program, please email [contact@mfraz.ovh](mailto:contact@mfraz.ovh). We will add you to the program and provide further instructions for submitting your report.
48
+
49
+ ___
50
+
51
+
52
+ ## License
53
+
54
+ Copyright 2026 mfraz
55
+
56
+ This project is licensed under the [Apache License 2.0](./LICENSE).
57
+
58
+ ___
package/dist/core.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare class VattsAuth {
11
11
  /**
12
12
  * Autentica um usuário usando um provider específico
13
13
  */
14
- signIn(providerId: string, credentials: Record<string, string>): Promise<{
14
+ signIn(providerId: string, credentials: Record<string, string>, req: VattsRequest): Promise<{
15
15
  session: Session;
16
16
  token: string;
17
17
  } | {
@@ -44,6 +44,7 @@ export declare class VattsAuth {
44
44
  provider: string;
45
45
  route: any;
46
46
  }>;
47
+ updateSession(request: VattsRequest, data: any): VattsResponse;
47
48
  /**
48
49
  * Cria resposta com cookie de autenticação - Secure implementation
49
50
  */
package/dist/core.js CHANGED
@@ -45,7 +45,7 @@ class VattsAuth {
45
45
  /**
46
46
  * Autentica um usuário usando um provider específico
47
47
  */
48
- async signIn(providerId, credentials) {
48
+ async signIn(providerId, credentials, req) {
49
49
  const provider = this.config.providers.find(p => p.id === providerId);
50
50
  if (!provider) {
51
51
  console.error(`[vatts-auth] Provider not found: ${providerId}`);
@@ -53,7 +53,7 @@ class VattsAuth {
53
53
  }
54
54
  try {
55
55
  // Usa o método handleSignIn do provider
56
- const result = await provider.handleSignIn(credentials);
56
+ const result = await provider.handleSignIn(credentials, req);
57
57
  if (!result)
58
58
  return null;
59
59
  // Se resultado é string, é URL de redirecionamento OAuth
@@ -61,18 +61,18 @@ class VattsAuth {
61
61
  return { redirectUrl: result };
62
62
  }
63
63
  // Se resultado é User, cria sessão
64
- const user = result;
64
+ let user = result;
65
65
  // Callback de signIn se definido
66
66
  if (this.config.callbacks?.signIn) {
67
67
  const allowed = await this.config.callbacks.signIn(user, { provider: providerId }, {});
68
68
  if (!allowed)
69
69
  return null;
70
70
  }
71
- const sessionResult = this.sessionManager.createSession(user);
72
- // Callback de sessão se definido
73
- if (this.config.callbacks?.session) {
74
- sessionResult.session = await this.config.callbacks.session({ session: sessionResult.session, user, provider: providerId });
71
+ if (this.config.callbacks?.user) {
72
+ const updatedUser = await this.config.callbacks.user({ user, provider: providerId });
73
+ user = updatedUser;
75
74
  }
75
+ const sessionResult = this.sessionManager.createSession(user);
76
76
  return sessionResult;
77
77
  }
78
78
  catch (error) {
@@ -152,6 +152,18 @@ class VattsAuth {
152
152
  }
153
153
  return routes;
154
154
  }
155
+ updateSession(request, data) {
156
+ const token = this.getTokenFromRequest(request);
157
+ if (!token) {
158
+ return vatts_1.VattsResponse.json({ error: "No session token found" }, { status: 400 });
159
+ }
160
+ const session = this.sessionManager.verifySession(token);
161
+ if (!session) {
162
+ throw new Error("Session not found");
163
+ }
164
+ const update = this.sessionManager.updateUserSession(data);
165
+ return this.createAuthResponse(update.token, { session: update.session });
166
+ }
155
167
  /**
156
168
  * Cria resposta com cookie de autenticação - Secure implementation
157
169
  */
package/dist/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export * from './providers';
3
3
  export * from './core';
4
4
  export * from './routes';
5
5
  export * from './jwt';
6
- export { CredentialsProvider, DiscordProvider, GoogleProvider } from './providers';
6
+ export { CredentialsProvider, DiscordProvider, GoogleProvider, PasskeyStorage, PasskeysProvider } from './providers';
7
7
  export { createAuthRoutes } from './routes';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.createAuthRoutes = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
17
+ exports.createAuthRoutes = exports.PasskeysProvider = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
18
18
  /*
19
19
  * This file is part of the Vatts.js Project.
20
20
  * Copyright (c) 2026 mfraz
@@ -41,5 +41,6 @@ var providers_1 = require("./providers");
41
41
  Object.defineProperty(exports, "CredentialsProvider", { enumerable: true, get: function () { return providers_1.CredentialsProvider; } });
42
42
  Object.defineProperty(exports, "DiscordProvider", { enumerable: true, get: function () { return providers_1.DiscordProvider; } });
43
43
  Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return providers_1.GoogleProvider; } });
44
+ Object.defineProperty(exports, "PasskeysProvider", { enumerable: true, get: function () { return providers_1.PasskeysProvider; } });
44
45
  var routes_1 = require("./routes");
45
46
  Object.defineProperty(exports, "createAuthRoutes", { enumerable: true, get: function () { return routes_1.createAuthRoutes; } });
package/dist/jwt.d.ts CHANGED
@@ -38,4 +38,8 @@ export declare class SessionManager {
38
38
  session: Session;
39
39
  token: string;
40
40
  } | null;
41
+ updateUserSession(user: User): {
42
+ session: Session;
43
+ token: string;
44
+ } | null;
41
45
  }
package/dist/jwt.js CHANGED
@@ -223,5 +223,8 @@ class SessionManager {
223
223
  return null;
224
224
  return this.createSession(currentSession.user);
225
225
  }
226
+ updateUserSession(user) {
227
+ return this.createSession(user);
228
+ }
226
229
  }
227
230
  exports.SessionManager = SessionManager;
@@ -0,0 +1,59 @@
1
+ import type { AuthProviderClass, AuthRoute, User } from '../types';
2
+ import { VattsRequest } from 'vatts';
3
+ /**
4
+ * Interface que o desenvolvedor final DEVE implementar para conectar o próprio Banco de Dados.
5
+ * Como Passkeys exigem guardar "Challenges" temporários e as Chaves Públicas,
6
+ * abstraímos isso para dar liberdade total.
7
+ */
8
+ export interface PasskeyStorage {
9
+ saveChallenge: (username: string, challenge: string) => Promise<void>;
10
+ getChallenge: (username: string) => Promise<string | null>;
11
+ deleteChallenge: (username: string) => Promise<void>;
12
+ saveCredential: (username: string, credential: any) => Promise<void>;
13
+ getUserCredentials: (username: string) => Promise<any[]>;
14
+ updateCredentialCounter: (credentialID: string, newCounter: number) => Promise<void>;
15
+ getUserByUsername: (username: string) => Promise<User | null>;
16
+ }
17
+ export interface PasskeysConfig {
18
+ id?: string;
19
+ name?: string;
20
+ rpName: string;
21
+ rpID?: string;
22
+ origin?: string;
23
+ storage: PasskeyStorage;
24
+ }
25
+ /**
26
+ * Provider para autenticação sem senha usando Passkeys (WebAuthn / FIDO2)
27
+ * * Fluxo de Registro:
28
+ * 1. POST /api/auth/passkeys/register/start -> Gera opções pro dispositivo
29
+ * 2. POST /api/auth/passkeys/register/finish -> Valida e salva no DB (via Storage)
30
+ * * Fluxo de Login:
31
+ * 1. POST /api/auth/passkeys/login/start -> Gera opções de login
32
+ * 2. POST /api/auth/signin -> O Vatts direciona para o `handleSignIn` deste provider
33
+ */
34
+ export declare class PasskeysProvider implements AuthProviderClass {
35
+ readonly id: string;
36
+ readonly name: string;
37
+ readonly type: string;
38
+ private config;
39
+ private loginChallenges;
40
+ constructor(config: PasskeysConfig);
41
+ /**
42
+ * Função auxiliar para pegar a origem e o RPID dinamicamente pela requisição,
43
+ * se não foram informados estaticamente na configuração.
44
+ */
45
+ private getRequestInfo;
46
+ /**
47
+ * O Vatts chama este método quando o front-end envia as credenciais para o endpoint genérico de login.
48
+ * Para Passkeys, o "credentials" conterá o username e o response JSON gerado pelo navegador.
49
+ */
50
+ handleSignIn(credentials: Record<string, string>, req?: VattsRequest): Promise<User | null>;
51
+ /**
52
+ * Retorna configuração pública do provider
53
+ */
54
+ getConfig(): any;
55
+ /**
56
+ * Rotas adicionais para lidar com a geração de chaves (Registro) e inicio de Login
57
+ */
58
+ additionalRoutes: AuthRoute[];
59
+ }
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PasskeysProvider = void 0;
4
+ const vatts_1 = require("vatts");
5
+ const server_1 = require("@simplewebauthn/server");
6
+ /**
7
+ * Funções auxiliares para serializar/desserializar Buffer
8
+ */
9
+ function bufferToBase64(buffer) {
10
+ if (typeof buffer === 'string')
11
+ return buffer; // Já é string
12
+ if (buffer instanceof Uint8Array || Buffer.isBuffer(buffer)) {
13
+ return Buffer.from(buffer).toString('base64');
14
+ }
15
+ return String(buffer);
16
+ }
17
+ function toUint8Array(input) {
18
+ if (input == null)
19
+ throw new TypeError("toUint8Array: input is null/undefined");
20
+ // Uint8Array / Buffer
21
+ if (input instanceof Uint8Array) {
22
+ const ab = input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
23
+ return new Uint8Array(ab);
24
+ }
25
+ // ArrayBuffer
26
+ if (input instanceof ArrayBuffer) {
27
+ return new Uint8Array(input);
28
+ }
29
+ // number[]
30
+ if (Array.isArray(input) && input.every(n => typeof n === "number")) {
31
+ const u8 = Uint8Array.from(input);
32
+ const ab = u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
33
+ return new Uint8Array(ab);
34
+ }
35
+ // base64/base64url string
36
+ if (typeof input === "string") {
37
+ const b64 = input.replace(/-/g, "+").replace(/_/g, "/");
38
+ const pad = b64.length % 4 ? "=".repeat(4 - (b64.length % 4)) : "";
39
+ const buf = Buffer.from(b64 + pad, "base64");
40
+ const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
41
+ return new Uint8Array(ab);
42
+ }
43
+ // object cases
44
+ if (typeof input === "object") {
45
+ const obj = input;
46
+ // { type:"Buffer", data:[...] }
47
+ if (obj.type === "Buffer" && Array.isArray(obj.data)) {
48
+ return toUint8Array(obj.data);
49
+ }
50
+ // { data:[...] }
51
+ if (Array.isArray(obj.data)) {
52
+ return toUint8Array(obj.data);
53
+ }
54
+ // { bytes:[...] }
55
+ if (Array.isArray(obj.bytes)) {
56
+ return toUint8Array(obj.bytes);
57
+ }
58
+ // { buffer: ... } wrapper
59
+ if (obj.buffer != null) {
60
+ // às vezes vem { buffer: { type:"Buffer", data:[...] } }
61
+ return toUint8Array(obj.buffer);
62
+ }
63
+ // array-like: {0:12,1:34,length:2}
64
+ if (typeof obj.length === "number") {
65
+ const arr = Array.from({ length: obj.length }, (_, i) => obj[i]);
66
+ if (arr.every(n => typeof n === "number")) {
67
+ return toUint8Array(arr);
68
+ }
69
+ }
70
+ // numeric-key object without length: {"0":165,"1":1,...}
71
+ const numericKeys = Object.keys(obj).filter(key => /^\d+$/.test(key));
72
+ if (numericKeys.length > 0) {
73
+ const arr = numericKeys
74
+ .sort((a, b) => Number(a) - Number(b))
75
+ .map(key => obj[key]);
76
+ if (arr.every(n => typeof n === "number")) {
77
+ return toUint8Array(arr);
78
+ }
79
+ }
80
+ }
81
+ throw new TypeError(`toUint8Array: unsupported input type: ${Object.prototype.toString.call(input)}`);
82
+ }
83
+ /**
84
+ * Provider para autenticação sem senha usando Passkeys (WebAuthn / FIDO2)
85
+ * * Fluxo de Registro:
86
+ * 1. POST /api/auth/passkeys/register/start -> Gera opções pro dispositivo
87
+ * 2. POST /api/auth/passkeys/register/finish -> Valida e salva no DB (via Storage)
88
+ * * Fluxo de Login:
89
+ * 1. POST /api/auth/passkeys/login/start -> Gera opções de login
90
+ * 2. POST /api/auth/signin -> O Vatts direciona para o `handleSignIn` deste provider
91
+ */
92
+ class PasskeysProvider {
93
+ constructor(config) {
94
+ this.type = 'passkeys';
95
+ // Map para armazenar challenges temporários de login (não-persistentes)
96
+ this.loginChallenges = new Map();
97
+ /**
98
+ * Rotas adicionais para lidar com a geração de chaves (Registro) e inicio de Login
99
+ */
100
+ this.additionalRoutes = [
101
+ // ==========================================
102
+ // REGISTRO - PASSO 1 (START)
103
+ // ==========================================
104
+ {
105
+ method: 'POST',
106
+ path: '/api/auth/passkeys/register/start',
107
+ handler: async (req) => {
108
+ try {
109
+ const body = await req.json();
110
+ const { username } = body;
111
+ const { rpID } = this.getRequestInfo(req);
112
+ if (!username)
113
+ return vatts_1.VattsResponse.json({ error: 'Username required' }, { status: 400 });
114
+ // Usando buffer corretamente para V13+
115
+ const options = await (0, server_1.generateRegistrationOptions)({
116
+ rpName: this.config.rpName,
117
+ rpID,
118
+ userID: new Uint8Array(Buffer.from(username)),
119
+ userName: username,
120
+ attestationType: 'none',
121
+ authenticatorSelection: {
122
+ residentKey: 'required', // Obrigatório para discoverable credentials
123
+ userVerification: 'preferred',
124
+ },
125
+ });
126
+ // Salva desafio no banco de dados customizado do usuário
127
+ await this.config.storage.saveChallenge(username, options.challenge);
128
+ return vatts_1.VattsResponse.json(options);
129
+ }
130
+ catch (error) {
131
+ return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
132
+ }
133
+ }
134
+ },
135
+ // ==========================================
136
+ // REGISTRO - PASSO 2 (FINISH)
137
+ // ==========================================
138
+ {
139
+ method: 'POST',
140
+ path: '/api/auth/passkeys/register/finish',
141
+ handler: async (req) => {
142
+ try {
143
+ const body = await req.json();
144
+ const { origin, rpID } = this.getRequestInfo(req);
145
+ // Agora aceitamos um "name" opcional no body (Ex: "PC do Trabalho")
146
+ const { username, response, name } = body;
147
+ const expectedChallenge = await this.config.storage.getChallenge(username);
148
+ if (!expectedChallenge)
149
+ return vatts_1.VattsResponse.json({ error: 'Challenge expired/not found' }, { status: 400 });
150
+ const verification = await (0, server_1.verifyRegistrationResponse)({
151
+ response,
152
+ expectedChallenge,
153
+ expectedOrigin: origin,
154
+ expectedRPID: rpID,
155
+ });
156
+ if (verification.verified && verification.registrationInfo) {
157
+ const { credential } = verification.registrationInfo;
158
+ // Formata a credencial e salva no DB do dev
159
+ await this.config.storage.saveCredential(username, {
160
+ credentialID: credential.id,
161
+ credentialPublicKey: bufferToBase64(credential.publicKey),
162
+ counter: credential.counter,
163
+ transports: response.response.transports,
164
+ name: name || 'Chave de Acesso', // Salva o nome ou um padrão
165
+ });
166
+ await this.config.storage.deleteChallenge(username);
167
+ return vatts_1.VattsResponse.json({ success: true });
168
+ }
169
+ return vatts_1.VattsResponse.json({ success: false }, { status: 400 });
170
+ }
171
+ catch (error) {
172
+ return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
173
+ }
174
+ }
175
+ },
176
+ // ==========================================
177
+ // LOGIN - PASSO 1 (START)
178
+ // ==========================================
179
+ {
180
+ method: 'POST',
181
+ path: '/api/auth/passkeys/login/start',
182
+ handler: async (req) => {
183
+ try {
184
+ const { rpID } = this.getRequestInfo(req);
185
+ // Não precisa de username - usa discoverable credentials
186
+ // O navegador mostrará as keys disponíveis automaticamente
187
+ const options = await (0, server_1.generateAuthenticationOptions)({
188
+ rpID,
189
+ userVerification: 'preferred',
190
+ // Omitindo allowCredentials para permitir discoverable credentials
191
+ });
192
+ // Gera um sessionId único para esta tentativa de login
193
+ const sessionId = typeof crypto !== 'undefined' && crypto.randomUUID
194
+ ? crypto.randomUUID()
195
+ : Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
196
+ // Salva o challenge no Map temporário (não usa storage)
197
+ this.loginChallenges.set(`passkey:${sessionId}`, {
198
+ challenge: options.challenge,
199
+ timestamp: Date.now()
200
+ });
201
+ return vatts_1.VattsResponse.json({ options, sessionId });
202
+ }
203
+ catch (error) {
204
+ return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
205
+ }
206
+ }
207
+ },
208
+ // ==========================================
209
+ // LISTAR CREDENCIAIS (Para mostrar na UI)
210
+ // ==========================================
211
+ {
212
+ method: 'POST', // Usando POST para facilitar o envio do username no body
213
+ path: '/api/auth/passkeys/list',
214
+ handler: async (req) => {
215
+ try {
216
+ const body = await req.json();
217
+ const { username } = body;
218
+ if (!username)
219
+ return vatts_1.VattsResponse.json({ error: 'Username required' }, { status: 400 });
220
+ const credentials = await this.config.storage.getUserCredentials(username);
221
+ // Retorna as credenciais limpando a chave pública (boa prática de segurança para o Front-end)
222
+ const safeCredentials = credentials.map(cred => ({
223
+ id: cred.credentialID,
224
+ name: cred.name || 'Chave de Acesso',
225
+ transports: cred.transports,
226
+ createdAt: cred.createdAt // Caso o dev salve a data no banco dele
227
+ }));
228
+ return vatts_1.VattsResponse.json({ success: true, credentials: safeCredentials });
229
+ }
230
+ catch (error) {
231
+ return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
232
+ }
233
+ }
234
+ }
235
+ ];
236
+ this.config = config;
237
+ this.id = config.id || 'passkeys';
238
+ this.name = config.name || 'Passkeys';
239
+ // Limpa challenges expirados a cada 5 minutos (TTL de 10 minutos)
240
+ setInterval(() => {
241
+ const now = Date.now();
242
+ for (const [key, value] of this.loginChallenges.entries()) {
243
+ if (now - value.timestamp > 10 * 60 * 1000) {
244
+ this.loginChallenges.delete(key);
245
+ }
246
+ }
247
+ }, 5 * 60 * 1000);
248
+ }
249
+ /**
250
+ * Função auxiliar para pegar a origem e o RPID dinamicamente pela requisição,
251
+ * se não foram informados estaticamente na configuração.
252
+ */
253
+ getRequestInfo(req) {
254
+ let origin = this.config.origin;
255
+ if (!origin && req) {
256
+ // Lida tanto com API Fetch nativa (headers.get) quanto Node.js plain (headers.origin)
257
+ if (typeof req.headers?.get === 'function') {
258
+ const originHeader = req.headers['origin'] || req.headers['host'];
259
+ origin = Array.isArray(originHeader) ? originHeader[0] : originHeader;
260
+ if (!origin) {
261
+ const hostHeader = req.headers['host'];
262
+ const host = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
263
+ if (host)
264
+ origin = `http://${host}`; // Fallback para host
265
+ }
266
+ }
267
+ else if (req.headers) {
268
+ // @ts-ignore
269
+ origin = req.headers.origin || (req.headers.host ? `http://${req.headers.host}` : undefined);
270
+ }
271
+ }
272
+ // Fallback final
273
+ origin = origin || 'http://localhost:3000';
274
+ let rpID = this.config.rpID;
275
+ if (!rpID) {
276
+ try {
277
+ rpID = new URL(origin).hostname;
278
+ }
279
+ catch (e) {
280
+ rpID = 'localhost';
281
+ }
282
+ }
283
+ return { origin, rpID };
284
+ }
285
+ /**
286
+ * O Vatts chama este método quando o front-end envia as credenciais para o endpoint genérico de login.
287
+ * Para Passkeys, o "credentials" conterá o username e o response JSON gerado pelo navegador.
288
+ */
289
+ async handleSignIn(credentials, req) {
290
+ try {
291
+ const { response: responseString, sessionId } = credentials;
292
+ if (!responseString || !sessionId) {
293
+ console.error(`[${this.id} Provider] Missing response payload or sessionId`);
294
+ return null;
295
+ }
296
+ const { origin, rpID } = this.getRequestInfo(req);
297
+ const response = JSON.parse(responseString);
298
+ // Recupera o challenge do Map de login (temporário, não do storage)
299
+ const challengeData = this.loginChallenges.get(`passkey:${sessionId}`);
300
+ const expectedChallenge = challengeData?.challenge || null;
301
+ if (!expectedChallenge) {
302
+ console.error(`[${this.id} Provider] No active challenge found`);
303
+ return null;
304
+ }
305
+ // O response.id é a credentialID. Precisamos encontrar qual usuário possui essa chave
306
+ // Como não sabemos o username, a implementação do storage deve permitir buscar por credentialID
307
+ // Para isso, você pode adicionar um método como findUsernameByCredentialID
308
+ //
309
+ // Alternativa: Como o userHandle é enviado no response da discoverable credential,
310
+ // podemos decodificá-lo para obter o username original
311
+ let username = null;
312
+ // Tenta extrair username do userHandle (armazenado durante registro)
313
+ if (response.response.userHandle) {
314
+ try {
315
+ username = Buffer.from(response.response.userHandle, 'base64').toString('utf-8');
316
+ }
317
+ catch (e) {
318
+ console.error(`[${this.id} Provider] Could not decode userHandle`);
319
+ }
320
+ }
321
+ if (!username) {
322
+ console.error(`[${this.id} Provider] Could not determine username from credential`);
323
+ return null;
324
+ }
325
+ // Agora que temos o username, busca a chave específica
326
+ const userPasskeys = await this.config.storage.getUserCredentials(username);
327
+ const passkey = userPasskeys.find(key => key.credentialID === response.id);
328
+ if (!passkey) {
329
+ console.error(`[${this.id} Provider] Credential not found for user`);
330
+ return null;
331
+ }
332
+ // Valida a resposta com a biblioteca (V13+)
333
+ const verification = await (0, server_1.verifyAuthenticationResponse)({
334
+ response,
335
+ expectedChallenge,
336
+ expectedOrigin: origin,
337
+ expectedRPID: rpID,
338
+ credential: {
339
+ id: passkey.credentialID,
340
+ publicKey: toUint8Array(passkey.credentialPublicKey),
341
+ counter: passkey.counter,
342
+ transports: passkey.transports,
343
+ },
344
+ });
345
+ if (verification.verified) {
346
+ // Atualiza o contador de segurança e deleta o challenge do Map
347
+ await this.config.storage.updateCredentialCounter(passkey.credentialID, verification.authenticationInfo.newCounter);
348
+ this.loginChallenges.delete(`passkey:${sessionId}`);
349
+ // Busca o objeto completo do usuário no banco para o Vatts.js iniciar a sessão
350
+ const user = await this.config.storage.getUserByUsername(username);
351
+ if (!user)
352
+ return null;
353
+ return {
354
+ ...user,
355
+ provider: this.id,
356
+ providerId: username
357
+ };
358
+ }
359
+ return null;
360
+ }
361
+ catch (error) {
362
+ console.error(`[${this.id} Provider] Error during sign in (WebAuthn):`, error);
363
+ return null;
364
+ }
365
+ }
366
+ /**
367
+ * Retorna configuração pública do provider
368
+ */
369
+ getConfig() {
370
+ return {
371
+ id: this.id,
372
+ name: this.name,
373
+ type: this.type,
374
+ rpName: this.config.rpName,
375
+ rpID: this.config.rpID, // Se omitido, continuará extraindo on-the-fly
376
+ };
377
+ }
378
+ }
379
+ exports.PasskeysProvider = PasskeysProvider;
@@ -2,3 +2,4 @@ export { CredentialsProvider } from './providers/credentials';
2
2
  export { DiscordProvider } from './providers/discord';
3
3
  export { GoogleProvider } from './providers/google';
4
4
  export { GithubProvider } from "./providers/github";
5
+ export { PasskeyStorage, PasskeysProvider } from "./providers/passkey";
package/dist/providers.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GithubProvider = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
3
+ exports.PasskeysProvider = exports.GithubProvider = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
4
4
  /*
5
5
  * This file is part of the Vatts.js Project.
6
6
  * Copyright (c) 2026 mfraz
@@ -26,3 +26,5 @@ var google_1 = require("./providers/google");
26
26
  Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return google_1.GoogleProvider; } });
27
27
  var github_1 = require("./providers/github");
28
28
  Object.defineProperty(exports, "GithubProvider", { enumerable: true, get: function () { return github_1.GithubProvider; } });
29
+ var passkey_1 = require("./providers/passkey");
30
+ Object.defineProperty(exports, "PasskeysProvider", { enumerable: true, get: function () { return passkey_1.PasskeysProvider; } });
@@ -19,4 +19,12 @@ export declare function useAuth(): {
19
19
  isAuthenticated: boolean;
20
20
  isLoading: boolean;
21
21
  };
22
+ /**
23
+ * Função utilitária para registrar uma nova Passkey (WebAuthn).
24
+ * Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
25
+ */
26
+ export declare function registerPasskey(username: string, name?: string, basePath?: string): Promise<{
27
+ success: boolean;
28
+ error?: string;
29
+ }>;
22
30
  export {};
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SessionProvider = SessionProvider;
4
4
  exports.useSession = useSession;
5
5
  exports.useAuth = useAuth;
6
+ exports.registerPasskey = registerPasskey;
6
7
  const jsx_runtime_1 = require("react/jsx-runtime");
7
8
  /*
8
9
  * This file is part of the Vatts.js Project.
@@ -121,6 +122,38 @@ function SessionProvider({ children, basePath = '/api/auth', refetchInterval = 0
121
122
  const signIn = (0, react_1.useCallback)(async (provider = 'credentials', options = {}) => {
122
123
  try {
123
124
  const { redirect = true, callbackUrl, popup = false, ...credentials } = options;
125
+ // --- INÍCIO DA INTERCEPTAÇÃO PASSKEYS ---
126
+ if (provider === 'passkeys') {
127
+ try {
128
+ // 1. Pede ao backend as opções de desafio para login (SEM username)
129
+ // Com discoverable credentials, o navegador mostra as keys disponíveis
130
+ const startRes = await fetch(`${basePath}/passkeys/login/start`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({})
134
+ });
135
+ if (!startRes.ok) {
136
+ const errData = await startRes.json().catch(() => ({}));
137
+ return { error: errData.error || 'Failed to start passkey login', status: startRes.status, ok: false };
138
+ }
139
+ const { options, sessionId } = await startRes.json();
140
+ if (!sessionId) {
141
+ return { error: 'No session ID received from server', status: 500, ok: false };
142
+ }
143
+ // 2. Importa dinamicamente para não quebrar em SSR e chama o navegador (FaceID, TouchID, etc)
144
+ // O navegador mostrará as credenciais disponíveis automaticamente
145
+ const { startAuthentication } = await import('@simplewebauthn/browser');
146
+ const asseResp = await startAuthentication(options);
147
+ // 3. Injeta a resposta assinada e o sessionId no credentials para seguir o fluxo normal do signIn
148
+ credentials.response = JSON.stringify(asseResp);
149
+ credentials.sessionId = sessionId;
150
+ }
151
+ catch (error) {
152
+ console.error('[vatts-auth] Error during passkey login flow:', error);
153
+ return { error: error.message || 'Passkey authentication failed', status: 400, ok: false };
154
+ }
155
+ }
156
+ // --- FIM DA INTERCEPTAÇÃO PASSKEYS ---
124
157
  const response = await fetch(`${basePath}/signin`, {
125
158
  method: 'POST',
126
159
  headers: {
@@ -150,7 +183,7 @@ function SessionProvider({ children, basePath = '/api/auth', refetchInterval = 0
150
183
  url: data.redirectUrl
151
184
  };
152
185
  }
153
- // Se é sessão (credentials), atualiza e redireciona
186
+ // Se é sessão (credentials/passkeys), atualiza e redireciona
154
187
  await fetchSession();
155
188
  if (data.type === 'session') {
156
189
  if (redirect && typeof window !== 'undefined') {
@@ -263,3 +296,42 @@ function useAuth() {
263
296
  isLoading: status === 'loading'
264
297
  };
265
298
  }
299
+ /**
300
+ * Função utilitária para registrar uma nova Passkey (WebAuthn).
301
+ * Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
302
+ */
303
+ async function registerPasskey(username, name, basePath = '/api/auth') {
304
+ try {
305
+ const { startRegistration } = await import('@simplewebauthn/browser');
306
+ // 1. Pega as opções do servidor
307
+ const startRes = await fetch(`${basePath}/passkeys/register/start`, {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({ username })
311
+ });
312
+ if (!startRes.ok) {
313
+ const errData = await startRes.json().catch(() => ({}));
314
+ throw new Error(errData.error || 'Failed to start passkey registration');
315
+ }
316
+ const options = await startRes.json();
317
+ // 2. Chama a biometria/PIN do dispositivo
318
+ const attResp = await startRegistration(options);
319
+ // 3. Envia o resultado de volta pro servidor validar e salvar
320
+ const finishRes = await fetch(`${basePath}/passkeys/register/finish`, {
321
+ method: 'POST',
322
+ headers: { 'Content-Type': 'application/json' },
323
+ body: JSON.stringify({ username, response: attResp, name })
324
+ });
325
+ const result = await finishRes.json();
326
+ if (result.success) {
327
+ return { success: true };
328
+ }
329
+ else {
330
+ return { success: false, error: result.error || 'Verification failed on server' };
331
+ }
332
+ }
333
+ catch (error) {
334
+ console.error('[vatts-auth] Error during passkey registration:', error);
335
+ return { success: false, error: error.message || 'Passkey registration failed' };
336
+ }
337
+ }
package/dist/routes.js CHANGED
@@ -127,7 +127,7 @@ async function handleSignIn(req, auth) {
127
127
  const credentialsWithPopup = popup !== undefined
128
128
  ? { ...credentials, popup: String(popup) }
129
129
  : credentials;
130
- const result = await auth.signIn(provider, credentialsWithPopup);
130
+ const result = await auth.signIn(provider, credentialsWithPopup, req);
131
131
  if (!result) {
132
132
  return vatts_1.VattsResponse.json({ error: 'Invalid credentials' }, { status: 401 });
133
133
  }
@@ -139,6 +139,7 @@ async function handleSignIn(req, auth) {
139
139
  type: 'oauth'
140
140
  });
141
141
  }
142
+ console.log('result:', result);
142
143
  // Se tem session, é credentials - retorna sessão
143
144
  return auth.createAuthResponse(result.token, {
144
145
  success: true,
@@ -161,76 +162,76 @@ function handlePopupCallback(req) {
161
162
  const error = url.searchParams.get('error');
162
163
  const provider = url.searchParams.get('provider') || 'unknown';
163
164
  const callbackUrl = url.searchParams.get('callbackUrl') || '/';
164
- const html = `
165
- <!DOCTYPE html>
166
- <html>
167
- <head>
168
- <meta charset="UTF-8">
169
- <title>Authenticating...</title>
170
- <style>
171
- body {
172
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
173
- display: flex;
174
- justify-content: center;
175
- align-items: center;
176
- height: 100vh;
177
- margin: 0;
178
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
179
- color: white;
180
- }
181
- .container {
182
- text-align: center;
183
- }
184
- .spinner {
185
- border: 4px solid rgba(255,255,255,0.3);
186
- border-radius: 50%;
187
- border-top: 4px solid white;
188
- width: 40px;
189
- height: 40px;
190
- animation: spin 1s linear infinite;
191
- margin: 0 auto 20px;
192
- }
193
- @keyframes spin {
194
- 0% { transform: rotate(0deg); }
195
- 100% { transform: rotate(360deg); }
196
- }
197
- h2 {
198
- margin: 0;
199
- font-size: 24px;
200
- }
201
- p {
202
- margin: 10px 0 0;
203
- opacity: 0.9;
204
- }
205
- </style>
206
- </head>
207
- <body>
208
- <div class="container">
209
- <div class="spinner"></div>
210
- <h2>${success ? '✓ Autenticação bem-sucedida' : '✗ Erro na autenticação'}</h2>
211
- <p>${success ? 'Fechando janela...' : (error || 'Algo deu errado')}</p>
212
- </div>
213
- <script>
214
- (function() {
215
- try {
216
- if (window.opener) {
217
- console.log('Enviando mensagem para janela pai:')
218
- window.opener.postMessage({
219
- type: ${success ? "'oauth-success'" : "'oauth-error'"},
220
- provider: "${provider}",
221
- ${success ? `callbackUrl: "${callbackUrl}"` : `error: "${error || 'Authentication failed'}"`}
222
- }, window.location.origin);
223
- }
224
- setTimeout(() => {
225
- window.close();
226
- }, 1000);
227
- } catch (e) {
228
- console.error('Error communicating with parent window:', e);
229
- }
230
- })();
231
- </script>
232
- </body>
233
- </html>
165
+ const html = `
166
+ <!DOCTYPE html>
167
+ <html>
168
+ <head>
169
+ <meta charset="UTF-8">
170
+ <title>Authenticating...</title>
171
+ <style>
172
+ body {
173
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
174
+ display: flex;
175
+ justify-content: center;
176
+ align-items: center;
177
+ height: 100vh;
178
+ margin: 0;
179
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
180
+ color: white;
181
+ }
182
+ .container {
183
+ text-align: center;
184
+ }
185
+ .spinner {
186
+ border: 4px solid rgba(255,255,255,0.3);
187
+ border-radius: 50%;
188
+ border-top: 4px solid white;
189
+ width: 40px;
190
+ height: 40px;
191
+ animation: spin 1s linear infinite;
192
+ margin: 0 auto 20px;
193
+ }
194
+ @keyframes spin {
195
+ 0% { transform: rotate(0deg); }
196
+ 100% { transform: rotate(360deg); }
197
+ }
198
+ h2 {
199
+ margin: 0;
200
+ font-size: 24px;
201
+ }
202
+ p {
203
+ margin: 10px 0 0;
204
+ opacity: 0.9;
205
+ }
206
+ </style>
207
+ </head>
208
+ <body>
209
+ <div class="container">
210
+ <div class="spinner"></div>
211
+ <h2>${success ? '✓ Autenticação bem-sucedida' : '✗ Erro na autenticação'}</h2>
212
+ <p>${success ? 'Fechando janela...' : (error || 'Algo deu errado')}</p>
213
+ </div>
214
+ <script>
215
+ (function() {
216
+ try {
217
+ if (window.opener) {
218
+ console.log('Enviando mensagem para janela pai:')
219
+ window.opener.postMessage({
220
+ type: ${success ? "'oauth-success'" : "'oauth-error'"},
221
+ provider: "${provider}",
222
+ ${success ? `callbackUrl: "${callbackUrl}"` : `error: "${error || 'Authentication failed'}"`}
223
+ }, window.location.origin);
224
+ }
225
+ setTimeout(() => {
226
+ window.close();
227
+ }, 1000);
228
+ } catch (e) {
229
+ console.error('Error communicating with parent window:', e);
230
+ }
231
+ })();
232
+ </script>
233
+ </body>
234
+ </html>
234
235
  `;
235
236
  return vatts_1.VattsResponse.html(html);
236
237
  }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { VattsRequest } from "vatts";
1
2
  export type User = Record<string, any>;
2
3
  export interface Session {
3
4
  user: User;
@@ -35,7 +36,7 @@ export interface AuthProviderClass {
35
36
  name: string;
36
37
  type: string;
37
38
  handleOauth?(credentials: Record<string, string>): Promise<string> | string;
38
- handleSignIn(credentials: Record<string, string>): Promise<User | string | null>;
39
+ handleSignIn(credentials: Record<string, string>, _req?: VattsRequest): Promise<User | string | null>;
39
40
  handleSignOut?(): Promise<void>;
40
41
  additionalRoutes?: AuthRoute[];
41
42
  getConfig?(): any;
@@ -49,11 +50,10 @@ export interface AuthConfig {
49
50
  };
50
51
  callbacks?: {
51
52
  signIn?: (user: User, account: any, profile: any) => boolean | Promise<boolean>;
52
- session?: ({ session, user, provider }: {
53
- session: Session;
53
+ user?: ({ user, provider }: {
54
54
  user: User;
55
55
  provider: string;
56
- }) => Session | Promise<Session>;
56
+ }) => User | Promise<User>;
57
57
  jwt?: (token: any, user: User, account: any, profile: any) => any | Promise<any>;
58
58
  };
59
59
  session?: {
@@ -1,30 +1,30 @@
1
- <script setup>
2
- /**
3
- * This file is part of the Vatts.js Project.
4
- * Copyright (c) 2026 mfraz
5
- */
6
- import { useSessionProviderLogic } from './session';
7
-
8
- // Definição das props com valores padrão (Sintaxe JavaScript)
9
- const props = defineProps({
10
- basePath: {
11
- type: String,
12
- default: '/api/auth'
13
- },
14
- refetchInterval: {
15
- type: Number,
16
- default: 0
17
- },
18
- refetchOnWindowFocus: {
19
- type: Boolean,
20
- default: true
21
- }
22
- });
23
-
24
- // Inicializa a lógica do provider passando as props reativas
25
- useSessionProviderLogic(props);
26
- </script>
27
-
28
- <template>
29
- <slot />
1
+ <script setup>
2
+ /**
3
+ * This file is part of the Vatts.js Project.
4
+ * Copyright (c) 2026 mfraz
5
+ */
6
+ import { useSessionProviderLogic } from './session';
7
+
8
+ // Definição das props com valores padrão (Sintaxe JavaScript)
9
+ const props = defineProps({
10
+ basePath: {
11
+ type: String,
12
+ default: '/api/auth'
13
+ },
14
+ refetchInterval: {
15
+ type: Number,
16
+ default: 0
17
+ },
18
+ refetchOnWindowFocus: {
19
+ type: Boolean,
20
+ default: true
21
+ }
22
+ });
23
+
24
+ // Inicializa a lógica do provider passando as props reativas
25
+ useSessionProviderLogic(props);
26
+ </script>
27
+
28
+ <template>
29
+ <slot />
30
30
  </template>
@@ -34,3 +34,11 @@ export declare function useAuth(): {
34
34
  isAuthenticated: boolean;
35
35
  isLoading: boolean;
36
36
  };
37
+ /**
38
+ * Função utilitária para registrar uma nova Passkey (WebAuthn).
39
+ * Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
40
+ */
41
+ export declare function registerPasskey(username: string, name?: string, basePath?: string): Promise<{
42
+ success: boolean;
43
+ error?: string;
44
+ }>;
@@ -4,6 +4,7 @@ exports.SessionKey = void 0;
4
4
  exports.useSessionProviderLogic = useSessionProviderLogic;
5
5
  exports.useSession = useSession;
6
6
  exports.useAuth = useAuth;
7
+ exports.registerPasskey = registerPasskey;
7
8
  /*
8
9
  * This file is part of the Vatts.js Project.
9
10
  * Copyright (c) 2026 mfraz
@@ -128,6 +129,38 @@ function useSessionProviderLogic(props) {
128
129
  const signIn = async (provider = 'credentials', options = {}) => {
129
130
  try {
130
131
  const { redirect = true, callbackUrl, popup = false, ...credentials } = options;
132
+ // --- INÍCIO DA INTERCEPTAÇÃO PASSKEYS ---
133
+ if (provider === 'passkeys') {
134
+ try {
135
+ // 1. Pede ao backend as opções de desafio para login (SEM username)
136
+ // Com discoverable credentials, o navegador mostra as keys disponíveis
137
+ const startRes = await fetch(`${props.basePath}/passkeys/login/start`, {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json' },
140
+ body: JSON.stringify({})
141
+ });
142
+ if (!startRes.ok) {
143
+ const errData = await startRes.json().catch(() => ({}));
144
+ return { error: errData.error || 'Failed to start passkey login', status: startRes.status, ok: false };
145
+ }
146
+ const { options, sessionId } = await startRes.json();
147
+ if (!sessionId) {
148
+ return { error: 'No session ID received from server', status: 500, ok: false };
149
+ }
150
+ // 2. Importa dinamicamente para não quebrar em SSR e chama o navegador (FaceID, TouchID, etc)
151
+ // O navegador mostrará as credenciais disponíveis automaticamente
152
+ const { startAuthentication } = await import('@simplewebauthn/browser');
153
+ const asseResp = await startAuthentication(options);
154
+ // 3. Injeta a resposta assinada e o sessionId no credentials para seguir o fluxo normal do signIn
155
+ credentials.response = JSON.stringify(asseResp);
156
+ credentials.sessionId = sessionId;
157
+ }
158
+ catch (error) {
159
+ console.error('[vatts-auth] Error during passkey login flow:', error);
160
+ return { error: error.message || 'Passkey authentication failed', status: 400, ok: false };
161
+ }
162
+ }
163
+ // --- FIM DA INTERCEPTAÇÃO PASSKEYS ---
131
164
  const response = await fetch(`${props.basePath}/signin`, {
132
165
  method: 'POST',
133
166
  headers: {
@@ -151,7 +184,7 @@ function useSessionProviderLogic(props) {
151
184
  }
152
185
  return { ok: true, status: 200, url: data.redirectUrl };
153
186
  }
154
- // Se é sessão (credentials), atualiza e redireciona
187
+ // Se é sessão (credentials/passkeys), atualiza e redireciona
155
188
  await fetchSession();
156
189
  if (data.type === 'session') {
157
190
  const finalUrl = callbackUrl || '/';
@@ -276,3 +309,42 @@ function useAuth() {
276
309
  isLoading: statusVal === 'loading'
277
310
  };
278
311
  }
312
+ /**
313
+ * Função utilitária para registrar uma nova Passkey (WebAuthn).
314
+ * Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
315
+ */
316
+ async function registerPasskey(username, name, basePath = '/api/auth') {
317
+ try {
318
+ const { startRegistration } = await import('@simplewebauthn/browser');
319
+ // 1. Pega as opções do servidor
320
+ const startRes = await fetch(`${basePath}/passkeys/register/start`, {
321
+ method: 'POST',
322
+ headers: { 'Content-Type': 'application/json' },
323
+ body: JSON.stringify({ username })
324
+ });
325
+ if (!startRes.ok) {
326
+ const errData = await startRes.json().catch(() => ({}));
327
+ throw new Error(errData.error || 'Failed to start passkey registration');
328
+ }
329
+ const options = await startRes.json();
330
+ // 2. Chama a biometria/PIN do dispositivo
331
+ const attResp = await startRegistration(options);
332
+ // 3. Envia o resultado de volta pro servidor validar e salvar
333
+ const finishRes = await fetch(`${basePath}/passkeys/register/finish`, {
334
+ method: 'POST',
335
+ headers: { 'Content-Type': 'application/json' },
336
+ body: JSON.stringify({ username, response: attResp, name })
337
+ });
338
+ const result = await finishRes.json();
339
+ if (result.success) {
340
+ return { success: true };
341
+ }
342
+ else {
343
+ return { success: false, error: result.error || 'Verification failed on server' };
344
+ }
345
+ }
346
+ catch (error) {
347
+ console.error('[vatts-auth] Error during passkey registration:', error);
348
+ return { success: false, error: error.message || 'Passkey registration failed' };
349
+ }
350
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vatts/auth",
3
- "version": "2.1.3-canary.1.0.2",
3
+ "version": "2.1.3-canary.1.0.4",
4
4
  "description": "Authentication package for Vatts.js framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,13 +50,17 @@
50
50
  "react": "^19.2.0",
51
51
  "react-dom": "^19.2.0",
52
52
  "vue": "^3.5.27",
53
- "vatts": "^2.1.3-canary.1.0.2"
53
+ "vatts": "^2.1.3-canary.1.0.4"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "react-dom": {
57
57
  "optional": true
58
58
  }
59
59
  },
60
+ "dependencies": {
61
+ "@simplewebauthn/browser": "^13.2.2",
62
+ "@simplewebauthn/server": "^13.2.3"
63
+ },
60
64
  "scripts": {
61
65
  "build": "tsc && copyfiles -u 1 \"src/**/*.vue\" dist",
62
66
  "build:watch": "tsc --watch",