@opensaas/stack-auth 0.27.1 → 0.29.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 (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +44 -0
  3. package/CLAUDE.md +35 -1
  4. package/dist/config/derive-auth-lists.d.ts +15 -5
  5. package/dist/config/derive-auth-lists.d.ts.map +1 -1
  6. package/dist/config/derive-auth-lists.js +59 -98
  7. package/dist/config/derive-auth-lists.js.map +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +1 -0
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/plugin.d.ts.map +1 -1
  12. package/dist/config/plugin.js +31 -13
  13. package/dist/config/plugin.js.map +1 -1
  14. package/dist/config/types.d.ts +58 -0
  15. package/dist/config/types.d.ts.map +1 -1
  16. package/dist/lists/index.d.ts +9 -4
  17. package/dist/lists/index.d.ts.map +1 -1
  18. package/dist/lists/index.js +3 -2
  19. package/dist/lists/index.js.map +1 -1
  20. package/dist/runtime/types.d.ts +7 -5
  21. package/dist/runtime/types.d.ts.map +1 -1
  22. package/dist/server/schema-converter.d.ts +34 -2
  23. package/dist/server/schema-converter.d.ts.map +1 -1
  24. package/dist/server/schema-converter.js +47 -163
  25. package/dist/server/schema-converter.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/config/derive-auth-lists.ts +61 -87
  28. package/src/config/index.ts +1 -0
  29. package/src/config/plugin.ts +36 -13
  30. package/src/config/types.ts +64 -0
  31. package/src/lists/index.ts +10 -4
  32. package/src/runtime/types.ts +7 -5
  33. package/src/server/schema-converter.ts +68 -158
  34. package/tests/config.test.ts +110 -0
  35. package/tests/derive-auth-lists.test.ts +54 -1
  36. package/tests/lists.test.ts +42 -145
  37. package/tests/plugin-derived-keys.test.ts +130 -18
  38. package/tests/schema-converter.test.ts +58 -21
  39. package/tsconfig.tsbuildinfo +1 -1
@@ -13,9 +13,12 @@
13
13
  * - relationship refs between the auth lists wired to the *derived* keys
14
14
  * (e.g. `Session.user → AuthUser.sessions`)
15
15
  *
16
- * When the developer supplies no `modelName`/`fields` overrides, the output is
17
- * byte-for-byte the historical default set keyed `User`/`Session`/`Account`/
18
- * `Verification` with the original field shapes — see the unit tests.
16
+ * When the developer supplies no `modelName`/`fields` overrides, the output
17
+ * keeps the historical default keys (`User`/`Session`/`Account`/
18
+ * `Verification`) and field shapes — see the unit tests. Per ADR-0013, each
19
+ * list ships with **no** operation-level access unless the caller supplies it
20
+ * (`accessConfig`, or `userConfig.access` for the user list) — deny-by-default,
21
+ * not the plugin's former permissive defaults.
19
22
  *
20
23
  * `getAuthLists`/`convertBetterAuthSchema` (and the runtime user-key
21
24
  * resolution) consume this module so derivation lives in exactly one place.
@@ -24,8 +27,9 @@
24
27
  import { list } from '@opensaas/stack-core'
25
28
  import { text, timestamp, checkbox, relationship } from '@opensaas/stack-core/fields'
26
29
  import type { ListConfig } from '@opensaas/stack-core'
30
+ import type { RelationshipField } from '@opensaas/stack-core/fields'
27
31
  import type { ExtendUserListConfig } from '../lists/index.js'
28
- import type { NormalizedAuthModelConfig, NormalizedAuthModels } from './types.js'
32
+ import type { AuthAccessConfig, NormalizedAuthModelConfig, NormalizedAuthModels } from './types.js'
29
33
 
30
34
  /**
31
35
  * Default better-auth model names — used to decide whether a `@@map` is needed
@@ -103,24 +107,39 @@ function fieldDb(fieldName: string, fields: Record<string, string>): { map: stri
103
107
  }
104
108
 
105
109
  /**
106
- * Build the foreign-key `db` config for a `user` relationship, honouring a
107
- * `userId` column override from the better-auth `fields` map.
110
+ * Build the `db` config for a `user` relationship (`Session.user` /
111
+ * `Account.user`), honouring a `userId` column override from the better-auth
112
+ * `fields` map and mirroring better-auth's own FK shape: no separate FK index
113
+ * — the index is applied at the field level via `isIndexed: false` — and
114
+ * `onDelete: Cascade`, so a generated Auth schema diffs clean against a live
115
+ * better-auth database on both dimensions instead of showing a spurious index
116
+ * drop and a referential-action change (issue #679).
108
117
  */
109
- function userForeignKeyDb(
110
- fields: Record<string, string>,
111
- ): { foreignKey: { map: string } } | undefined {
118
+ function userRelationshipDb(fields: Record<string, string>): NonNullable<RelationshipField['db']> {
112
119
  const column = fields.userId
113
- return column ? { foreignKey: { map: column } } : undefined
120
+ return {
121
+ ...(column ? { foreignKey: { map: column } } : {}),
122
+ extendPrismaSchema: ({ fkLine, relationLine }) => ({
123
+ fkLine,
124
+ relationLine: relationLine.replace('@relation(', '@relation(onDelete: Cascade, '),
125
+ }),
126
+ }
114
127
  }
115
128
 
116
129
  /**
117
130
  * Create the Auth user list, applying derived field column maps + table map and
118
131
  * wiring the session/account relationships to the derived keys.
132
+ *
133
+ * Per ADR-0013, the plugin ships no permissive access default: the list is
134
+ * closed unless the application supplies access via `extendUserList.access`
135
+ * (takes precedence — it predates the keyed `access` passthrough and is
136
+ * User-specific) or the `access.user` passthrough.
119
137
  */
120
138
  function createUserList(
121
139
  model: NormalizedAuthModelConfig,
122
140
  keys: DerivedAuthLists['keys'],
123
141
  userConfig: ExtendUserListConfig,
142
+ access: AuthAccessConfig['user'],
124
143
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
125
144
  ): ListConfig<any> {
126
145
  const f = model.fields
@@ -143,34 +162,21 @@ function createUserList(
143
162
  ...(userConfig.fields || {}),
144
163
  },
145
164
  db: listDb(model, DEFAULT_MODEL_NAMES.user),
146
- access: userConfig.access || {
147
- operation: {
148
- query: () => true,
149
- create: () => true,
150
- update: ({ session, item }) => {
151
- if (!session) return false
152
- const userId = (session as { userId?: string }).userId
153
- const itemId = (item as { id?: string })?.id
154
- return userId === itemId
155
- },
156
- delete: ({ session, item }) => {
157
- if (!session) return false
158
- const userId = (session as { userId?: string }).userId
159
- const itemId = (item as { id?: string })?.id
160
- return userId === itemId
161
- },
162
- },
163
- },
165
+ access: userConfig.access || access,
164
166
  hooks: userConfig.hooks,
165
167
  })
166
168
  }
