@pikku/inspector 0.12.13 → 0.12.16

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 (54) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/add/add-ai-agent.js +1 -1
  3. package/dist/add/add-auth.d.ts +2 -0
  4. package/dist/add/add-auth.js +34 -0
  5. package/dist/add/add-channel.js +25 -7
  6. package/dist/add/add-functions.js +28 -13
  7. package/dist/add/add-gateway.js +1 -1
  8. package/dist/add/add-http-route.js +23 -1
  9. package/dist/add/add-mcp-prompt.js +1 -1
  10. package/dist/add/add-mcp-resource.js +1 -1
  11. package/dist/add/add-queue-worker.js +1 -1
  12. package/dist/add/add-schedule.js +1 -1
  13. package/dist/add/add-trigger.js +1 -1
  14. package/dist/add/add-workflow.js +1 -1
  15. package/dist/inspector.js +4 -0
  16. package/dist/types.d.ts +4 -0
  17. package/dist/utils/check-pii-output.d.ts +13 -8
  18. package/dist/utils/check-pii-output.js +22 -12
  19. package/dist/utils/ensure-function-metadata.js +1 -1
  20. package/dist/utils/extract-node-value.d.ts +1 -1
  21. package/dist/utils/extract-node-value.js +10 -1
  22. package/dist/utils/get-property-value.d.ts +1 -1
  23. package/dist/utils/get-property-value.js +35 -9
  24. package/dist/utils/serialize-inspector-state.d.ts +4 -0
  25. package/dist/utils/serialize-inspector-state.js +8 -0
  26. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +20 -9
  27. package/dist/visit.js +2 -0
  28. package/package.json +2 -2
  29. package/src/add/add-ai-agent.ts +1 -1
  30. package/src/add/add-auth.test.ts +175 -0
  31. package/src/add/add-auth.ts +49 -0
  32. package/src/add/add-channel.ts +37 -7
  33. package/src/add/add-functions.ts +44 -13
  34. package/src/add/add-gateway.ts +1 -1
  35. package/src/add/add-http-route.ts +26 -1
  36. package/src/add/add-mcp-prompt.ts +1 -1
  37. package/src/add/add-mcp-resource.ts +1 -1
  38. package/src/add/add-queue-worker.ts +1 -1
  39. package/src/add/add-schedule.ts +1 -1
  40. package/src/add/add-trigger.ts +1 -1
  41. package/src/add/add-workflow.test.ts +152 -0
  42. package/src/add/add-workflow.ts +2 -1
  43. package/src/add/pii-check.test.ts +79 -32
  44. package/src/inspector.ts +4 -0
  45. package/src/types.ts +4 -0
  46. package/src/utils/check-pii-output.ts +41 -19
  47. package/src/utils/ensure-function-metadata.ts +3 -1
  48. package/src/utils/extract-node-value.test.ts +12 -10
  49. package/src/utils/extract-node-value.ts +15 -1
  50. package/src/utils/get-property-value.ts +33 -13
  51. package/src/utils/serialize-inspector-state.ts +12 -0
  52. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +22 -9
  53. package/src/visit.ts +2 -0
  54. package/tsconfig.tsbuildinfo +1 -1
@@ -98,6 +98,10 @@ export function serializeInspectorState(state) {
98
98
  meta: state.nodes.meta,
99
99
  files: Array.from(state.nodes.files),
100
100
  },
