@pikku/inspector 0.12.50 → 0.12.54

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 (116) hide show
  1. package/CHANGELOG.md +743 -0
  2. package/OPTIMIZATION-PLAN.md +9 -3
  3. package/dist/add/add-auth.d.ts +1 -1
  4. package/dist/add/add-auth.js +1 -1
  5. package/dist/add/add-cli.js +7 -0
  6. package/dist/add/add-credential.js +1 -1
  7. package/dist/add/add-functions.js +199 -8
  8. package/dist/add/add-http-route.js +16 -6
  9. package/dist/add/add-middleware.js +46 -3
  10. package/dist/add/add-personas.d.ts +13 -0
  11. package/dist/add/add-personas.js +216 -0
  12. package/dist/add/add-scope.js +1 -1
  13. package/dist/add/add-secret.js +1 -1
  14. package/dist/add/add-system-role.d.ts +2 -0
  15. package/dist/add/add-system-role.js +102 -0
  16. package/dist/add/add-variable.js +1 -1
  17. package/dist/add/add-wire-addon.js +31 -0
  18. package/dist/add/add-workflow.js +12 -1
  19. package/dist/error-codes.d.ts +17 -1
  20. package/dist/error-codes.js +23 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/inspector.js +33 -1
  23. package/dist/types.d.ts +55 -2
  24. package/dist/utils/annotate-http-route-auth.d.ts +22 -0
  25. package/dist/utils/annotate-http-route-auth.js +48 -0
  26. package/dist/utils/check-secret-sinks.d.ts +31 -0
  27. package/dist/utils/check-secret-sinks.js +117 -0
  28. package/dist/utils/extract-function-name.js +13 -4
  29. package/dist/utils/extract-function-node.js +18 -1
  30. package/dist/utils/extract-secret-usage.d.ts +9 -0
  31. package/dist/utils/extract-secret-usage.js +61 -0
  32. package/dist/utils/literal-properties.d.ts +22 -0
  33. package/dist/utils/literal-properties.js +69 -0
  34. package/dist/utils/load-addon-functions-meta.js +26 -1
  35. package/dist/utils/permissions.js +8 -0
  36. package/dist/utils/post-process.d.ts +68 -3
  37. package/dist/utils/post-process.js +246 -13
  38. package/dist/utils/secret-alias-services.d.ts +8 -0
  39. package/dist/utils/secret-alias-services.js +45 -0
  40. package/dist/utils/secret-brokers.d.ts +2 -0
  41. package/dist/utils/secret-brokers.js +9 -0
  42. package/dist/utils/serialize-inspector-state.d.ts +10 -0
  43. package/dist/utils/serialize-inspector-state.js +18 -0
  44. package/dist/utils/serialize-middleware-groups-meta.js +3 -0
  45. package/dist/utils/single-declaration.d.ts +32 -0
  46. package/dist/utils/single-declaration.js +43 -0
  47. package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
  48. package/dist/utils/validate-exposed-functions-gated.js +93 -0
  49. package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
  50. package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
  51. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +6 -0
  52. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +48 -1
  53. package/dist/utils/workflow/dsl/patterns.d.ts +9 -4
  54. package/dist/utils/workflow/dsl/patterns.js +26 -4
  55. package/dist/visit.js +4 -0
  56. package/package.json +3 -3
  57. package/src/add/add-auth.test.ts +5 -3
  58. package/src/add/add-auth.ts +1 -1
  59. package/src/add/add-cli.ts +8 -0
  60. package/src/add/add-credential.ts +1 -1
  61. package/src/add/add-functions.test.ts +263 -1
  62. package/src/add/add-functions.ts +273 -11
  63. package/src/add/add-http-route.ts +20 -7
  64. package/src/add/add-middleware.test.ts +196 -0
  65. package/src/add/add-middleware.ts +64 -4
  66. package/src/add/add-personas.test.ts +382 -0
  67. package/src/add/add-personas.ts +285 -0
  68. package/src/add/add-scenario-step.test.ts +87 -5
  69. package/src/add/add-scope.test.ts +103 -30
  70. package/src/add/add-scope.ts +1 -1
  71. package/src/add/add-secret.ts +1 -1
  72. package/src/add/add-subject-scenario-steps.test.ts +161 -0
  73. package/src/add/add-system-role.test.ts +297 -0
  74. package/src/add/add-system-role.ts +164 -0
  75. package/src/add/add-variable.ts +1 -1
  76. package/src/add/add-wire-addon.test.ts +111 -0
  77. package/src/add/add-wire-addon.ts +29 -0
  78. package/src/add/add-workflow.ts +21 -1
  79. package/src/add/addon-bans.test.ts +4 -4
  80. package/src/add/allow-escape-hatches.test.ts +144 -0
  81. package/src/add/audit-meta.test.ts +75 -0
  82. package/src/add/config-value-metadata.test.ts +11 -11
  83. package/src/add/secret-sink-check.test.ts +211 -0
  84. package/src/add/wire-name-literal.test.ts +2 -2
  85. package/src/error-codes.ts +26 -0
  86. package/src/index.ts +4 -1
  87. package/src/inspector.ts +39 -0
  88. package/src/types.ts +56 -2
  89. package/src/utils/annotate-http-route-auth.test.ts +163 -0
  90. package/src/utils/annotate-http-route-auth.ts +52 -0
  91. package/src/utils/check-secret-sinks.ts +145 -0
  92. package/src/utils/extract-function-name.ts +14 -4
  93. package/src/utils/extract-function-node.ts +18 -6
  94. package/src/utils/extract-secret-usage.test.ts +81 -0
  95. package/src/utils/extract-secret-usage.ts +73 -0
  96. package/src/utils/literal-properties.ts +97 -0
  97. package/src/utils/load-addon-functions-meta.ts +32 -2
  98. package/src/utils/permissions-shorthand.test.ts +79 -0
  99. package/src/utils/permissions.ts +7 -0
  100. package/src/utils/post-process.test.ts +133 -0
  101. package/src/utils/post-process.ts +313 -12
  102. package/src/utils/secret-alias-services.test.ts +146 -0
  103. package/src/utils/secret-alias-services.ts +58 -0
  104. package/src/utils/secret-brokers.test.ts +53 -0
  105. package/src/utils/secret-brokers.ts +11 -0
  106. package/src/utils/serialize-inspector-state.ts +28 -0
  107. package/src/utils/serialize-middleware-groups-meta.ts +3 -0
  108. package/src/utils/single-declaration.ts +57 -0
  109. package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
  110. package/src/utils/validate-exposed-functions-gated.ts +103 -0
  111. package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
  112. package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
  113. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +57 -0
  114. package/src/utils/workflow/dsl/patterns.ts +33 -4
  115. package/src/visit.ts +4 -0
  116. package/tsconfig.tsbuildinfo +1 -1
