@quatrain/auth-firebase 1.1.13 → 1.1.15

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.md ADDED
@@ -0,0 +1,15 @@
1
+ # LICENSE UPDATE NOTICE
2
+
3
+ As of 01/01/2026, Quatrain Core is licensed under the **GNU Affero General Public License v3.0 (AGPL v3)**.
4
+ Previous versions remain under the MIT License.
5
+
6
+ ## Why AGPL?
7
+
8
+ We believe in open collaboration for the development ecosystem. The AGPL ensures that any modification or deployment of this BaaS stack, including over a network, benefits the entire community.
9
+
10
+ ## Commercial Services & Enterprise Usage
11
+
12
+ We provide official deployment services, technical training, and certification for Quatrain Core.
13
+ For organizations requiring a non-copyleft license (commercial license) or custom proprietary integrations, please contact the copyright holder: **Quatrain Technologies**.
14
+
15
+ Copyright © 2024-2026 Quatrain Technologies. All Rights Reserved.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @quatrain/auth-firebase
2
+
3
+ An authentication adapter for Firebase Authentication. This package integrates Quatrain's auth system with Google's managed authentication service.
4
+
5
+ ## Features
6
+
7
+ - Implements the `@quatrain/auth` abstract adapter.
8
+ - Supports email/password, social logins (Google, Facebook, etc.), and custom tokens.
9
+ - Integrates with Firebase security rules.
10
+ - Uses the `firebase-admin` SDK for server-side operations.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @quatrain/auth-firebase firebase-admin
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Setup
21
+
22
+ ```typescript
23
+ import { Auth } from '@quatrain/auth'
24
+ import { FirebaseAuthAdapter } from '@quatrain/auth-firebase'
25
+
26
+ const adapter = new FirebaseAuthAdapter({
27
+ config: {
28
+ projectId: 'your-project-id',
29
+ // Other Firebase Admin SDK config
30
+ apiKey: 'your-api-key', // Required for token refresh
31
+ },
32
+ })
33
+ Auth.addProvider(adapter, 'default', true)
34
+ ```
35
+
36
+ ### Register a New User
37
+
38
+ ```typescript
39
+ import { User } from '@quatrain/backend'
40
+
41
+ const user = new User({
42
+ uid: 'unique-user-id',
43
+ _: {
44
+ name: 'John Doe',
45
+ email: 'john@example.com',
46
+ phone: '+1234567890',
47
+ },
48
+ })
49
+
50
+ const userId = await adapter.register(user, 'password123')
51
+ console.log('User registered with ID:', userId)
52
+ ```
53
+
54
+ ### Verify Auth Token
55
+
56
+ ```typescript
57
+ const decodedToken = await adapter.getAuthToken('firebase-id-token')
58
+ console.log('Token verified for user:', decodedToken.uid)
59
+ ```
60
+
61
+ ### Update User Profile
62
+
63
+ ```typescript
64
+ await adapter.update(user, {
65
+ email: 'newemail@example.com',
66
+ displayName: 'Jane Doe',
67
+ phoneNumber: '+0987654321',
68
+ })
69
+ ```
70
+
71
+ ### Delete User
72
+
73
+ ```typescript
74
+ await adapter.delete(user)
75
+ console.log('User deleted')
76
+ ```
77
+
78
+ ### Refresh Token
79
+
80
+ ```typescript
81
+ const newTokens = await adapter.refreshToken(refreshToken)
82
+ console.log('New access token:', newTokens.access_token)
83
+ ```
84
+
85
+ > [!WARNING] > **Incomplete Implementation**
86
+ >
87
+ > The following methods are currently not implemented (empty stubs):
88
+ >
89
+ > - `signup()` - Client-side sign in
90
+ > - `signout()` - Sign out user
91
+ > - `revokeAuthToken()` - Revoke tokens
92
+ > - `setCustomUserClaims()` - Set custom claims
93
+ >
94
+ > These methods exist to satisfy the interface but do not perform any operations.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ const FirebaseAuthAdapter_1 = require("./FirebaseAuthAdapter");
36
+ const auth_1 = require("@quatrain/auth");
37
+ const auth_2 = require("firebase-admin/auth");
38
+ const app_1 = require("firebase-admin/app");
39
+ const nativeFetch = __importStar(require("node-fetch-native"));
40
+ // Mock the dependencies
41
+ jest.mock('firebase-admin/auth');
42
+ jest.mock('firebase-admin/app');
43
+ jest.mock('node-fetch-native');
44
+ describe('FirebaseAuthAdapter', () => {
45
+ let adapter;
46
+ let mockAuth;
47
+ beforeEach(() => {
48
+ // Clear all mocks before each test
49
+ jest.clearAllMocks();
50
+ // Mock Firebase Admin SDK
51
+ mockAuth = {
52
+ createUser: jest.fn(),
53
+ verifyIdToken: jest.fn(),
54
+ updateUser: jest.fn(),
55
+ deleteUser: jest.fn(),
56
+ };
57
+ auth_2.getAuth.mockReturnValue(mockAuth);
58
+ app_1.getApps.mockReturnValue([]);
59
+ // Create adapter instance
60
+ adapter = new FirebaseAuthAdapter_1.FirebaseAuthAdapter({
61
+ config: {
62
+ projectId: 'test-project',
63
+ apiKey: 'test-api-key',
64
+ },
65
+ });
66
+ });
67
+ afterEach(() => {
68
+ jest.restoreAllMocks();
69
+ });
70
+ describe('Constructor', () => {
71
+ it('should initialize Firebase app when no apps exist', () => {
72
+ expect(app_1.getApps).toHaveBeenCalled();
73
+ expect(app_1.initializeApp).toHaveBeenCalledWith({
74
+ projectId: 'test-project',
75
+ apiKey: 'test-api-key',
76
+ });
77
+ });
78
+ it('should not initialize Firebase app when app already exists', () => {
79
+ ;
80
+ app_1.getApps.mockReturnValue([{ name: 'existing-app' }]);
81
+ new FirebaseAuthAdapter_1.FirebaseAuthAdapter({
82
+ config: {
83
+ projectId: 'test-project',
84
+ },
85
+ });
86
+ // initializeApp should still be called once from the first adapter creation
87
+ expect(app_1.initializeApp).toHaveBeenCalledTimes(1);
88
+ });
89
+ });
90
+ describe('register()', () => {
91
+ const mockUser = {
92
+ uid: 'user-123',
93
+ _: {
94
+ name: 'John Doe',
95
+ email: 'john@example.com',
96
+ phone: '+1234567890',
97
+ password: 'hashed-password',
98
+ disabled: false,
99
+ },
100
+ };
101
+ it('should successfully register a new user', () => __awaiter(void 0, void 0, void 0, function* () {
102
+ const mockUserRecord = {
103
+ uid: 'user-123',
104
+ email: 'john@example.com',
105
+ };
106
+ mockAuth.createUser.mockResolvedValue(mockUserRecord);
107
+ const result = yield adapter.register(mockUser, 'clearPassword123');
108
+ expect(mockAuth.createUser).toHaveBeenCalledWith({
109
+ uid: 'user-123',
110
+ email: 'john@example.com',
111
+ phoneNumber: '+1234567890',
112
+ password: 'clearPassword123',
113
+ disabled: false,
114
+ displayName: 'John Doe',
115
+ });
116
+ expect(result).toBe('user-123');
117
+ }));
118
+ it('should use hashed password when clearPassword is not provided', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ const mockUserRecord = {
120
+ uid: 'user-123',
121
+ email: 'john@example.com',
122
+ };
123
+ mockAuth.createUser.mockResolvedValue(mockUserRecord);
124
+ yield adapter.register(mockUser);
125
+ expect(mockAuth.createUser).toHaveBeenCalledWith({
126
+ uid: 'user-123',
127
+ email: 'john@example.com',
128
+ phoneNumber: '+1234567890',
129
+ password: 'hashed-password',
130
+ disabled: false,
131
+ displayName: 'John Doe',
132
+ });
133
+ }));
134
+ it('should handle user with default disabled value', () => __awaiter(void 0, void 0, void 0, function* () {
135
+ const userWithoutDisabled = {
136
+ uid: 'user-456',
137
+ _: {
138
+ name: 'Jane Doe',
139
+ email: 'jane@example.com',
140
+ phone: '+0987654321',
141
+ password: 'password456',
142
+ },
143
+ };
144
+ mockAuth.createUser.mockResolvedValue({ uid: 'user-456' });
145
+ yield adapter.register(userWithoutDisabled, 'password');
146
+ const createUserCall = mockAuth.createUser.mock.calls[0][0];
147
+ expect(createUserCall.disabled).toBe(false);
148
+ }));
149
+ it('should throw AuthenticationError on Firebase error', () => __awaiter(void 0, void 0, void 0, function* () {
150
+ mockAuth.createUser.mockRejectedValue(new Error('Email already exists'));
151
+ yield expect(adapter.register(mockUser, 'password')).rejects.toThrow(auth_1.AuthenticationError);
152
+ yield expect(adapter.register(mockUser, 'password')).rejects.toThrow('Email already exists');
153
+ }));
154
+ });
155
+ describe('getAuthToken()', () => {
156
+ it('should successfully verify auth token', () => __awaiter(void 0, void 0, void 0, function* () {
157
+ const mockDecodedToken = {
158
+ uid: 'user-123',
159
+ email: 'test@example.com',
160
+ iat: Date.now(),
161
+ };
162
+ mockAuth.verifyIdToken.mockResolvedValue(mockDecodedToken);
163
+ const result = yield adapter.getAuthToken('bearer-token-123');
164
+ expect(mockAuth.verifyIdToken).toHaveBeenCalledWith('bearer-token-123');
165
+ expect(result).toEqual(mockDecodedToken);
166
+ }));
167
+ it('should throw error when token is invalid', () => __awaiter(void 0, void 0, void 0, function* () {
168
+ mockAuth.verifyIdToken.mockRejectedValue(new Error('Invalid token'));
169
+ yield expect(adapter.getAuthToken('invalid-token')).rejects.toThrow('Invalid token');
170
+ }));
171
+ });
172
+ describe('update()', () => {
173
+ const mockUser = {
174
+ uid: 'user-123',
175
+ _: {
176
+ email: 'test@example.com',
177
+ },
178
+ };
179
+ it('should successfully update user', () => __awaiter(void 0, void 0, void 0, function* () {
180
+ const updatable = {
181
+ email: 'newemail@example.com',
182
+ displayName: 'New Name',
183
+ };
184
+ mockAuth.updateUser.mockResolvedValue({});
185
+ yield adapter.update(mockUser, updatable);
186
+ expect(mockAuth.updateUser).toHaveBeenCalledWith('user-123', updatable);
187
+ }));
188
+ it('should skip update when updatable is empty', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ yield adapter.update(mockUser, {});
190
+ expect(mockAuth.updateUser).not.toHaveBeenCalled();
191
+ }));
192
+ it('should silently handle errors during update', () => __awaiter(void 0, void 0, void 0, function* () {
193
+ const updatable = { displayName: 'New Name' };
194
+ mockAuth.updateUser.mockRejectedValue(new Error('Update failed'));
195
+ // Should not throw
196
+ yield expect(adapter.update(mockUser, updatable)).resolves.toBeUndefined();
197
+ expect(mockAuth.updateUser).toHaveBeenCalledWith('user-123', updatable);
198
+ }));
199
+ });
200
+ describe('delete()', () => {
201
+ const mockUser = {
202
+ uid: 'user-123',
203
+ };
204
+ it('should successfully delete user', () => __awaiter(void 0, void 0, void 0, function* () {
205
+ mockAuth.deleteUser.mockResolvedValue(undefined);
206
+ yield adapter.delete(mockUser);
207
+ expect(mockAuth.deleteUser).toHaveBeenCalledWith('user-123');
208
+ }));
209
+ it('should throw error on deletion failure', () => __awaiter(void 0, void 0, void 0, function* () {
210
+ mockAuth.deleteUser.mockRejectedValue(new Error('User not found'));
211
+ yield expect(adapter.delete(mockUser)).rejects.toThrow('User not found');
212
+ }));
213
+ });
214
+ describe('refreshToken()', () => {
215
+ it('should successfully refresh token', () => __awaiter(void 0, void 0, void 0, function* () {
216
+ const mockResponse = {
217
+ access_token: 'new-access-token',
218
+ refresh_token: 'new-refresh-token',
219
+ expires_in: 3600,
220
+ };
221
+ const mockFetchResponse = {
222
+ json: jest.fn().mockResolvedValue(mockResponse),
223
+ };
224
+ nativeFetch.fetch.mockResolvedValue(mockFetchResponse);
225
+ const result = yield adapter.refreshToken('old-refresh-token');
226
+ expect(nativeFetch.fetch).toHaveBeenCalledWith('https://securetoken.googleapis.com/v1/token?key=test-api-key', {
227
+ method: 'POST',
228
+ body: JSON.stringify({
229
+ grant_type: 'refresh_token',
230
+ refresh_token: 'old-refresh-token',
231
+ }),
232
+ headers: { 'Content-Type': 'application/json' },
233
+ });
234
+ // Note: Implementation doesn't await json(), so result is a promise
235
+ const resultValue = yield result;
236
+ expect(resultValue).toEqual(mockResponse);
237
+ }));
238
+ it('should return empty object when API key is missing', () => __awaiter(void 0, void 0, void 0, function* () {
239
+ const adapterWithoutKey = new FirebaseAuthAdapter_1.FirebaseAuthAdapter({
240
+ config: {
241
+ projectId: 'test-project',
242
+ },
243
+ });
244
+ const result = yield adapterWithoutKey.refreshToken('refresh-token');
245
+ expect(result).toEqual({});
246
+ expect(nativeFetch.fetch).not.toHaveBeenCalled();
247
+ }));
248
+ it('should construct correct URL with API key', () => __awaiter(void 0, void 0, void 0, function* () {
249
+ const mockFetchResponse = {
250
+ json: jest.fn().mockResolvedValue({}),
251
+ };
252
+ nativeFetch.fetch.mockResolvedValue(mockFetchResponse);
253
+ yield adapter.refreshToken('refresh-token');
254
+ const callArgs = nativeFetch.fetch.mock.calls[0];
255
+ expect(callArgs[0]).toBe('https://securetoken.googleapis.com/v1/token?key=test-api-key');
256
+ }));
257
+ });
258
+ describe('Unimplemented Methods (Stubs)', () => {
259
+ describe('signup()', () => {
260
+ it('should have signup method defined', () => {
261
+ expect(adapter.signup).toBeDefined();
262
+ expect(typeof adapter.signup).toBe('function');
263
+ });
264
+ it('should return undefined (not implemented)', () => __awaiter(void 0, void 0, void 0, function* () {
265
+ const result = yield adapter.signup('test@example.com', 'password');
266
+ expect(result).toBeUndefined();
267
+ }));
268
+ });
269
+ describe('signout()', () => {
270
+ it('should have signout method defined', () => {
271
+ expect(adapter.signout).toBeDefined();
272
+ expect(typeof adapter.signout).toBe('function');
273
+ });
274
+ it('should return undefined (not implemented)', () => __awaiter(void 0, void 0, void 0, function* () {
275
+ const mockUser = { uid: 'user-123' };
276
+ const result = yield adapter.signout(mockUser);
277
+ expect(result).toBeUndefined();
278
+ }));
279
+ });
280
+ describe('revokeAuthToken()', () => {
281
+ it('should have revokeAuthToken method defined', () => {
282
+ expect(adapter.revokeAuthToken).toBeDefined();
283
+ expect(typeof adapter.revokeAuthToken).toBe('function');
284
+ });
285
+ it('should return undefined (not implemented)', () => __awaiter(void 0, void 0, void 0, function* () {
286
+ const result = yield adapter.revokeAuthToken('token-123');
287
+ expect(result).toBeUndefined();
288
+ }));
289
+ });
290
+ describe('setCustomUserClaims()', () => {
291
+ it('should have setCustomUserClaims method defined', () => {
292
+ expect(adapter.setCustomUserClaims).toBeDefined();
293
+ expect(typeof adapter.setCustomUserClaims).toBe('function');
294
+ });
295
+ it('should return undefined (not implemented)', () => __awaiter(void 0, void 0, void 0, function* () {
296
+ const result = yield adapter.setCustomUserClaims('user-123', {
297
+ role: 'admin',
298
+ });
299
+ expect(result).toBeUndefined();
300
+ }));
301
+ });
302
+ });
303
+ });
package/package.json CHANGED
@@ -1,42 +1,43 @@
1
1
  {
2
- "name": "@quatrain/auth-firebase",
3
- "version": "1.1.13",
4
- "license": "MIT",
5
- "main": "lib/index.js",
6
- "types": "lib/index.d.ts",
7
- "files": [
8
- "lib/",
9
- "README.md"
10
- ],
11
- "author": "Quatrain Développement SAS <developers@quatrain.com>",
12
- "peerDependencies": {
13
- "@quatrain/core": "^1.1.19"
14
- },
15
- "dependencies": {
16
- "@quatrain/auth": "^1.1.17",
17
- "firebase-admin": "^13.0.1"
18
- },
19
- "devDependencies": {
20
- "@faker-js/faker": "^7.6.0",
21
- "@tsconfig/recommended": "^1.0.1",
22
- "@types/jest": "^27.0.3",
23
- "@types/node": "^22.10.1",
24
- "jest": "^27.4.7",
25
- "jest-node-exports-resolver": "^1.1.6",
26
- "jest-serial-runner": "^1.2.1",
27
- "trace-unhandled": "^2.0.1",
28
- "ts-jest": "^27.1.2",
29
- "ts-node": "^10.9.1",
30
- "typescript": "^5.1.5"
31
- },
32
- "scripts": {
33
- "build": "tsc",
34
- "wbuild": "tsc --watch",
35
- "bump-to": "yarn version",
36
- "hash": "node ../../bin/hashFolder.js",
37
- "hash:persist": "yarn hash > .hash_latest.txt",
38
- "hash:compare": "yarn hash > .hash_newest.txt && cmp -s .hash_latest.txt .hash_newest.txt",
39
- "publish": "yarn hash:compare || yarn publish:process",
40
- "publish:process": "yarn version patch && yarn build && yarn npm publish --access public && yarn hash:persist"
41
- }
42
- }
2
+ "name": "@quatrain/auth-firebase",
3
+ "version": "1.1.15",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "bun": "src/index.ts",
8
+ "files": [
9
+ "LICENSE.md",
10
+ "src/",
11
+ "lib/",
12
+ "README.md"
13
+ ],
14
+ "author": "Quatrain Développement SAS <developers@quatrain.com>",
15
+ "peerDependencies": {
16
+ "@quatrain/core": "^1.1.43"
17
+ },
18
+ "dependencies": {
19
+ "@quatrain/auth": "^1.1.20",
20
+ "@quatrain/backend": "^1.1.26",
21
+ "@quatrain/core": "^1.1.43",
22
+ "firebase-admin": "^13.0.1",
23
+ "node-fetch-native": "^1.6.4"
24
+ },
25
+ "devDependencies": {
26
+ "@tsconfig/recommended": "^1.0.1",
27
+ "@types/jest": "^30.0.0",
28
+ "@types/node": "^22.10.1",
29
+ "jest": "^30.2.0",
30
+ "jest-node-exports-resolver": "^1.1.6",
31
+ "jest-serial-runner": "^1.2.1",
32
+ "trace-unhandled": "^2.0.1",
33
+ "ts-jest": "^29.4.1",
34
+ "ts-node": "^10.9.1",
35
+ "typescript": "^5.2.2"
36
+ },
37
+ "scripts": {
38
+ "test-ci": "jest --runInBand",
39
+ "build": "tsc",
40
+ "wbuild": "tsc --watch",
41
+ "bump-to": "yarn version"
42
+ }
43
+ }
@@ -0,0 +1,357 @@
1
+ import { FirebaseAuthAdapter } from './FirebaseAuthAdapter'
2
+ import { User } from '@quatrain/backend'
3
+ import { AuthenticationError } from '@quatrain/auth'
4
+ import { getAuth } from 'firebase-admin/auth'
5
+ import { getApps, initializeApp } from 'firebase-admin/app'
6
+ import * as nativeFetch from 'node-fetch-native'
7
+
8
+ // Mock the dependencies
9
+ jest.mock('firebase-admin/auth')
10
+ jest.mock('firebase-admin/app')
11
+ jest.mock('node-fetch-native')
12
+
13
+ describe('FirebaseAuthAdapter', () => {
14
+ let adapter: FirebaseAuthAdapter
15
+ let mockAuth: any
16
+
17
+ beforeEach(() => {
18
+ // Clear all mocks before each test
19
+ jest.clearAllMocks()
20
+
21
+ // Mock Firebase Admin SDK
22
+ mockAuth = {
23
+ createUser: jest.fn(),
24
+ verifyIdToken: jest.fn(),
25
+ updateUser: jest.fn(),
26
+ deleteUser: jest.fn(),
27
+ }
28
+ ;(getAuth as jest.Mock).mockReturnValue(mockAuth)
29
+ ;(getApps as jest.Mock).mockReturnValue([])
30
+
31
+ // Create adapter instance
32
+ adapter = new FirebaseAuthAdapter({
33
+ config: {
34
+ projectId: 'test-project',
35
+ apiKey: 'test-api-key',
36
+ },
37
+ })
38
+ })
39
+
40
+ afterEach(() => {
41
+ jest.restoreAllMocks()
42
+ })
43
+
44
+ describe('Constructor', () => {
45
+ it('should initialize Firebase app when no apps exist', () => {
46
+ expect(getApps).toHaveBeenCalled()
47
+ expect(initializeApp).toHaveBeenCalledWith({
48
+ projectId: 'test-project',
49
+ apiKey: 'test-api-key',
50
+ })
51
+ })
52
+
53
+ it('should not initialize Firebase app when app already exists', () => {
54
+ ;(getApps as jest.Mock).mockReturnValue([{ name: 'existing-app' }])
55
+
56
+ new FirebaseAuthAdapter({
57
+ config: {
58
+ projectId: 'test-project',
59
+ },
60
+ })
61
+
62
+ // initializeApp should still be called once from the first adapter creation
63
+ expect(initializeApp).toHaveBeenCalledTimes(1)
64
+ })
65
+ })
66
+
67
+ describe('register()', () => {
68
+ const mockUser = {
69
+ uid: 'user-123',
70
+ _: {
71
+ name: 'John Doe',
72
+ email: 'john@example.com',
73
+ phone: '+1234567890',
74
+ password: 'hashed-password',
75
+ disabled: false,
76
+ },
77
+ } as unknown as User
78
+
79
+ it('should successfully register a new user', async () => {
80
+ const mockUserRecord = {
81
+ uid: 'user-123',
82
+ email: 'john@example.com',
83
+ }
84
+
85
+ mockAuth.createUser.mockResolvedValue(mockUserRecord)
86
+
87
+ const result = await adapter.register(mockUser, 'clearPassword123')
88
+
89
+ expect(mockAuth.createUser).toHaveBeenCalledWith({
90
+ uid: 'user-123',
91
+ email: 'john@example.com',
92
+ phoneNumber: '+1234567890',
93
+ password: 'clearPassword123',
94
+ disabled: false,
95
+ displayName: 'John Doe',
96
+ })
97
+
98
+ expect(result).toBe('user-123')
99
+ })
100
+
101
+ it('should use hashed password when clearPassword is not provided', async () => {
102
+ const mockUserRecord = {
103
+ uid: 'user-123',
104
+ email: 'john@example.com',
105
+ }
106
+
107
+ mockAuth.createUser.mockResolvedValue(mockUserRecord)
108
+
109
+ await adapter.register(mockUser)
110
+
111
+ expect(mockAuth.createUser).toHaveBeenCalledWith({
112
+ uid: 'user-123',
113
+ email: 'john@example.com',
114
+ phoneNumber: '+1234567890',
115
+ password: 'hashed-password',
116
+ disabled: false,
117
+ displayName: 'John Doe',
118
+ })
119
+ })
120
+
121
+ it('should handle user with default disabled value', async () => {
122
+ const userWithoutDisabled = {
123
+ uid: 'user-456',
124
+ _: {
125
+ name: 'Jane Doe',
126
+ email: 'jane@example.com',
127
+ phone: '+0987654321',
128
+ password: 'password456',
129
+ },
130
+ } as unknown as User
131
+
132
+ mockAuth.createUser.mockResolvedValue({ uid: 'user-456' })
133
+
134
+ await adapter.register(userWithoutDisabled, 'password')
135
+
136
+ const createUserCall = mockAuth.createUser.mock.calls[0][0]
137
+ expect(createUserCall.disabled).toBe(false)
138
+ })
139
+
140
+ it('should throw AuthenticationError on Firebase error', async () => {
141
+ mockAuth.createUser.mockRejectedValue(
142
+ new Error('Email already exists')
143
+ )
144
+
145
+ await expect(adapter.register(mockUser, 'password')).rejects.toThrow(
146
+ AuthenticationError
147
+ )
148
+
149
+ await expect(adapter.register(mockUser, 'password')).rejects.toThrow(
150
+ 'Email already exists'
151
+ )
152
+ })
153
+ })
154
+
155
+ describe('getAuthToken()', () => {
156
+ it('should successfully verify auth token', async () => {
157
+ const mockDecodedToken = {
158
+ uid: 'user-123',
159
+ email: 'test@example.com',
160
+ iat: Date.now(),
161
+ }
162
+
163
+ mockAuth.verifyIdToken.mockResolvedValue(mockDecodedToken)
164
+
165
+ const result = await adapter.getAuthToken('bearer-token-123')
166
+
167
+ expect(mockAuth.verifyIdToken).toHaveBeenCalledWith('bearer-token-123')
168
+ expect(result).toEqual(mockDecodedToken)
169
+ })
170
+
171
+ it('should throw error when token is invalid', async () => {
172
+ mockAuth.verifyIdToken.mockRejectedValue(new Error('Invalid token'))
173
+
174
+ await expect(adapter.getAuthToken('invalid-token')).rejects.toThrow(
175
+ 'Invalid token'
176
+ )
177
+ })
178
+ })
179
+
180
+ describe('update()', () => {
181
+ const mockUser = {
182
+ uid: 'user-123',
183
+ _: {
184
+ email: 'test@example.com',
185
+ },
186
+ } as unknown as User
187
+
188
+ it('should successfully update user', async () => {
189
+ const updatable = {
190
+ email: 'newemail@example.com',
191
+ displayName: 'New Name',
192
+ }
193
+
194
+ mockAuth.updateUser.mockResolvedValue({})
195
+
196
+ await adapter.update(mockUser, updatable)
197
+
198
+ expect(mockAuth.updateUser).toHaveBeenCalledWith('user-123', updatable)
199
+ })
200
+
201
+ it('should skip update when updatable is empty', async () => {
202
+ await adapter.update(mockUser, {})
203
+
204
+ expect(mockAuth.updateUser).not.toHaveBeenCalled()
205
+ })
206
+
207
+ it('should silently handle errors during update', async () => {
208
+ const updatable = { displayName: 'New Name' }
209
+
210
+ mockAuth.updateUser.mockRejectedValue(new Error('Update failed'))
211
+
212
+ // Should not throw
213
+ await expect(
214
+ adapter.update(mockUser, updatable)
215
+ ).resolves.toBeUndefined()
216
+
217
+ expect(mockAuth.updateUser).toHaveBeenCalledWith('user-123', updatable)
218
+ })
219
+ })
220
+
221
+ describe('delete()', () => {
222
+ const mockUser = {
223
+ uid: 'user-123',
224
+ } as unknown as User
225
+
226
+ it('should successfully delete user', async () => {
227
+ mockAuth.deleteUser.mockResolvedValue(undefined)
228
+
229
+ await adapter.delete(mockUser)
230
+
231
+ expect(mockAuth.deleteUser).toHaveBeenCalledWith('user-123')
232
+ })
233
+
234
+ it('should throw error on deletion failure', async () => {
235
+ mockAuth.deleteUser.mockRejectedValue(new Error('User not found'))
236
+
237
+ await expect(adapter.delete(mockUser)).rejects.toThrow(
238
+ 'User not found'
239
+ )
240
+ })
241
+ })
242
+
243
+ describe('refreshToken()', () => {
244
+ it('should successfully refresh token', async () => {
245
+ const mockResponse = {
246
+ access_token: 'new-access-token',
247
+ refresh_token: 'new-refresh-token',
248
+ expires_in: 3600,
249
+ }
250
+
251
+ const mockFetchResponse = {
252
+ json: jest.fn().mockResolvedValue(mockResponse),
253
+ }
254
+
255
+ ;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockFetchResponse)
256
+
257
+ const result = await adapter.refreshToken('old-refresh-token')
258
+
259
+ expect(nativeFetch.fetch).toHaveBeenCalledWith(
260
+ 'https://securetoken.googleapis.com/v1/token?key=test-api-key',
261
+ {
262
+ method: 'POST',
263
+ body: JSON.stringify({
264
+ grant_type: 'refresh_token',
265
+ refresh_token: 'old-refresh-token',
266
+ }),
267
+ headers: { 'Content-Type': 'application/json' },
268
+ }
269
+ )
270
+
271
+ // Note: Implementation doesn't await json(), so result is a promise
272
+ const resultValue = await result
273
+ expect(resultValue).toEqual(mockResponse)
274
+ })
275
+
276
+ it('should return empty object when API key is missing', async () => {
277
+ const adapterWithoutKey = new FirebaseAuthAdapter({
278
+ config: {
279
+ projectId: 'test-project',
280
+ },
281
+ })
282
+
283
+ const result = await adapterWithoutKey.refreshToken('refresh-token')
284
+
285
+ expect(result).toEqual({})
286
+ expect(nativeFetch.fetch).not.toHaveBeenCalled()
287
+ })
288
+
289
+ it('should construct correct URL with API key', async () => {
290
+ const mockFetchResponse = {
291
+ json: jest.fn().mockResolvedValue({}),
292
+ }
293
+
294
+ ;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockFetchResponse)
295
+
296
+ await adapter.refreshToken('refresh-token')
297
+
298
+ const callArgs = (nativeFetch.fetch as jest.Mock).mock.calls[0]
299
+ expect(callArgs[0]).toBe(
300
+ 'https://securetoken.googleapis.com/v1/token?key=test-api-key'
301
+ )
302
+ })
303
+ })
304
+
305
+ describe('Unimplemented Methods (Stubs)', () => {
306
+ describe('signup()', () => {
307
+ it('should have signup method defined', () => {
308
+ expect(adapter.signup).toBeDefined()
309
+ expect(typeof adapter.signup).toBe('function')
310
+ })
311
+
312
+ it('should return undefined (not implemented)', async () => {
313
+ const result = await adapter.signup('test@example.com', 'password')
314
+ expect(result).toBeUndefined()
315
+ })
316
+ })
317
+
318
+ describe('signout()', () => {
319
+ it('should have signout method defined', () => {
320
+ expect(adapter.signout).toBeDefined()
321
+ expect(typeof adapter.signout).toBe('function')
322
+ })
323
+
324
+ it('should return undefined (not implemented)', async () => {
325
+ const mockUser = { uid: 'user-123' } as unknown as User
326
+ const result = await adapter.signout(mockUser)
327
+ expect(result).toBeUndefined()
328
+ })
329
+ })
330
+
331
+ describe('revokeAuthToken()', () => {
332
+ it('should have revokeAuthToken method defined', () => {
333
+ expect(adapter.revokeAuthToken).toBeDefined()
334
+ expect(typeof adapter.revokeAuthToken).toBe('function')
335
+ })
336
+
337
+ it('should return undefined (not implemented)', async () => {
338
+ const result = await adapter.revokeAuthToken('token-123')
339
+ expect(result).toBeUndefined()
340
+ })
341
+ })
342
+
343
+ describe('setCustomUserClaims()', () => {
344
+ it('should have setCustomUserClaims method defined', () => {
345
+ expect(adapter.setCustomUserClaims).toBeDefined()
346
+ expect(typeof adapter.setCustomUserClaims).toBe('function')
347
+ })
348
+
349
+ it('should return undefined (not implemented)', async () => {
350
+ const result = await adapter.setCustomUserClaims('user-123', {
351
+ role: 'admin',
352
+ })
353
+ expect(result).toBeUndefined()
354
+ })
355
+ })
356
+ })
357
+ })
@@ -0,0 +1,99 @@
1
+ import { User } from '@quatrain/backend'
2
+ import {
3
+ Auth,
4
+ AbstractAuthAdapter,
5
+ AuthenticationError,
6
+ AuthParameters,
7
+ } from '@quatrain/auth'
8
+ import { CreateRequest, UpdateRequest, getAuth } from 'firebase-admin/auth'
9
+ import { getApps, initializeApp } from 'firebase-admin/app'
10
+ import * as nativeFetch from 'node-fetch-native'
11
+
12
+ export class FirebaseAuthAdapter extends AbstractAuthAdapter {
13
+ constructor(params: AuthParameters = {}) {
14
+ super(params)
15
+ if (getApps().length === 0) {
16
+ initializeApp(params.config)
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Register new user in authentication
22
+ * @param user
23
+ * @returns user unique id
24
+ */
25
+ async register(user: User, clearPassword?: string) {
26
+ try {
27
+ const {
28
+ name: displayName,
29
+ email,
30
+ phone: phoneNumber,
31
+ password,
32
+ disabled = false,
33
+ } = user._
34
+ const authData: CreateRequest = {
35
+ uid: user.uid,
36
+ email,
37
+ phoneNumber,
38
+ password: clearPassword || password,
39
+ disabled,
40
+ displayName,
41
+ }
42
+ Auth.log(`[FAA] Adding user '${displayName}'`)
43
+ const userRecord = await getAuth().createUser(authData)
44
+
45
+ return userRecord.uid
46
+ } catch (err) {
47
+ throw new AuthenticationError((err as Error).message)
48
+ }
49
+ }
50
+
51
+ async getAuthToken(bearer: string) {
52
+ return await getAuth().verifyIdToken(bearer)
53
+ }
54
+
55
+ async signup(login: string, password: string) {}
56
+
57
+ async signout(user: User): Promise<any> {}
58
+
59
+ async update(user: User, updatable: UpdateRequest): Promise<any> {
60
+ Auth.log('auth data to update', JSON.stringify(updatable))
61
+
62
+ try {
63
+ if (Object.keys(updatable).length > 0) {
64
+ Auth.log(`Updating ${updatable.displayName} Auth record`)
65
+ await getAuth().updateUser(user.uid, updatable)
66
+ }
67
+ } catch (e) {}
68
+ }
69
+
70
+ async delete(user: User): Promise<any> {
71
+ return await getAuth().deleteUser(user.uid)
72
+ }
73
+
74
+ async revokeAuthToken(token: string) {}
75
+
76
+ async setCustomUserClaims(id: string, claims: any) {}
77
+
78
+ async refreshToken(refreshToken: string): Promise<any> {
79
+ if (!this._params.config.apiKey) {
80
+ Auth.warn(`Can't get refresh token, no API key provided`)
81
+ return {}
82
+ }
83
+ const response = await nativeFetch.fetch(
84
+ `https://securetoken.googleapis.com/v1/token?key=${this._params.config.apiKey}`,
85
+ {
86
+ method: 'POST',
87
+ body: JSON.stringify({
88
+ grant_type: 'refresh_token',
89
+ refresh_token: refreshToken,
90
+ }),
91
+ headers: { 'Content-Type': 'application/json' }, //, crossdomain: true },
92
+ }
93
+ )
94
+
95
+ const data = response.json()
96
+
97
+ return data
98
+ }
99
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { FirebaseAuthAdapter } from './FirebaseAuthAdapter'
2
+
3
+ export { FirebaseAuthAdapter }