@pikku/inspector 0.12.24 → 0.12.26

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/CHANGELOG.md CHANGED
@@ -1,3 +1,51 @@
1
+ ## 0.12.26
2
+
3
+ ### Patch Changes
4
+
5
+ - ed548d5: fix(auth): skip the generated global `betterAuthSession()` when the user registers their own
6
+
7
+ The CLI's `auth.gen.ts` unconditionally wired a global
8
+ `addHTTPMiddleware('*', [betterAuthSession()])` (default map) on the stateful
9
+ path. A project that needs a customized session bridge — `mapSession`,
10
+ `impersonation`, `apiKey` — had to register a second global
11
+ `betterAuthSession({...})`, leaving two in the chain; the generated default ran
12
+ first and short-circuited (`if (session) next()`) so the custom one never took
13
+ effect.
14
+
15
+ The inspector now records `state.auth.hasUserSessionMiddleware` when it sees a
16
+ user-authored **global** `betterAuthSession` registration (route-scoped and
17
+ `.gen.ts` registrations are ignored, so regeneration never self-suppresses).
18
+ The CLI omits its own global `betterAuthSession()` from `auth.gen.ts` when that
19
+ flag is set — exactly one session bridge in the chain, the user's. Mirrors the
20
+ existing stateless skip (`userStatelessSession`, #754).
21
+
22
+ ## 0.12.25
23
+
24
+ ### Patch Changes
25
+
26
+ - b6ba601: fix(lint): don't flag pikkuAuth's session param as a non-destructured wire
27
+
28
+ `pikkuAuth`'s handler is `(services, session)` — the second parameter is the
29
+ resolved user session, not a wires bag. The inspector was extracting "wires"
30
+ from that parameter (`extractUsedWires(handler, 1)`), so a permission like
31
+ `pikkuAuth(async ({ logger }, session) => !!session)` tripped
32
+ `wiresNotDestructured` even though `session` cannot be destructured. pikkuAuth
33
+ exposes no user-facing wires parameter, so no wires meta is recorded for it.
34
+
35
+ - ae7fc5d: Include gateway platform and auth fields in inspected gateway metadata.
36
+ - decdad5: fix(lint): don't fail the build on framework-synthesized functions
37
+
38
+ The `servicesNotDestructured`/`wiresNotDestructured` defaults (`error`) were
39
+ tripping on functions the user can't edit: generated `.gen.ts` wrappers (the
40
+ opaque `authHandler`, the cli channel raw dispatcher) and synthetic route→addon
41
+ bridges (`http:<method>:<route>`, no source file). `computeDiagnostics` now skips
42
+ any function without a real, non-generated source file, so the lint only nudges
43
+ hand-written user code. Also destructures the CLI's own `all` command.
44
+
45
+ - Updated dependencies [ae7fc5d]
46
+ - Updated dependencies [fa7a09c]
47
+ - @pikku/core@0.12.37
48
+
1
49
  ## 0.12.24
2
50
 
3
51
  ### Patch Changes
@@ -137,6 +137,17 @@ export const addAuth = (logger, node, _checker, state) => {
137
137
  isInsideGlobalMiddlewareRegistration(node)) {
138
138
  state.auth.userStatelessSession = true;
139
139
  }
140
+ // Same rule for the stateful variant: a user-registered global
141
+ // betterAuthSession(...) (custom mapSession/impersonation/apiKey) means the CLI
142
+ // must NOT auto-generate its own default one — the generated one runs first and
143
+ // pre-empts the user's via the `if (session) next()` short-circuit. Stateful
144
+ // analogue of the betterAuthStatelessSession skip above.
145
+ if (ts.isIdentifier(expression) &&
146
+ expression.text === 'betterAuthSession' &&
147
+ !node.getSourceFile().fileName.endsWith('.gen.ts') &&
148
+ isInsideGlobalMiddlewareRegistration(node)) {
149
+ state.auth.hasUserSessionMiddleware = true;
150
+ }
140
151
  if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
