@pikku/inspector 0.12.14 → 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 +22 -0
- package/dist/add/add-ai-agent.js +1 -1
- 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/utils/check-pii-output.d.ts +9 -4
- package/dist/utils/check-pii-output.js +17 -7
- 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/workflow/dsl/extract-dsl-workflow.js +20 -9
- package/package.json +1 -1
- package/src/add/add-ai-agent.ts +1 -1
- 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 +70 -28
- package/src/utils/check-pii-output.ts +27 -11
- 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/workflow/dsl/extract-dsl-workflow.ts +22 -9
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 0.12.16
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 646c5a8: Fix inspector failing to extract descriptions written as string concatenation (`+`). Descriptions like `'line one ' + 'line two'` are now correctly resolved to their full value. The `checker` parameter is also threaded through `getCommonWireMetaData` so all wiring types benefit from static string evaluation.
|
|
6
|
+
|
|
7
|
+
## 0.12.15
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 0db854e: Fix workflow DSL extractor treating `x = await workflow.do(...)` as a set-step when `x` was previously declared as `null`. The referenced function is now correctly registered in `invokedFunctions` and `internalFiles`, so it appears in the generated `pikku-functions.gen.ts`.
|
|
12
|
+
- 8249f6f: Fix `isStringLike` to unwrap type assertion expressions (`as T` / `<T>expr`) so that `workflow.do('step', 'rpcName' as any, data)` is correctly parsed as an RPC step rather than silently dropped as an inline step. Also removes the `as any` cast from the `Emails` step in `all.workflow.ts` now that the inspector handles it, and ensures `pikku all` generates email template artifacts.
|
|
13
|
+
- f373a87: Fix PKU910 classification semantics and Postgres annotation propagation.
|
|
14
|
+
|
|
15
|
+
**Inspector (`@pikku/inspector`):**
|
|
16
|
+
- `findPiiPaths()` now returns `ClassifiedField[]` (path + classification level) so `private`/`pii` and `secret` brands are distinguished
|
|
17
|
+
- `Secret<T>` fields are blocked in the output of all exposed functions (sessioned or not)
|
|
18
|
+
- `Private<T>` / `Pii<T>` fields are only blocked in sessionless functions — authenticated (sessioned) functions may return private-classified data to their callers
|
|
19
|
+
|
|
20
|
+
**CLI (`@pikku/cli`):**
|
|
21
|
+
- Fix missing `rootDir` in the Postgres `generateSchemaTypes` call — the annotations sidecar file (`db/annotations.gen.json`) was silently ignored during Postgres migrations, causing columns annotated `@public` to remain branded as `Private<T>` in the generated schema
|
|
22
|
+
|
|
1
23
|
## 0.12.14
|
|
2
24
|
|
|
3
25
|
### Patch Changes
|
package/dist/add/add-ai-agent.js
CHANGED
|
@@ -159,7 +159,7 @@ export const addAIAgent = (logger, node, checker, state, options) => {
|
|
|
159
159
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
160
160
|
const obj = firstArg;
|
|
161
161
|
const nameValue = getPropertyValue(obj, 'name');
|
|
162
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'AI agent', nameValue, logger);
|
|
162
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'AI agent', nameValue, logger, checker);
|
|
163
163
|
if (disabled)
|
|
164
164
|
return;
|
|
165
165
|
const modelValue = getPropertyValue(obj, 'model');
|
package/dist/add/add-channel.js
CHANGED
|
@@ -165,7 +165,7 @@ export function addMessagesRoutes(logger, obj, state, checker) {
|
|
|
165
165
|
if (fnMeta) {
|
|
166
166
|
// Resolve middleware for this route
|
|
167
167
|
const routeTags = ts.isObjectLiteralExpression(init)
|
|
168
|
-
? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
|
|
168
|
+
? getCommonWireMetaData(init, 'Channel message', routeKey, logger, checker).tags
|
|
169
169
|
: undefined;
|
|
170
170
|
const routeMiddleware = ts.isObjectLiteralExpression(init)
|
|
171
171
|
? resolveMiddleware(state, init, routeTags, checker)
|
|
@@ -187,7 +187,7 @@ export function addMessagesRoutes(logger, obj, state, checker) {
|
|
|
187
187
|
if (fnMeta) {
|
|
188
188
|
// Resolve middleware for this route
|
|
189
189
|
const routeTags = ts.isObjectLiteralExpression(init)
|
|
190
|
-
? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
|
|
190
|
+
? getCommonWireMetaData(init, 'Channel message', routeKey, logger, checker).tags
|
|
191
191
|
: undefined;
|
|
192
192
|
const routeMiddleware = ts.isObjectLiteralExpression(init)
|
|
193
193
|
? resolveMiddleware(state, init, routeTags, checker)
|
|
@@ -220,7 +220,7 @@ export function addMessagesRoutes(logger, obj, state, checker) {
|
|
|
220
220
|
if (fnMeta) {
|
|
221
221
|
// Resolve middleware for this route
|
|
222
222
|
const routeTags = ts.isObjectLiteralExpression(init)
|
|
223
|
-
? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
|
|
223
|
+
? getCommonWireMetaData(init, 'Channel message', routeKey, logger, checker).tags
|
|
224
224
|
: undefined;
|
|
225
225
|
const routeMiddleware = ts.isObjectLiteralExpression(init)
|
|
226
226
|
? resolveMiddleware(state, init, routeTags, checker)
|
|
@@ -239,7 +239,7 @@ export function addMessagesRoutes(logger, obj, state, checker) {
|
|
|
239
239
|
if (fnMeta) {
|
|
240
240
|
// Resolve middleware for this route
|
|
241
241
|
const routeTags = ts.isObjectLiteralExpression(init)
|
|
242
|
-
? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
|
|
242
|
+
? getCommonWireMetaData(init, 'Channel message', routeKey, logger, checker).tags
|
|
243
243
|
: undefined;
|
|
244
244
|
const routeMiddleware = ts.isObjectLiteralExpression(init)
|
|
245
245
|
? resolveMiddleware(state, init, routeTags, checker)
|
|
@@ -303,7 +303,7 @@ export function addMessagesRoutes(logger, obj, state, checker) {
|
|
|
303
303
|
if (fnMeta) {
|
|
304
304
|
// Resolve middleware for this route
|
|
305
305
|
const routeTags = ts.isObjectLiteralExpression(init)
|
|
306
|
-
? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
|
|
306
|
+
? getCommonWireMetaData(init, 'Channel message', routeKey, logger, checker).tags
|
|
307
307
|
: undefined;
|
|
308
308
|
const routeMiddleware = ts.isObjectLiteralExpression(init)
|
|
309
309
|
? resolveMiddleware(state, init, routeTags, checker)
|
|
@@ -332,7 +332,7 @@ export function addMessagesRoutes(logger, obj, state, checker) {
|
|
|
332
332
|
// Resolve middleware and permissions for this route
|
|
333
333
|
// Check if the route config is an object literal with middleware/permissions
|
|
334
334
|
const routeTags = ts.isObjectLiteralExpression(init)
|
|
335
|
-
? getCommonWireMetaData(init, 'Channel message', routeKey, logger).tags
|
|
335
|
+
? getCommonWireMetaData(init, 'Channel message', routeKey, logger, checker).tags
|
|
336
336
|
: undefined;
|
|
337
337
|
const routeMiddleware = ts.isObjectLiteralExpression(init)
|
|
338
338
|
? resolveMiddleware(state, init, routeTags, checker)
|
|
@@ -378,7 +378,7 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
378
378
|
.keys.filter((k) => k.type === 'param')
|
|
379
379
|
.map((k) => k.name)
|
|
380
380
|
: [];
|
|
381
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Channel', name, logger);
|
|
381
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Channel', name, logger, checker);
|
|
382
382
|
if (disabled)
|
|
383
383
|
return;
|
|
384
384
|
const query = getPropertyValue(obj, 'query');
|
|
@@ -437,6 +437,24 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
437
437
|
: null;
|
|
438
438
|
state.serviceAggregation.usedFunctions.add(disconnectFuncId);
|
|
439
439
|
}
|
|
440
|
+
// Synthesize function meta for connect/disconnect handlers that are defined
|
|
441
|
+
// with pikkuChannelConnectionFunc/pikkuChannelDisconnectionFunc (not pikkuFunc/
|
|
442
|
+
// pikkuSessionlessFunc), so the runtime has correct sessionless info without
|
|
443
|
+
// needing to inject it at runtime.
|
|
444
|
+
{
|
|
445
|
+
const routeAuth = getPropertyValue(obj, 'auth');
|
|
446
|
+
const sessionless = routeAuth === false ? true : undefined;
|
|
447
|
+
for (const funcId of [connectFuncId, disconnectFuncId]) {
|
|
448
|
+
if (funcId && !state.functions.meta[funcId]) {
|
|
449
|
+
state.functions.meta[funcId] = {
|
|
450
|
+
pikkuFuncId: funcId,
|
|
451
|
+
sessionless,
|
|
452
|
+
inputSchemaName: null,
|
|
453
|
+
outputSchemaName: null,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
440
458
|
if (message) {
|
|
441
459
|
state.serviceAggregation.usedFunctions.add(message.pikkuFuncId);
|
|
442
460
|
}
|
|
@@ -330,7 +330,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
330
330
|
// Extract config properties if using object form
|
|
331
331
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
332
332
|
objectNode = firstArg;
|
|
333
|
-
const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger);
|
|
333
|
+
const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger, checker);
|
|
334
334
|
if (metadata.disabled)
|
|
335
335
|
return;
|
|
336
336
|
title = metadata.title;
|
|
@@ -627,22 +627,38 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
|
-
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
630
|
+
const sessionless = expression.text !== 'pikkuFunc';
|
|
631
|
+
// ── Classification brand check ─────────────────────────────────────────────
|
|
632
|
+
// Walk the function body's ACTUAL inferred return type looking for classification
|
|
633
|
+
// brands (__classification__ property on Private<T>, Pii<T>, Secret<T>).
|
|
634
|
+
//
|
|
635
|
+
// Semantics:
|
|
636
|
+
// secret → never returned by any exposed function (sessioned or not)
|
|
637
|
+
// private → only visible to authenticated (sessioned) users; ok for pikkuFunc
|
|
638
|
+
// public → safe for sessionless functions
|
|
635
639
|
{
|
|
636
640
|
const sig = checker.getSignatureFromDeclaration(handler);
|
|
637
641
|
if (sig) {
|
|
638
642
|
const rawRet = checker.getReturnTypeOfSignature(sig);
|
|
639
643
|
const unwrapped = unwrapPromise(checker, rawRet);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
644
|
+
const classifiedFields = findPiiPaths(checker, unwrapped);
|
|
645
|
+
const secretPaths = classifiedFields
|
|
646
|
+
.filter((f) => f.classification === 'secret')
|
|
647
|
+
.map((f) => f.path);
|
|
648
|
+
const privatePaths = classifiedFields
|
|
649
|
+
.filter((f) => f.classification === 'private' || f.classification === 'pii')
|
|
650
|
+
.map((f) => f.path);
|
|
651
|
+
if (secretPaths.length > 0) {
|
|
652
|
+
logger.critical(ErrorCode.PII_IN_OUTPUT, `Function '${name}' exposes secret-classified field(s) in its return type: ` +
|
|
653
|
+
secretPaths.map((p) => `'${p}'`).join(', ') +
|
|
654
|
+
`.\n Secret fields must never appear in function output. ` +
|
|
655
|
+
`Strip these fields before returning or change the column classification.`);
|
|
656
|
+
}
|
|
657
|
+
if (sessionless && privatePaths.length > 0) {
|
|
658
|
+
logger.critical(ErrorCode.PII_IN_OUTPUT, `Sessionless function '${name}' exposes private-classified field(s) in its return type: ` +
|
|
659
|
+
privatePaths.map((p) => `'${p}'`).join(', ') +
|
|
660
|
+
`.\n Private fields are only safe to return from authenticated (sessioned) functions. ` +
|
|
661
|
+
`Either require a session (use pikkuFunc) or mark the column @public if it is safe to expose publicly.`);
|
|
646
662
|
}
|
|
647
663
|
}
|
|
648
664
|
}
|
|
@@ -675,7 +691,6 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
675
691
|
permissions = [...(permissions || []), ...newPermissions];
|
|
676
692
|
}
|
|
677
693
|
}
|
|
678
|
-
const sessionless = expression.text !== 'pikkuFunc';
|
|
679
694
|
const implementationHash = computeImplementationHash({
|
|
680
695
|
wrapper: expression.text,
|
|
681
696
|
handler,
|
package/dist/add/add-gateway.js
CHANGED
|
@@ -23,7 +23,7 @@ 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 { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Gateway', nameValue, logger);
|
|
26
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker);
|
|
27
27
|
if (disabled)
|
|
28
28
|
return;
|
|
29
29
|
const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
|
|
@@ -97,7 +97,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
// Get common metadata
|
|
100
|
-
const { disabled, title, tags: routeTags, summary, description, errors, } = getCommonWireMetaData(obj, 'HTTP route', fullRoute, logger);
|
|
100
|
+
const { disabled, title, tags: routeTags, summary, description, errors, } = getCommonWireMetaData(obj, 'HTTP route', fullRoute, logger, checker);
|
|
101
101
|
if (disabled)
|
|
102
102
|
return;
|
|
103
103
|
// Merge inherited tags with route tags
|
|
@@ -136,6 +136,28 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
|
|
139
|
+
// Propagate sessionless from the addon target so the runtime can correctly
|
|
140
|
+
// determine whether a session is required for ref()-based routes.
|
|
141
|
+
if (refAddonTarget) {
|
|
142
|
+
const targetMeta = resolveFunctionMeta(state, refAddonTarget);
|
|
143
|
+
const inlineMeta = state.functions.meta[funcName];
|
|
144
|
+
if (targetMeta && inlineMeta && targetMeta.sessionless !== undefined) {
|
|
145
|
+
inlineMeta.sessionless = targetMeta.sessionless;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// For inline functions (e.g. oauth2 handlers), propagate sessionless: true
|
|
149
|
+
// when auth is explicitly disabled at the route or group level — the
|
|
150
|
+
// developer opted out of auth so no session is required.
|
|
151
|
+
{
|
|
152
|
+
const inlineMeta = state.functions.meta[funcName];
|
|
153
|
+
if (inlineMeta && inlineMeta.sessionless === undefined) {
|
|
154
|
+
const routeAuth = getPropertyValue(obj, 'auth');
|
|
155
|
+
const resolvedAuth = routeAuth === true || routeAuth === false ? routeAuth : inheritedAuth;
|
|
156
|
+
if (resolvedAuth === false) {
|
|
157
|
+
inlineMeta.sessionless = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
139
161
|
// Lookup existing function metadata
|
|
140
162
|
const fnMeta = resolveFunctionMeta(state, funcName);
|
|
141
163
|
if (!fnMeta) {
|
|
@@ -25,7 +25,7 @@ export const addMCPPrompt = (logger, node, checker, state, options) => {
|
|
|
25
25
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
26
26
|
const obj = firstArg;
|
|
27
27
|
const nameValue = getPropertyValue(obj, 'name');
|
|
28
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'MCP prompt', nameValue, logger);
|
|
28
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'MCP prompt', nameValue, logger, checker);
|
|
29
29
|
if (disabled)
|
|
30
30
|
return;
|
|
31
31
|
const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
|
|
@@ -26,7 +26,7 @@ export const addMCPResource = (logger, node, checker, state, options) => {
|
|
|
26
26
|
const obj = firstArg;
|
|
27
27
|
const uriValue = getPropertyValue(obj, 'uri');
|
|
28
28
|
const titleValue = getPropertyValue(obj, 'title');
|
|
29
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'MCP resource', uriValue, logger);
|
|
29
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'MCP resource', uriValue, logger, checker);
|
|
30
30
|
if (disabled)
|
|
31
31
|
return;
|
|
32
32
|
const streamingValue = getPropertyValue(obj, 'streaming');
|
|
@@ -23,7 +23,7 @@ export const addQueueWorker = (logger, node, checker, state) => {
|
|
|
23
23
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
24
24
|
const obj = firstArg;
|
|
25
25
|
const name = getPropertyValue(obj, 'name');
|
|
26
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Queue worker', name, logger);
|
|
26
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Queue worker', name, logger, checker);
|
|
27
27
|
if (disabled)
|
|
28
28
|
return;
|
|
29
29
|
// --- find the referenced function ---
|
package/dist/add/add-schedule.js
CHANGED
|
@@ -24,7 +24,7 @@ export const addSchedule = (logger, node, checker, state, options) => {
|
|
|
24
24
|
const obj = firstArg;
|
|
25
25
|
const nameValue = getPropertyValue(obj, 'name');
|
|
26
26
|
const scheduleValue = getPropertyValue(obj, 'schedule');
|
|
27
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Scheduler', nameValue, logger);
|
|
27
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Scheduler', nameValue, logger, checker);
|
|
28
28
|
if (disabled)
|
|
29
29
|
return;
|
|
30
30
|
const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
|
package/dist/add/add-trigger.js
CHANGED
|
@@ -29,7 +29,7 @@ const addWireTrigger = (logger, node, checker, state, firstArg) => {
|
|
|
29
29
|
}
|
|
30
30
|
const obj = firstArg;
|
|
31
31
|
const nameValue = getPropertyValue(obj, 'name');
|
|
32
|
-
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Trigger', nameValue, logger);
|
|
32
|
+
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Trigger', nameValue, logger, checker);
|
|
33
33
|
if (disabled)
|
|
34
34
|
return;
|
|
35
35
|
const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -185,7 +185,7 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
185
185
|
let inline;
|
|
186
186
|
let expose;
|
|
187
187
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
188
|
-
const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger);
|
|
188
|
+
const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger, checker);
|
|
189
189
|
if (metadata.disabled)
|
|
190
190
|
return;
|
|
191
191
|
tags = metadata.tags;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
export type ClassifiedField = {
|
|
3
|
+
path: string;
|
|
4
|
+
classification: 'private' | 'pii' | 'secret' | string;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* Recursively walks a resolved TypeScript type looking for `__classification__` brands —
|
|
4
|
-
* the structural marker emitted by `Private<T
|
|
8
|
+
* the structural marker emitted by `Private<T>`, `Pii<T>`, and `Secret<T>`.
|
|
5
9
|
*
|
|
6
10
|
* `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
|
|
7
11
|
* system as an intersection whose constituents include a type with a `__classification__`
|
|
8
12
|
* property. We detect that by checking whether any constituent of an
|
|
9
13
|
* intersection exposes a property named `__classification__`.
|
|
10
14
|
*
|
|
11
|
-
* Returns the list of
|
|
12
|
-
* (e.g. `['email', '
|
|
15
|
+
* Returns the list of classified fields found, each with its dotted path and
|
|
16
|
+
* classification level (e.g. `[{ path: 'email', classification: 'private' }]`).
|
|
17
|
+
* An empty array means clean.
|
|
13
18
|
*/
|
|
14
|
-
export declare function findPiiPaths(checker: ts.TypeChecker, type: ts.Type, path?: string, depth?: number, seen?: Set<ts.Type>):
|
|
19
|
+
export declare function findPiiPaths(checker: ts.TypeChecker, type: ts.Type, path?: string, depth?: number, seen?: Set<ts.Type>): ClassifiedField[];
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
/**
|
|
3
3
|
* Recursively walks a resolved TypeScript type looking for `__classification__` brands —
|
|
4
|
-
* the structural marker emitted by `Private<T
|
|
4
|
+
* the structural marker emitted by `Private<T>`, `Pii<T>`, and `Secret<T>`.
|
|
5
5
|
*
|
|
6
6
|
* `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
|
|
7
7
|
* system as an intersection whose constituents include a type with a `__classification__`
|
|
8
8
|
* property. We detect that by checking whether any constituent of an
|
|
9
9
|
* intersection exposes a property named `__classification__`.
|
|
10
10
|
*
|
|
11
|
-
* Returns the list of
|
|
12
|
-
* (e.g. `['email', '
|
|
11
|
+
* Returns the list of classified fields found, each with its dotted path and
|
|
12
|
+
* classification level (e.g. `[{ path: 'email', classification: 'private' }]`).
|
|
13
|
+
* An empty array means clean.
|
|
13
14
|
*/
|
|
14
15
|
export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set()) {
|
|
15
16
|
if (depth > 8 || seen.has(type))
|
|
@@ -17,11 +18,20 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
|
|
|
17
18
|
seen.add(type);
|
|
18
19
|
// ── Is this type itself branded? ─────────────────────────────────────────
|
|
19
20
|
// Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
|
|
20
|
-
// where one constituent has a `__classification__` property.
|
|
21
|
+
// where one constituent has a `__classification__` property whose type is a string literal.
|
|
21
22
|
if (type.isIntersection()) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
for (const t of type.types) {
|
|
24
|
+
const classificationProp = t
|
|
25
|
+
.getProperties()
|
|
26
|
+
.find((p) => p.name === '__classification__');
|
|
27
|
+
if (classificationProp) {
|
|
28
|
+
const decl = classificationProp.valueDeclaration ??
|
|
29
|
+
classificationProp.declarations?.[0];
|
|
30
|
+
const classification = decl
|
|
31
|
+
? (checker.getTypeOfSymbolAtLocation(classificationProp, decl)?.value ?? 'private')
|
|
32
|
+
: 'private';
|
|
33
|
+
return [{ path: path || '<return value>', classification }];
|
|
34
|
+
}
|
|
25
35
|
}
|
|
26
36
|
}
|
|
27
37
|
const violations = [];
|
|
@@ -192,7 +192,7 @@ export function ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcIni
|
|
|
192
192
|
const firstArg = pikkuFuncCall.arguments[0];
|
|
193
193
|
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
194
194
|
if (!meta.tags) {
|
|
195
|
-
const { tags } = getCommonWireMetaData(firstArg, 'Function', fallbackName || pikkuFuncId);
|
|
195
|
+
const { tags } = getCommonWireMetaData(firstArg, 'Function', fallbackName || pikkuFuncId, undefined, checker);
|
|
196
196
|
if (tags) {
|
|
197
197
|
meta.tags = tags;
|
|
198
198
|
}
|
|
@@ -8,7 +8,7 @@ export declare function extractStringLiteral(node: ts.Node, checker: ts.TypeChec
|
|
|
8
8
|
/**
|
|
9
9
|
* Check if node is string-like (string literal or template expression)
|
|
10
10
|
*/
|
|
11
|
-
export declare function isStringLike(node: ts.Node,
|
|
11
|
+
export declare function isStringLike(node: ts.Node, checker: ts.TypeChecker): boolean;
|
|
12
12
|
/**
|
|
13
13
|
* Check if node is function-like (arrow, function expression, or function declaration)
|
|
14
14
|
*/
|
|
@@ -24,6 +24,11 @@ export function extractStringLiteral(node, checker) {
|
|
|
24
24
|
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
25
25
|
return extractStringLiteral(node.expression, checker);
|
|
26
26
|
}
|
|
27
|
+
if (ts.isBinaryExpression(node) &&
|
|
28
|
+
node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
29
|
+
return (extractStringLiteral(node.left, checker) +
|
|
30
|
+
extractStringLiteral(node.right, checker));
|
|
31
|
+
}
|
|
27
32
|
// Try to evaluate constant identifiers
|
|
28
33
|
if (ts.isIdentifier(node)) {
|
|
29
34
|
const symbol = checker.getSymbolAtLocation(node);
|
|
@@ -40,7 +45,7 @@ export function extractStringLiteral(node, checker) {
|
|
|
40
45
|
/**
|
|
41
46
|
* Check if node is string-like (string literal or template expression)
|
|
42
47
|
*/
|
|
43
|
-
export function isStringLike(node,
|
|
48
|
+
export function isStringLike(node, checker) {
|
|
44
49
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
45
50
|
return true;
|
|
46
51
|
}
|
|
@@ -48,6 +53,10 @@ export function isStringLike(node, _checker) {
|
|
|
48
53
|
if (ts.isTemplateExpression(node)) {
|
|
49
54
|
return true;
|
|
50
55
|
}
|
|
56
|
+
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
57
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
58
|
+
return isStringLike(node.expression, checker);
|
|
59
|
+
}
|
|
51
60
|
return false;
|
|
52
61
|
}
|
|
53
62
|
/**
|
|
@@ -25,7 +25,7 @@ export declare const getPropertyValue: (obj: ts.ObjectLiteralExpression, propert
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const getCommonWireMetaData: (obj: ts.ObjectLiteralExpression, wiringType: string, wiringName: string | null, logger?: {
|
|
27
27
|
critical: (code: ErrorCode, message: string) => void;
|
|
28
|
-
}) => {
|
|
28
|
+
}, checker?: ts.TypeChecker) => {
|
|
29
29
|
disabled?: true;
|
|
30
30
|
title?: string;
|
|
31
31
|
tags?: string[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { ErrorCode } from '../error-codes.js';
|
|
3
|
+
import { extractStringLiteral } from './extract-node-value.js';
|
|
3
4
|
/**
|
|
4
5
|
* Extracts an array of strings from an object property.
|
|
5
6
|
*/
|
|
@@ -94,7 +95,7 @@ export const getPropertyValue = (obj, propertyName) => {
|
|
|
94
95
|
* @param logger - Optional logger instance; if not provided, uses console.error
|
|
95
96
|
* @returns Object containing the common wire metadata fields
|
|
96
97
|
*/
|
|
97
|
-
export const getCommonWireMetaData = (obj, wiringType, wiringName, logger) => {
|
|
98
|
+
export const getCommonWireMetaData = (obj, wiringType, wiringName, logger, checker) => {
|
|
98
99
|
const metadata = {};
|
|
99
100
|
assertStringLiteralProperty(obj, 'name', wiringType, logger);
|
|
100
101
|
obj.properties.forEach((prop) => {
|
|
@@ -104,16 +105,41 @@ export const getCommonWireMetaData = (obj, wiringType, wiringName, logger) => {
|
|
|
104
105
|
prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
105
106
|
metadata.disabled = true;
|
|
106
107
|
}
|
|
107
|
-
else if (propName === 'title'
|
|
108
|
-
|
|
108
|
+
else if (propName === 'title') {
|
|
109
|
+
try {
|
|
110
|
+
metadata.title = checker
|
|
111
|
+
? extractStringLiteral(prop.initializer, checker)
|
|
112
|
+
: ts.isStringLiteral(prop.initializer)
|
|
113
|
+
? prop.initializer.text
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// non-static title — skip
|
|
118
|
+
}
|
|
109
119
|
}
|
|
110
|
-
else if (propName === 'summary'
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
else if (propName === 'summary') {
|
|
121
|
+
try {
|
|
122
|
+
metadata.summary = checker
|
|
123
|
+
? extractStringLiteral(prop.initializer, checker)
|
|
124
|
+
: ts.isStringLiteral(prop.initializer)
|
|
125
|
+
? prop.initializer.text
|
|
126
|
+
: undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// non-static summary — skip
|
|
130
|
+
}
|
|
113
131
|
}
|
|
114
|
-
else if (propName === 'description'
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
else if (propName === 'description') {
|
|
133
|
+
try {
|
|
134
|
+
metadata.description = checker
|
|
135
|
+
? extractStringLiteral(prop.initializer, checker)
|
|
136
|
+
: ts.isStringLiteral(prop.initializer)
|
|
137
|
+
? prop.initializer.text
|
|
138
|
+
: undefined;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// non-static description — skip
|
|
142
|
+
}
|
|
117
143
|
}
|
|
118
144
|
else if (propName === 'tags') {
|
|
119
145
|
if (ts.isArrayLiteralExpression(prop.initializer)) {
|
|
@@ -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/package.json
CHANGED
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
|
|