@prmichaelsen/agentbase-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/dist/lib/auth/guards.test.d.ts +2 -0
- package/dist/lib/auth/guards.test.d.ts.map +1 -0
- package/dist/lib/auth/guards.test.js +105 -0
- package/dist/lib/auth/guards.test.js.map +1 -0
- package/dist/lib/auth/helpers.test.d.ts +2 -0
- package/dist/lib/auth/helpers.test.d.ts.map +1 -0
- package/dist/lib/auth/helpers.test.js +43 -0
- package/dist/lib/auth/helpers.test.js.map +1 -0
- package/dist/lib/auth/session.test.d.ts +2 -0
- package/dist/lib/auth/session.test.d.ts.map +1 -0
- package/dist/lib/auth/session.test.js +114 -0
- package/dist/lib/auth/session.test.js.map +1 -0
- package/dist/lib/firebase-admin.test.d.ts +2 -0
- package/dist/lib/firebase-admin.test.d.ts.map +1 -0
- package/dist/lib/firebase-admin.test.js +36 -0
- package/dist/lib/firebase-admin.test.js.map +1 -0
- package/dist/lib/firebase-client.test.d.ts +2 -0
- package/dist/lib/firebase-client.test.d.ts.map +1 -0
- package/dist/lib/firebase-client.test.js +167 -0
- package/dist/lib/firebase-client.test.js.map +1 -0
- package/dist/lib/format-time.test.d.ts +2 -0
- package/dist/lib/format-time.test.d.ts.map +1 -0
- package/dist/lib/format-time.test.js +41 -0
- package/dist/lib/format-time.test.js.map +1 -0
- package/dist/lib/linkify.test.d.ts +2 -0
- package/dist/lib/linkify.test.d.ts.map +1 -0
- package/dist/lib/linkify.test.js +40 -0
- package/dist/lib/linkify.test.js.map +1 -0
- package/dist/lib/logger.test.d.ts +2 -0
- package/dist/lib/logger.test.d.ts.map +1 -0
- package/dist/lib/logger.test.js +167 -0
- package/dist/lib/logger.test.js.map +1 -0
- package/dist/lib/rate-limiter.test.d.ts +2 -0
- package/dist/lib/rate-limiter.test.d.ts.map +1 -0
- package/dist/lib/rate-limiter.test.js +70 -0
- package/dist/lib/rate-limiter.test.js.map +1 -0
- package/dist/lib/uuid.test.d.ts +2 -0
- package/dist/lib/uuid.test.d.ts.map +1 -0
- package/dist/lib/uuid.test.js +22 -0
- package/dist/lib/uuid.test.js.map +1 -0
- package/dist/services/base.service.test.d.ts +2 -0
- package/dist/services/base.service.test.d.ts.map +1 -0
- package/dist/services/base.service.test.js +62 -0
- package/dist/services/base.service.test.js.map +1 -0
- package/dist/services/confirmation-token.service.test.d.ts +2 -0
- package/dist/services/confirmation-token.service.test.d.ts.map +1 -0
- package/dist/services/confirmation-token.service.test.js +65 -0
- package/dist/services/confirmation-token.service.test.js.map +1 -0
- package/dist/smoke.test.d.ts +2 -0
- package/dist/smoke.test.d.ts.map +1 -0
- package/dist/smoke.test.js +7 -0
- package/dist/smoke.test.js.map +1 -0
- package/package.json +8 -3
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @prmichaelsen/agentbase-core
|
|
2
|
+
|
|
3
|
+
Shared service infrastructure for agentbase projects — BaseService, auth, Firebase wrappers, logging, and common utilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @prmichaelsen/agentbase-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer Dependencies
|
|
12
|
+
|
|
13
|
+
All peer dependencies are **optional** — install only what you use:
|
|
14
|
+
|
|
15
|
+
| Package | When needed |
|
|
16
|
+
|---------|-------------|
|
|
17
|
+
| `@prmichaelsen/firebase-admin-sdk-v8` | Server-side auth (`getServerSession`, `requireAuth`, `createSessionCookie`) |
|
|
18
|
+
| `firebase` | Client-side auth (`signIn`, `signUp`, `initializeFirebase`) |
|
|
19
|
+
| `jsonwebtoken` | JWT handling (if used directly) |
|
|
20
|
+
|
|
21
|
+
## Export Entry Points
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { ... } from '@prmichaelsen/agentbase-core' // everything
|
|
25
|
+
import { ... } from '@prmichaelsen/agentbase-core/services' // BaseService, ConfirmationTokenService
|
|
26
|
+
import { ... } from '@prmichaelsen/agentbase-core/lib' // logger, firebase, utilities
|
|
27
|
+
import { ... } from '@prmichaelsen/agentbase-core/lib/auth' // server auth guards
|
|
28
|
+
import type { ... } from '@prmichaelsen/agentbase-core/types' // AuthUser, ServerSession, AuthResult
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Services
|
|
32
|
+
|
|
33
|
+
### BaseService
|
|
34
|
+
|
|
35
|
+
Abstract base class for all services. Provides config injection, structured logging, and lifecycle hooks.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { BaseService, type Logger } from '@prmichaelsen/agentbase-core/services'
|
|
39
|
+
|
|
40
|
+
interface MyConfig { dbUrl: string }
|
|
41
|
+
|
|
42
|
+
class UserService extends BaseService<MyConfig> {
|
|
43
|
+
constructor(config: MyConfig, logger: Logger) {
|
|
44
|
+
super(config, logger)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async initialize() {
|
|
48
|
+
this.logger.info('Connecting to database', { url: this.config.dbUrl })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async shutdown() {
|
|
52
|
+
this.logger.info('Closing connections')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### ConfirmationTokenService
|
|
58
|
+
|
|
59
|
+
In-memory token store for two-step confirmation flows. Tokens are single-use, user-scoped, and expire after TTL.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { ConfirmationTokenService } from '@prmichaelsen/agentbase-core/services'
|
|
63
|
+
|
|
64
|
+
const confirmations = new ConfirmationTokenService(5 * 60 * 1000) // 5 min TTL
|
|
65
|
+
|
|
66
|
+
// Step 1: Generate token
|
|
67
|
+
const token = confirmations.generateToken({
|
|
68
|
+
type: 'delete-account',
|
|
69
|
+
userId: 'user-123',
|
|
70
|
+
params: { accountId: 'abc' },
|
|
71
|
+
summary: 'Delete account abc',
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Step 2: Consume token (single-use)
|
|
76
|
+
const action = confirmations.consumeToken(token, 'user-123')
|
|
77
|
+
if (action) {
|
|
78
|
+
// Execute the confirmed action
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Auth
|
|
83
|
+
|
|
84
|
+
### Server-Side (requires `@prmichaelsen/firebase-admin-sdk-v8`)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { getServerSession, requireAuth, requireAdmin, isAdmin } from '@prmichaelsen/agentbase-core/lib/auth'
|
|
88
|
+
|
|
89
|
+
// Get session from request cookies
|
|
90
|
+
const session = await getServerSession(request)
|
|
91
|
+
|
|
92
|
+
// Guard: returns null if authorized, Response(401) if not
|
|
93
|
+
const authError = await requireAuth(request)
|
|
94
|
+
if (authError) return authError
|
|
95
|
+
|
|
96
|
+
// Guard: returns null if admin, Response(401/403) if not
|
|
97
|
+
const adminError = await requireAdmin(request, 'admin@example.com,other@example.com')
|
|
98
|
+
if (adminError) return adminError
|
|
99
|
+
|
|
100
|
+
// Check admin status
|
|
101
|
+
const admin = await isAdmin(request, 'admin@example.com')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Client-Side (requires `firebase`)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import {
|
|
108
|
+
initializeFirebase, signIn, signUp, signInAnonymously,
|
|
109
|
+
upgradeAnonymousAccount, logout, onAuthChange, getIdToken,
|
|
110
|
+
} from '@prmichaelsen/agentbase-core/lib'
|
|
111
|
+
|
|
112
|
+
// Initialize once
|
|
113
|
+
initializeFirebase({ apiKey: '...', authDomain: '...', projectId: '...' })
|
|
114
|
+
|
|
115
|
+
// Auth flows
|
|
116
|
+
await signIn('user@example.com', 'password')
|
|
117
|
+
await signUp('user@example.com', 'password')
|
|
118
|
+
await signInAnonymously()
|
|
119
|
+
await upgradeAnonymousAccount('user@example.com', 'password')
|
|
120
|
+
await logout()
|
|
121
|
+
|
|
122
|
+
// Listen for auth changes
|
|
123
|
+
const unsubscribe = onAuthChange((user) => {
|
|
124
|
+
console.log(user ? `Signed in: ${user.uid}` : 'Signed out')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Get ID token for API calls
|
|
128
|
+
const token = await getIdToken()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Helpers
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { isRealUser, isRealUserServer } from '@prmichaelsen/agentbase-core/lib/auth'
|
|
135
|
+
|
|
136
|
+
// Client-side: works with any { isAnonymous?: boolean }
|
|
137
|
+
if (isRealUser(user)) { /* non-anonymous */ }
|
|
138
|
+
|
|
139
|
+
// Server-side: typed for AuthUser
|
|
140
|
+
if (isRealUserServer(authUser)) { /* non-anonymous */ }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Logger
|
|
144
|
+
|
|
145
|
+
Structured logger with automatic sensitive data redaction.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createLogger, authLogger, apiLogger, dbLogger, chatLogger } from '@prmichaelsen/agentbase-core/lib'
|
|
149
|
+
|
|
150
|
+
const logger = createLogger('MyService')
|
|
151
|
+
logger.info('Processing request', { userId: 'abc', apiKey: 'secret' })
|
|
152
|
+
// Output: [MyService] Processing request { userId: 'abc', apiKey: '[REDACTED]' }
|
|
153
|
+
|
|
154
|
+
logger.debug('Only in development') // Only logs when NODE_ENV=development
|
|
155
|
+
logger.error('Failed', new Error('boom'))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Sanitization Utilities
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { sanitizeToken, sanitizeEmail, sanitizeUserId, sanitizeObject } from '@prmichaelsen/agentbase-core/lib'
|
|
162
|
+
|
|
163
|
+
sanitizeToken('abc123...') // "a1b2c3d4..."
|
|
164
|
+
sanitizeEmail('john@x.com') // "jo***@x.com"
|
|
165
|
+
sanitizeUserId('user-12345') // "user_user-123"
|
|
166
|
+
sanitizeObject({ password: 'secret', name: 'ok' }) // { password: '[REDACTED]', name: 'ok' }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Firebase Admin
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { initFirebaseAdmin } from '@prmichaelsen/agentbase-core/lib'
|
|
173
|
+
|
|
174
|
+
// Reads FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY and FIREBASE_PROJECT_ID from env
|
|
175
|
+
initFirebaseAdmin()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Rate Limiter
|
|
179
|
+
|
|
180
|
+
Works with Cloudflare Workers Rate Limiting API.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { checkRateLimit, createRateLimitResponse, getRateLimitIdentifier } from '@prmichaelsen/agentbase-core/lib'
|
|
184
|
+
|
|
185
|
+
const identifier = getRateLimitIdentifier(request, userId)
|
|
186
|
+
const result = await checkRateLimit(rateLimiter, identifier, {
|
|
187
|
+
limit: 100,
|
|
188
|
+
period: 60,
|
|
189
|
+
keyPrefix: 'api',
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (!result.success) {
|
|
193
|
+
return createRateLimitResponse(result) // 429 with proper headers
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Utilities
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { formatExactTime, getRelativeTime, linkifyText, generateUUID } from '@prmichaelsen/agentbase-core/lib'
|
|
201
|
+
|
|
202
|
+
formatExactTime('2026-03-15T14:30:00Z') // "2:30 PM Sat 3/15/26"
|
|
203
|
+
getRelativeTime('2026-03-15T14:00:00Z') // "30m ago"
|
|
204
|
+
linkifyText('Visit https://example.com') // "Visit [https://example.com](https://example.com)"
|
|
205
|
+
generateUUID() // "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Types
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import type { AuthUser, ServerSession, AuthResult } from '@prmichaelsen/agentbase-core/types'
|
|
212
|
+
|
|
213
|
+
interface AuthUser {
|
|
214
|
+
uid: string
|
|
215
|
+
email: string | null
|
|
216
|
+
displayName: string | null
|
|
217
|
+
photoURL: string | null
|
|
218
|
+
emailVerified: boolean
|
|
219
|
+
isAnonymous: boolean
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface ServerSession {
|
|
223
|
+
user: AuthUser
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface AuthResult {
|
|
227
|
+
success: boolean
|
|
228
|
+
error?: string
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.test.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/guards.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const { mockVerifySessionCookie, mockVerifyIdToken } = vi.hoisted(() => ({
|
|
3
|
+
mockVerifySessionCookie: vi.fn(),
|
|
4
|
+
mockVerifyIdToken: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('@prmichaelsen/firebase-admin-sdk-v8', () => ({
|
|
7
|
+
verifySessionCookie: mockVerifySessionCookie,
|
|
8
|
+
verifyIdToken: mockVerifyIdToken,
|
|
9
|
+
createSessionCookie: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
// Suppress console output
|
|
12
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
13
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
14
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
15
|
+
vi.spyOn(console, 'debug').mockImplementation(() => { });
|
|
16
|
+
import { requireAuth, requireAdmin, isAdmin } from './guards.js';
|
|
17
|
+
function makeRequest(cookie) {
|
|
18
|
+
const headers = new Headers();
|
|
19
|
+
if (cookie)
|
|
20
|
+
headers.set('cookie', cookie);
|
|
21
|
+
return new Request('https://example.com', { headers });
|
|
22
|
+
}
|
|
23
|
+
const decodedToken = {
|
|
24
|
+
sub: 'user-123',
|
|
25
|
+
email: 'admin@example.com',
|
|
26
|
+
name: 'Admin',
|
|
27
|
+
picture: null,
|
|
28
|
+
email_verified: true,
|
|
29
|
+
firebase: { sign_in_provider: 'password' },
|
|
30
|
+
};
|
|
31
|
+
describe('requireAuth', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it('returns null (pass) for authenticated request', async () => {
|
|
36
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
37
|
+
const result = await requireAuth(makeRequest('session=valid'));
|
|
38
|
+
expect(result).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it('returns 401 for unauthenticated request', async () => {
|
|
41
|
+
const result = await requireAuth(makeRequest());
|
|
42
|
+
expect(result).toBeInstanceOf(Response);
|
|
43
|
+
expect(result.status).toBe(401);
|
|
44
|
+
const body = await result.json();
|
|
45
|
+
expect(body.error).toContain('Unauthorized');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('requireAdmin', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
it('returns null for admin email', async () => {
|
|
53
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
54
|
+
const result = await requireAdmin(makeRequest('session=valid'), 'admin@example.com,other@ex.com');
|
|
55
|
+
expect(result).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
it('returns 401 for unauthenticated request', async () => {
|
|
58
|
+
const result = await requireAdmin(makeRequest());
|
|
59
|
+
expect(result).toBeInstanceOf(Response);
|
|
60
|
+
expect(result.status).toBe(401);
|
|
61
|
+
});
|
|
62
|
+
it('returns 403 for non-admin authenticated user', async () => {
|
|
63
|
+
mockVerifySessionCookie.mockResolvedValue({
|
|
64
|
+
...decodedToken,
|
|
65
|
+
email: 'regular@example.com',
|
|
66
|
+
});
|
|
67
|
+
const result = await requireAdmin(makeRequest('session=valid'), 'admin@example.com');
|
|
68
|
+
expect(result).toBeInstanceOf(Response);
|
|
69
|
+
expect(result.status).toBe(403);
|
|
70
|
+
const body = await result.json();
|
|
71
|
+
expect(body.error).toContain('Forbidden');
|
|
72
|
+
});
|
|
73
|
+
it('reads OWNER_EMAILS from env when not passed', async () => {
|
|
74
|
+
const original = process.env.OWNER_EMAILS;
|
|
75
|
+
process.env.OWNER_EMAILS = 'admin@example.com';
|
|
76
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
77
|
+
const result = await requireAdmin(makeRequest('session=valid'));
|
|
78
|
+
expect(result).toBeNull();
|
|
79
|
+
process.env.OWNER_EMAILS = original;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('isAdmin', () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
vi.clearAllMocks();
|
|
85
|
+
});
|
|
86
|
+
it('returns true for admin email', async () => {
|
|
87
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
88
|
+
expect(await isAdmin(makeRequest('session=valid'), 'admin@example.com')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('returns false for non-admin', async () => {
|
|
91
|
+
mockVerifySessionCookie.mockResolvedValue({
|
|
92
|
+
...decodedToken,
|
|
93
|
+
email: 'nope@example.com',
|
|
94
|
+
});
|
|
95
|
+
expect(await isAdmin(makeRequest('session=valid'), 'admin@example.com')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it('returns false for unauthenticated', async () => {
|
|
98
|
+
expect(await isAdmin(makeRequest(), 'admin@example.com')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
it('handles comma-separated emails', async () => {
|
|
101
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
102
|
+
expect(await isAdmin(makeRequest('session=valid'), 'other@x.com, admin@example.com')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=guards.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.test.js","sourceRoot":"","sources":["../../../src/lib/auth/guards.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.test.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/helpers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isRealUser, isRealUserServer } from './helpers.js';
|
|
3
|
+
describe('isRealUser', () => {
|
|
4
|
+
it('returns true for non-anonymous user', () => {
|
|
5
|
+
expect(isRealUser({ isAnonymous: false })).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
it('returns false for anonymous user', () => {
|
|
8
|
+
expect(isRealUser({ isAnonymous: true })).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('returns false for null', () => {
|
|
11
|
+
expect(isRealUser(null)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
it('returns false for undefined', () => {
|
|
14
|
+
expect(isRealUser(undefined)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
it('returns true when isAnonymous is undefined (real user assumed)', () => {
|
|
17
|
+
expect(isRealUser({})).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('isRealUserServer', () => {
|
|
21
|
+
const makeUser = (overrides = {}) => ({
|
|
22
|
+
uid: 'u1',
|
|
23
|
+
email: 'a@b.com',
|
|
24
|
+
displayName: null,
|
|
25
|
+
photoURL: null,
|
|
26
|
+
emailVerified: true,
|
|
27
|
+
isAnonymous: false,
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
it('returns true for non-anonymous AuthUser', () => {
|
|
31
|
+
expect(isRealUserServer(makeUser())).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('returns false for anonymous AuthUser', () => {
|
|
34
|
+
expect(isRealUserServer(makeUser({ isAnonymous: true }))).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('returns false for null', () => {
|
|
37
|
+
expect(isRealUserServer(null)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it('returns false for undefined', () => {
|
|
40
|
+
expect(isRealUserServer(undefined)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=helpers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.test.js","sourceRoot":"","sources":["../../../src/lib/auth/helpers.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE3D,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,UAAU,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,UAAU,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,MAAM,QAAQ,GAAG,CAAC,SAAS,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QACpC,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,SAAS;QAChB,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE,IAAI;QACd,aAAa,EAAE,IAAI;QACnB,WAAW,EAAE,KAAK;QAClB,GAAG,SAAS;KACb,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.test.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/session.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const { mockVerifySessionCookie, mockVerifyIdToken, mockCreateFirebaseSessionCookie } = vi.hoisted(() => ({
|
|
3
|
+
mockVerifySessionCookie: vi.fn(),
|
|
4
|
+
mockVerifyIdToken: vi.fn(),
|
|
5
|
+
mockCreateFirebaseSessionCookie: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('@prmichaelsen/firebase-admin-sdk-v8', () => ({
|
|
8
|
+
verifySessionCookie: mockVerifySessionCookie,
|
|
9
|
+
verifyIdToken: mockVerifyIdToken,
|
|
10
|
+
createSessionCookie: mockCreateFirebaseSessionCookie,
|
|
11
|
+
}));
|
|
12
|
+
// Suppress console output in tests
|
|
13
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
14
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
15
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
16
|
+
vi.spyOn(console, 'debug').mockImplementation(() => { });
|
|
17
|
+
import { getServerSession, isAuthenticated, createSessionCookie, revokeSession } from './session.js';
|
|
18
|
+
function makeRequest(cookie) {
|
|
19
|
+
const headers = new Headers();
|
|
20
|
+
if (cookie)
|
|
21
|
+
headers.set('cookie', cookie);
|
|
22
|
+
return new Request('https://example.com', { headers });
|
|
23
|
+
}
|
|
24
|
+
const decodedToken = {
|
|
25
|
+
sub: 'user-123',
|
|
26
|
+
email: 'test@example.com',
|
|
27
|
+
name: 'Test User',
|
|
28
|
+
picture: 'https://photo.url',
|
|
29
|
+
email_verified: true,
|
|
30
|
+
firebase: { sign_in_provider: 'password' },
|
|
31
|
+
};
|
|
32
|
+
describe('getServerSession', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
it('returns session for valid session cookie', async () => {
|
|
37
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
38
|
+
const session = await getServerSession(makeRequest('session=valid-cookie'));
|
|
39
|
+
expect(session).toEqual({
|
|
40
|
+
user: {
|
|
41
|
+
uid: 'user-123',
|
|
42
|
+
email: 'test@example.com',
|
|
43
|
+
displayName: 'Test User',
|
|
44
|
+
photoURL: 'https://photo.url',
|
|
45
|
+
emailVerified: true,
|
|
46
|
+
isAnonymous: false,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
it('returns null when no cookie header', async () => {
|
|
51
|
+
const session = await getServerSession(makeRequest());
|
|
52
|
+
expect(session).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
it('returns null when no session cookie in header', async () => {
|
|
55
|
+
const session = await getServerSession(makeRequest('other=value'));
|
|
56
|
+
expect(session).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it('falls back to verifyIdToken on session cookie failure', async () => {
|
|
59
|
+
mockVerifySessionCookie.mockRejectedValue(new Error('invalid'));
|
|
60
|
+
mockVerifyIdToken.mockResolvedValue(decodedToken);
|
|
61
|
+
const session = await getServerSession(makeRequest('session=id-token'));
|
|
62
|
+
expect(session).not.toBeNull();
|
|
63
|
+
expect(mockVerifyIdToken).toHaveBeenCalledWith('id-token');
|
|
64
|
+
});
|
|
65
|
+
it('maps anonymous provider correctly', async () => {
|
|
66
|
+
mockVerifySessionCookie.mockResolvedValue({
|
|
67
|
+
...decodedToken,
|
|
68
|
+
email: undefined,
|
|
69
|
+
firebase: { sign_in_provider: 'anonymous' },
|
|
70
|
+
});
|
|
71
|
+
const session = await getServerSession(makeRequest('session=anon-cookie'));
|
|
72
|
+
expect(session.user.isAnonymous).toBe(true);
|
|
73
|
+
expect(session.user.email).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
it('returns null on complete verification failure', async () => {
|
|
76
|
+
mockVerifySessionCookie.mockRejectedValue(new Error('fail'));
|
|
77
|
+
mockVerifyIdToken.mockRejectedValue(new Error('fail'));
|
|
78
|
+
const session = await getServerSession(makeRequest('session=bad'));
|
|
79
|
+
expect(session).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('isAuthenticated', () => {
|
|
83
|
+
it('returns true for authenticated request', async () => {
|
|
84
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
85
|
+
expect(await isAuthenticated(makeRequest('session=valid'))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it('returns false for unauthenticated request', async () => {
|
|
88
|
+
expect(await isAuthenticated(makeRequest())).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('createSessionCookie', () => {
|
|
92
|
+
it('calls Firebase admin with correct params', async () => {
|
|
93
|
+
mockCreateFirebaseSessionCookie.mockResolvedValue('session-cookie-value');
|
|
94
|
+
const result = await createSessionCookie('id-token-123');
|
|
95
|
+
expect(mockCreateFirebaseSessionCookie).toHaveBeenCalledWith('id-token-123', {
|
|
96
|
+
expiresIn: 60 * 60 * 24 * 14 * 1000,
|
|
97
|
+
});
|
|
98
|
+
expect(result).toBe('session-cookie-value');
|
|
99
|
+
});
|
|
100
|
+
it('throws on failure', async () => {
|
|
101
|
+
mockCreateFirebaseSessionCookie.mockRejectedValue(new Error('nope'));
|
|
102
|
+
await expect(createSessionCookie('bad')).rejects.toThrow('Failed to create session cookie');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('revokeSession', () => {
|
|
106
|
+
it('does not throw for valid session', async () => {
|
|
107
|
+
mockVerifySessionCookie.mockResolvedValue(decodedToken);
|
|
108
|
+
await expect(revokeSession(makeRequest('session=valid'))).resolves.toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
it('does not throw for no session', async () => {
|
|
111
|
+
await expect(revokeSession(makeRequest())).resolves.toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
//# sourceMappingURL=session.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.test.js","sourceRoot":"","sources":["../../../src/lib/auth/session.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-admin.test.d.ts","sourceRoot":"","sources":["../../src/lib/firebase-admin.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
const mockInitializeApp = vi.fn();
|
|
3
|
+
vi.mock('@prmichaelsen/firebase-admin-sdk-v8', () => ({
|
|
4
|
+
initializeApp: mockInitializeApp,
|
|
5
|
+
}));
|
|
6
|
+
describe('initFirebaseAdmin', () => {
|
|
7
|
+
const originalEnv = { ...process.env };
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
process.env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY = '{"type":"service_account"}';
|
|
11
|
+
process.env.FIREBASE_PROJECT_ID = 'test-project';
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
process.env = { ...originalEnv };
|
|
15
|
+
vi.resetModules();
|
|
16
|
+
});
|
|
17
|
+
it('calls initializeApp with correct config from env vars', async () => {
|
|
18
|
+
const { initFirebaseAdmin } = await import('./firebase-admin.js');
|
|
19
|
+
initFirebaseAdmin();
|
|
20
|
+
expect(mockInitializeApp).toHaveBeenCalledWith({
|
|
21
|
+
serviceAccount: '{"type":"service_account"}',
|
|
22
|
+
projectId: 'test-project',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it('passes undefined when env vars not set', async () => {
|
|
26
|
+
delete process.env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY;
|
|
27
|
+
delete process.env.FIREBASE_PROJECT_ID;
|
|
28
|
+
const { initFirebaseAdmin } = await import('./firebase-admin.js');
|
|
29
|
+
initFirebaseAdmin();
|
|
30
|
+
expect(mockInitializeApp).toHaveBeenCalledWith({
|
|
31
|
+
serviceAccount: undefined,
|
|
32
|
+
projectId: undefined,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=firebase-admin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-admin.test.js","sourceRoot":"","sources":["../../src/lib/firebase-admin.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAExE,MAAM,iBAAiB,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;AAEjC,EAAE,CAAC,IAAI,CAAC,qCAAqC,EAAE,GAAG,EAAE,CAAC,CAAC;IACpD,aAAa,EAAE,iBAAiB;CACjC,CAAC,CAAC,CAAA;AAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,MAAM,WAAW,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAEtC,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,aAAa,EAAE,CAAA;QAClB,OAAO,CAAC,GAAG,CAAC,kCAAkC,GAAG,4BAA4B,CAAA;QAC7E,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,cAAc,CAAA;IAClD,CAAC,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,CAAC,GAAG,GAAG,EAAE,GAAG,WAAW,EAAE,CAAA;QAChC,EAAE,CAAC,YAAY,EAAE,CAAA;IACnB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAA;QACjE,iBAAiB,EAAE,CAAA;QACnB,MAAM,CAAC,iBAAiB,CAAC,CAAC,oBAAoB,CAAC;YAC7C,cAAc,EAAE,4BAA4B;YAC5C,SAAS,EAAE,cAAc;SAC1B,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,OAAO,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAA;QACrD,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAA;QACtC,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAA;QACjE,iBAAiB,EAAE,CAAA;QACnB,MAAM,CAAC,iBAAiB,CAAC,CAAC,oBAAoB,CAAC;YAC7C,cAAc,EAAE,SAAS;YACzB,SAAS,EAAE,SAAS;SACrB,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase-client.test.d.ts","sourceRoot":"","sources":["../../src/lib/firebase-client.test.ts"],"names":[],"mappings":""}
|