@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.
Files changed (54) hide show
  1. package/README.md +234 -0
  2. package/dist/lib/auth/guards.test.d.ts +2 -0
  3. package/dist/lib/auth/guards.test.d.ts.map +1 -0
  4. package/dist/lib/auth/guards.test.js +105 -0
  5. package/dist/lib/auth/guards.test.js.map +1 -0
  6. package/dist/lib/auth/helpers.test.d.ts +2 -0
  7. package/dist/lib/auth/helpers.test.d.ts.map +1 -0
  8. package/dist/lib/auth/helpers.test.js +43 -0
  9. package/dist/lib/auth/helpers.test.js.map +1 -0
  10. package/dist/lib/auth/session.test.d.ts +2 -0
  11. package/dist/lib/auth/session.test.d.ts.map +1 -0
  12. package/dist/lib/auth/session.test.js +114 -0
  13. package/dist/lib/auth/session.test.js.map +1 -0
  14. package/dist/lib/firebase-admin.test.d.ts +2 -0
  15. package/dist/lib/firebase-admin.test.d.ts.map +1 -0
  16. package/dist/lib/firebase-admin.test.js +36 -0
  17. package/dist/lib/firebase-admin.test.js.map +1 -0
  18. package/dist/lib/firebase-client.test.d.ts +2 -0
  19. package/dist/lib/firebase-client.test.d.ts.map +1 -0
  20. package/dist/lib/firebase-client.test.js +167 -0
  21. package/dist/lib/firebase-client.test.js.map +1 -0
  22. package/dist/lib/format-time.test.d.ts +2 -0
  23. package/dist/lib/format-time.test.d.ts.map +1 -0
  24. package/dist/lib/format-time.test.js +41 -0
  25. package/dist/lib/format-time.test.js.map +1 -0
  26. package/dist/lib/linkify.test.d.ts +2 -0
  27. package/dist/lib/linkify.test.d.ts.map +1 -0
  28. package/dist/lib/linkify.test.js +40 -0
  29. package/dist/lib/linkify.test.js.map +1 -0
  30. package/dist/lib/logger.test.d.ts +2 -0
  31. package/dist/lib/logger.test.d.ts.map +1 -0
  32. package/dist/lib/logger.test.js +167 -0
  33. package/dist/lib/logger.test.js.map +1 -0
  34. package/dist/lib/rate-limiter.test.d.ts +2 -0
  35. package/dist/lib/rate-limiter.test.d.ts.map +1 -0
  36. package/dist/lib/rate-limiter.test.js +70 -0
  37. package/dist/lib/rate-limiter.test.js.map +1 -0
  38. package/dist/lib/uuid.test.d.ts +2 -0
  39. package/dist/lib/uuid.test.d.ts.map +1 -0
  40. package/dist/lib/uuid.test.js +22 -0
  41. package/dist/lib/uuid.test.js.map +1 -0
  42. package/dist/services/base.service.test.d.ts +2 -0
  43. package/dist/services/base.service.test.d.ts.map +1 -0
  44. package/dist/services/base.service.test.js +62 -0
  45. package/dist/services/base.service.test.js.map +1 -0
  46. package/dist/services/confirmation-token.service.test.d.ts +2 -0
  47. package/dist/services/confirmation-token.service.test.d.ts.map +1 -0
  48. package/dist/services/confirmation-token.service.test.js +65 -0
  49. package/dist/services/confirmation-token.service.test.js.map +1 -0
  50. package/dist/smoke.test.d.ts +2 -0
  51. package/dist/smoke.test.d.ts.map +1 -0
  52. package/dist/smoke.test.js +7 -0
  53. package/dist/smoke.test.js.map +1 -0
  54. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=guards.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=helpers.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=firebase-admin.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=firebase-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firebase-client.test.d.ts","sourceRoot":"","sources":["../../src/lib/firebase-client.test.ts"],"names":[],"mappings":""}