101
+ auth: {
102
+ providers: state.auth.providers,
103
+ files: Array.from(state.auth.files),
104
+ },
101
105
  secrets: {
102
106
  definitions: state.secrets.definitions,
103
107
  files: Array.from(state.secrets.files),
@@ -246,6 +250,10 @@ export function deserializeInspectorState(data) {
246
250
  meta: data.nodes?.meta || {},
247
251
  files: new Set(data.nodes?.files || []),
248
252
  },
253
+ auth: {
254
+ providers: data.auth?.providers || [],
255
+ files: new Set(data.auth?.files || []),
256
+ },
249
257
  secrets: {
250
258
  definitions: data.secrets?.definitions || [],
251
259
  files: new Set(data.secrets?.files || []),
@@ -293,21 +293,32 @@ function extractExpressionStatement(statement, context) {
293
293
  if (ts.isIdentifier(expr.left)) {
294
294
  outputVar = expr.left.text;
295
295
  // Check if this is an assignment to a context variable (set step)
296
+ // But if the RHS is a workflow.do() call, fall through to RPC extraction —
297
+ // reassigning a pre-declared variable with a workflow step is valid and common.
296
298
  if (context.contextVars.has(outputVar)) {
297
- const literalValue = extractLiteralValue(expr.right);
298
- if (literalValue !== undefined) {
299
+ const rhs = expr.right;
300
+ const rhsCall = ts.isAwaitExpression(rhs) && ts.isCallExpression(rhs.expression)
301
+ ? rhs.expression
302
+ : null;
303
+ const isWorkflowCall = rhsCall
304
+ ? isWorkflowDoCall(rhsCall, context.checker)
305
+ : false;
306
+ if (!isWorkflowCall) {
307
+ const literalValue = extractLiteralValue(expr.right);
308
+ if (literalValue !== undefined) {
309
+ return {
310
+ type: 'set',
311
+ variable: outputVar,
312
+ value: literalValue,
313
+ };
314
+ }
315
+ // Non-literal assignment to context var - use expression as string
299
316
  return {
300
317
  type: 'set',
301
318
  variable: outputVar,
302
- value: literalValue,
319
+ value: getSourceText(expr.right),
303
320
  };
304
321
  }
305
- // Non-literal assignment to context var - use expression as string
306
- return {
307
- type: 'set',
308
- variable: outputVar,
309
- value: getSourceText(expr.right),
310
- };
311
322
  }
312
323
  }
313
324
  // Use right side as the expression to extract from
package/dist/visit.js CHANGED
@@ -17,6 +17,7 @@ import { addWireAddon } from './add/add-wire-addon.js';
17
17
  import { addMiddleware } from './add/add-middleware.js';
18
18
  import { addPermission } from './add/add-permission.js';
19
19
  import { addCLI, addCLIRenderers } from './add/add-cli.js';
20
+ import { addAuth } from './add/add-auth.js';
20
21
  import { addSecret } from './add/add-secret.js';
21
22
  import { addCredential } from './add/add-credential.js';
22
23
  import { addVariable } from './add/add-variable.js';
@@ -41,6 +42,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
41
42
  };
42
43
  export const visitRoutes = (logger, checker, node, state, options) => {
43
44
  addFunctions(logger, node, checker, state, options);
45
+ addAuth(logger, node, checker, state, options);
44
46
  addSecret(logger, node, checker, state, options);
45
47
  addCredential(logger, node, checker, state, options);
46
48
  addVariable(logger, node, checker, state, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.13",
3
+ "version": "0.12.16",
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.25",
38
+ "@pikku/core": "^0.12.27",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -252,7 +252,7 @@ export const addAIAgent: AddWiring = (
252
252
 
253
253
  const nameValue = getPropertyValue(obj, 'name') as string | null
254
254
  const { disabled, tags, summary, description, errors } =
255
- getCommonWireMetaData(obj, 'AI agent', nameValue, logger)
255
+ getCommonWireMetaData(obj, 'AI agent', nameValue, logger, checker)
256
256
 
257
257
  if (disabled) return
258
258
 
@@ -0,0 +1,175 @@
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
+ critical: (code: ErrorCode, message: string) => {
17
+ criticals.push({ code, message })
18
+ },
19
+ hasCriticalErrors: () => criticals.length > 0,
20
+ }) satisfies InspectorLogger
21
+
22
+ describe('addAuth inspector', () => {
23
+ test('extracts provider string literals from wireAuth call', async () => {
24
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-'))
25
+ const file = join(rootDir, 'auth.ts')
26
+
27
+ await writeFile(
28
+ file,
29
+ [
30
+ "import { wireAuth } from '@pikku/auth-js'",
31
+ "wireAuth({ providers: ['github', 'google'] })",
32
+ ].join('\n')
33
+ )
34
+
35
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
36
+ try {
37
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
38
+ assert.equal(criticals.length, 0)
39
+ assert.deepEqual(state.auth.providers, ['github', 'google'])
40
+ } finally {
41
+ await rm(rootDir, { recursive: true, force: true })
42
+ }
43
+ })
44
+
45
+ test('deduplicates providers across multiple wireAuth calls', async () => {
46
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-dedup-'))
47
+ const file = join(rootDir, 'auth.ts')
48
+
49
+ await writeFile(
50
+ file,
51
+ [
52
+ "import { wireAuth } from '@pikku/auth-js'",
53
+ "wireAuth({ providers: ['github'] })",
54
+ "wireAuth({ providers: ['github', 'google'] })",
55
+ ].join('\n')
56
+ )
57
+
58
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
59
+ try {
60
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
61
+ assert.equal(criticals.length, 0)
62
+ assert.deepEqual(state.auth.providers, ['github', 'google'])
63
+ } finally {
64
+ await rm(rootDir, { recursive: true, force: true })
65
+ }
66
+ })
67
+
68
+ test('logs critical error when a provider is a non-literal reference', async () => {
69
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-nonlit-'))
70
+ const file = join(rootDir, 'auth.ts')
71
+
72
+ await writeFile(
73
+ file,
74
+ [
75
+ "import { wireAuth } from '@pikku/auth-js'",
76
+ "const PROVIDER = 'github'",
77
+ 'wireAuth({ providers: [PROVIDER] })',
78
+ ].join('\n')
79
+ )
80
+
81
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
82
+ try {
83
+ await inspect(makeLogger(criticals), [file], { rootDir })
84
+ const hit = criticals.find(
85
+ (e) => e.code === ErrorCode.NON_LITERAL_WIRE_NAME
86
+ )
87
+ assert.ok(hit, 'expected NON_LITERAL_WIRE_NAME critical')
88
+ assert.match(hit!.message, /PROVIDER/)
89
+ } finally {
90
+ await rm(rootDir, { recursive: true, force: true })
91
+ }
92
+ })
93
+
94
+ test('does not error when providers is absent (credentials-only wireAuth)', async () => {
95
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-creds-only-'))
96
+ const file = join(rootDir, 'auth.ts')
97
+
98
+ await writeFile(
99
+ file,
100
+ [
101
+ "import { wireAuth } from '@pikku/auth-js'",
102
+ 'wireAuth({ credentials: { authorize: async () => null } })',
103
+ ].join('\n')
104
+ )
105
+
106
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
107
+ try {
108
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
109
+ assert.equal(
110
+ criticals.length,
111
+ 0,
112
+ 'credentials-only wireAuth must not produce errors'
113
+ )
114
+ assert.deepEqual(
115
+ state.auth.providers,
116
+ [],
117
+ 'no providers should be extracted'
118
+ )
119
+ assert.ok(state.auth.files.has(file), 'source file still tracked')
120
+ } finally {
121
+ await rm(rootDir, { recursive: true, force: true })
122
+ }
123
+ })
124
+
125
+ test('logs critical error when providers is not an array literal', async () => {
126
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-nonarray-'))
127
+ const file = join(rootDir, 'auth.ts')
128
+
129
+ await writeFile(
130
+ file,
131
+ [
132
+ "import { wireAuth } from '@pikku/auth-js'",
133
+ "const PROVIDERS = ['github']",
134
+ 'wireAuth({ providers: PROVIDERS })',
135
+ ].join('\n')
136
+ )
137
+
138
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
139
+ try {
140
+ await inspect(makeLogger(criticals), [file], { rootDir })
141
+ const hit = criticals.find((e) => e.code === ErrorCode.MISSING_NAME)
142
+ assert.ok(
143
+ hit,
144
+ 'expected MISSING_NAME critical for non-array-literal providers'
145
+ )
146
+ } finally {
147
+ await rm(rootDir, { recursive: true, force: true })
148
+ }
149
+ })
150
+
151
+ test('tracks source file in state.auth.files', async () => {
152
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-files-'))
153
+ const file = join(rootDir, 'auth.wiring.ts')
154
+
155
+ await writeFile(
156
+ file,
157
+ [
158
+ "import { wireAuth } from '@pikku/auth-js'",
159
+ "wireAuth({ providers: ['discord'] })",
160
+ ].join('\n')
161
+ )
162
+
163
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
164
+ try {
165
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
166
+ assert.equal(criticals.length, 0)
167
+ assert.ok(
168
+ state.auth.files.has(file),
169
+ 'source file should be tracked in state.auth.files'
170
+ )
171
+ } finally {
172
+ await rm(rootDir, { recursive: true, force: true })
173
+ }
174
+ })
175
+ })
@@ -0,0 +1,49 @@
1
+ import * as ts from 'typescript'
2
+ import type { AddWiring } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+
5
+ export const addAuth: AddWiring = (logger, node, _checker, state) => {
6
+ if (!ts.isCallExpression(node)) return
7
+
8
+ const expression = node.expression
9
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireAuth') return
10
+
11
+ const firstArg = node.arguments[0]
12
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
13
+
14
+ const providersProp = firstArg.properties.find(
15
+ (p) =>
16
+ ts.isPropertyAssignment(p) &&
17
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
18
+ p.name.text === 'providers'
19
+ ) as ts.PropertyAssignment | undefined
20
+
21
+ const sourceFile = node.getSourceFile().fileName
22
+ state.auth.files.add(sourceFile)
23
+
24
+ if (!providersProp) {
25
+ return
26
+ }
27
+
28
+ if (!ts.isArrayLiteralExpression(providersProp.initializer)) {
29
+ logger.critical(
30
+ ErrorCode.MISSING_NAME,
31
+ 'wireAuth: providers must be an array literal of string literals.'
32
+ )
33
+ return
34
+ }
35
+
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
44
+ }
45
+ if (!state.auth.providers.includes(element.text)) {
46
+ state.auth.providers.push(element.text)
47
+ }
48
+ }
49
+ }
@@ -238,7 +238,8 @@ export function addMessagesRoutes(
238
238
  init,
239
239
  'Channel message',
240
240
  routeKey,
241
- logger
241
+ logger,
242
+ checker
242
243
  ).tags
243
244
  : undefined
244
245
  const routeMiddleware = ts.isObjectLiteralExpression(init)
@@ -270,7 +271,8 @@ export function addMessagesRoutes(
270
271
  init,
271
272
  'Channel message',
272
273
  routeKey,
273
- logger
274
+ logger,
275
+ checker
274
276
  ).tags
275
277
  : undefined
276
278
  const routeMiddleware = ts.isObjectLiteralExpression(init)
@@ -319,7 +321,8 @@ export function addMessagesRoutes(
319
321
  init,
320
322
  'Channel message',
321
323
  routeKey,
322
- logger
324
+ logger,
325
+ checker
323
326
  ).tags
324
327
  : undefined
325
328
  const routeMiddleware = ts.isObjectLiteralExpression(
@@ -350,7 +353,8 @@ export function addMessagesRoutes(
350
353
  init,
351
354
  'Channel message',
352
355
  routeKey,
353
- logger
356
+ logger,
357
+ checker
354
358
  ).tags
355
359
  : undefined
356
360
  const routeMiddleware = ts.isObjectLiteralExpression(
@@ -438,7 +442,8 @@ export function addMessagesRoutes(
438
442
  init,
439
443
  'Channel message',
440
444
  routeKey,
441
- logger
445
+ logger,
446
+ checker
442
447
  ).tags
443
448
  : undefined
444
449
  const routeMiddleware = ts.isObjectLiteralExpression(init)
@@ -481,7 +486,13 @@ export function addMessagesRoutes(
481
486
  // Resolve middleware and permissions for this route
482
487
  // Check if the route config is an object literal with middleware/permissions
483
488
  const routeTags = ts.isObjectLiteralExpression(init)
484
- ? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
489
+ ? getCommonWireMetaData(
490
+ init,
491
+ 'Channel message',
492
+ routeKey,
493
+ logger,
494
+ checker
495
+ ).tags
485
496
  : undefined
486
497
  const routeMiddleware = ts.isObjectLiteralExpression(init)
487
498
  ? resolveMiddleware(state, init, routeTags, checker)
@@ -540,7 +551,7 @@ export const addChannel: AddWiring = (
540
551
  : []
541
552
 
542
553
  const { disabled, tags, summary, description, errors } =
543
- getCommonWireMetaData(obj, 'Channel', name, logger)
554
+ getCommonWireMetaData(obj, 'Channel', name, logger, checker)
544
555
 
545
556
  if (disabled) return
546
557
 
@@ -630,6 +641,25 @@ export const addChannel: AddWiring = (
630
641
  state.serviceAggregation.usedFunctions.add(disconnectFuncId)
631
642
  }
632
643
 
644
+ // Synthesize function meta for connect/disconnect handlers that are defined
645
+ // with pikkuChannelConnectionFunc/pikkuChannelDisconnectionFunc (not pikkuFunc/
646
+ // pikkuSessionlessFunc), so the runtime has correct sessionless info without
647
+ // needing to inject it at runtime.
648
+ {
649
+ const routeAuth = getPropertyValue(obj, 'auth')
650
+ const sessionless = routeAuth === false ? true : undefined
651
+ for (const funcId of [connectFuncId, disconnectFuncId]) {
652
+ if (funcId && !state.functions.meta[funcId]) {
653
+ state.functions.meta[funcId] = {
654
+ pikkuFuncId: funcId,
655
+ sessionless,
656
+ inputSchemaName: null,
657
+ outputSchemaName: null,
658
+ }
659
+ }
660
+ }
661
+ }
662
+
633
663
  if (message) {
634
664
  state.serviceAggregation.usedFunctions.add(message.pikkuFuncId)
635
665
  }
@@ -462,7 +462,13 @@ export const addFunctions: AddWiring = (
462
462
  // Extract config properties if using object form
463
463
  if (ts.isObjectLiteralExpression(firstArg)) {
464
464
  objectNode = firstArg
465
- const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger)
465
+ const metadata = getCommonWireMetaData(
466
+ firstArg,
467
+ 'Function',
468
+ name,
469
+ logger,
470
+ checker
471
+ )
466
472
  if (metadata.disabled) return
467
473
  title = metadata.title
468
474
  tags = metadata.tags
@@ -883,24 +889,50 @@ export const addFunctions: AddWiring = (
883
889
  }
884
890
  }
885
891
 
886
- // ── PII brand check ───────────────────────────────────────────────────────
887
- // Walk the function body's ACTUAL inferred return type looking for Private<T>
888
- // / Secret<T> brands (__pii__ property). This runs for every function,
889
- // including those with a Zod output schema, because the TS return type
890
- // reflects what the body actually returns before any Zod coercion.
892
+ const sessionless = expression.text !== 'pikkuFunc'
893
+
894
+ // ── Classification brand check ─────────────────────────────────────────────
895
+ // Walk the function body's ACTUAL inferred return type looking for classification
896
+ // brands (__classification__ property on Private<T>, Pii<T>, Secret<T>).
897
+ //
898
+ // Semantics:
899
+ // secret → never returned by any exposed function (sessioned or not)
900
+ // private → only visible to authenticated (sessioned) users; ok for pikkuFunc
901
+ // public → safe for sessionless functions
891
902
  {
892
903
  const sig = checker.getSignatureFromDeclaration(handler)
893
904
  if (sig) {
894
905
  const rawRet = checker.getReturnTypeOfSignature(sig)
895
906
  const unwrapped = unwrapPromise(checker, rawRet)
896
- const piiPaths = findPiiPaths(checker, unwrapped)
897
- if (piiPaths.length > 0) {
907
+ const classifiedFields = findPiiPaths(checker, unwrapped)
908
+
909
+ const secretPaths = classifiedFields
910
+ .filter((f) => f.classification === 'secret')
911
+ .map((f) => f.path)
912
+
913
+ const privatePaths = classifiedFields
914
+ .filter(
915
+ (f) => f.classification === 'private' || f.classification === 'pii'
916
+ )
917
+ .map((f) => f.path)
918
+
919
+ if (secretPaths.length > 0) {
920
+ logger.critical(
921
+ ErrorCode.PII_IN_OUTPUT,
922
+ `Function '${name}' exposes secret-classified field(s) in its return type: ` +
923
+ secretPaths.map((p) => `'${p}'`).join(', ') +
924
+ `.\n Secret fields must never appear in function output. ` +
925
+ `Strip these fields before returning or change the column classification.`
926
+ )
927
+ }
928
+
929
+ if (sessionless && privatePaths.length > 0) {
898
930
  logger.critical(
899
931
  ErrorCode.PII_IN_OUTPUT,
900
- `Function '${name}' exposes PII-classified field(s) in its return type: ` +
901
- piiPaths.map((p) => `'${p}'`).join(', ') +
902
- `.\n Either strip these fields before returning or mark the column ` +
903
- `@public in the migration if it is safe to expose.`
932
+ `Sessionless function '${name}' exposes private-classified field(s) in its return type: ` +
933
+ privatePaths.map((p) => `'${p}'`).join(', ') +
934
+ `.\n Private fields are only safe to return from authenticated (sessioned) functions. ` +
935
+ `Either require a session (use pikkuFunc) or mark the column @public if it is safe to expose publicly.`
904
936
  )
905
937
  }
906
938
  }
@@ -946,7 +978,6 @@ export const addFunctions: AddWiring = (
946
978
  }
947
979
  }
948
980
 
949
- const sessionless = expression.text !== 'pikkuFunc'
950
981
  const implementationHash = computeImplementationHash({
951
982
  wrapper: expression.text,
952
983
  handler,
@@ -45,7 +45,7 @@ export const addGateway: AddWiring = (
45
45
  const typeValue = getPropertyValue(obj, 'type') as GatewayTransportType | null
46
46
  const routeValue = getPropertyValue(obj, 'route') as string | undefined
47
47
  const { disabled, tags, summary, description, errors } =
48
- getCommonWireMetaData(obj, 'Gateway', nameValue, logger)
48
+ getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker)
49
49
 
50
50
  if (disabled) return
51
51
 
@@ -175,7 +175,7 @@ export function registerHTTPRoute({
175
175
  summary,
176
176
  description,
177
177
  errors,
178
- } = getCommonWireMetaData(obj, 'HTTP route', fullRoute, logger)
178
+ } = getCommonWireMetaData(obj, 'HTTP route', fullRoute, logger, checker)
179
179
 
180
180
  if (disabled) return
181
181
 
@@ -248,6 +248,31 @@ export function registerHTTPRoute({
248
248
  extracted.isHelper
249
249
  )
250
250
 
251
+ // Propagate sessionless from the addon target so the runtime can correctly
252
+ // determine whether a session is required for ref()-based routes.
253
+ if (refAddonTarget) {
254
+ const targetMeta = resolveFunctionMeta(state, refAddonTarget)
255
+ const inlineMeta = state.functions.meta[funcName]
256
+ if (targetMeta && inlineMeta && targetMeta.sessionless !== undefined) {
257
+ inlineMeta.sessionless = targetMeta.sessionless
258
+ }
259
+ }
260
+
261
+ // For inline functions (e.g. oauth2 handlers), propagate sessionless: true
262
+ // when auth is explicitly disabled at the route or group level — the
263
+ // developer opted out of auth so no session is required.
264
+ {
265
+ const inlineMeta = state.functions.meta[funcName]
266
+ if (inlineMeta && inlineMeta.sessionless === undefined) {
267
+ const routeAuth = getPropertyValue(obj, 'auth')
268
+ const resolvedAuth =
269
+ routeAuth === true || routeAuth === false ? routeAuth : inheritedAuth
270
+ if (resolvedAuth === false) {
271
+ inlineMeta.sessionless = true
272
+ }
273
+ }
274
+ }
275
+
251
276
  // Lookup existing function metadata
252
277
  const fnMeta = resolveFunctionMeta(state, funcName)
253
278
  if (!fnMeta) {
@@ -45,7 +45,7 @@ export const addMCPPrompt: AddWiring = (
45
45
 
46
46
  const nameValue = getPropertyValue(obj, 'name') as string | null
47
47
  const { disabled, tags, summary, description, errors } =
48
- getCommonWireMetaData(obj, 'MCP prompt', nameValue, logger)
48
+ getCommonWireMetaData(obj, 'MCP prompt', nameValue, logger, checker)
49
49
 
50
50
  if (disabled) return
51
51
 
@@ -46,7 +46,7 @@ export const addMCPResource: AddWiring = (
46
46
  const uriValue = getPropertyValue(obj, 'uri') as string | null
47
47
  const titleValue = getPropertyValue(obj, 'title') as string | null
48
48
  const { disabled, tags, summary, description, errors } =
49
- getCommonWireMetaData(obj, 'MCP resource', uriValue, logger)
49
+ getCommonWireMetaData(obj, 'MCP resource', uriValue, logger, checker)
50
50
 
51
51
  if (disabled) return
52
52
 
@@ -37,7 +37,7 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
37
37
 
38
38
  const name = getPropertyValue(obj, 'name') as string | null
39
39
  const { disabled, tags, summary, description, errors } =
40
- getCommonWireMetaData(obj, 'Queue worker', name, logger)
40
+ getCommonWireMetaData(obj, 'Queue worker', name, logger, checker)
41
41
 
42
42
  if (disabled) return
43
43
 
@@ -44,7 +44,7 @@ export const addSchedule: AddWiring = (
44
44
  const nameValue = getPropertyValue(obj, 'name') as string | null
45
45
  const scheduleValue = getPropertyValue(obj, 'schedule') as string | null
46
46
  const { disabled, tags, summary, description, errors } =
47
- getCommonWireMetaData(obj, 'Scheduler', nameValue, logger)
47
+ getCommonWireMetaData(obj, 'Scheduler', nameValue, logger, checker)
48
48
 
49
49
  if (disabled) return
50
50
 
@@ -55,7 +55,7 @@ const addWireTrigger: (
55
55
 
56
56
  const nameValue = getPropertyValue(obj, 'name') as string | null
57
57
  const { disabled, tags, summary, description, errors } =
58
- getCommonWireMetaData(obj, 'Trigger', nameValue, logger)
58
+ getCommonWireMetaData(obj, 'Trigger', nameValue, logger, checker)
59
59
 
60
60
  if (disabled) return
61
61