@pikku/inspector 0.12.19 → 0.12.21

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.
@@ -1,49 +1,269 @@
1
1
  import * as ts from 'typescript'
2
+ import type { FunctionServicesMeta } from '@pikku/core'
2
3
  import type { AddWiring } from '../types.js'
3
4
  import { ErrorCode } from '../error-codes.js'
5
+ import { extractServicesFromFunction } from '../utils/extract-services.js'
4
6
 
5
- export const addAuth: AddWiring = (logger, node, _checker, state) => {
6
- if (!ts.isCallExpression(node)) return
7
+ /**
8
+ * The pikku function id of the single shared auth handler the CLI generates
9
+ * (`export const authHandler = pikkuSessionlessFunc(...)` in auth.gen.ts). An
10
+ * exported top-level const collapses the catch-all `/api/auth/**` route onto one
11
+ * worker, and the export name becomes the function id. Shared with the CLI
12
+ * codegen and the post-process service stamp so all three agree on the same id
13
+ * without the inspector having to import `@pikku/better-auth`.
14
+ */
15
+ export const AUTH_HANDLER_FUNC_ID = 'authHandler'
7
16
 
8
- const expression = node.expression
9
- if (!ts.isIdentifier(expression) || expression.text !== 'wireAuth') return
17
+ /** The default better-auth base path when `basePath` is not configured. */
18
+ const DEFAULT_BASE_PATH = '/api/auth'
19
+
20
+ /**
21
+ * Find the first `betterAuth({...})` call anywhere inside the `pikkuBetterAuth`
22
+ * factory body. Supports both `(s) => betterAuth({...})` and
23
+ * `(s) => { ...; return betterAuth({...}) }`.
24
+ */
25
+ const findBetterAuthCall = (node: ts.Node): ts.CallExpression | undefined => {
26
+ let found: ts.CallExpression | undefined
27
+ const visit = (n: ts.Node) => {
28
+ if (found) return
29
+ if (
30
+ ts.isCallExpression(n) &&
31
+ ts.isIdentifier(n.expression) &&
32
+ n.expression.text === 'betterAuth'
33
+ ) {
34
+ found = n
35
+ return
36
+ }
37
+ ts.forEachChild(n, visit)
38
+ }
39
+ visit(node)
40
+ return found
41
+ }
42
+
43
+ /** Read a string-literal property off an object literal, if present. */
44
+ const readStringProp = (
45
+ obj: ts.ObjectLiteralExpression,
46
+ name: string
47
+ ): string | undefined => {
48
+ const prop = obj.properties.find(
49
+ (p) =>
50
+ ts.isPropertyAssignment(p) &&
51
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
52
+ p.name.text === name
53
+ ) as ts.PropertyAssignment | undefined
54
+ if (prop && ts.isStringLiteral(prop.initializer)) return prop.initializer.text
55
+ return undefined
56
+ }
10
57
 
11
- const firstArg = node.arguments[0]
12
- if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
58
+ /** Find an object-literal-valued property off an object literal, if present. */
59
+ const readObjectProp = (
60
+ obj: ts.ObjectLiteralExpression,
61
+ name: string
62
+ ): ts.ObjectLiteralExpression | undefined => {
63
+ const prop = obj.properties.find(
64
+ (p) =>
65
+ ts.isPropertyAssignment(p) &&
66
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
67
+ p.name.text === name
68
+ ) as ts.PropertyAssignment | undefined
69
+ if (prop && ts.isObjectLiteralExpression(prop.initializer))
70
+ return prop.initializer
71
+ return undefined
72
+ }
13
73
 
14
- const providersProp = firstArg.properties.find(
74
+ /** Find an array-literal-valued property off an object literal, if present. */
75
+ const readArrayProp = (
76
+ obj: ts.ObjectLiteralExpression,
77
+ name: string
78
+ ): ts.ArrayLiteralExpression | undefined => {
79
+ const prop = obj.properties.find(
15
80
  (p) =>
16
81
  ts.isPropertyAssignment(p) &&
17
82
  (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
18
- p.name.text === 'providers'
83
+ p.name.text === name
19
84
  ) as ts.PropertyAssignment | undefined
85
+ if (prop && ts.isArrayLiteralExpression(prop.initializer))
86
+ return prop.initializer
87
+ return undefined
88
+ }
89
+
90
+ /**
91
+ * Read the callee name of a `plugins: [...]` entry. better-auth plugins are
92
+ * factory calls (`bearer()`, `twoFactor({ ... })`, `admin()`); the entry's id
93
+ * is the called function's name. Member-expression callees (`foo.bar()`) and
94
+ * non-call entries are ignored.
95
+ */
96
+ const readPluginId = (el: ts.Expression): string | undefined => {
97
+ if (ts.isCallExpression(el) && ts.isIdentifier(el.expression))
98
+ return el.expression.text
99
+ return undefined
100
+ }
101
+
102
+ /**
103
+ * Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
104
+ *
105
+ * `pikkuBetterAuth` is pure: it wraps a factory that returns a configured better-auth
106
+ * instance and has NO side effects. The user assigns it to an exported binding,
107
+ * e.g.
108
+ *
109
+ * export const auth = pikkuBetterAuth(async (services) => betterAuth({ ... }))
110
+ *
111
+ * The pikku CLI discovers that single export and generates a catch-all
112
+ * `auth.gen.ts` that wires `${basePath}/**` to one shared handler, registers the
113
+ * better-auth session middleware, and emits a `wireSecret` for every configured
114
+ * social provider — so the auth routes and secret requirements flow through
115
+ * normal inspection into the deploy manifest.
116
+ *
117
+ * This add-wiring records the exported binding name, source file, basePath, the
118
+ * `socialProviders` keys, whether email/password is enabled, and the services
119
+ * the factory touches. Exactly one `pikkuBetterAuth` is allowed per codebase.
120
+ */
121
+ export const addAuth: AddWiring = (logger, node, _checker, state) => {
122
+ if (!ts.isCallExpression(node)) return
123
+
124
+ const expression = node.expression
125
+ if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
126
+ return
20
127
 
21
128
  const sourceFile = node.getSourceFile().fileName
22
- state.auth.files.add(sourceFile)
23
129
 
24
- if (!providersProp) {
130
+ // Walk up to the `export const <name> = pikkuBetterAuth(...)` binding.
131
+ const varDecl = node.parent
132
+ if (!ts.isVariableDeclaration(varDecl) || !ts.isIdentifier(varDecl.name)) {
133
+ logger.critical(
134
+ ErrorCode.AUTH_NOT_EXPORTED,
135
+ `pikkuBetterAuth(...) must be assigned to an exported const, e.g. \`export const auth = pikkuBetterAuth((services) => betterAuth({...}))\` in ${sourceFile}`
136
+ )
137
+ return
138
+ }
139
+ const exportName = varDecl.name.text
140
+
141
+ // VariableDeclaration -> VariableDeclarationList -> VariableStatement
142
+ const declList = varDecl.parent
143
+ const varStatement = declList?.parent
144
+ const isConst =
145
+ ts.isVariableDeclarationList(declList) &&
146
+ (declList.flags & ts.NodeFlags.Const) !== 0
147
+ const isExported =
148
+ varStatement &&
149
+ ts.isVariableStatement(varStatement) &&
150
+ varStatement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
151
+ if (!isExported || !isConst) {
152
+ logger.critical(
153
+ ErrorCode.AUTH_NOT_EXPORTED,
154
+ `pikkuBetterAuth(...) must be assigned to an exported const so the CLI can import it. Use \`export const ${exportName} = pikkuBetterAuth(...)\` in ${sourceFile}`
155
+ )
156
+ return
157
+ }
158
+
159
+ if (state.auth.definition) {
160
+ logger.critical(
161
+ ErrorCode.DUPLICATE_AUTH_DEFINITION,
162
+ `Only one pikkuBetterAuth(...) is allowed per codebase. Found a second in ${sourceFile} (first: ${state.auth.definition.sourceFile}).`
163
+ )
25
164
  return
26
165
  }
27
166
 
28
- if (!ts.isArrayLiteralExpression(providersProp.initializer)) {
167
+ // The single argument must be the factory: (services) => betterAuth({...}).
168
+ const factory = node.arguments[0]
169
+ if (
170
+ !factory ||
171
+ (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))
172
+ ) {
29
173
  logger.critical(
30
174
  ErrorCode.MISSING_NAME,
31
- 'wireAuth: providers must be an array literal of string literals.'
175
+ `pikkuBetterAuth(...) must take a factory function returning betterAuth(...), e.g. \`pikkuBetterAuth((services) => betterAuth({...}))\` in ${sourceFile}`
32
176
  )
33
177
  return
34
178
  }
35
179
 
36
- for (const element of (providersProp.initializer as ts.ArrayLiteralExpression)
37
- .elements) {
38
- if (!ts.isStringLiteral(element)) {
39
- logger.critical(
40
- ErrorCode.NON_LITERAL_WIRE_NAME,
41
- `wireAuth: each provider must be a string literal. Found: ${element.getText()}`
42
- )
43
- return
180
+ state.auth.files.add(sourceFile)
181
+
182
+ // Derive the services the factory touches from its destructured first param —
183
+ // the same convention as every other pikku wiring. A non-destructured param
184
+ // yields optimized:false (handler gets all singleton services), with the
185
+ // standard diagnostic steering the user to destructure.
186
+ const services: FunctionServicesMeta = extractServicesFromFunction(factory)
187
+
188
+ // Find the inner betterAuth({...}) call to read providers/basePath/credentials.
189
+ let basePath = DEFAULT_BASE_PATH
190
+ let hasCredentials = false
191
+ let cookieCache = false
192
+ const betterAuthCall = findBetterAuthCall(factory)
193
+ const config = betterAuthCall?.arguments[0]
194
+
195
+ if (config && ts.isObjectLiteralExpression(config)) {
196
+ basePath = readStringProp(config, 'basePath') ?? DEFAULT_BASE_PATH
197
+
198
+ // Detect `session.cookieCache.enabled` → drives the stateless middleware split.
199
+ const session = readObjectProp(config, 'session')
200
+ if (session) {
201
+ const cookieCacheBlock = readObjectProp(session, 'cookieCache')
202
+ if (cookieCacheBlock) {
203
+ const enabledProp = cookieCacheBlock.properties.find(
204
+ (p) =>
205
+ ts.isPropertyAssignment(p) &&
206
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
207
+ p.name.text === 'enabled'
208
+ ) as ts.PropertyAssignment | undefined
209
+ cookieCache =
210
+ !enabledProp ||
211
+ enabledProp.initializer.kind !== ts.SyntaxKind.FalseKeyword
212
+ }
44
213
  }
45
- if (!state.auth.providers.includes(element.text)) {
46
- state.auth.providers.push(element.text)
214
+
215
+ const emailAndPassword = readObjectProp(config, 'emailAndPassword')
216
+ if (emailAndPassword) {
217
+ // `emailAndPassword: { enabled: true }` — treat a present block without an
218
+ // explicit `enabled: false` as credentials being available.
219
+ const enabledProp = emailAndPassword.properties.find(
220
+ (p) =>
221
+ ts.isPropertyAssignment(p) &&
222
+ ts.isIdentifier(p.name) &&
223
+ p.name.text === 'enabled'
224
+ ) as ts.PropertyAssignment | undefined
225
+ hasCredentials =
226
+ !enabledProp ||
227
+ enabledProp.initializer.kind !== ts.SyntaxKind.FalseKeyword
47
228
  }
229
+
230
+ const socialProviders = readObjectProp(config, 'socialProviders')
231
+ if (socialProviders) {
232
+ for (const prop of socialProviders.properties) {
233
+ const key =
234
+ (ts.isPropertyAssignment(prop) ||
235
+ ts.isShorthandPropertyAssignment(prop)) &&
236
+ (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
237
+ ? prop.name.text
238
+ : undefined
239
+ if (key && !state.auth.providers.includes(key)) {
240
+ state.auth.providers.push(key)
241
+ }
242
+ }
243
+ }
244
+
245
+ const plugins = readArrayProp(config, 'plugins')
246
+ if (plugins) {
247
+ for (const el of plugins.elements) {
248
+ const id = readPluginId(el)
249
+ if (id && !state.auth.plugins.includes(id)) {
250
+ state.auth.plugins.push(id)
251
+ }
252
+ }
253
+ }
254
+ } else {
255
+ logger.warn(
256
+ `pikkuBetterAuth in ${sourceFile}: could not statically find a betterAuth({...}) call inside the factory — social provider secrets will not be auto-wired.`
257
+ )
258
+ }
259
+
260
+ state.auth.definition = {
261
+ exportName,
262
+ sourceFile,
263
+ basePath,
264
+ hasCredentials,
265
+ cookieCache,
266
+ plugins: [...state.auth.plugins],
267
+ services,
48
268
  }
49
269
  }
@@ -0,0 +1,73 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import type { InspectorLogger } from '../types.js'
8
+
9
+ const makeLogger = () =>
10
+ ({
11
+ debug: () => {},
12
+ info: () => {},
13
+ warn: () => {},
14
+ error: () => {},
15
+ critical: () => {},
16
+ hasCriticalErrors: () => false,
17
+ }) satisfies InspectorLogger
18
+
19
+ describe('addCLIRenderers inspector', () => {
20
+ test('extracts destructured renderer services from the callback param', async () => {
21
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-cli-renderer-'))
22
+ const file = join(rootDir, 'cli.ts')
23
+
24
+ await writeFile(
25
+ file,
26
+ [
27
+ "import { wireCLI, pikkuCLIRender } from '@pikku/core/cli'",
28
+ 'export const render = pikkuCLIRender(({ logger }, data) => {',
29
+ ' logger.info(data.message)',
30
+ '})',
31
+ "wireCLI({ program: 'pikku', render, commands: {} })",
32
+ ].join('\n')
33
+ )
34
+
35
+ try {
36
+ const state = await inspect(makeLogger(), [file], { rootDir })
37
+ assert.deepEqual(state.cli.meta.renderers['render'], {
38
+ name: 'render',
39
+ exportedName: 'render',
40
+ services: { optimized: true, services: ['logger'] },
41
+ filePath: file,
42
+ })
43
+ } finally {
44
+ await rm(rootDir, { recursive: true, force: true })
45
+ }
46
+ })
47
+
48
+ test('marks non-destructured renderer services as unoptimized', async () => {
49
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-cli-renderer-all-'))
50
+ const file = join(rootDir, 'cli.ts')
51
+
52
+ await writeFile(
53
+ file,
54
+ [
55
+ "import { wireCLI, pikkuCLIRender } from '@pikku/core/cli'",
56
+ 'export const render = pikkuCLIRender((services, data) => {',
57
+ ' services.logger.info(data.message)',
58
+ '})',
59
+ "wireCLI({ program: 'pikku', render, commands: {} })",
60
+ ].join('\n')
61
+ )
62
+
63
+ try {
64
+ const state = await inspect(makeLogger(), [file], { rootDir })
65
+ assert.deepEqual(state.cli.meta.renderers['render']?.services, {
66
+ optimized: false,
67
+ services: [],
68
+ })
69
+ } finally {
70
+ await rm(rootDir, { recursive: true, force: true })
71
+ }
72
+ })
73
+ })
@@ -18,6 +18,7 @@ import { getPropertyValue } from '../utils/get-property-value.js'
18
18
  import { resolveIdentifier } from '../utils/resolve-identifier.js'
19
19
  import { resolveAddonName } from '../utils/resolve-addon-package.js'
20
20
  import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
21
+ import { extractServicesFromFunction } from '../utils/extract-services.js'
21
22
 
22
23
  // Track if we've warned about missing Config type to avoid duplicate warnings
23
24
  const configTypeWarningShown = new Set<string>()
@@ -924,7 +925,7 @@ export const addCLIRenderers: AddWiring = (
924
925
  ) => {
925
926
  if (!ts.isCallExpression(node)) return
926
927
 
927
- const { expression, arguments: args, typeArguments } = node
928
+ const { expression, arguments: args } = node
928
929
 
929
930
  // Only handle pikkuCLIRender calls
930
931
  if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
@@ -944,30 +945,13 @@ export const addCLIRenderers: AddWiring = (
944
945
  const sourceFile = node.getSourceFile()
945
946
  const filePath = sourceFile.fileName
946
947
 
947
- // Extract services from type parameters (second type param is Services)
948
- const services: { optimized: boolean; services: string[] } = {
949
- optimized: true,
950
- services: [],
951
- }
952
-
953
- if (typeArguments && typeArguments.length >= 2) {
954
- // Second type parameter is the Services type
955
- const servicesTypeNode = typeArguments[1]
956
- if (servicesTypeNode) {
957
- const servicesType = typeChecker.getTypeFromTypeNode(servicesTypeNode)
958
-
959
- // Extract property names from the Services type
960
- const properties = servicesType.getProperties()
961
- for (const prop of properties) {
962
- services.services.push(prop.getName())
963
- }
964
-
965
- // If no specific services found, it might be using the full services object
966
- if (properties.length === 0) {
967
- services.optimized = false
968
- }
969
- }
970
- }
948
+ // Renderer service usage is defined by the callback's first parameter shape,
949
+ // the same way function/auth service usage is tracked elsewhere in the inspector.
950
+ const renderFunc = args[0]
951
+ const services =
952
+ ts.isArrowFunction(renderFunc) || ts.isFunctionExpression(renderFunc)
953
+ ? extractServicesFromFunction(renderFunc)
954
+ : { optimized: true, services: [] }
971
955
 
972
956
  // Store renderer metadata
973
957
  inspectorState.cli.meta.renderers[pikkuFuncId] = {
@@ -37,6 +37,10 @@ export enum ErrorCode {
37
37
  FUNCTION_METADATA_NOT_FOUND = 'PKU559',
38
38
  HANDLER_NOT_RESOLVED = 'PKU568',
39
39
 
40
+ // Auth errors
41
+ DUPLICATE_AUTH_DEFINITION = 'PKU581',
42
+ AUTH_NOT_EXPORTED = 'PKU582',
43
+
40
44
  // HTTP Route errors
41
45
  ROUTE_PARAM_MISMATCH = 'PKU571',
42
46
  ROUTE_QUERY_MISMATCH = 'PKU572',
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export type { TypesMap } from './types-map.js'
3
3
  export type * from './types.js'
4
4
  export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js'
5
5
  export { ErrorCode } from './error-codes.js'
6
+ export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js'
6
7
  export {
7
8
  serializeInspectorState,
8
9
  deserializeInspectorState,
package/src/inspector.ts CHANGED
@@ -11,6 +11,7 @@ import { getFilesAndMethods } from './utils/get-files-and-methods.js'
11
11
  import { findCommonAncestor } from './utils/find-root-dir.js'
12
12
  import {
13
13
  aggregateRequiredServices,
14
+ stampAuthHandlerServices,
14
15
  validateAgentModels,
15
16
  validateSecretOverrides,
16
17
  validateVariableOverrides,
@@ -147,7 +148,9 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
147
148
  },
148
149
  auth: {
149
150
  providers: [],
151
+ plugins: [],
150
152
  files: new Set(),
153
+ definition: null,
151
154
  },
152
155
  secrets: {
153
156
  definitions: [],
@@ -333,6 +336,9 @@ export const inspect = async (
333
336
 
334
337
  if (!options.setupOnly) {
335
338
  const startAggregate = performance.now()
339
+ // Apply the inspected auth handler service set before aggregation so it
340
+ // flows into requiredServices (the generated handler's own func is opaque).
341
+ stampAuthHandlerServices(state)
336
342
  aggregateRequiredServices(state)
337
343
  logger.debug(
338
344
  `Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`
package/src/types.ts CHANGED
@@ -301,6 +301,41 @@ export interface InspectorDiagnostic {
301
301
  position: number
302
302
  }
303
303
 
304
+ /** A single discovered `export const X = pikkuBetterAuth((services) => betterAuth({...}))`. */
305
+ export interface AuthDefinition {
306
+ /** The exported binding name the CLI imports (`export const <exportName>`). */
307
+ exportName: string
308
+ /** Absolute path of the file declaring it. */
309
+ sourceFile: string
310
+ /** better-auth base path (the `basePath` option, default `/api/auth`). */
311
+ basePath: string
312
+ /** Whether email/password auth is enabled (`emailAndPassword.enabled`). Written
313
+ * into the generated `auth-meta.gen.json` so the console knows credentials are
314
+ * available alongside the OAuth providers. */
315
+ hasCredentials: boolean
316
+ /** better-auth plugin ids used in the config's `plugins: [...]` array, read
317
+ * from each entry's callee name (e.g. `bearer()` → `'bearer'`). Written into
318
+ * `auth-meta.gen.json` so the console SSO page can show which plugins are
319
+ * enabled. */
320
+ plugins: string[]
321
+ /** Whether `session.cookieCache` is enabled — drives the stateless session
322
+ * middleware split in the auth codegen. Absent/false ⇒ stateful middleware. */
323
+ cookieCache?: boolean
324
+ /**
325
+ * Singleton services the generated auth handler must have available at
326
+ * runtime — the services the `pikkuBetterAuth` factory reaches for (either
327
+ * destructured from its first param, or accessed as `services.<name>` in its
328
+ * body). better-auth's factory typically touches `secrets` and the DB.
329
+ *
330
+ * The generated `authHandler` calls `createAuthHandler(...).func`, an opaque
331
+ * property access the inspector can't see through; without this stamp the
332
+ * deployed auth worker would instantiate none of these services and the
333
+ * factory would receive an undefined `kysely`. Re-derived every inspect and
334
+ * applied to the handler meta before service aggregation runs.
335
+ */
336
+ services: FunctionServicesMeta
337
+ }
338
+
304
339
  export interface InspectorState {
305
340
  rootDir: string // Root directory inferred from source files
306
341
  singletonServicesTypeImportMap: PathToNameAndType
@@ -384,7 +419,12 @@ export interface InspectorState {
384
419
  }
385
420
  auth: {
386
421
  providers: string[]
422
+ plugins: string[]
387
423
  files: Set<string>
424
+ /** The single `export const X = pikkuBetterAuth({...})` discovered in the
425
+ * codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
426
+ * More than one `pikkuBetterAuth` is a critical error. */
427
+ definition: AuthDefinition | null
388
428
  }
389
429
  secrets: {
390
430
  definitions: SecretDefinitions
@@ -12,6 +12,33 @@ import type {
12
12
  } from '@pikku/core'
13
13
  import { extractTypeKeys } from './type-utils.js'
14
14
  import { ErrorCode } from '../error-codes.js'
15
+ import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
16
+
17
+ /**
18
+ * Stamp the inspected authorize/callbacks service set onto the generated auth
19
+ * handler's function meta.
20
+ *
21
+ * The CLI generates `export const authHandler = pikkuSessionlessFunc({ func:
22
+ * createAuthHandler(...).func })`. That `func` is an opaque property access, so
23
+ * normal extraction records zero services for the handler — which would leave
24
+ * the deployed auth worker without `kysely`/`variables`/`secrets` and break
25
+ * `authorize` at runtime. `add-auth` already computed the real dependency set
26
+ * (from the pikkuBetterAuth source) into `state.auth.definition.services`; copy it
27
+ * onto the handler meta. Re-derived every inspect and ordered BEFORE
28
+ * `aggregateRequiredServices` so it flows into `requiredServices`.
29
+ */
30
+ export function stampAuthHandlerServices(
31
+ state: InspectorState | Omit<InspectorState, 'typesLookup'>
32
+ ): void {
33
+ const definition = state.auth.definition
34
+ if (!definition) return
35
+ const handlerMeta = state.functions.meta[AUTH_HANDLER_FUNC_ID]
36
+ if (!handlerMeta) return
37
+ handlerMeta.services = {
38
+ optimized: definition.services.optimized,
39
+ services: [...definition.services.services],
40
+ }
41
+ }
15
42
 
16
43
  /**
17
44
  * Helper to extract wire-level middleware/permission names from metadata.
@@ -183,7 +183,9 @@ export interface SerializableInspectorState {
183
183
  }
184
184
  auth: {
185
185
  providers: string[]
186
+ plugins: string[]
186
187
  files: string[]
188
+ definition: InspectorState['auth']['definition']
187
189
  }
188
190
  secrets: {
189
191
  definitions: InspectorState['secrets']['definitions']
@@ -389,7 +391,9 @@ export function serializeInspectorState(
389
391
  },
390
392
  auth: {
391
393
  providers: state.auth.providers,
394
+ plugins: state.auth.plugins,
392
395
  files: Array.from(state.auth.files),
396
+ definition: state.auth.definition,
393
397
  },
394
398
  secrets: {
395
399
  definitions: state.secrets.definitions,
@@ -566,7 +570,9 @@ export function deserializeInspectorState(
566
570
  },
567
571
  auth: {
568
572
  providers: data.auth?.providers || [],
573
+ plugins: data.auth?.plugins || [],
569
574
  files: new Set(data.auth?.files || []),
575
+ definition: data.auth?.definition || null,
570
576
  },
571
577
  secrets: {
572
578
  definitions: data.secrets?.definitions || [],