167
169
 
168
170
  /**
169
171
  * Create the Auth session list.
172
+ *
173
+ * Per ADR-0013, the plugin ships no permissive access default — closed unless
174
+ * the application supplies `access.session`.
170
175
  */
171
176
  function createSessionList(
172
177
  model: NormalizedAuthModelConfig,
173
178
  keys: DerivedAuthLists['keys'],
179
+ access: AuthAccessConfig['session'],
174
180
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
175
181
  ): ListConfig<any> {
176
182
  const f = model.fields
@@ -186,39 +192,25 @@ function createSessionList(
186
192
  userAgent: text({ db: fieldDb('userAgent', f) }),
187
193
  user: relationship({
188
194
  ref: `${keys.user}.sessions`,
189
- db: userForeignKeyDb(f),
195
+ isIndexed: false,
196
+ db: userRelationshipDb(f),
190
197
  }),
191
198
  },
192
199
  db: listDb(model, DEFAULT_MODEL_NAMES.session),
193
- access: {
194
- operation: {
195
- query: ({ session }) => {
196
- if (!session) return false
197
- const userId = (session as { userId?: string }).userId
198
- if (!userId) return false
199
- return {
200
- user: { id: { equals: userId } },
201
- } as Record<string, unknown>
202
- },
203
- create: () => true,
204
- update: () => false,
205
- delete: ({ session, item }) => {
206
- if (!session) return false
207
- const userId = (session as { userId?: string }).userId
208
- const itemUserId = (item as { user?: { id?: string } })?.user?.id
209
- return userId === itemUserId
210
- },
211
- },
212
- },
200
+ access,
213
201
  })