@@ -272,18 +272,27 @@ export function registerHTTPRoute({
272
272
  }
273
273
  }
274
274
 
275
+ // The route's own `auth`, falling back to the group's. Recorded on the meta
276
+ // below so an audit can answer "is this route open?" without re-reading the
277
+ // source: the route can tighten a sessionless function, and until this was
278
+ // written down that tightening existed only in the wiring.
279
+ const routeAuthProp = getPropertyValue(obj, 'auth')
280
+ const resolvedAuth =
281
+ routeAuthProp === true || routeAuthProp === false
282
+ ? routeAuthProp
283
+ : inheritedAuth
284
+
275
285
  // For inline functions (e.g. oauth2 handlers), propagate sessionless: true
276
286
  // when auth is explicitly disabled at the route or group level — the
277
287
  // developer opted out of auth so no session is required.
278
288
  {
279
289
  const inlineMeta = state.functions.meta[funcName]
280
- if (inlineMeta && inlineMeta.sessionless === undefined) {
281
- const routeAuth = getPropertyValue(obj, 'auth')
282
- const resolvedAuth =
283
- routeAuth === true || routeAuth === false ? routeAuth : inheritedAuth
284
- if (resolvedAuth === false) {
285
- inlineMeta.sessionless = true
286
- }
290
+ if (
291
+ inlineMeta &&
292
+ inlineMeta.sessionless === undefined &&
293
+ resolvedAuth === false
294
+ ) {
295
+ inlineMeta.sessionless = true
287
296
  }
288
297
  }
289
298
 
@@ -416,6 +425,7 @@ export function registerHTTPRoute({
416
425
  route: fullRoute,
417
426
  sourceFile: sourceFile.fileName,
418
427
  method: method as HTTPMethod,
428
+ ...(resolvedAuth !== undefined && { auth: resolvedAuth }),
419
429
  params: params.length > 0 ? params : undefined,
420
430
  query: query.length > 0 ? query : undefined,
421
431
  inputTypes: undefined,
@@ -484,6 +494,9 @@ export function registerHTTPRouteMeta({
484
494
  route: fullRoute,
485
495
  sourceFile: sourceFile.fileName,
486
496
  method: method as HTTPMethod,
497
+ ...((route.auth ?? inheritedAuth) !== undefined && {
498
+ auth: route.auth ?? inheritedAuth,
499
+ }),
487
500
  params: params.length > 0 ? params : undefined,
488
501
  inputTypes: undefined,
489
502
  tags: tags.length > 0 ? tags : undefined,
@@ -0,0 +1,196 @@
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 { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger, MiddlewareGroupMeta } 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
+ /** Every file that registered middleware for one pattern or tag. */
26
+ const sourceFiles = (group: MiddlewareGroupMeta | undefined): string[] =>
27
+ group
28
+ ? [
29
+ group.sourceFile,
30
+ ...(group.additionalRegistrations ?? []).map((r) => r.sourceFile),
31
+ ]
32
+ : []
33
+
34
+ /** Runs the inspector over files written into a throwaway directory. */
35
+ const withProject = async (
36
+ prefix: string,
37
+ files: Record<string, string[]>,
38
+ assertions: (state: Awaited<ReturnType<typeof inspect>>) => void
39
+ ) => {
40
+ const rootDir = await mkdtemp(join(tmpdir(), prefix))
41
+ const paths: string[] = []
42
+ for (const [name, lines] of Object.entries(files)) {
43
+ const path = join(rootDir, name)
44
+ await writeFile(path, lines.join('\n'))
45
+ paths.push(path)
46
+ }
47
+ try {
48
+ assertions(await inspect(makeLogger([]), paths, { rootDir }))
49
+ } finally {
50
+ await rm(rootDir, { recursive: true, force: true })
51
+ }
52
+ }
53
+
54
+ describe('addMiddleware inspector', () => {
55
+ describe('two files registering the same HTTP pattern', () => {
56
+ // The group is keyed by pattern, so the second call used to overwrite the
57
+ // first. Codegen emits imports from what is stored, so the losing file was
58
+ // never imported and its middleware never registered — an app could lose
59
+ // its session bridge to an unrelated '*' group and see nothing go wrong
60
+ // until a request arrived without a session.
61
+ const project = {
62
+ 'session.ts': [
63
+ "import { addHTTPMiddleware, pikkuMiddleware } from '#pikku'",
64
+ 'export const sessionMiddleware = () =>',
65
+ " addHTTPMiddleware('*', [",
66
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
67
+ ' ])',
68
+ ],
69
+ 'logging.ts': [
70
+ "import { addHTTPMiddleware, pikkuMiddleware } from '#pikku'",
71
+ 'export const loggingMiddleware = () =>',
72
+ " addHTTPMiddleware('*', [",
73
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
74
+ ' ])',
75
+ ],
76
+ }
77
+
78
+ test('keeps both registrations', async () => {
79
+ await withProject('pikku-mw-http-collide-', project, (state) => {
80
+ const group = state.http.routeMiddleware.get('*')
81
+ assert.ok(group, "expected a group for '*'")
82
+ assert.equal(
83
+ sourceFiles(group).length,
84
+ 2,
85
+ 'both files must survive, or one middleware silently stops running'
86
+ )
87
+ })
88
+ })
89
+
90
+ test('keeps both export names, so both can be imported', async () => {
91
+ await withProject('pikku-mw-http-names-', project, (state) => {
92
+ const group = state.http.routeMiddleware.get('*')!
93
+ const names = [
94
+ group.exportName,
95
+ ...(group.additionalRegistrations ?? []).map((r) => r.exportName),
96
+ ]
97
+ assert.deepEqual(names.sort(), [
98
+ 'loggingMiddleware',
99
+ 'sessionMiddleware',
100
+ ])
101
+ })
102
+ })
103
+
104
+ test('unions the services both registrations need', async () => {
105
+ await withProject('pikku-mw-http-services-', project, (state) => {
106
+ const group = state.http.routeMiddleware.get('*')!
107
+ assert.equal(
108
+ new Set(group.services.services).size,
109
+ group.services.services.length,
110
+ 'services must not repeat when two groups merge'
111
+ )
112
+ })
113
+ })
114
+ })
115
+
116
+ test('a single registration is unchanged', async () => {
117
+ await withProject(
118
+ 'pikku-mw-http-single-',
119
+ {
120
+ 'session.ts': [
121
+ "import { addHTTPMiddleware, pikkuMiddleware } from '#pikku'",
122
+ 'export const sessionMiddleware = () =>',
123
+ " addHTTPMiddleware('*', [",
124
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
125
+ ' ])',
126
+ ],
127
+ },
128
+ (state) => {
129
+ const group = state.http.routeMiddleware.get('*')!
130
+ assert.equal(group.exportName, 'sessionMiddleware')
131
+ assert.equal(
132
+ group.additionalRegistrations,
133
+ undefined,
134
+ 'nothing to merge means nothing extra to serialize'
135
+ )
136
+ }
137
+ )
138
+ })
139
+
140
+ test('different patterns stay separate groups', async () => {
141
+ await withProject(
142
+ 'pikku-mw-http-distinct-',
143
+ {
144
+ 'session.ts': [
145
+ "import { addHTTPMiddleware, pikkuMiddleware } from '#pikku'",
146
+ 'export const sessionMiddleware = () =>',
147
+ " addHTTPMiddleware('*', [",
148
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
149
+ ' ])',
150
+ ],
151
+ 'api.ts': [
152
+ "import { addHTTPMiddleware, pikkuMiddleware } from '#pikku'",
153
+ 'export const apiMiddleware = () =>',
154
+ " addHTTPMiddleware('/api/*', [",
155
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
156
+ ' ])',
157
+ ],
158
+ },
159
+ (state) => {
160
+ assert.equal(sourceFiles(state.http.routeMiddleware.get('*')).length, 1)
161
+ assert.equal(
162
+ sourceFiles(state.http.routeMiddleware.get('/api/*')).length,
163
+ 1
164
+ )
165
+ }
166
+ )
167
+ })
168
+
169
+ test('two files registering the same tag keep both registrations', async () => {
170
+ await withProject(
171
+ 'pikku-mw-tag-collide-',
172
+ {
173
+ 'session.ts': [
174
+ "import { addTagMiddleware, pikkuMiddleware } from '#pikku'",
175
+ 'export const sessionTag = () =>',
176
+ " addTagMiddleware('api', [",
177
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
178
+ ' ])',
179
+ ],
180
+ 'logging.ts': [
181
+ "import { addTagMiddleware, pikkuMiddleware } from '#pikku'",
182
+ 'export const loggingTag = () =>',
183
+ " addTagMiddleware('api', [",
184
+ ' pikkuMiddleware(async (_s, _w, next) => next()),',
185
+ ' ])',
186
+ ],
187
+ },
188
+ (state) => {
189
+ assert.equal(
190
+ sourceFiles(state.middleware.tagMiddleware.get('api')).length,
191
+ 2
192
+ )
193
+ }
194
+ )
195
+ })
196
+ })
@@ -1,5 +1,9 @@
1
1
  import * as ts from 'typescript'
2
- import type { AddWiring, InspectorState } from '../types.js'
2
+ import type {
3
+ AddWiring,
4
+ InspectorState,
5
+ MiddlewareGroupMeta,
6
+ } from '../types.js'
3
7
  import {
4
8
  extractFunctionName,
5
9
  isNamedExport,
@@ -13,6 +17,62 @@ import { extractMiddlewareRefs } from '../utils/middleware.js'
13
17
  import { getPropertyValue } from '../utils/get-property-value.js'
14
18
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
15
19
 
20
+ /**
21
+ * Record a middleware group, keeping any registration already stored under the
22
+ * same pattern or tag.
23
+ *
24
+ * Several files may add middleware for one pattern — an app's own `'*'` group
25
+ * alongside the generated better-auth session bridge, say — and the runtime
26
+ * composes them. A plain `set` kept only the last, and codegen emits imports
27
+ * from what is stored, so the other file was never imported and its middleware
28
+ * silently stopped running. Merging is the only shape that cannot lose a gate;
29
+ * the first registration stays primary so single-registration groups serialize
30
+ * exactly as before.
31
+ */
32
+ function recordMiddlewareGroup(
33
+ groups: Map<string, MiddlewareGroupMeta>,
34
+ key: string,
35
+ group: MiddlewareGroupMeta
36
+ ): void {
37
+ const existing = groups.get(key)
38
+ if (!existing) {
39
+ groups.set(key, group)
40
+ return
41
+ }
42
+
43
+ // The same call seen twice — the inspector may revisit a file — is not a
44
+ // second registration, and appending it would emit a duplicate import.
45
+ const alreadyRecorded = [
46
+ existing,
47
+ ...(existing.additionalRegistrations ?? []),
48
+ ]
49
+ if (
50
+ alreadyRecorded.some(
51
+ (r) => r.sourceFile === group.sourceFile && r.position === group.position
52
+ )
53
+ ) {
54
+ return
55
+ }
56
+
57
+ existing.additionalRegistrations = [
58
+ ...(existing.additionalRegistrations ?? []),
59
+ {
60
+ exportName: group.exportName,
61
+ sourceFile: group.sourceFile,
62
+ position: group.position,
63
+ isFactory: group.isFactory,
64
+ },
65
+ ]
66
+ existing.count += group.count
67
+ existing.instanceIds = [...existing.instanceIds, ...group.instanceIds]
68
+ existing.services = {
69
+ optimized: false,
70
+ services: Array.from(
71
+ new Set([...existing.services.services, ...group.services.services])
72
+ ),
73
+ }
74
+ }
75
+
16
76
  function renameTempDefinitions(
17
77
  state: InspectorState,
18
78
  definitionIds: string[],
@@ -333,7 +393,7 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
333
393
  )
334
394
  }
335
395
 
336
- state.middleware.tagMiddleware.set(tag, {
396
+ recordMiddlewareGroup(state.middleware.tagMiddleware, tag, {
337
397
  exportName: exportedName,
338
398
  sourceFile,
339
399
  position: node.getStart(),
@@ -477,7 +537,7 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
477
537
  )
478
538
  }
479
539
 
480
- state.http.routeMiddleware.set(pattern, {
540
+ recordMiddlewareGroup(state.http.routeMiddleware, pattern, {
481
541
  exportName: exportedName,
482
542
  sourceFile,
483
543
  position: node.getStart(),
@@ -796,7 +856,7 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
796
856
  )
797
857
  }
798
858
 
799
- state.channelMiddleware.tagMiddleware.set(tag, {
859
+ recordMiddlewareGroup(state.channelMiddleware.tagMiddleware, tag, {
800
860
  exportName: exportedName,
801
861
  sourceFile,
802
862
  position: node.getStart(),