141
152
  return;
142
153
  const sourceFile = node.getSourceFile().fileName;
@@ -23,6 +23,8 @@ export const addGateway = (logger, node, checker, state, _options) => {
23
23
  const nameValue = getPropertyValue(obj, 'name');
24
24
  const typeValue = getPropertyValue(obj, 'type');
25
25
  const routeValue = getPropertyValue(obj, 'route');
26
+ const platformValue = getPropertyValue(obj, 'platform');
27
+ const authValue = getPropertyValue(obj, 'auth');
26
28
  const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker);
27
29
  if (disabled)
28
30
  return;
@@ -51,7 +53,9 @@ export const addGateway = (logger, node, checker, state, _options) => {
51
53
  ...(packageName && { packageName }),
52
54
  name: nameValue,
53
55
  type: typeValue,
54
- route: routeValue,
56
+ ...(routeValue && { route: routeValue }),
57
+ ...(platformValue && { platform: platformValue }),
58
+ ...(typeof authValue === 'boolean' && { auth: authValue }),
55
59
  gateway: true,
56
60
  summary,
57
61
  description,
@@ -141,7 +141,11 @@ export const addPermission = (logger, node, checker, state) => {
141
141
  return;
142
142
  }
143
143
  const services = extractServicesFromFunction(actualHandler);
144
- const wires = extractUsedWires(actualHandler, 1);
144
+ // pikkuAuth's handler is (services, session) — its second parameter is the
145
+ // resolved user session, NOT a wires bag, so it must not be analyzed (or
146
+ // flagged) as a non-destructured wires parameter. pikkuAuth exposes no
147
+ // user-facing wires parameter.
148
+ const wires = { optimized: true, wires: [] };
145
149
  let { pikkuFuncId, exportedName } = extractFunctionName(node, checker, state.rootDir);
146
150
  if (pikkuFuncId.startsWith('__temp_')) {
147
151
  if (ts.isVariableDeclaration(node.parent) &&
package/dist/types.d.ts CHANGED
@@ -436,6 +436,12 @@ export interface InspectorState {
436
436
  * own default-map stateless middleware, which would otherwise pre-empt the
437
437
  * user's custom mapSession (pikkujs/pikku#754). */
438
438
  userStatelessSession?: boolean;
439
+ /** True when a user (non-generated) file already registers a global
440
+ * `betterAuthSession(...)`. The CLI then skips auto-generating its own
441
+ * default stateful middleware, which would otherwise run first and pre-empt
442
+ * the user's config (mapSession/impersonation/apiKey). Stateful analogue of
443
+ * `userStatelessSession`. */
444
+ hasUserSessionMiddleware?: boolean;
439
445
  };
440
446
  secrets: {
441
447
  definitions: SecretDefinitions;
@@ -525,6 +525,14 @@ export function validateSchemaWiringSeparation(logger, state) {
525
525
  export function computeDiagnostics(state) {
526
526
  const diagnostics = [];
527
527
  for (const [id, meta] of Object.entries(state.functions.meta)) {
528
+ // Skip framework-synthesized functions: generated wrappers (auth.gen.ts's
529
+ // opaque authHandler, the cli channel's raw dispatcher) and synthetic route
530
+ // bridges that reference addon functions (id `http:<method>:<route>`, no
531
+ // source file). The user can't edit any of these, so a destructure lint
532
+ // meant to nudge them about their own code must not fail the build over them.
533
+ if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
534
+ continue;
535
+ }
528
536
  if (meta.services && !meta.services.optimized) {
529
537
  diagnostics.push({
530
538
  code: ErrorCode.SERVICES_NOT_DESTRUCTURED,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.24",
3
+ "version": "0.12.26",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.35",
38
+ "@pikku/core": "^0.12.37",
39
39
  "openapi-types": "^12.1.3",
40
40
  "path-to-regexp": "^8.3.0",
41
41
  "ts-json-schema-generator": "^2.5.0",
@@ -616,4 +616,54 @@ describe('addAuth inspector', () => {
616
616
  await rm(rootDir, { recursive: true, force: true })
617
617
  }
618
618
  })
619
+
620
+ test('user-registered global betterAuthSession sets hasUserSessionMiddleware', async () => {
621
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-session-'))
622
+ const file = join(rootDir, 'middleware.ts')
623
+ await writeFile(
624
+ file,
625
+ [
626
+ "import { addHTTPMiddleware } from '#pikku'",
627
+ "import { betterAuthSession } from '@pikku/better-auth'",
628
+ "addHTTPMiddleware('*', [",
629
+ ' betterAuthSession({',
630
+ ' impersonation: { loadUser: (id: string) => ({ id }) },',
631
+ ' }),',
632
+ '])',
633
+ ].join('\n')
634
+ )
635
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
636
+ try {
637
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
638
+ assert.equal(state.auth.hasUserSessionMiddleware, true)
639
+ } finally {
640
+ await rm(rootDir, { recursive: true, force: true })
641
+ }
642
+ })
643
+
644
+ test('betterAuthSession in a .gen.ts file does NOT set hasUserSessionMiddleware', async () => {
645
+ // Critical: the CLI's own auth.gen.ts contains addHTTPMiddleware('*',
646
+ // [betterAuthSession()]). It must not count as a user registration, or it
647
+ // would suppress itself and leave the chain with no session middleware.
648
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-session-gen-'))
649
+ const file = join(rootDir, 'auth.gen.ts')
650
+ await writeFile(
651
+ file,
652
+ [
653
+ "import { addHTTPMiddleware } from '#pikku'",
654
+ "import { betterAuthSession } from '@pikku/better-auth'",
655
+ "addHTTPMiddleware('*', [betterAuthSession()])",
656
+ ].join('\n')
657
+ )
658
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
659
+ try {
660
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
661
+ assert.ok(
662
+ !state.auth.hasUserSessionMiddleware,
663
+ 'generated file must not self-trigger the skip'
664
+ )
665
+ } finally {
666
+ await rm(rootDir, { recursive: true, force: true })
667
+ }
668
+ })
619
669
  })
@@ -168,6 +168,20 @@ export const addAuth: AddWiring = (logger, node, _checker, state) => {
168
168
  state.auth.userStatelessSession = true
169
169
  }
170
170
 
171
+ // Same rule for the stateful variant: a user-registered global
172
+ // betterAuthSession(...) (custom mapSession/impersonation/apiKey) means the CLI
173
+ // must NOT auto-generate its own default one — the generated one runs first and
174
+ // pre-empts the user's via the `if (session) next()` short-circuit. Stateful
175
+ // analogue of the betterAuthStatelessSession skip above.
176
+ if (
177
+ ts.isIdentifier(expression) &&
178
+ expression.text === 'betterAuthSession' &&
179
+ !node.getSourceFile().fileName.endsWith('.gen.ts') &&
180
+ isInsideGlobalMiddlewareRegistration(node)
181
+ ) {
182
+ state.auth.hasUserSessionMiddleware = true
183
+ }
184
+
171
185
  if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
172
186
  return
173
187
 
@@ -43,7 +43,9 @@ export const addGateway: AddWiring = (
43
43
 
44
44
  const nameValue = getPropertyValue(obj, 'name') as string | null
45
45
  const typeValue = getPropertyValue(obj, 'type') as GatewayTransportType | null
46
- const routeValue = getPropertyValue(obj, 'route') as string | undefined
46
+ const routeValue = getPropertyValue(obj, 'route') as string | null
47
+ const platformValue = getPropertyValue(obj, 'platform') as string | null
48
+ const authValue = getPropertyValue(obj, 'auth')
47
49
  const { disabled, tags, summary, description, errors } =
48
50
  getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker)
49
51
 
@@ -94,7 +96,9 @@ export const addGateway: AddWiring = (
94
96
  ...(packageName && { packageName }),
95
97
  name: nameValue,
96
98
  type: typeValue,
97
- route: routeValue,
99
+ ...(routeValue && { route: routeValue }),
100
+ ...(platformValue && { platform: platformValue }),
101
+ ...(typeof authValue === 'boolean' && { auth: authValue }),
98
102
  gateway: true,
99
103
  summary,
100
104
  description,
@@ -0,0 +1,59 @@
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 { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
+ ({
12
+ debug: () => {},
13
+ info: () => {},
14
+ warn: () => {},
15
+ error: () => {},
16
+ diagnostic: ({ code, message }) => {
17
+ criticals.push({ code, message })
18
+ },
19
+ critical: (code: ErrorCode, message: string) => {
20
+ criticals.push({ code, message })
21
+ },
22
+ hasCriticalErrors: () => criticals.length > 0,
23
+ }) satisfies InspectorLogger
24
+
25
+ describe('addPermission — pikkuAuth', () => {
26
+ test('does not record a wires meta for the session parameter', async () => {
27
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-auth-wires-'))
28
+ const file = join(rootDir, 'auth.ts')
29
+
30
+ await writeFile(
31
+ file,
32
+ [
33
+ 'const pikkuAuth = (x: any) => x',
34
+ 'export const isAuthenticated = pikkuAuth(async ({ logger }, session) => {',
35
+ ' logger.info({ type: "auth-check" })',
36
+ ' return !!session',
37
+ '})',
38
+ ].join('\n')
39
+ )
40
+
41
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
42
+ try {
43
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
44
+ const def = state.permissions.definitions['isAuthenticated']
45
+ assert.ok(def, 'isAuthenticated permission should be recorded')
46
+ // The pikkuAuth handler is (services, session) — session is NOT a wires
47
+ // bag and must not be flagged as a non-destructured wires parameter.
48
+ assert.equal(def.wires, undefined)
49
+ const wireDiag = (state.diagnostics ?? []).find(
50
+ (d) =>
51
+ d.code === ErrorCode.WIRES_NOT_DESTRUCTURED &&
52
+ d.message.includes('isAuthenticated')
53
+ )
54
+ assert.equal(wireDiag, undefined)
55
+ } finally {
56
+ await rm(rootDir, { recursive: true, force: true })
57
+ }
58
+ })
59
+ })
@@ -1,4 +1,5 @@
1
1
  import * as ts from 'typescript'
2
+ import type { FunctionWiresMeta } from '@pikku/core'
2
3
  import type { AddWiring, InspectorState } from '../types.js'
3
4
  import {
4
5
  extractFunctionName,
@@ -195,7 +196,11 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
195
196
  }
196
197
 
197
198
  const services = extractServicesFromFunction(actualHandler)
198
- const wires = extractUsedWires(actualHandler, 1)
199
+ // pikkuAuth's handler is (services, session) — its second parameter is the
200
+ // resolved user session, NOT a wires bag, so it must not be analyzed (or
201
+ // flagged) as a non-destructured wires parameter. pikkuAuth exposes no
202
+ // user-facing wires parameter.
203
+ const wires: FunctionWiresMeta = { optimized: true, wires: [] }
199
204
  let { pikkuFuncId, exportedName } = extractFunctionName(
200
205
  node,
201
206
  checker,
package/src/types.ts CHANGED
@@ -502,6 +502,12 @@ export interface InspectorState {
502
502
  * own default-map stateless middleware, which would otherwise pre-empt the
503
503
  * user's custom mapSession (pikkujs/pikku#754). */
504
504
  userStatelessSession?: boolean
505
+ /** True when a user (non-generated) file already registers a global
506
+ * `betterAuthSession(...)`. The CLI then skips auto-generating its own
507
+ * default stateful middleware, which would otherwise run first and pre-empt
508
+ * the user's config (mapSession/impersonation/apiKey). Stateful analogue of
509
+ * `userStatelessSession`. */
510
+ hasUserSessionMiddleware?: boolean
505
511
  }
506
512
  secrets: {
507
513
  definitions: SecretDefinitions
@@ -0,0 +1,69 @@
1
+ import { test, describe } from 'node:test'
2
+ import { strict as assert } from 'node:assert'
3
+ import { computeDiagnostics } from './post-process.js'
4
+ import type { InspectorState } from '../types.js'
5
+ import { ErrorCode } from '../error-codes.js'
6
+
7
+ function stateWithFunctions(
8
+ meta: InspectorState['functions']['meta']
9
+ ): InspectorState {
10
+ return {
11
+ functions: { meta },
12
+ middleware: { definitions: {} },
13
+ permissions: { definitions: {} },
14
+ } as unknown as InspectorState
15
+ }
16
+
17
+ describe('computeDiagnostics', () => {
18
+ test('flags a user-authored function that does not destructure services', () => {
19
+ const state = stateWithFunctions({
20
+ myFunc: {
21
+ pikkuFuncId: 'myFunc',
22
+ inputSchemaName: null,
23
+ outputSchemaName: null,
24
+ sourceFile: '/project/src/my-func.ts',
25
+ services: { optimized: false, services: ['kysely'] },
26
+ },
27
+ })
28
+ computeDiagnostics(state)
29
+ assert.equal(state.diagnostics.length, 1)
30
+ assert.equal(
31
+ state.diagnostics[0].code,
32
+ ErrorCode.SERVICES_NOT_DESTRUCTURED
33
+ )
34
+ })
35
+
36
+ test('does NOT flag a generated .gen.ts function (user cannot edit it)', () => {
37
+ const state = stateWithFunctions({
38
+ cliRaw: {
39
+ pikkuFuncId: 'cliRaw',
40
+ inputSchemaName: null,
41
+ outputSchemaName: null,
42
+ sourceFile: '/project/src/wirings/cli-channel.gen.ts',
43
+ services: { optimized: false, services: ['kysely'] },
44
+ },
45
+ authHandler: {
46
+ pikkuFuncId: 'authHandler',
47
+ inputSchemaName: null,
48
+ outputSchemaName: null,
49
+ sourceFile: '/project/.pikku/auth.gen.ts',
50
+ wires: { optimized: false, wires: ['http'] },
51
+ },
52
+ })
53
+ computeDiagnostics(state)
54
+ assert.equal(state.diagnostics.length, 0)
55
+ })
56
+
57
+ test('does NOT flag a synthetic route bridge with no source file', () => {
58
+ const state = stateWithFunctions({
59
+ 'http:get:/workflow-run/:runId/stream': {
60
+ pikkuFuncId: 'http:get:/workflow-run/:runId/stream',
61
+ inputSchemaName: null,
62
+ outputSchemaName: null,
63
+ services: { optimized: false, services: [] },
64
+ },
65
+ })
66
+ computeDiagnostics(state)
67
+ assert.equal(state.diagnostics.length, 0)
68
+ })
69
+ })
@@ -661,6 +661,14 @@ export function computeDiagnostics(state: InspectorState): void {
661
661
  const diagnostics: InspectorDiagnostic[] = []
662
662
 
663
663
  for (const [id, meta] of Object.entries(state.functions.meta)) {
664
+ // Skip framework-synthesized functions: generated wrappers (auth.gen.ts's
665
+ // opaque authHandler, the cli channel's raw dispatcher) and synthetic route
666
+ // bridges that reference addon functions (id `http:<method>:<route>`, no
667
+ // source file). The user can't edit any of these, so a destructure lint
668
+ // meant to nudge them about their own code must not fail the build over them.
669
+ if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
670
+ continue
671
+ }
664
672
  if (meta.services && !meta.services.optimized) {
665
673
  diagnostics.push({
666
674
  code: ErrorCode.SERVICES_NOT_DESTRUCTURED,