214
202
  }
215
203
 
216
204
  /**
217
205
  * Create the Auth account list.
206
+ *
207
+ * Per ADR-0013, the plugin ships no permissive access default — closed unless
208
+ * the application supplies `access.account`.
218
209
  */
219
210
  function createAccountList(
220
211
  model: NormalizedAuthModelConfig,
221
212
  keys: DerivedAuthLists['keys'],
213
+ access: AuthAccessConfig['account'],
222
214
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
223
215
  ): ListConfig<any> {
224
216
  const f = model.fields
@@ -228,7 +220,8 @@ function createAccountList(
228
220
  providerId: text({ validation: { isRequired: true }, db: fieldDb('providerId', f) }),
229
221
  user: relationship({
230
222
  ref: `${keys.user}.accounts`,
231
- db: userForeignKeyDb(f),
223
+ isIndexed: false,
224
+ db: userRelationshipDb(f),
232
225
  }),
233
226
  accessToken: text({ db: fieldDb('accessToken', f) }),
234
227
  refreshToken: text({ db: fieldDb('refreshToken', f) }),
@@ -239,39 +232,19 @@ function createAccountList(
239
232
  password: text({ db: fieldDb('password', f) }),
240
233
  },
241
234
  db: listDb(model, DEFAULT_MODEL_NAMES.account),
242
- access: {
243
- operation: {
244
- query: ({ session }) => {
245
- if (!session) return false
246
- const userId = (session as { userId?: string }).userId
247
- if (!userId) return false
248
- return {
249
- user: { id: { equals: userId } },
250
- } as Record<string, unknown>
251
- },
252
- create: () => true,
253
- update: ({ session, item }) => {
254
- if (!session) return false
255
- const userId = (session as { userId?: string }).userId
256
- const itemUserId = (item as { user?: { id?: string } })?.user?.id
257
- return userId === itemUserId
258
- },
259
- delete: ({ session, item }) => {
260
- if (!session) return false
261
- const userId = (session as { userId?: string }).userId
262
- const itemUserId = (item as { user?: { id?: string } })?.user?.id
263
- return userId === itemUserId
264
- },
265
- },
266
- },
235
+ access,
267
236
  })
268
237
  }
269
238
 
270
239
  /**
271
240
  * Create the Auth verification list.
241
+ *
242
+ * Per ADR-0013, the plugin ships no permissive access default — closed unless
243
+ * the application supplies `access.verification`.
272
244
  */
273
245
  function createVerificationList(
274
246
  model: NormalizedAuthModelConfig,
247
+ access: AuthAccessConfig['verification'],
275
248
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
276
249
  ): ListConfig<any> {
277
250
  const f = model.fields
@@ -282,27 +255,28 @@ function createVerificationList(
282
255
  expiresAt: timestamp({ db: fieldDb('expiresAt', f) }),
283
256
  },
284
257
  db: listDb(model, DEFAULT_MODEL_NAMES.verification),
285
- access: {
286
- operation: {
287
- query: () => false,
288
- create: () => true,
289
- update: () => false,
290
- delete: () => true,
291
- },
292
- },
258
+ access,
293
259
  })
294
260
  }
295
261
 
296
262
  /**
297
263
  * Derive the OpenSaaS Auth lists from the resolved better-auth model config.
298
264
  *
265
+ * Per ADR-0013 the derived lists ship **closed** (no permissive operation
266
+ * access) unless the application supplies access via `accessConfig` (the
267
+ * `authPlugin({ access: … })` passthrough, keyed by better-auth model name) or,
268
+ * for the user list specifically, `userConfig.access` (`extendUserList.access`,
269
+ * which takes precedence — see {@link AuthAccessConfig}).
270
+ *
299
271
  * @param models - Resolved better-auth per-model config (modelName + field column maps)
300
272
  * @param userConfig - Extra User-list fields/access/hooks supplied via `extendUserList`
273
+ * @param accessConfig - App-authored access for each Auth list, keyed by better-auth model name
301
274
  * @returns The derived list keys and the four Auth list configs keyed by those keys
302
275
  */
