@opensaas/stack-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +447 -0
  3. package/dist/access/engine.d.ts +73 -0
  4. package/dist/access/engine.d.ts.map +1 -0
  5. package/dist/access/engine.js +244 -0
  6. package/dist/access/engine.js.map +1 -0
  7. package/dist/access/field-transforms.d.ts +47 -0
  8. package/dist/access/field-transforms.d.ts.map +1 -0
  9. package/dist/access/field-transforms.js +2 -0
  10. package/dist/access/field-transforms.js.map +1 -0
  11. package/dist/access/index.d.ts +3 -0
  12. package/dist/access/index.d.ts.map +1 -0
  13. package/dist/access/index.js +2 -0
  14. package/dist/access/index.js.map +1 -0
  15. package/dist/access/types.d.ts +83 -0
  16. package/dist/access/types.d.ts.map +1 -0
  17. package/dist/access/types.js +2 -0
  18. package/dist/access/types.js.map +1 -0
  19. package/dist/config/index.d.ts +39 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +38 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/types.d.ts +413 -0
  24. package/dist/config/types.d.ts.map +1 -0
  25. package/dist/config/types.js +2 -0
  26. package/dist/config/types.js.map +1 -0
  27. package/dist/context/index.d.ts +31 -0
  28. package/dist/context/index.d.ts.map +1 -0
  29. package/dist/context/index.js +524 -0
  30. package/dist/context/index.js.map +1 -0
  31. package/dist/context/nested-operations.d.ts +10 -0
  32. package/dist/context/nested-operations.d.ts.map +1 -0
  33. package/dist/context/nested-operations.js +261 -0
  34. package/dist/context/nested-operations.js.map +1 -0
  35. package/dist/fields/index.d.ts +78 -0
  36. package/dist/fields/index.d.ts.map +1 -0
  37. package/dist/fields/index.js +381 -0
  38. package/dist/fields/index.js.map +1 -0
  39. package/dist/hooks/index.d.ts +58 -0
  40. package/dist/hooks/index.d.ts.map +1 -0
  41. package/dist/hooks/index.js +79 -0
  42. package/dist/hooks/index.js.map +1 -0
  43. package/dist/index.d.ts +11 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +12 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lib/case-utils.d.ts +49 -0
  48. package/dist/lib/case-utils.d.ts.map +1 -0
  49. package/dist/lib/case-utils.js +68 -0
  50. package/dist/lib/case-utils.js.map +1 -0
  51. package/dist/lib/case-utils.test.d.ts +2 -0
  52. package/dist/lib/case-utils.test.d.ts.map +1 -0
  53. package/dist/lib/case-utils.test.js +101 -0
  54. package/dist/lib/case-utils.test.js.map +1 -0
  55. package/dist/utils/password.d.ts +81 -0
  56. package/dist/utils/password.d.ts.map +1 -0
  57. package/dist/utils/password.js +132 -0
  58. package/dist/utils/password.js.map +1 -0
  59. package/dist/validation/schema.d.ts +17 -0
  60. package/dist/validation/schema.d.ts.map +1 -0
  61. package/dist/validation/schema.js +42 -0
  62. package/dist/validation/schema.js.map +1 -0
  63. package/dist/validation/schema.test.d.ts +2 -0
  64. package/dist/validation/schema.test.d.ts.map +1 -0
  65. package/dist/validation/schema.test.js +143 -0
  66. package/dist/validation/schema.test.js.map +1 -0
  67. package/docs/type-distribution-fix.md +136 -0
  68. package/package.json +48 -0
  69. package/src/access/engine.ts +360 -0
  70. package/src/access/field-transforms.ts +99 -0
  71. package/src/access/index.ts +20 -0
  72. package/src/access/types.ts +103 -0
  73. package/src/config/index.ts +71 -0
  74. package/src/config/types.ts +478 -0
  75. package/src/context/index.ts +814 -0
  76. package/src/context/nested-operations.ts +412 -0
  77. package/src/fields/index.ts +438 -0
  78. package/src/hooks/index.ts +132 -0
  79. package/src/index.ts +62 -0
  80. package/src/lib/case-utils.test.ts +127 -0
  81. package/src/lib/case-utils.ts +74 -0
  82. package/src/utils/password.ts +147 -0
  83. package/src/validation/schema.test.ts +171 -0
  84. package/src/validation/schema.ts +59 -0
  85. package/tests/access-relationships.test.ts +613 -0
  86. package/tests/access.test.ts +499 -0
  87. package/tests/config.test.ts +195 -0
  88. package/tests/context.test.ts +248 -0
  89. package/tests/hooks.test.ts +417 -0
  90. package/tests/password-type-distribution.test.ts +155 -0
  91. package/tests/password-types.test.ts +147 -0
  92. package/tests/password.test.ts +249 -0
  93. package/tsconfig.json +12 -0
  94. package/tsconfig.tsbuildinfo +1 -0
  95. package/vitest.config.ts +27 -0
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { config, list, getContext } from '../src/index.js'
3
+ import { text, password, relationship } from '../src/fields/index.js'
4
+ import { PrismaClient } from '@prisma/client'
5
+ import type { HashedPassword } from '../src/utils/password.js'
6
+
7
+ /**
8
+ * Test to verify that password field transformation doesn't cause union type distribution
9
+ * This was the bug: all fields were becoming `string | HashedPassword` instead of just password field
10
+ */
11
+ describe('Password Field Type Distribution Bug Fix', () => {
12
+ const mockPrismaClient = {
13
+ user: {
14
+ findUnique: async () => ({
15
+ id: '1',
16
+ name: 'John Doe',
17
+ email: 'john@example.com',
18
+ password: '$2a$10$hashedpassword',
19
+ }),
20
+ findMany: async () => [
21
+ {
22
+ id: '1',
23
+ name: 'John Doe',
24
+ email: 'john@example.com',
25
+ password: '$2a$10$hashedpassword',
26
+ posts: [
27
+ {
28
+ id: 'post1',
29
+ title: 'Test Post',
30
+ authorId: '1',
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ },
36
+ post: {
37
+ findMany: async () => [],
38
+ },
39
+ } as unknown as PrismaClient
40
+
41
+ const testConfig = config({
42
+ db: {
43
+ provider: 'sqlite',
44
+ url: 'file:./test.db',
45
+ },
46
+ lists: {
47
+ User: list({
48
+ fields: {
49
+ name: text({ validation: { isRequired: true } }),
50
+ email: text({ validation: { isRequired: true } }),
51
+ password: password({ validation: { isRequired: true } }),
52
+ posts: relationship({ ref: 'Post.author', many: true }),
53
+ },
54
+ }),
55
+ Post: list({
56
+ fields: {
57
+ title: text({ validation: { isRequired: true } }),
58
+ author: relationship({ ref: 'User.posts' }),
59
+ },
60
+ }),
61
+ },
62
+ })
63
+
64
+ it('should NOT make all fields a union of string | HashedPassword', async () => {
65
+ const context = getContext(testConfig, mockPrismaClient, null)
66
+
67
+ const user = await context.db.user.findUnique({ where: { id: '1' } })
68
+
69
+ if (user) {
70
+ // Type assertions that should compile correctly
71
+ // These would fail if the bug exists (type would be string | HashedPassword for all fields)
72
+
73
+ // name should be string, not string | HashedPassword
74
+ const name: string = user.name
75
+ expect(typeof name).toBe('string')
76
+
77
+ // email should be string, not string | HashedPassword
78
+ const email: string = user.email
79
+ expect(typeof email).toBe('string')
80
+
81
+ // id should be string, not string | HashedPassword
82
+ const id: string = user.id
83
+ expect(typeof id).toBe('string')
84
+
85
+ // password SHOULD be HashedPassword
86
+ const password: HashedPassword = user.password
87
+ expect(typeof password.compare).toBe('function')
88
+
89
+ // If the bug existed, this would be a type error because
90
+ // string | HashedPassword doesn't have .compare()
91
+ const canCompare: boolean = await password.compare('test')
92
+ expect(typeof canCompare).toBe('boolean')
93
+ }
94
+ })
95
+
96
+ it('should preserve types with included relationships', async () => {
97
+ const context = getContext(testConfig, mockPrismaClient, null)
98
+
99
+ const users = await context.db.user.findMany({
100
+ include: {
101
+ posts: true,
102
+ },
103
+ })
104
+
105
+ if (users.length > 0) {
106
+ const user = users[0]
107
+
108
+ // Regular fields should have their original types
109
+ const name: string = user.name
110
+ const email: string = user.email
111
+ const id: string = user.id
112
+
113
+ // Password should be HashedPassword
114
+ const password: HashedPassword = user.password
115
+
116
+ // Posts should be included and typed
117
+ if (user.posts && user.posts.length > 0) {
118
+ const post = user.posts[0]
119
+ const title: string = post.title
120
+ expect(typeof title).toBe('string')
121
+ }
122
+
123
+ expect(typeof name).toBe('string')
124
+ expect(typeof email).toBe('string')
125
+ expect(typeof id).toBe('string')
126
+ expect(typeof password.compare).toBe('function')
127
+ }
128
+ })
129
+
130
+ it('should verify TypeScript narrowing works correctly', async () => {
131
+ const context = getContext(testConfig, mockPrismaClient, null)
132
+
133
+ const user = await context.db.user.findUnique({ where: { id: '1' } })
134
+
135
+ if (user) {
136
+ // This is the key test: if all fields were string | HashedPassword,
137
+ // we couldn't assign to string without type assertion
138
+
139
+ // These should compile without type assertions
140
+ function expectString(val: string): void {
141
+ expect(typeof val).toBe('string')
142
+ }
143
+
144
+ function expectHashedPassword(val: HashedPassword): void {
145
+ expect(typeof val.compare).toBe('function')
146
+ }
147
+
148
+ // These calls prove the types are correctly narrowed
149
+ expectString(user.name)
150
+ expectString(user.email)
151
+ expectString(user.id)
152
+ expectHashedPassword(user.password)
153
+ }
154
+ })
155
+ })
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { config, list, getContext } from '../src/index.js'
3
+ import { text, password } from '../src/fields/index.js'
4
+ import { PrismaClient } from '@prisma/client'
5
+ import type { HashedPassword } from '../src/utils/password.js'
6
+
7
+ /**
8
+ * Type tests for password field transformations
9
+ * These tests verify that TypeScript correctly infers HashedPassword type
10
+ */
11
+ describe('Password Field Type Safety', () => {
12
+ // Mock Prisma client for type testing
13
+ const mockPrismaClient = {
14
+ user: {
15
+ findUnique: async () => ({
16
+ id: '1',
17
+ email: 'test@example.com',
18
+ password: '$2a$10$hashedpassword',
19
+ }),
20
+ findMany: async () => [
21
+ {
22
+ id: '1',
23
+ email: 'test@example.com',
24
+ password: '$2a$10$hashedpassword',
25
+ },
26
+ ],
27
+ create: async () => ({
28
+ id: '1',
29
+ email: 'test@example.com',
30
+ password: '$2a$10$hashedpassword',
31
+ }),
32
+ update: async () => ({
33
+ id: '1',
34
+ email: 'test@example.com',
35
+ password: '$2a$10$hashedpassword',
36
+ }),
37
+ delete: async () => ({
38
+ id: '1',
39
+ email: 'test@example.com',
40
+ password: '$2a$10$hashedpassword',
41
+ }),
42
+ count: async () => 1,
43
+ },
44
+ } as unknown as PrismaClient
45
+
46
+ const testConfig = config({
47
+ db: {
48
+ provider: 'sqlite',
49
+ url: 'file:./test.db',
50
+ },
51
+ lists: {
52
+ User: list({
53
+ fields: {
54
+ email: text({ validation: { isRequired: true } }),
55
+ password: password({ validation: { isRequired: true } }),
56
+ },
57
+ }),
58
+ },
59
+ })
60
+
61
+ it('should transform password field to HashedPassword type in findUnique', async () => {
62
+ const context = getContext(testConfig, mockPrismaClient, null)
63
+
64
+ const user = await context.db.user.findUnique({ where: { id: '1' } })
65
+
66
+ // Type check: TypeScript should know that user.password is HashedPassword
67
+ if (user) {
68
+ // This should compile - compare method should exist
69
+ const isValid: boolean = await user.password.compare('plaintext')
70
+ expect(typeof isValid).toBe('boolean')
71
+
72
+ // Runtime check: verify it's actually a HashedPassword instance
73
+ expect(user.password).toBeDefined()
74
+ expect(typeof user.password.compare).toBe('function')
75
+ expect(typeof user.password.toString).toBe('function')
76
+ }
77
+ })
78
+
79
+ it('should transform password field to HashedPassword type in findMany', async () => {
80
+ const context = getContext(testConfig, mockPrismaClient, null)
81
+
82
+ const users = await context.db.user.findMany()
83
+
84
+ // Type check: TypeScript should know that users[0].password is HashedPassword
85
+ if (users.length > 0) {
86
+ const user = users[0]
87
+
88
+ // This should compile - compare method should exist
89
+ const isValid: boolean = await user.password.compare('plaintext')
90
+ expect(typeof isValid).toBe('boolean')
91
+
92
+ // Runtime check
93
+ expect(user.password).toBeDefined()
94
+ expect(typeof user.password.compare).toBe('function')
95
+ }
96
+ })
97
+
98
+ it('should transform password field to HashedPassword type in create', async () => {
99
+ const context = getContext(testConfig, mockPrismaClient, null)
100
+
101
+ const user = await context.db.user.create({
102
+ data: {
103
+ email: 'new@example.com',
104
+ password: 'plaintext',
105
+ },
106
+ })
107
+
108
+ // Type check: TypeScript should know that user.password is HashedPassword
109
+ if (user) {
110
+ // This should compile
111
+ const isValid: boolean = await user.password.compare('plaintext')
112
+ expect(typeof isValid).toBe('boolean')
113
+ }
114
+ })
115
+
116
+ it('should transform password field to HashedPassword type in update', async () => {
117
+ const context = getContext(testConfig, mockPrismaClient, null)
118
+
119
+ const user = await context.db.user.update({
120
+ where: { id: '1' },
121
+ data: {
122
+ password: 'newpassword',
123
+ },
124
+ })
125
+
126
+ // Type check: TypeScript should know that user.password is HashedPassword
127
+ if (user) {
128
+ // This should compile
129
+ const isValid: boolean = await user.password.compare('newpassword')
130
+ expect(typeof isValid).toBe('boolean')
131
+ }
132
+ })
133
+
134
+ it('should allow checking the HashedPassword type explicitly', async () => {
135
+ const context = getContext(testConfig, mockPrismaClient, null)
136
+
137
+ const user = await context.db.user.findUnique({ where: { id: '1' } })
138
+
139
+ if (user) {
140
+ // Explicit type assertion to verify TypeScript inference
141
+ const passwordField: HashedPassword = user.password
142
+
143
+ expect(passwordField).toBeDefined()
144
+ expect(await passwordField.compare('test')).toBe(false)
145
+ }
146
+ })
147
+ })
@@ -0,0 +1,249 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ hashPassword,
4
+ comparePassword,
5
+ isHashedPassword,
6
+ HashedPassword,
7
+ } from '../src/utils/password.js'
8
+
9
+ describe('Password Utilities', () => {
10
+ describe('hashPassword', () => {
11
+ it('should hash a plaintext password', async () => {
12
+ const plain = 'mypassword123'
13
+ const hashed = await hashPassword(plain)
14
+
15
+ expect(hashed).toBeDefined()
16
+ expect(hashed).not.toBe(plain)
17
+ expect(hashed.length).toBeGreaterThan(50)
18
+ expect(hashed).toMatch(/^\$2[aby]\$\d{2}\$/)
19
+ })
20
+
21
+ it('should generate different hashes for same password', async () => {
22
+ const plain = 'mypassword123'
23
+ const hash1 = await hashPassword(plain)
24
+ const hash2 = await hashPassword(plain)
25
+
26
+ expect(hash1).not.toBe(hash2) // Different salts
27
+ expect(await comparePassword(plain, hash1)).toBe(true)
28
+ expect(await comparePassword(plain, hash2)).toBe(true)
29
+ })
30
+
31
+ it('should reject empty string', async () => {
32
+ await expect(hashPassword('')).rejects.toThrow('Password must be a non-empty string')
33
+ })
34
+
35
+ it('should reject non-string input', async () => {
36
+ // @ts-expect-error - Testing invalid input
37
+ await expect(hashPassword(null)).rejects.toThrow('Password must be a non-empty string')
38
+ // @ts-expect-error - Testing invalid input
39
+ await expect(hashPassword(undefined)).rejects.toThrow('Password must be a non-empty string')
40
+ // @ts-expect-error - Testing invalid input
41
+ await expect(hashPassword(123)).rejects.toThrow('Password must be a non-empty string')
42
+ })
43
+
44
+ it('should accept custom cost factor', async () => {
45
+ const plain = 'mypassword123'
46
+ const hashed = await hashPassword(plain, 4) // Low cost for testing
47
+
48
+ expect(hashed).toMatch(/^\$2[aby]\$04\$/) // Cost factor 04
49
+ expect(await comparePassword(plain, hashed)).toBe(true)
50
+ })
51
+ })
52
+
53
+ describe('comparePassword', () => {
54
+ it('should return true for matching passwords', async () => {
55
+ const plain = 'mypassword123'
56
+ const hashed = await hashPassword(plain)
57
+
58
+ const result = await comparePassword(plain, hashed)
59
+ expect(result).toBe(true)
60
+ })
61
+
62
+ it('should return false for non-matching passwords', async () => {
63
+ const plain1 = 'mypassword123'
64
+ const plain2 = 'wrongpassword'
65
+ const hashed = await hashPassword(plain1)
66
+
67
+ const result = await comparePassword(plain2, hashed)
68
+ expect(result).toBe(false)
69
+ })
70
+
71
+ it('should return false for empty strings', async () => {
72
+ const hashed = await hashPassword('test')
73
+
74
+ expect(await comparePassword('', hashed)).toBe(false)
75
+ expect(await comparePassword('test', '')).toBe(false)
76
+ })
77
+
78
+ it('should return false for non-string inputs', async () => {
79
+ const hashed = await hashPassword('test')
80
+
81
+ // @ts-expect-error - Testing invalid input
82
+ expect(await comparePassword(null, hashed)).toBe(false)
83
+ // @ts-expect-error - Testing invalid input
84
+ expect(await comparePassword('test', null)).toBe(false)
85
+ // @ts-expect-error - Testing invalid input
86
+ expect(await comparePassword(undefined, hashed)).toBe(false)
87
+ })
88
+
89
+ it('should return false for invalid hash format', async () => {
90
+ const result = await comparePassword('test', 'not-a-valid-hash')
91
+ expect(result).toBe(false)
92
+ })
93
+
94
+ it('should be case sensitive', async () => {
95
+ const plain = 'MyPassword123'
96
+ const hashed = await hashPassword(plain)
97
+
98
+ expect(await comparePassword('MyPassword123', hashed)).toBe(true)
99
+ expect(await comparePassword('mypassword123', hashed)).toBe(false)
100
+ expect(await comparePassword('MYPASSWORD123', hashed)).toBe(false)
101
+ })
102
+ })
103
+
104
+ describe('isHashedPassword', () => {
105
+ it('should return true for bcrypt hashes', async () => {
106
+ const hash = await hashPassword('test')
107
+ expect(isHashedPassword(hash)).toBe(true)
108
+ })
109
+
110
+ it('should return true for various bcrypt formats', () => {
111
+ const hash2a = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy'
112
+ const hash2b = '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy'
113
+ const hash2y = '$2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy'
114
+
115
+ expect(isHashedPassword(hash2a)).toBe(true)
116
+ expect(isHashedPassword(hash2b)).toBe(true)
117
+ expect(isHashedPassword(hash2y)).toBe(true)
118
+ })
119
+
120
+ it('should return false for plaintext passwords', () => {
121
+ expect(isHashedPassword('mypassword123')).toBe(false)
122
+ expect(isHashedPassword('$2a$')).toBe(false) // Too short
123
+ expect(isHashedPassword('$3a$10$...')).toBe(false) // Wrong version
124
+ expect(isHashedPassword('')).toBe(false)
125
+ })
126
+
127
+ it('should return false for non-string inputs', () => {
128
+ // @ts-expect-error - Testing invalid input
129
+ expect(isHashedPassword(null)).toBe(false)
130
+ // @ts-expect-error - Testing invalid input
131
+ expect(isHashedPassword(undefined)).toBe(false)
132
+ // @ts-expect-error - Testing invalid input
133
+ expect(isHashedPassword(123)).toBe(false)
134
+ })
135
+ })
136
+
137
+ describe('HashedPassword class', () => {
138
+ it('should wrap a hash and provide compare method', async () => {
139
+ const plain = 'mypassword123'
140
+ const hash = await hashPassword(plain)
141
+ const wrapped = new HashedPassword(hash)
142
+
143
+ const result = await wrapped.compare(plain)
144
+ expect(result).toBe(true)
145
+ })
146
+
147
+ it('should return false for wrong password', async () => {
148
+ const hash = await hashPassword('correct')
149
+ const wrapped = new HashedPassword(hash)
150
+
151
+ const result = await wrapped.compare('wrong')
152
+ expect(result).toBe(false)
153
+ })
154
+
155
+ it('should convert to string correctly', async () => {
156
+ const hash = await hashPassword('test')
157
+ const wrapped = new HashedPassword(hash)
158
+
159
+ expect(wrapped.toString()).toBe(hash)
160
+ expect(String(wrapped)).toBe(hash)
161
+ expect(`Password: ${wrapped}`).toBe(`Password: ${hash}`)
162
+ })
163
+
164
+ it('should serialize to JSON correctly', async () => {
165
+ const hash = await hashPassword('test')
166
+ const wrapped = new HashedPassword(hash)
167
+
168
+ const json = JSON.stringify({ password: wrapped })
169
+ expect(json).toBe(JSON.stringify({ password: hash }))
170
+ })
171
+
172
+ it('should work with valueOf', async () => {
173
+ const hash = await hashPassword('test')
174
+ const wrapped = new HashedPassword(hash)
175
+
176
+ expect(wrapped.valueOf()).toBe(hash)
177
+ })
178
+
179
+ it('should throw error for invalid input', () => {
180
+ // @ts-expect-error - Testing invalid input
181
+ expect(() => new HashedPassword(null)).toThrow(
182
+ 'HashedPassword requires a non-empty hash string',
183
+ )
184
+ // @ts-expect-error - Testing invalid input
185
+ expect(() => new HashedPassword(undefined)).toThrow(
186
+ 'HashedPassword requires a non-empty hash string',
187
+ )
188
+ // @ts-expect-error - Testing invalid input
189
+ expect(() => new HashedPassword('')).toThrow(
190
+ 'HashedPassword requires a non-empty hash string',
191
+ )
192
+ })
193
+
194
+ it('should work in comparisons and object operations', async () => {
195
+ const hash = await hashPassword('test')
196
+ const wrapped = new HashedPassword(hash)
197
+
198
+ // Should be usable as a string
199
+ expect(wrapped == hash).toBe(true)
200
+ expect(wrapped === hash).toBe(false) // Not strictly equal (different types)
201
+
202
+ // Should work in object spread
203
+ const obj = { password: wrapped }
204
+ expect(obj.password.toString()).toBe(hash)
205
+ })
206
+ })
207
+
208
+ describe('Integration: Full password lifecycle', () => {
209
+ it('should hash, store, retrieve, and verify password', async () => {
210
+ // 1. Hash password (would happen in create operation)
211
+ const plainPassword = 'userPassword123!'
212
+ const hashedPassword = await hashPassword(plainPassword)
213
+
214
+ // 2. Store in database (simulated)
215
+ const storedUser = {
216
+ id: '1',
217
+ email: 'test@example.com',
218
+ password: hashedPassword,
219
+ }
220
+
221
+ // 3. Retrieve from database and wrap with HashedPassword
222
+ const retrievedUser = {
223
+ ...storedUser,
224
+ password: new HashedPassword(storedUser.password),
225
+ }
226
+
227
+ // 4. Verify password on login
228
+ const loginAttempt1 = await retrievedUser.password.compare(plainPassword)
229
+ expect(loginAttempt1).toBe(true)
230
+
231
+ const loginAttempt2 = await retrievedUser.password.compare('wrongPassword')
232
+ expect(loginAttempt2).toBe(false)
233
+ })
234
+
235
+ it('should not re-hash already hashed passwords', async () => {
236
+ const plain = 'password123'
237
+ const hash = await hashPassword(plain)
238
+
239
+ // If password is already hashed, don't hash again
240
+ if (isHashedPassword(hash)) {
241
+ expect(isHashedPassword(hash)).toBe(true)
242
+ // Would skip hashing in actual implementation
243
+ } else {
244
+ // Would hash in actual implementation
245
+ await hashPassword(plain)
246
+ }
247
+ })
248
+ })
249
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true,
7
+ "declaration": true,
8
+ "declarationMap": true
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist"]
12
+ }