@opensaas/stack-auth 0.27.1 → 0.28.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +35 -1
- package/dist/config/derive-auth-lists.d.ts +15 -5
- package/dist/config/derive-auth-lists.d.ts.map +1 -1
- package/dist/config/derive-auth-lists.js +59 -98
- package/dist/config/derive-auth-lists.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/plugin.d.ts.map +1 -1
- package/dist/config/plugin.js +31 -13
- package/dist/config/plugin.js.map +1 -1
- package/dist/config/types.d.ts +58 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/lists/index.d.ts +9 -4
- package/dist/lists/index.d.ts.map +1 -1
- package/dist/lists/index.js +3 -2
- package/dist/lists/index.js.map +1 -1
- package/dist/runtime/types.d.ts +7 -5
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/server/schema-converter.d.ts +34 -2
- package/dist/server/schema-converter.d.ts.map +1 -1
- package/dist/server/schema-converter.js +47 -163
- package/dist/server/schema-converter.js.map +1 -1
- package/package.json +2 -2
- package/src/config/derive-auth-lists.ts +61 -87
- package/src/config/index.ts +1 -0
- package/src/config/plugin.ts +36 -13
- package/src/config/types.ts +64 -0
- package/src/lists/index.ts +10 -4
- package/src/runtime/types.ts +7 -5
- package/src/server/schema-converter.ts +68 -158
- package/tests/config.test.ts +110 -0
- package/tests/derive-auth-lists.test.ts +54 -1
- package/tests/lists.test.ts +42 -145
- package/tests/plugin-derived-keys.test.ts +130 -18
- package/tests/schema-converter.test.ts +58 -21
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -91,157 +91,16 @@ function convertField(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
operation: {
|
|
105
|
-
query: () => true, // Anyone can query users
|
|
106
|
-
create: () => true, // Anyone can create (sign up)
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
108
|
-
update: ({ session, item }: any) => {
|
|
109
|
-
if (!session) return false
|
|
110
|
-
const userId = (session as { userId?: string }).userId
|
|
111
|
-
const itemId = (item as { id?: string })?.id
|
|
112
|
-
return userId === itemId
|
|
113
|
-
},
|
|
114
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
115
|
-
delete: ({ session, item }: any) => {
|
|
116
|
-
if (!session) return false
|
|
117
|
-
const userId = (session as { userId?: string }).userId
|
|
118
|
-
const itemId = (item as { id?: string })?.id
|
|
119
|
-
return userId === itemId
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Session table
|
|
126
|
-
if (lowerTableName === 'session') {
|
|
127
|
-
return {
|
|
128
|
-
operation: {
|
|
129
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
130
|
-
query: ({ session }: any) => {
|
|
131
|
-
if (!session) return false
|
|
132
|
-
const userId = (session as { userId?: string }).userId
|
|
133
|
-
if (!userId) return false
|
|
134
|
-
return {
|
|
135
|
-
user: { id: { equals: userId } },
|
|
136
|
-
} as Record<string, unknown>
|
|
137
|
-
},
|
|
138
|
-
create: () => true, // Better-auth handles session creation
|
|
139
|
-
update: () => false, // No manual updates
|
|
140
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
141
|
-
delete: ({ session, item }: any) => {
|
|
142
|
-
if (!session) return false
|
|
143
|
-
const userId = (session as { userId?: string }).userId
|
|
144
|
-
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
145
|
-
return userId === itemUserId
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Account table
|
|
152
|
-
if (lowerTableName === 'account') {
|
|
153
|
-
return {
|
|
154
|
-
operation: {
|
|
155
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
156
|
-
query: ({ session }: any) => {
|
|
157
|
-
if (!session) return false
|
|
158
|
-
const userId = (session as { userId?: string }).userId
|
|
159
|
-
if (!userId) return false
|
|
160
|
-
return {
|
|
161
|
-
user: { id: { equals: userId } },
|
|
162
|
-
} as Record<string, unknown>
|
|
163
|
-
},
|
|
164
|
-
create: () => true, // Better-auth handles account creation
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
166
|
-
update: ({ session, item }: any) => {
|
|
167
|
-
if (!session) return false
|
|
168
|
-
const userId = (session as { userId?: string }).userId
|
|
169
|
-
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
170
|
-
return userId === itemUserId
|
|
171
|
-
},
|
|
172
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
173
|
-
delete: ({ session, item }: any) => {
|
|
174
|
-
if (!session) return false
|
|
175
|
-
const userId = (session as { userId?: string }).userId
|
|
176
|
-
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
177
|
-
return userId === itemUserId
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Verification table
|
|
184
|
-
if (lowerTableName === 'verification') {
|
|
185
|
-
return {
|
|
186
|
-
operation: {
|
|
187
|
-
query: () => false, // No public querying
|
|
188
|
-
create: () => true, // Better-auth creates verification tokens
|
|
189
|
-
update: () => false, // No updates
|
|
190
|
-
delete: () => true, // Better-auth deletes used/expired tokens
|
|
191
|
-
},
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// OAuth tables (from MCP/OIDC plugins)
|
|
196
|
-
if (
|
|
197
|
-
lowerTableName === 'oauthapplication' ||
|
|
198
|
-
lowerTableName === 'oauthaccesstoken' ||
|
|
199
|
-
lowerTableName === 'oauthconsent'
|
|
200
|
-
) {
|
|
201
|
-
return {
|
|
202
|
-
operation: {
|
|
203
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
204
|
-
query: ({ session }: any) => {
|
|
205
|
-
if (!session) return false
|
|
206
|
-
const userId = (session as { userId?: string }).userId
|
|
207
|
-
if (!userId) return false
|
|
208
|
-
// Filter by userId if field exists
|
|
209
|
-
return {
|
|
210
|
-
userId: { equals: userId },
|
|
211
|
-
} as Record<string, unknown>
|
|
212
|
-
},
|
|
213
|
-
create: () => true, // Better-auth/plugins handle creation
|
|
214
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
215
|
-
update: ({ session, item }: any) => {
|
|
216
|
-
if (!session) return false
|
|
217
|
-
const userId = (session as { userId?: string }).userId
|
|
218
|
-
const itemUserId = (item as { userId?: string })?.userId
|
|
219
|
-
return userId === itemUserId
|
|
220
|
-
},
|
|
221
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
|
|
222
|
-
delete: ({ session, item }: any) => {
|
|
223
|
-
if (!session) return false
|
|
224
|
-
const userId = (session as { userId?: string }).userId
|
|
225
|
-
const itemUserId = (item as { userId?: string })?.userId
|
|
226
|
-
return userId === itemUserId
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Default: restrict all access (safest default)
|
|
233
|
-
return {
|
|
234
|
-
operation: {
|
|
235
|
-
query: () => false,
|
|
236
|
-
create: () => false,
|
|
237
|
-
update: () => false,
|
|
238
|
-
delete: () => false,
|
|
239
|
-
},
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Convert Better Auth table schema to OpenSaaS ListConfig
|
|
94
|
+
* Convert Better Auth table schema to OpenSaaS ListConfig.
|
|
95
|
+
*
|
|
96
|
+
* Per ADR-0013, plugin-injected lists ship **closed** — no access is set here,
|
|
97
|
+
* so the list denies every operation until the application grants it. For a
|
|
98
|
+
* table that resolves onto one of the four base auth models (user/session/
|
|
99
|
+
* account/verification), that means `authPlugin({ access: { ... } })`; for any
|
|
100
|
+
* other better-auth-plugin-declared table (e.g. OAuth client tables from the
|
|
101
|
+
* `mcp` plugin), the application must declare the list itself under the same
|
|
102
|
+
* derived key so the plugin's field-only extend path can merge in (its access
|
|
103
|
+
* then stands untouched).
|
|
245
104
|
*/
|
|
246
105
|
export function convertTableToList(
|
|
247
106
|
tableName: string,
|
|
@@ -274,26 +133,77 @@ export function convertTableToList(
|
|
|
274
133
|
}
|
|
275
134
|
}
|
|
276
135
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
136
|
+
return list({ fields })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Base better-auth model keys — the four tables the auth plugin's own model
|
|
141
|
+
* config (`user`/`session`/`account`/`verification`) can remap via `modelName`.
|
|
142
|
+
*/
|
|
143
|
+
type BaseAuthModelKey = 'user' | 'session' | 'account' | 'verification'
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolved list key for each base better-auth model, as derived by
|
|
147
|
+
* {@link deriveAuthLists} from the plugin's configured model-key remap (e.g.
|
|
148
|
+
* `user.modelName: 'AuthUser'`). Passed to {@link convertBetterAuthSchema} so a
|
|
149
|
+
* provider plugin's schema extension of a base model (e.g. `user`) lands on the
|
|
150
|
+
* same adopted Auth list rather than being re-derived from the plugin's own
|
|
151
|
+
* table name.
|
|
152
|
+
*/
|
|
153
|
+
export type BaseAuthModelKeys = Partial<Record<BaseAuthModelKey, string>>
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Look up the configured remap for a table name, if it names one of the four
|
|
157
|
+
* base better-auth models (case-insensitively). Written as an explicit switch
|
|
158
|
+
* rather than an indexed lookup so no type assertion is needed to narrow the
|
|
159
|
+
* table name into {@link BaseAuthModelKey}.
|
|
160
|
+
*/
|
|
161
|
+
function resolveBaseModelKey(
|
|
162
|
+
tableName: string,
|
|
163
|
+
baseModelKeys: BaseAuthModelKeys,
|
|
164
|
+
): string | undefined {
|
|
165
|
+
switch (tableName.toLowerCase()) {
|
|
166
|
+
case 'user':
|
|
167
|
+
return baseModelKeys.user
|
|
168
|
+
case 'session':
|
|
169
|
+
return baseModelKeys.session
|
|
170
|
+
case 'account':
|
|
171
|
+
return baseModelKeys.account
|
|
172
|
+
case 'verification':
|
|
173
|
+
return baseModelKeys.verification
|
|
174
|
+
default:
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
282
177
|
}
|
|
283
178
|
|
|
284
179
|
/**
|
|
285
180
|
* Convert all Better Auth tables to OpenSaaS list configs
|
|
286
181
|
* This is called by authPlugin() to generate lists from Better Auth + plugins
|
|
182
|
+
*
|
|
183
|
+
* @param tables - The better-auth plugin's declared schema extension
|
|
184
|
+
* @param baseModelKeys - The resolved list keys for the four base auth models
|
|
185
|
+
* (`user`/`session`/`account`/`verification`), from the same model-key remap
|
|
186
|
+
* the Auth lists themselves use. When a table name matches one of these keys
|
|
187
|
+
* (case-insensitively), its list key is taken from here instead of being
|
|
188
|
+
* re-derived from the table name — so a schema extension targeting `user`
|
|
189
|
+
* lands on the adopted Auth list (e.g. `AuthUser`) rather than a re-derived
|
|
190
|
+
* `User`, even when that key collides with an unrelated host list.
|
|
287
191
|
*/
|
|
288
192
|
export function convertBetterAuthSchema(
|
|
289
193
|
tables: Record<string, BetterAuthTableSchema>,
|
|
194
|
+
baseModelKeys: BaseAuthModelKeys = {},
|
|
290
195
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
291
196
|
): Record<string, ListConfig<any>> {
|
|
292
197
|
const lists: Record<string, ListConfig<any>> = {} // eslint-disable-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
293
198
|
|
|
294
199
|
for (const [tableName, tableSchema] of Object.entries(tables)) {
|
|
295
|
-
//
|
|
296
|
-
|
|
200
|
+
// A base-model remap always wins: it reflects the actual configured list
|
|
201
|
+
// key for that model, whereas `tableSchema.modelName`/the table name are
|
|
202
|
+
// just how the provider plugin happens to describe its own extension.
|
|
203
|
+
const listKey =
|
|
204
|
+
resolveBaseModelKey(tableName, baseModelKeys) ||
|
|
205
|
+
tableSchema.modelName ||
|
|
206
|
+
toPascalCase(tableName)
|
|
297
207
|
lists[listKey] = convertTableToList(tableName, tableSchema)
|
|
298
208
|
}
|
|
299
209
|
|
package/tests/config.test.ts
CHANGED
|
@@ -147,6 +147,24 @@ describe('normalizeAuthConfig', () => {
|
|
|
147
147
|
|
|
148
148
|
expect(result.betterAuthPlugins).toEqual([mockPlugin])
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
it('should default access to an empty object', () => {
|
|
152
|
+
const result = normalizeAuthConfig({})
|
|
153
|
+
|
|
154
|
+
expect(result.access).toEqual({})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should include the access passthrough, keyed by better-auth model name', () => {
|
|
158
|
+
const userAccess = { operation: { query: () => true } }
|
|
159
|
+
const sessionAccess = { operation: { query: () => true } }
|
|
160
|
+
|
|
161
|
+
const result = normalizeAuthConfig({
|
|
162
|
+
access: { user: userAccess, session: sessionAccess },
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
expect(result.access.user).toBe(userAccess)
|
|
166
|
+
expect(result.access.session).toBe(sessionAccess)
|
|
167
|
+
})
|
|
150
168
|
})
|
|
151
169
|
|
|
152
170
|
describe('authPlugin', () => {
|
|
@@ -417,4 +435,96 @@ describe('authPlugin', () => {
|
|
|
417
435
|
expect(testTableList.fields).toHaveProperty('isActive')
|
|
418
436
|
expect(testTableList.fields).toHaveProperty('count')
|
|
419
437
|
})
|
|
438
|
+
|
|
439
|
+
describe('access control (ADR-0013)', () => {
|
|
440
|
+
it('ships all four auth lists closed (no access) when no access is configured', async () => {
|
|
441
|
+
const result = await config({
|
|
442
|
+
plugins: [authPlugin({})],
|
|
443
|
+
lists: {},
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
expect(result.lists.User.access).toBeUndefined()
|
|
447
|
+
expect(result.lists.Session.access).toBeUndefined()
|
|
448
|
+
expect(result.lists.Account.access).toBeUndefined()
|
|
449
|
+
expect(result.lists.Verification.access).toBeUndefined()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('applies authPlugin({ access }) to each corresponding created list', async () => {
|
|
453
|
+
const userQuery = () => true
|
|
454
|
+
const sessionQuery = () => true
|
|
455
|
+
const accountQuery = () => true
|
|
456
|
+
const verificationQuery = () => true
|
|
457
|
+
|
|
458
|
+
const result = await config({
|
|
459
|
+
plugins: [
|
|
460
|
+
authPlugin({
|
|
461
|
+
access: {
|
|
462
|
+
user: { operation: { query: userQuery } },
|
|
463
|
+
session: { operation: { query: sessionQuery } },
|
|
464
|
+
account: { operation: { query: accountQuery } },
|
|
465
|
+
verification: { operation: { query: verificationQuery } },
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
],
|
|
469
|
+
lists: {},
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
expect(result.lists.User.access?.operation?.query).toBe(userQuery)
|
|
473
|
+
expect(result.lists.Session.access?.operation?.query).toBe(sessionQuery)
|
|
474
|
+
expect(result.lists.Account.access?.operation?.query).toBe(accountQuery)
|
|
475
|
+
expect(result.lists.Verification.access?.operation?.query).toBe(verificationQuery)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('honors field-level access in the access passthrough (e.g. hiding Account tokens)', async () => {
|
|
479
|
+
const result = await config({
|
|
480
|
+
plugins: [
|
|
481
|
+
authPlugin({
|
|
482
|
+
access: {
|
|
483
|
+
account: {
|
|
484
|
+
operation: { query: () => true },
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
}),
|
|
488
|
+
],
|
|
489
|
+
lists: {},
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// The passthrough carries the full list access shape (operation + fields);
|
|
493
|
+
// this test locks in that the plugin forwards it verbatim rather than
|
|
494
|
+
// only reading `.operation`.
|
|
495
|
+
expect(result.lists.Account.access?.operation?.query).toBeDefined()
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it('applies the access passthrough with a remapped model name (remap-proof keying)', async () => {
|
|
499
|
+
const userQuery = () => true
|
|
500
|
+
|
|
501
|
+
const result = await config({
|
|
502
|
+
plugins: [
|
|
503
|
+
authPlugin({
|
|
504
|
+
user: { modelName: 'AuthUser' },
|
|
505
|
+
access: { user: { operation: { query: userQuery } } },
|
|
506
|
+
}),
|
|
507
|
+
],
|
|
508
|
+
lists: {},
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
expect(result.lists.AuthUser.access?.operation?.query).toBe(userQuery)
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('extendUserList.access still applies and takes precedence over access.user', async () => {
|
|
515
|
+
const extendAccess = { operation: { query: () => false } }
|
|
516
|
+
|
|
517
|
+
const result = await config({
|
|
518
|
+
plugins: [
|
|
519
|
+
authPlugin({
|
|
520
|
+
extendUserList: { access: extendAccess },
|
|
521
|
+
access: { user: { operation: { query: () => true } } },
|
|
522
|
+
}),
|
|
523
|
+
],
|
|
524
|
+
lists: {},
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
expect(result.lists.User.access).toBe(extendAccess)
|
|
528
|
+
})
|
|
529
|
+
})
|
|
420
530
|
})
|
|
@@ -55,7 +55,7 @@ describe('deriveAuthLists - default behaviour (no overrides)', () => {
|
|
|
55
55
|
expect(lists.User.fields.name.db?.map).toBeUndefined()
|
|
56
56
|
expect(lists.Session.fields.token.db?.map).toBeUndefined()
|
|
57
57
|
// FK column not overridden -> no foreignKey map on the relationship
|
|
58
|
-
expect(lists.Session.fields.user.db).toBeUndefined()
|
|
58
|
+
expect(lists.Session.fields.user.db?.foreignKey).toBeUndefined()
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('opts every auth list into auto-timestamps', () => {
|
|
@@ -72,6 +72,59 @@ describe('deriveAuthLists - default behaviour (no overrides)', () => {
|
|
|
72
72
|
})
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
describe('deriveAuthLists - user FK shape mirrors better-auth (issue #679)', () => {
|
|
76
|
+
it('disables the default FK index on Session.user and Account.user', () => {
|
|
77
|
+
const { lists } = deriveAuthLists(defaultModels)
|
|
78
|
+
|
|
79
|
+
expect(lists.Session.fields.user.isIndexed).toBe(false)
|
|
80
|
+
expect(lists.Account.fields.user.isIndexed).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('does not touch isIndexed on the email/token unique fields', () => {
|
|
84
|
+
// Existing @@unique mirroring must be unaffected by the FK shape change.
|
|
85
|
+
const { lists } = deriveAuthLists(defaultModels)
|
|
86
|
+
|
|
87
|
+
expect(lists.User.fields.email.isIndexed).toBe('unique')
|
|
88
|
+
expect(lists.Session.fields.token.isIndexed).toBe('unique')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('adds onDelete: Cascade to the user relation line via extendPrismaSchema', () => {
|
|
92
|
+
const { lists } = deriveAuthLists(defaultModels)
|
|
93
|
+
|
|
94
|
+
for (const field of [lists.Session.fields.user, lists.Account.fields.user]) {
|
|
95
|
+
const extend = field.db?.extendPrismaSchema
|
|
96
|
+
expect(extend).toBeTypeOf('function')
|
|
97
|
+
|
|
98
|
+
const result = extend!({
|
|
99
|
+
fkLine: ' userId String?',
|
|
100
|
+
relationLine: ' user User? @relation(fields: [userId], references: [id])',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(result.relationLine).toBe(
|
|
104
|
+
' user User? @relation(onDelete: Cascade, fields: [userId], references: [id])',
|
|
105
|
+
)
|
|
106
|
+
// The FK line itself is untouched by the cascade rewrite.
|
|
107
|
+
expect(result.fkLine).toBe(' userId String?')
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('keeps the cascade extendPrismaSchema alongside a userId column override', () => {
|
|
112
|
+
const models: NormalizedAuthModels = {
|
|
113
|
+
user: { modelName: 'User', fields: {} },
|
|
114
|
+
session: { modelName: 'Session', fields: { userId: 'user_id' } },
|
|
115
|
+
account: { modelName: 'Account', fields: { userId: 'user_id' } },
|
|
116
|
+
verification: { modelName: 'Verification', fields: {} },
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { lists } = deriveAuthLists(models)
|
|
120
|
+
|
|
121
|
+
expect(lists.Session.fields.user.db?.foreignKey).toEqual({ map: 'user_id' })
|
|
122
|
+
expect(lists.Session.fields.user.db?.extendPrismaSchema).toBeTypeOf('function')
|
|
123
|
+
expect(lists.Account.fields.user.db?.foreignKey).toEqual({ map: 'user_id' })
|
|
124
|
+
expect(lists.Account.fields.user.db?.extendPrismaSchema).toBeTypeOf('function')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
75
128
|
describe('deriveAuthLists - custom modelName overrides', () => {
|
|
76
129
|
const customModels: NormalizedAuthModels = {
|
|
77
130
|
user: { modelName: 'AuthUser', fields: {} },
|
package/tests/lists.test.ts
CHANGED
|
@@ -58,56 +58,10 @@ describe('createUserList', () => {
|
|
|
58
58
|
expect(userList.fields).toHaveProperty('email')
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
it('
|
|
61
|
+
it('ships closed (no access control) by default, per ADR-0013', () => {
|
|
62
62
|
const userList = createUserList()
|
|
63
63
|
|
|
64
|
-
expect(userList.access).
|
|
65
|
-
expect(userList.access?.operation).toBeDefined()
|
|
66
|
-
expect(userList.access?.operation?.query).toBeDefined()
|
|
67
|
-
expect(userList.access?.operation?.create).toBeDefined()
|
|
68
|
-
expect(userList.access?.operation?.update).toBeDefined()
|
|
69
|
-
expect(userList.access?.operation?.delete).toBeDefined()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('should allow querying users', () => {
|
|
73
|
-
const userList = createUserList()
|
|
74
|
-
const queryAccess = userList.access?.operation?.query
|
|
75
|
-
|
|
76
|
-
expect(typeof queryAccess).toBe('function')
|
|
77
|
-
expect(queryAccess?.({})).toBe(true)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('should allow creating users', () => {
|
|
81
|
-
const userList = createUserList()
|
|
82
|
-
const createAccess = userList.access?.operation?.create
|
|
83
|
-
|
|
84
|
-
expect(typeof createAccess).toBe('function')
|
|
85
|
-
expect(createAccess?.({})).toBe(true)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('should allow users to update their own record', () => {
|
|
89
|
-
const userList = createUserList()
|
|
90
|
-
const updateAccess = userList.access?.operation?.update
|
|
91
|
-
|
|
92
|
-
expect(typeof updateAccess).toBe('function')
|
|
93
|
-
expect(
|
|
94
|
-
updateAccess?.({
|
|
95
|
-
session: { userId: 'user-1' },
|
|
96
|
-
item: { id: 'user-1' },
|
|
97
|
-
}),
|
|
98
|
-
).toBe(true)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('should deny users from updating other users', () => {
|
|
102
|
-
const userList = createUserList()
|
|
103
|
-
const updateAccess = userList.access?.operation?.update
|
|
104
|
-
|
|
105
|
-
expect(
|
|
106
|
-
updateAccess?.({
|
|
107
|
-
session: { userId: 'user-1' },
|
|
108
|
-
item: { id: 'user-2' },
|
|
109
|
-
}),
|
|
110
|
-
).toBe(false)
|
|
64
|
+
expect(userList.access).toBeUndefined()
|
|
111
65
|
})
|
|
112
66
|
|
|
113
67
|
it('should allow custom access control', () => {
|
|
@@ -170,43 +124,10 @@ describe('createSessionList', () => {
|
|
|
170
124
|
expect(sessionList.fields.user.ref).toBe('User.sessions')
|
|
171
125
|
})
|
|
172
126
|
|
|
173
|
-
it('
|
|
127
|
+
it('ships closed (no access control) by default, per ADR-0013', () => {
|
|
174
128
|
const sessionList = createSessionList()
|
|
175
129
|
|
|
176
|
-
expect(sessionList.access).
|
|
177
|
-
expect(sessionList.access?.operation).toBeDefined()
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('should deny querying sessions without session', () => {
|
|
181
|
-
const sessionList = createSessionList()
|
|
182
|
-
const queryAccess = sessionList.access?.operation?.query
|
|
183
|
-
|
|
184
|
-
expect(queryAccess?.({})).toBe(false)
|
|
185
|
-
expect(queryAccess?.({ session: null })).toBe(false)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('should allow querying own sessions with filter', () => {
|
|
189
|
-
const sessionList = createSessionList()
|
|
190
|
-
const queryAccess = sessionList.access?.operation?.query
|
|
191
|
-
|
|
192
|
-
const result = queryAccess?.({ session: { userId: 'user-1' } })
|
|
193
|
-
expect(result).toEqual({
|
|
194
|
-
user: { id: { equals: 'user-1' } },
|
|
195
|
-
})
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('should allow creating sessions', () => {
|
|
199
|
-
const sessionList = createSessionList()
|
|
200
|
-
const createAccess = sessionList.access?.operation?.create
|
|
201
|
-
|
|
202
|
-
expect(createAccess?.({})).toBe(true)
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('should deny manual session updates', () => {
|
|
206
|
-
const sessionList = createSessionList()
|
|
207
|
-
const updateAccess = sessionList.access?.operation?.update
|
|
208
|
-
|
|
209
|
-
expect(updateAccess?.({})).toBe(false)
|
|
130
|
+
expect(sessionList.access).toBeUndefined()
|
|
210
131
|
})
|
|
211
132
|
})
|
|
212
133
|
|
|
@@ -241,46 +162,10 @@ describe('createAccountList', () => {
|
|
|
241
162
|
expect(accountList.fields.user.ref).toBe('User.accounts')
|
|
242
163
|
})
|
|
243
164
|
|
|
244
|
-
it('
|
|
245
|
-
const accountList = createAccountList()
|
|
246
|
-
const queryAccess = accountList.access?.operation?.query
|
|
247
|
-
|
|
248
|
-
expect(queryAccess?.({})).toBe(false)
|
|
249
|
-
expect(queryAccess?.({ session: null })).toBe(false)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('should allow querying own accounts with filter', () => {
|
|
165
|
+
it('ships closed (no access control) by default, per ADR-0013', () => {
|
|
253
166
|
const accountList = createAccountList()
|
|
254
|
-
const queryAccess = accountList.access?.operation?.query
|
|
255
|
-
|
|
256
|
-
const result = queryAccess?.({ session: { userId: 'user-1' } })
|
|
257
|
-
expect(result).toEqual({
|
|
258
|
-
user: { id: { equals: 'user-1' } },
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
167
|
|
|
262
|
-
|
|
263
|
-
const accountList = createAccountList()
|
|
264
|
-
const updateAccess = accountList.access?.operation?.update
|
|
265
|
-
|
|
266
|
-
expect(
|
|
267
|
-
updateAccess?.({
|
|
268
|
-
session: { userId: 'user-1' },
|
|
269
|
-
item: { user: { id: 'user-1' } },
|
|
270
|
-
}),
|
|
271
|
-
).toBe(true)
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('should deny users from updating other accounts', () => {
|
|
275
|
-
const accountList = createAccountList()
|
|
276
|
-
const updateAccess = accountList.access?.operation?.update
|
|
277
|
-
|
|
278
|
-
expect(
|
|
279
|
-
updateAccess?.({
|
|
280
|
-
session: { userId: 'user-1' },
|
|
281
|
-
item: { user: { id: 'user-2' } },
|
|
282
|
-
}),
|
|
283
|
-
).toBe(false)
|
|
168
|
+
expect(accountList.access).toBeUndefined()
|
|
284
169
|
})
|
|
285
170
|
})
|
|
286
171
|
|
|
@@ -305,32 +190,10 @@ describe('createVerificationList', () => {
|
|
|
305
190
|
expect(verificationList.fields.value.validation?.isRequired).toBe(true)
|
|
306
191
|
})
|
|
307
192
|
|
|
308
|
-
it('
|
|
309
|
-
const verificationList = createVerificationList()
|
|
310
|
-
const queryAccess = verificationList.access?.operation?.query
|
|
311
|
-
|
|
312
|
-
expect(queryAccess?.({})).toBe(false)
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('should allow creating verification tokens', () => {
|
|
316
|
-
const verificationList = createVerificationList()
|
|
317
|
-
const createAccess = verificationList.access?.operation?.create
|
|
318
|
-
|
|
319
|
-
expect(createAccess?.({})).toBe(true)
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it('should deny updates to verification tokens', () => {
|
|
193
|
+
it('ships closed (no access control) by default, per ADR-0013', () => {
|
|
323
194
|
const verificationList = createVerificationList()
|
|
324
|
-
const updateAccess = verificationList.access?.operation?.update
|
|
325
195
|
|
|
326
|
-
expect(
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('should allow deleting verification tokens', () => {
|
|
330
|
-
const verificationList = createVerificationList()
|
|
331
|
-
const deleteAccess = verificationList.access?.operation?.delete
|
|
332
|
-
|
|
333
|
-
expect(deleteAccess?.({})).toBe(true)
|
|
196
|
+
expect(verificationList.access).toBeUndefined()
|
|
334
197
|
})
|
|
335
198
|
})
|
|
336
199
|
|
|
@@ -353,4 +216,38 @@ describe('getAuthLists', () => {
|
|
|
353
216
|
|
|
354
217
|
expect(lists.User.fields).toHaveProperty('role')
|
|
355
218
|
})
|
|
219
|
+
|
|
220
|
+
it('applies the accessConfig passthrough to each list, keyed by model name', () => {
|
|
221
|
+
const queryTrue = () => true
|
|
222
|
+
const lists = getAuthLists(undefined, undefined, {
|
|
223
|
+
user: { operation: { query: queryTrue } },
|
|
224
|
+
session: { operation: { query: queryTrue } },
|
|
225
|
+
account: { operation: { query: queryTrue } },
|
|
226
|
+
verification: { operation: { query: queryTrue } },
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
expect(lists.User.access?.operation?.query).toBe(queryTrue)
|
|
230
|
+
expect(lists.Session.access?.operation?.query).toBe(queryTrue)
|
|
231
|
+
expect(lists.Account.access?.operation?.query).toBe(queryTrue)
|
|
232
|
+
expect(lists.Verification.access?.operation?.query).toBe(queryTrue)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('prefers extendUserList.access over accessConfig.user when both are set', () => {
|
|
236
|
+
const extendAccess = { operation: { query: () => false } }
|
|
237
|
+
const accessConfigUser = { operation: { query: () => true } }
|
|
238
|
+
|
|
239
|
+
const lists = getAuthLists({ access: extendAccess }, undefined, { user: accessConfigUser })
|
|
240
|
+
|
|
241
|
+
expect(lists.User.access).toBe(extendAccess)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('leaves lists with no accessConfig entry closed', () => {
|
|
245
|
+
const lists = getAuthLists(undefined, undefined, {
|
|
246
|
+
user: { operation: { query: () => true } },
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
expect(lists.Session.access).toBeUndefined()
|
|
250
|
+
expect(lists.Account.access).toBeUndefined()
|
|
251
|
+
expect(lists.Verification.access).toBeUndefined()
|
|
252
|
+
})
|
|
356
253
|
})
|