303
276
  export function deriveAuthLists(
304
277
  models: NormalizedAuthModels,
305
278
  userConfig: ExtendUserListConfig = {},
279
+ accessConfig: AuthAccessConfig = {},
306
280
  ): DerivedAuthLists {
307
281
  const keys = {
308
282
  user: models.user.modelName,
@@ -313,10 +287,10 @@ export function deriveAuthLists(
313
287
 
314
288
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
315
289
  const lists: Record<string, ListConfig<any>> = {
316
- [keys.user]: createUserList(models.user, keys, userConfig),
317
- [keys.session]: createSessionList(models.session, keys),
318
- [keys.account]: createAccountList(models.account, keys),
319
- [keys.verification]: createVerificationList(models.verification),
290
+ [keys.user]: createUserList(models.user, keys, userConfig, accessConfig.user),
291
+ [keys.session]: createSessionList(models.session, keys, accessConfig.session),
292
+ [keys.account]: createAccountList(models.account, keys, accessConfig.account),
293
+ [keys.verification]: createVerificationList(models.verification, accessConfig.verification),
320
294
  }
321
295
 
322
296
  return { keys, lists }
@@ -113,6 +113,7 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
113
113
  schema: config.schema,
114
114
  sessionFields,
115
115
  extendUserList: config.extendUserList || {},
116
+ access: config.access || {},
116
117
  sendEmail:
117
118
  config.sendEmail ||
118
119
  (async ({ to, subject, html }) => {
@@ -44,23 +44,41 @@ export function authPlugin(config: AuthConfig): Plugin {
44
44
  // User/Session/Account/Verification keys; with overrides (e.g.
45
45
  // user.modelName: 'AuthUser') the lists are keyed and column-mapped to
46
46
  // match the developer's live better-auth tables.
47
- const authLists = getAuthLists(normalized.extendUserList, normalized.models)
47
+ const authLists = getAuthLists(
48
+ normalized.extendUserList,
49
+ normalized.models,
50
+ normalized.access,
51
+ )
52
+
53
+ // The same base-model list keys the Auth lists above were derived under
54
+ // (e.g. `user.modelName: 'AuthUser'`). A provider plugin's schema
55
+ // extension of a base model (e.g. `user`) must resolve against this
56
+ // remap too, so it lands on the adopted Auth list rather than a
57
+ // re-derived key that can collide with an unrelated host list.
58
+ const baseModelKeys = {
59
+ user: normalized.models.user.modelName,
60
+ session: normalized.models.session.modelName,
61
+ account: normalized.models.account.modelName,
62
+ verification: normalized.models.verification.modelName,
63
+ }
48
64
 
49
65
  // Extract additional lists from Better Auth plugins
50
66
  for (const plugin of normalized.betterAuthPlugins) {
51
67
  if (plugin && typeof plugin === 'object' && 'schema' in plugin) {
52
68
  // Plugin has schema property - convert to OpenSaaS lists
53
69
  const pluginSchema = plugin.schema
54
- const pluginLists = convertBetterAuthSchema(pluginSchema)
70
+ const pluginLists = convertBetterAuthSchema(pluginSchema, baseModelKeys)
55
71
 
56
72
  // Add or extend lists from plugin
57
73
  for (const [listName, listConfig] of Object.entries(pluginLists)) {
58
74
  if (context.config.lists[listName]) {
59
- // List exists, extend it
75
+ // List already exists merge fields/hooks/mcp in only. Access
76
+ // control belongs to whoever owns the list; per ADR-0013 an
77
+ // extension must never carry operation-level access for a
78
+ // pre-existing list (the plugin engine throws if it does).
60
79
  context.extendList(listName, {
61
80
  fields: listConfig.fields,
62
81
  hooks: listConfig.hooks,
63
- access: listConfig.access,
64
82
  mcp: listConfig.mcp,
65
83
  })
66
84
  } else {
@@ -82,11 +100,13 @@ export function authPlugin(config: AuthConfig): Plugin {
82
100
  // "merge auth fields into my User" behaviour.
83
101
  for (const [listName, listConfig] of Object.entries(authLists)) {
84
102
  if (context.config.lists[listName]) {
85
- // A list already exists under this derived key — merge auth fields in.
103
+ // A list already exists under this derived key — merge auth fields
104
+ // in only. Access control belongs to whoever owns the list (the
105
+ // application declared it first), so the plugin never forwards its
106
+ // own access here — see ADR-0013.
86
107
  context.extendList(listName, {
87
108
  fields: listConfig.fields,
88
109
  hooks: listConfig.hooks,
89
- access: listConfig.access,
90
110
  mcp: listConfig.mcp,
91
111
  })
92
112
  } else {
@@ -143,7 +163,7 @@ export function authPlugin(config: AuthConfig): Plugin {
143
163
  }
144
164
  },
145
165
 
146
- runtime: (context) => {
166
+ runtime: (context, sudo) => {
147
167
  // Resolve the user list's context.db key from the configured user model.
148
168
  // context.db is keyed camelCase, so 'User' -> 'user', 'AuthUser' -> 'authUser'.
149
169
  const userDbKey = getDbKey(normalized.models.user.modelName)
@@ -151,24 +171,27 @@ export function authPlugin(config: AuthConfig): Plugin {
151
171
  // Provide auth-related utilities at runtime
152
172
  return {
153
173
  /**
154
- * Get user by ID
155
- * Uses the access-controlled context to fetch user data
174
+ * Get user by ID.
175
+ *
176
+ * Resolves through `sudo()` (per ADR-0013): the User list ships closed
177
+ * by default, and "who is this session" must not depend on the
178
+ * application's User access policy.
156
179
  */
157
180
  getUser: async (userId: string) => {
158
- return await context.db[userDbKey].findUnique({
181
+ return await sudo().db[userDbKey].findUnique({
159
182
  where: { id: userId },
160
183
  })
161
184
  },
162
185
 
163
186
  /**
164
- * Get current user from session
165
- * Extracts userId from session and fetches user data
187
+ * Get current user from session. Extracts userId from session and
188
+ * fetches user data via `sudo()` see {@link getUser}.
166
189
  */
167
190
  getCurrentUser: async () => {
168
191
  if (!context.session?.userId) {
169
192
  return null
170
193
  }
171
- return await context.db[userDbKey].findUnique({
194
+ return await sudo().db[userDbKey].findUnique({
172
195
  where: { id: context.session.userId },
173
196
  })
174
197
  },
@@ -1,3 +1,4 @@
1
+ import type { ListConfig } from '@opensaas/stack-core'
1
2
  import type { ExtendUserListConfig } from '../lists/index.js'
2
3
 
3
4
  /**
@@ -100,6 +101,52 @@ export type SessionConfig = {
100
101
  * })
101
102
  * ```
102
103
  */
104
+ /**
105
+ * App-authored operation + field-level access control for the Auth lists,
106
+ * keyed by better-auth model name (not by the derived list key, so it stays
107
+ * remap-proof when e.g. `user.modelName: 'AuthUser'`).
108
+ *
109
+ * Per ADR-0013, the auth plugin ships its created lists (User/Session/
110
+ * Account/Verification) **closed** — no permissive defaults. This is the
111
+ * application's seam to grant them access: the plugin applies each entry to
112
+ * the corresponding list when it creates it (its own `addList` path), so the
113
+ * access rides along with the list's `@@map`/`@@schema`/fields and can't
114
+ * drift from the plugin's shape. A model with no entry here stays closed
115
+ * (deny-by-default).
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * authPlugin({
120
+ * access: {
121
+ * // Signed-in users can read the directory; only self can write.
122
+ * user: {
123
+ * operation: {
124
+ * query: ({ session }) => !!session,
125
+ * update: ({ session, item }) => session?.userId === item.id,
126
+ * },
127
+ * },
128
+ * // A user can read only their own sessions.
129
+ * session: {
130
+ * operation: {
131
+ * query: ({ session }) =>
132
+ * session ? { user: { id: { equals: session.userId } } } : false,
133
+ * },
134
+ * },
135
+ * },
136
+ * })
137
+ * ```
138
+ */
139
+ export type AuthAccessConfig = {
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
141
+ user?: ListConfig<any>['access']
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
143
+ session?: ListConfig<any>['access']
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
145
+ account?: ListConfig<any>['access']
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
147
+ verification?: ListConfig<any>['access']
148
+ }
149
+
103
150
  export type AuthModelConfig = {
104
151
  /**
105
152
  * The table/list name for this model.
@@ -239,6 +286,23 @@ export type AuthConfig = {
239
286
  */
240
287
  extendUserList?: ExtendUserListConfig
241
288
 
289
+ /**
290
+ * App-authored access control for the Auth lists (User/Session/Account/
291
+ * Verification), keyed by better-auth model name. See {@link AuthAccessConfig}.
292
+ *
293
+ * Per ADR-0013 the auth plugin ships these lists **closed** by default — a
294
+ * model with no entry here denies every operation (`context.db` reads/writes
295
+ * return `null`/`[]` and the list doesn't surface in the admin UI). Grant
296
+ * access explicitly for any Auth list your application reads or writes
297
+ * through `context.db`.
298
+ *
299
+ * For the `user` model specifically, {@link ExtendUserListConfig.access}
300
+ * (via `extendUserList.access`) is still honoured and takes precedence over
301
+ * `access.user` if both are set — it predates this option and remains the
302
+ * narrower, User-specific override.
303
+ */
304
+ access?: AuthAccessConfig
305
+
242
306
  /**
243
307
  * Custom email sending function for verification and password reset
244
308
  * If not provided, emails will be logged to console
@@ -1,5 +1,5 @@
1
1
  import type { ListConfig, FieldConfig } from '@opensaas/stack-core'
2
- import type { NormalizedAuthModels } from '../config/types.js'
2
+ import type { AuthAccessConfig, NormalizedAuthModels } from '../config/types.js'
3
3
  import { deriveAuthLists } from '../config/derive-auth-lists.js'
4
4
 
5
5
  /**
@@ -12,8 +12,12 @@ export type ExtendUserListConfig = {
12
12
  */
13
13
  fields?: Record<string, FieldConfig>
14
14
  /**
15
- * Access control for the User list
16
- * If not provided, defaults to basic access control (users can update their own records)
15
+ * Access control for the User list.
16
+ *
17
+ * Per ADR-0013, if neither this nor `authPlugin({ access: { user: … } })` is
18
+ * provided, the User list is **closed** (deny-by-default) — it no longer
19
+ * defaults to permissive access. Takes precedence over `access.user` when
20
+ * both are set.
17
21
  */
18
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
19
23
  access?: ListConfig<any>['access']
@@ -83,11 +87,13 @@ export function createVerificationList(): ListConfig<any> {
83
87
  *
84
88
  * @param userConfig - Extra User-list fields/access/hooks (from `extendUserList`)
85
89
  * @param models - Resolved better-auth model config; defaults to the better-auth defaults
90
+ * @param accessConfig - App-authored access for each Auth list, keyed by better-auth model name
86
91
  */
87
92
  export function getAuthLists(
88
93
  userConfig?: ExtendUserListConfig,
89
94
  models: NormalizedAuthModels = DEFAULT_MODELS,
95
+ accessConfig?: AuthAccessConfig,
90
96
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
91
97
  ): Record<string, ListConfig<any>> {
92
- return deriveAuthLists(models, userConfig || {}).lists
98
+ return deriveAuthLists(models, userConfig || {}, accessConfig || {}).lists
93
99
  }
@@ -9,17 +9,19 @@
9
9
  */
10
10
  export interface AuthRuntimeServices {
11
11
  /**
12
- * Get user by ID
13
- * Uses the access-controlled context to fetch user data
12
+ * Get user by ID.
13
+ * Resolves through the plugin runtime's `sudo` helper, so the result does
14
+ * not depend on the application's User list access policy (ADR-0013).
14
15
  *
15
16
  * @param userId - The ID of the user to fetch
16
- * @returns User object or null if not found or access denied
17
+ * @returns User object or null if not found
17
18
  */
18
19
  getUser: (userId: string) => Promise<unknown>
19
20
 
20
21
  /**
21
- * Get current user from session
22
- * Extracts userId from session and fetches user data
22
+ * Get current user from session.
23
+ * Extracts userId from session and fetches user data through the plugin
24
+ * runtime's `sudo` helper — see {@link getUser}.
23
25
  *
24
26
  * @returns Current user object or null if not authenticated or not found
25
27
  */