@opensaas/stack-auth 0.36.0 → 0.38.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 (47) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +146 -0
  3. package/CLAUDE.md +153 -9
  4. package/README.md +18 -7
  5. package/dist/config/adopt-better-auth-tables.d.ts +47 -0
  6. package/dist/config/adopt-better-auth-tables.d.ts.map +1 -1
  7. package/dist/config/adopt-better-auth-tables.js +29 -1
  8. package/dist/config/adopt-better-auth-tables.js.map +1 -1
  9. package/dist/config/derive-auth-lists.d.ts +7 -5
  10. package/dist/config/derive-auth-lists.d.ts.map +1 -1
  11. package/dist/config/derive-auth-lists.js +33 -34
  12. package/dist/config/derive-auth-lists.js.map +1 -1
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/index.js +41 -11
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/plugin.d.ts.map +1 -1
  17. package/dist/config/plugin.js +39 -27
  18. package/dist/config/plugin.js.map +1 -1
  19. package/dist/config/types.d.ts +143 -28
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/server/build-better-auth-options.test.d.ts +2 -0
  22. package/dist/server/build-better-auth-options.test.d.ts.map +1 -0
  23. package/dist/server/build-better-auth-options.test.js +29 -0
  24. package/dist/server/build-better-auth-options.test.js.map +1 -0
  25. package/dist/server/index.d.ts +112 -8
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +284 -96
  28. package/dist/server/index.js.map +1 -1
  29. package/dist/server/schema-converter.d.ts +3 -3
  30. package/dist/server/schema-converter.d.ts.map +1 -1
  31. package/package.json +5 -5
  32. package/src/config/adopt-better-auth-tables.ts +70 -1
  33. package/src/config/derive-auth-lists.ts +37 -38
  34. package/src/config/index.ts +47 -12
  35. package/src/config/plugin.ts +40 -28
  36. package/src/config/types.ts +144 -27
  37. package/src/server/build-better-auth-options.test.ts +59 -0
  38. package/src/server/index.ts +470 -106
  39. package/src/server/schema-converter.ts +3 -3
  40. package/tests/adopt-better-auth-tables.test.ts +99 -0
  41. package/tests/config.test.ts +66 -8
  42. package/tests/derive-auth-lists.test.ts +79 -5
  43. package/tests/generated-fk-shape.test.ts +65 -0
  44. package/tests/plugin-derived-keys.test.ts +48 -0
  45. package/tests/server.test.ts +723 -0
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/vitest.config.ts +7 -1
@@ -76,6 +76,105 @@ describe('adoptBetterAuthTables - recipe defaults', () => {
76
76
  })
77
77
  })
78
78
 
