@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.
- package/CHANGELOG.md +48 -0
- package/dist/add/add-ai-agent.js +1 -1
- package/dist/add/add-auth.d.ts +2 -0
- package/dist/add/add-auth.js +34 -0
- package/dist/add/add-channel.js +25 -7
- package/dist/add/add-functions.js +28 -13
- package/dist/add/add-gateway.js +1 -1
- package/dist/add/add-http-route.js +23 -1
- package/dist/add/add-mcp-prompt.js +1 -1
- package/dist/add/add-mcp-resource.js +1 -1
- package/dist/add/add-queue-worker.js +1 -1
- package/dist/add/add-schedule.js +1 -1
- package/dist/add/add-trigger.js +1 -1
- package/dist/add/add-workflow.js +1 -1
- package/dist/inspector.js +4 -0
- package/dist/types.d.ts +4 -0
- package/dist/utils/check-pii-output.d.ts +13 -8
- package/dist/utils/check-pii-output.js +22 -12
- package/dist/utils/ensure-function-metadata.js +1 -1
- package/dist/utils/extract-node-value.d.ts +1 -1
- package/dist/utils/extract-node-value.js +10 -1
- package/dist/utils/get-property-value.d.ts +1 -1
- package/dist/utils/get-property-value.js +35 -9
- package/dist/utils/serialize-inspector-state.d.ts +4 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +20 -9
- package/dist/visit.js +2 -0
- package/package.json +2 -2
- package/src/add/add-ai-agent.ts +1 -1
- package/src/add/add-auth.test.ts +175 -0
- package/src/add/add-auth.ts +49 -0
- package/src/add/add-channel.ts +37 -7
- package/src/add/add-functions.ts +44 -13
- package/src/add/add-gateway.ts +1 -1
- package/src/add/add-http-route.ts +26 -1
- package/src/add/add-mcp-prompt.ts +1 -1
- package/src/add/add-mcp-resource.ts +1 -1
- package/src/add/add-queue-worker.ts +1 -1
- package/src/add/add-schedule.ts +1 -1
- package/src/add/add-trigger.ts +1 -1
- package/src/add/add-workflow.test.ts +152 -0
- package/src/add/add-workflow.ts +2 -1
- package/src/add/pii-check.test.ts +79 -32
- package/src/inspector.ts +4 -0
- package/src/types.ts +4 -0
- package/src/utils/check-pii-output.ts +41 -19
- package/src/utils/ensure-function-metadata.ts +3 -1
- package/src/utils/extract-node-value.test.ts +12 -10
- package/src/utils/extract-node-value.ts +15 -1
- package/src/utils/get-property-value.ts +33 -13
- package/src/utils/serialize-inspector-state.ts +12 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +22 -9
- package/src/visit.ts +2 -0
- 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
|
|
298
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|
package/src/add/add-ai-agent.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/add/add-channel.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/src/add/add-functions.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
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
|
|
897
|
-
|
|
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
|
-
`
|
|
901
|
-
|
|
902
|
-
`.\n
|
|
903
|
-
|
|
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,
|
package/src/add/add-gateway.ts
CHANGED
|
@@ -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
|
|
package/src/add/add-schedule.ts
CHANGED
|
@@ -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
|
|
package/src/add/add-trigger.ts
CHANGED
|
@@ -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
|
|