79
+ describe('adoptBetterAuthTables - table names (issue #862)', () => {
80
+ it('sets no tableName by default (prefixed modelName also pins the table, as before)', () => {
81
+ const fragment = adoptBetterAuthTables()
82
+
83
+ expect(fragment.user).toEqual({ modelName: 'AuthUser' })
84
+ expect(fragment.session).toEqual({ modelName: 'AuthSession' })
85
+ })
86
+
87
+ it('useBetterAuthTableNames sets every model tableName to the better-auth default lowercase name', () => {
88
+ const fragment = adoptBetterAuthTables({ useBetterAuthTableNames: true })
89
+
90
+ expect(fragment.user).toEqual({ modelName: 'AuthUser', tableName: 'user' })
91
+ expect(fragment.session).toEqual({ modelName: 'AuthSession', tableName: 'session' })
92
+ expect(fragment.account).toEqual({ modelName: 'AuthAccount', tableName: 'account' })
93
+ expect(fragment.verification).toEqual({
94
+ modelName: 'AuthVerification',
95
+ tableName: 'verification',
96
+ })
97
+ })
98
+
99
+ it('tableNames sets explicit per-model table names', () => {
100
+ const fragment = adoptBetterAuthTables({
101
+ tableNames: { user: 'users', session: 'user_sessions' },
102
+ })
103
+
104
+ expect(fragment.user).toEqual({ modelName: 'AuthUser', tableName: 'users' })
105
+ expect(fragment.session).toEqual({ modelName: 'AuthSession', tableName: 'user_sessions' })
106
+ // Models without an explicit tableName entry stay unset.
107
+ expect(fragment.account).toEqual({ modelName: 'AuthAccount' })
108
+ })
109
+
110
+ it('tableNames takes precedence over useBetterAuthTableNames for the same model', () => {
111
+ const fragment = adoptBetterAuthTables({
112
+ useBetterAuthTableNames: true,
113
+ tableNames: { user: 'custom_user_table' },
114
+ })
115
+
116
+ expect(fragment.user?.tableName).toBe('custom_user_table')
117
+ // Other models still fall back to the better-auth default.
118
+ expect(fragment.session?.tableName).toBe('session')
119
+ })
120
+
121
+ it('combines tableName with a field column map on the same model', () => {
122
+ const fragment = adoptBetterAuthTables({
123
+ useBetterAuthTableNames: true,
124
+ fields: { user: { name: 'full_name' } },
125
+ })
126
+
127
+ expect(fragment.user).toEqual({
128
+ modelName: 'AuthUser',
129
+ tableName: 'user',
130
+ fields: { name: 'full_name' },
131
+ })
132
+ })
133
+ })
134
+
135
+ describe('adoptBetterAuthTables - clean-diff adoption with better-auth default table names', () => {
136
+ it('produces Auth lists that @@map to the live lowercase tables under prefixed list keys', async () => {
137
+ const result = await generationConfig({
138
+ db: { provider: 'postgresql' },
139
+ plugins: [
140
+ authPlugin({
141
+ ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
142
+ emailAndPassword: { enabled: true },
143
+ }),
144
+ ],
145
+ lists: {
146
+ User: list({
147
+ fields: { subjectId: text({ validation: { isRequired: true } }) },
148
+ }),
149
+ },
150
+ })
151
+
152
+ // List keys stay prefixed (no collision with the app's own User)...
153
+ expect(result.lists).toHaveProperty('AuthUser')
154
+ // ...but the physical table is better-auth's own default lowercase name.
155
+ expect(result.lists.AuthUser.db).toEqual({ timestamps: true, map: 'user', schema: 'auth' })
156
+ expect(result.lists.AuthSession.db).toEqual({
157
+ timestamps: true,
158
+ map: 'session',
159
+ schema: 'auth',
160
+ })
161
+ expect(result.lists.AuthAccount.db).toEqual({
162
+ timestamps: true,
163
+ map: 'account',
164
+ schema: 'auth',
165
+ })
166
+ expect(result.lists.AuthVerification.db).toEqual({
167
+ timestamps: true,
168
+ map: 'verification',
169
+ schema: 'auth',
170
+ })
171
+
172
+ // The app's own domain User is untouched.
173
+ expect(result.lists.User.fields).toHaveProperty('subjectId')
174
+ expect(result.lists.User.fields).not.toHaveProperty('email')
175
+ })
176
+ })
177
+
79
178
  describe('adoptBetterAuthTables - composes with authPlugin', () => {
80
179
  it('spreads into authPlugin alongside the rest of the auth config', async () => {
81
180
  const result = await config({
@@ -14,9 +14,19 @@ describe('normalizeAuthConfig', () => {
14
14
  expect(result.emailVerification.enabled).toBe(false)
15
15
  expect(result.passwordReset.enabled).toBe(false)
16
16
  expect(result.session.expiresIn).toBe(604800) // 7 days
17
+ expect(result.session.updateAge).toBe(86400) // 1 day, matching better-auth's own default
17
18
  expect(result.sessionFields).toEqual(['userId', 'email', 'name'])
18
19
  expect(result.socialProviders).toEqual({})
19
20
  expect(result.betterAuthPlugins).toEqual([])
21
+ expect(result.betterAuthOptions).toEqual({})
22
+ })
23
+
24
+ it('should pass through betterAuthOptions unchanged', () => {
25
+ const result = normalizeAuthConfig({
26
+ betterAuthOptions: { baseURL: 'https://example.com' },
27
+ })
28
+
29
+ expect(result.betterAuthOptions).toEqual({ baseURL: 'https://example.com' })
20
30
  })
21
31
 
22
32
  it('should normalize email and password config', () => {
@@ -81,6 +91,14 @@ describe('normalizeAuthConfig', () => {
81
91
  expect(result.session.updateAge).toBe(false)
82
92
  })
83
93
 
94
+ it('should normalize a custom numeric session.updateAge', () => {
95
+ const result = normalizeAuthConfig({
96
+ session: { updateAge: 3600 },
97
+ })
98
+
99
+ expect(result.session.updateAge).toBe(3600)
100
+ })
101
+
84
102
  it('should normalize custom session fields', () => {
85
103
  const result = normalizeAuthConfig({
86
104
  sessionFields: ['userId', 'email', 'role'],
@@ -120,22 +138,24 @@ describe('normalizeAuthConfig', () => {
120
138
  expect(result.extendUserList.fields).toHaveProperty('role')
121
139
  })
122
140
 
123
- it('should include custom sendEmail function', () => {
124
- const mockSendEmail = async () => {}
141
+ it('should include custom sendResetPassword and sendVerificationEmail functions', () => {
142
+ const mockSendResetPassword = async () => {}
143
+ const mockSendVerificationEmail = async () => {}
125
144
 
126
145
  const result = normalizeAuthConfig({
127
- sendEmail: mockSendEmail,
146
+ emailAndPassword: { enabled: true, sendResetPassword: mockSendResetPassword },
147
+ emailVerification: { enabled: true, sendVerificationEmail: mockSendVerificationEmail },
128
148
  })
129
149
 
130
- expect(result.sendEmail).toBe(mockSendEmail)
150
+ expect(result.emailAndPassword.sendResetPassword).toBe(mockSendResetPassword)
151
+ expect(result.emailVerification.sendVerificationEmail).toBe(mockSendVerificationEmail)
131
152
  })
132
153
 
133
- it('should provide default sendEmail that logs to console', () => {
154
+ it('should provide default sendResetPassword and sendVerificationEmail that log to console', () => {
134
155
  const result = normalizeAuthConfig({})
135
156
 
136
- expect(typeof result.sendEmail).toBe('function')
137
- // Default sendEmail should be a function that accepts email params
138
- expect(result.sendEmail.length).toBe(1)
157
+ expect(typeof result.emailAndPassword.sendResetPassword).toBe('function')
158
+ expect(typeof result.emailVerification.sendVerificationEmail).toBe('function')
139
159
  })
140
160
 
141
161
  it('should include betterAuthPlugins', () => {
@@ -165,6 +185,44 @@ describe('normalizeAuthConfig', () => {
165
185
  expect(result.access.user).toBe(userAccess)
166
186
  expect(result.access.session).toBe(sessionAccess)
167
187
  })
188
+
189
+ describe('models.tableName (issue #862)', () => {
190
+ it('defaults tableName to undefined for unrenamed models', () => {
191
+ const result = normalizeAuthConfig({})
192
+
193
+ expect(result.models.user.tableName).toBeUndefined()
194
+ expect(result.models.session.tableName).toBeUndefined()
195
+ expect(result.models.account.tableName).toBeUndefined()
196
+ expect(result.models.verification.tableName).toBeUndefined()
197
+ })
198
+
199
+ it('defaults tableName to the renamed modelName, matching pre-#862 behaviour', () => {
200
+ const result = normalizeAuthConfig({
201
+ user: { modelName: 'AuthUser' },
202
+ })
203
+
204
+ expect(result.models.user.modelName).toBe('AuthUser')
205
+ expect(result.models.user.tableName).toBe('AuthUser')
206
+ })
207
+
208
+ it('resolves an explicit tableName independent of modelName', () => {
209
+ const result = normalizeAuthConfig({
210
+ user: { modelName: 'AuthUser', tableName: 'user' },
211
+ })
212
+
213
+ expect(result.models.user.modelName).toBe('AuthUser')
214
+ expect(result.models.user.tableName).toBe('user')
215
+ })
216
+
217
+ it('honours an explicit tableName even when modelName is left at its default', () => {
218
+ const result = normalizeAuthConfig({
219
+ session: { tableName: 'sessions' },
220
+ })
221
+
222
+ expect(result.models.session.modelName).toBe('Session')
223
+ expect(result.models.session.tableName).toBe('sessions')
224
+ })
225
+ })
168
226
  })
169
227
 
170
228
  describe('authPlugin', () => {
@@ -44,6 +44,13 @@ describe('deriveAuthLists - default behaviour (no overrides)', () => {
44
44
  expect(lists.User.fields.accounts.ref).toBe('Account.user')
45
45
  })
46
46
 
47
+ it('marks Session/Verification expiresAt as DB-required, matching better-auth (issue #863)', () => {
48
+ const { lists } = deriveAuthLists(defaultModels)
49
+
50
+ expect(lists.Session.fields.expiresAt.db?.isNullable).toBe(false)
51
+ expect(lists.Verification.fields.expiresAt.db?.isNullable).toBe(false)
52
+ })
53
+
47
54
  it('emits no table @@map and no scalar @map for default keys', () => {
48
55
  const { lists } = deriveAuthLists(defaultModels)
49
56
 
@@ -108,6 +115,13 @@ describe('deriveAuthLists - user FK shape mirrors better-auth (issue #679)', ()
108
115
  }
109
116
  })
110
117
 
118
+ it('marks the user FK non-nullable, matching better-auth (issue #863)', () => {
119
+ const { lists } = deriveAuthLists(defaultModels)
120
+
121
+ expect(lists.Session.fields.user.db?.isNullable).toBe(false)
122
+ expect(lists.Account.fields.user.db?.isNullable).toBe(false)
123
+ })
124
+
111
125
  it('keeps the cascade extendPrismaSchema alongside a userId column override', () => {
112
126
  const models: NormalizedAuthModels = {
113
127
  user: { modelName: 'User', fields: {} },
@@ -126,11 +140,16 @@ describe('deriveAuthLists - user FK shape mirrors better-auth (issue #679)', ()
126
140
  })
127
141
 
128
142
  describe('deriveAuthLists - custom modelName overrides', () => {
143
+ // `deriveAuthLists` is the pure derivation step: it consumes an already-
144
+ // resolved `tableName` rather than re-deriving one from `modelName`. That
145
+ // default derivation (tableName follows modelName when it differs from the
146
+ // better-auth default) lives in `normalizeModelConfig` (config/index.ts) —
147
+ // mirror its output here since these fixtures bypass normalization.
129
148
  const customModels: NormalizedAuthModels = {
130
- user: { modelName: 'AuthUser', fields: {} },
131
- session: { modelName: 'AuthSession', fields: {} },
132
- account: { modelName: 'AuthAccount', fields: {} },
133
- verification: { modelName: 'AuthVerification', fields: {} },
149
+ user: { modelName: 'AuthUser', tableName: 'AuthUser', fields: {} },
150
+ session: { modelName: 'AuthSession', tableName: 'AuthSession', fields: {} },
151
+ account: { modelName: 'AuthAccount', tableName: 'AuthAccount', fields: {} },
152
+ verification: { modelName: 'AuthVerification', tableName: 'AuthVerification', fields: {} },
134
153
  }
135
154
 
136
155
  it('derives list keys from modelName', () => {
@@ -179,6 +198,61 @@ describe('deriveAuthLists - custom modelName overrides', () => {
179
198
  })
180
199
  })
181
200
 
201
+ describe('deriveAuthLists - tableName independent of modelName (issue #862)', () => {
202
+ it('emits @@map from tableName even though modelName is unchanged', () => {
203
+ const models: NormalizedAuthModels = {
204
+ user: { modelName: 'User', tableName: 'users', fields: {} },
205
+ session: { modelName: 'Session', fields: {} },
206
+ account: { modelName: 'Account', fields: {} },
207
+ verification: { modelName: 'Verification', fields: {} },
208
+ }
209
+
210
+ const { keys, lists } = deriveAuthLists(models)
211
+
212
+ // The list key follows modelName, unaffected by tableName.
213
+ expect(keys.user).toBe('User')
214
+ expect(lists.User.db?.map).toBe('users')
215
+ })
216
+
217
+ it('adopts a better-auth default lowercase table under a prefixed list key', () => {
218
+ // The shape issue #862 exists for: a prefixed list key (to avoid
219
+ // colliding with the app's own domain User) whose live table is still
220
+ // better-auth's own default lowercase name.
221
+ const models: NormalizedAuthModels = {
222
+ user: { modelName: 'AuthUser', tableName: 'user', fields: {} },
223
+ session: { modelName: 'AuthSession', tableName: 'session', fields: {} },
224
+ account: { modelName: 'AuthAccount', tableName: 'account', fields: {} },
225
+ verification: { modelName: 'AuthVerification', tableName: 'verification', fields: {} },
226
+ }
227
+
228
+ const { keys, lists } = deriveAuthLists(models)
229
+
230
+ expect(keys).toEqual({
231
+ user: 'AuthUser',
232
+ session: 'AuthSession',
233
+ account: 'AuthAccount',
234
+ verification: 'AuthVerification',
235
+ })
236
+ expect(lists.AuthUser.db?.map).toBe('user')
237
+ expect(lists.AuthSession.db?.map).toBe('session')
238
+ expect(lists.AuthAccount.db?.map).toBe('account')
239
+ expect(lists.AuthVerification.db?.map).toBe('verification')
240
+ })
241
+
242
+ it('emits no @@map when tableName is unset, even with a renamed modelName', () => {
243
+ const models: NormalizedAuthModels = {
244
+ user: { modelName: 'AuthUser', fields: {} },
245
+ session: { modelName: 'Session', fields: {} },
246
+ account: { modelName: 'Account', fields: {} },
247
+ verification: { modelName: 'Verification', fields: {} },
248
+ }
249
+
250
+ const { lists } = deriveAuthLists(models)
251
+
252
+ expect(lists.AuthUser.db?.map).toBeUndefined()
253
+ })
254
+ })
255
+
182
256
  describe('deriveAuthLists - custom field column maps', () => {
183
257
  const models: NormalizedAuthModels = {
184
258
  user: { modelName: 'AuthUser', fields: { name: 'full_name', emailVerified: 'is_verified' } },
@@ -229,7 +303,7 @@ describe('deriveAuthLists - schema placement', () => {
229
303
 
230
304
  it('carries both @@map and @@schema for renamed + relocated lists', () => {
231
305
  const models: NormalizedAuthModels = {
232
- user: { modelName: 'AuthUser', fields: {}, schema: 'auth' },
306
+ user: { modelName: 'AuthUser', tableName: 'AuthUser', fields: {}, schema: 'auth' },
233
307
  session: { modelName: 'AuthSession', fields: {}, schema: 'auth' },
234
308
  account: { modelName: 'AuthAccount', fields: {}, schema: 'auth' },
235
309
  verification: { modelName: 'AuthVerification', fields: {}, schema: 'auth' },
@@ -79,3 +79,68 @@ describe('generated auth schema — Session/Account user FK mirrors better-auth
79
79
  expect(widget).toContain('@@index([ownerId])')
80
80
  })
81
81
  })
82
+
83
+ describe('generated auth schema — required columns mirror better-auth (issue #863)', () => {
84
+ it('emits a required (non-nullable) userId FK and user relation on Session and Account', async () => {
85
+ const schema = await generateSchema({
86
+ db: { provider: 'sqlite' },
87
+ plugins: [authPlugin({ emailAndPassword: { enabled: true } })],
88
+ lists: {},
89
+ })
90
+
91
+ for (const model of ['Session', 'Account']) {
92
+ const block = modelBlock(schema, model)
93
+
94
+ expect(block).toMatch(/userId\s+String\s/)
95
+ expect(block).not.toMatch(/userId\s+String\?/)
96
+ expect(block).toMatch(/user\s+User\s+@relation/)
97
+ expect(block).not.toMatch(/user\s+User\?/)
98
+ }
99
+ })
100
+
101
+ it('emits a required (non-nullable) expiresAt on Session and Verification', async () => {
102
+ const schema = await generateSchema({
103
+ db: { provider: 'sqlite' },
104
+ plugins: [authPlugin({ emailAndPassword: { enabled: true } })],
105
+ lists: {},
106
+ })
107
+
108
+ for (const model of ['Session', 'Verification']) {
109
+ const block = modelBlock(schema, model)
110
+
111
+ expect(block).toMatch(/expiresAt\s+DateTime\s/)
112
+ expect(block).not.toMatch(/expiresAt\s+DateTime\?/)
113
+ }
114
+ })
115
+ })
116
+
117
+ describe('generated auth schema — tableName independent of modelName (issue #862)', () => {
118
+ it('emits a prefixed model name with a @@map to a better-auth default lowercase table', async () => {
119
+ const schema = await generateSchema({
120
+ db: { provider: 'postgresql' },
121
+ plugins: [
122
+ authPlugin({
123
+ user: { modelName: 'AuthUser', tableName: 'user' },
124
+ session: { modelName: 'AuthSession', tableName: 'session' },
125
+ account: { modelName: 'AuthAccount', tableName: 'account' },
126
+ verification: { modelName: 'AuthVerification', tableName: 'verification' },
127
+ emailAndPassword: { enabled: true },
128
+ }),
129
+ ],
130
+ lists: {},
131
+ })
132
+
133
+ for (const [model, table] of [
134
+ ['AuthUser', 'user'],
135
+ ['AuthSession', 'session'],
136
+ ['AuthAccount', 'account'],
137
+ ['AuthVerification', 'verification'],
138
+ ]) {
139
+ const block = modelBlock(schema, model)
140
+ // The generated model keeps the prefixed name but maps to the live
141
+ // lowercase table — no DROP/CREATE rename against a real better-auth
142
+ // install using its own default table names.
143
+ expect(block).toContain(`@@map("${table}")`)
144
+ }
145
+ })
146
+ })
@@ -161,6 +161,54 @@ describe('authPlugin - add-vs-extend with derived keys', () => {
161
161
  expect(appUser.access?.operation?.update?.({} as any)).toBe(false)
162
162
  expect(appUser.access?.operation?.update).toBe(hostUserUpdate)
163
163
  })
164
+
165
+ it('preserves the derived Auth list db (map/schema/timestamps) and access when a better-auth plugin extends a base model (#861)', async () => {
166
+ // A better-auth provider plugin (e.g. the `anonymous` plugin) that ships a
167
+ // schema extension for the base `user` model — the exact shape adoptBetterAuthTables()
168
+ // + betterAuthPlugins produces in the field. Before the #861 fix, the
169
+ // plugin's schema loop ran BEFORE the derived Auth lists were added, so it
170
+ // pre-empted `AuthUser` with a bare `list({ fields })` (no `db`, no
171
+ // `access`) — and the real derived list then lost to the field-only
172
+ // `extendList` merge, silently dropping `@@map`/`@@schema`/timestamps/access.
173
+ const anonymousBetterAuthPlugin = {
174
+ id: 'anonymous',
175
+ schema: {
176
+ user: {
177
+ fields: {
178
+ isAnonymous: { type: 'boolean', required: false },
179
+ },
180
+ },
181
+ },
182
+ }
183
+
184
+ const userQuery = vi.fn(() => true)
185
+ const result = await config({
186
+ db: { provider: 'postgresql' },
187
+ plugins: [
188
+ authPlugin({
189
+ schema: 'auth',
190
+ user: { modelName: 'AuthUser' },
191
+ session: { modelName: 'AuthSession' },
192
+ account: { modelName: 'AuthAccount' },
193
+ verification: { modelName: 'AuthVerification' },
194
+ betterAuthPlugins: [anonymousBetterAuthPlugin],
195
+ access: { user: { operation: { query: userQuery } } },
196
+ }),
197
+ ],
198
+ lists: {},
199
+ })
200
+
201
+ const authUser = result.lists.AuthUser
202
+
203
+ // The plugin's schema extension is merged in...
204
+ expect(authUser.fields).toHaveProperty('isAnonymous')
205
+ // ...without displacing the derived list's own db config...
206
+ expect(authUser.db?.map).toBe('AuthUser')
207
+ expect(authUser.db?.schema).toBe('auth')
208
+ expect(authUser.db?.timestamps).toBe(true)
209
+ // ...or its access config.
210
+ expect(authUser.access?.operation?.query).toBe(userQuery)
211
+ })
164
212
  })
165
213
 
166
214
  describe('authPlugin - runtime user-key resolution', () => {