@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,51 @@
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
+
23
+ ## 0.12.14
24
+
25
+ ### Patch Changes
26
+
27
+ - 4b5c75b: feat(auth-js): wire OIDC config (issuer/tenantId) as variables, expand provider registry
28
+ - Move `issuer` and `tenantId` out of the secret blob for OIDC providers (auth0, okta, azure-ad, keycloak, cognito, microsoft-entra-id) — they are public config URLs, not secrets. Now registered via `wireVariable` and loaded at runtime via `services.variables.get()`.
29
+ - Expand provider registry from 13 to 31 providers: reddit, notion, instagram, zoom, figma, tiktok, threads, patreon, dropbox, bitbucket, hubspot, salesforce, atlassian, strava, keycloak, cognito, microsoft-entra-id added.
30
+ - `serialize-auth-gen` emits `wireVariable({...})` declarations and `services.variables.get()` calls in the generated factory for OIDC providers.
31
+ - Integration verifier exercises real `/auth/providers` endpoint with `LocalSecretService` + `LocalVariablesService`, including a spy test proving `services.variables.get('AUTH0_ISSUER')` is called at request time.
32
+
33
+ - 4b5c75b: Add end-to-end data classification for SQLite and Postgres projects.
34
+
35
+ **Core (`@pikku/core`):** New `Private<T>` and `Secret<T>` intersection brands, `ClassificationManifest`, `ColumnClassification`, and `AnonymizeStrategy` types exported from `data-classification.ts`.
36
+
37
+ **CLI (`@pikku/cli`):**
38
+ - SQL comment annotations: `-- @public`, `-- @private[:strategy]`, `-- @secret[:strategy]` on `CREATE TABLE` columns and `ALTER TABLE ... ADD COLUMN` statements. Unannotated columns default to `private`.
39
+ - `pikku db migrate` now emits a `classification.gen.ts` manifest alongside `schema.d.ts`.
40
+ - New `pikku db audit` command — prints a per-column classification summary and warns on `private`/`secret` columns with no anonymize strategy.
41
+ - Postgres dialect support in `resolveDb`, `PostgresMigrationExecutor`, and `PostgresIntrospector`.
42
+
43
+ **Inspector (`@pikku/inspector`):** New PKU910 check — `findPiiPaths()` walks inferred function return types looking for `__pii__` brands (including inside `Array<T>`, `Record<K,V>`, and index signatures) and fails the build if a function exposes branded fields in its output.
44
+
45
+ - Updated dependencies [4b5c75b]
46
+ - Updated dependencies [4b5c75b]
47
+ - @pikku/core@0.12.27
48
+
1
49
  ## 0.12.13
2
50
 
3
51
  ### Patch Changes
@@ -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');
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addAuth: AddWiring;
@@ -0,0 +1,34 @@
1
+ import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
3
+ export const addAuth = (logger, node, _checker, state) => {
4
+ if (!ts.isCallExpression(node))
5
+ return;
6
+ const expression = node.expression;
7
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireAuth')
8
+ return;
9
+ const firstArg = node.arguments[0];
10
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
11
+ return;
12
+ const providersProp = firstArg.properties.find((p) => ts.isPropertyAssignment(p) &&
13
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
14
+ p.name.text === 'providers');
15
+ const sourceFile = node.getSourceFile().fileName;
16
+ state.auth.files.add(sourceFile);
17
+ if (!providersProp) {
18
+ return;
19
+ }
20
+ if (!ts.isArrayLiteralExpression(providersProp.initializer)) {
21
+ logger.critical(ErrorCode.MISSING_NAME, 'wireAuth: providers must be an array literal of string literals.');
22
+ return;
23
+ }
24
+ for (const element of providersProp.initializer
25
+ .elements) {
26
+ if (!ts.isStringLiteral(element)) {
27
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `wireAuth: each provider must be a string literal. Found: ${element.getText()}`);
28
+ return;
29
+ }
30
+ if (!state.auth.providers.includes(element.text)) {
31
+ state.auth.providers.push(element.text);
32
+ }
33
+ }
34
+ };
@@ -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
- // ── PII brand check ───────────────────────────────────────────────────────
631
- // Walk the function body's ACTUAL inferred return type looking for Private<T>
632
- // / Secret<T> brands (__pii__ property). This runs for every function,
633
- // including those with a Zod output schema, because the TS return type
634
- // reflects what the body actually returns before any Zod coercion.
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 piiPaths = findPiiPaths(checker, unwrapped);
641
- if (piiPaths.length > 0) {
642
- logger.critical(ErrorCode.PII_IN_OUTPUT, `Function '${name}' exposes PII-classified field(s) in its return type: ` +
643
- piiPaths.map((p) => `'${p}'`).join(', ') +
644
- `.\n Either strip these fields before returning or mark the column ` +
645
- `@public in the migration if it is safe to expose.`);
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,
@@ -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 ---
@@ -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);
@@ -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);
@@ -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;
package/dist/inspector.js CHANGED
@@ -115,6 +115,10 @@ export function getInitialInspectorState(rootDir) {
115
115
  meta: {},
116
116
  files: new Set(),
117
117
  },
118
+ auth: {
119
+ providers: [],
120
+ files: new Set(),
121
+ },
118
122
  secrets: {
119
123
  definitions: [],
120
124
  files: new Set(),
package/dist/types.d.ts CHANGED
@@ -339,6 +339,10 @@ export interface InspectorState {
339
339
  meta: NodesMeta;
340
340
  files: Set<string>;
341
341
  };
342
+ auth: {
343
+ providers: string[];
344
+ files: Set<string>;
345
+ };
342
346
  secrets: {
343
347
  definitions: SecretDefinitions;
344
348
  files: Set<string>;
@@ -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
- * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
4
- * the structural marker emitted by `Private<T>` and `Secret<T>`.
7
+ * Recursively walks a resolved TypeScript type looking for `__classification__` brands —
8
+ * the structural marker emitted by `Private<T>`, `Pii<T>`, and `Secret<T>`.
5
9
  *
6
- * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
7
- * system as an intersection whose constituents include a type with a `__pii__`
10
+ * `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
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
- * intersection exposes a property named `__pii__`.
13
+ * intersection exposes a property named `__classification__`.
10
14
  *
11
- * Returns the list of dotted field paths where a brand was found
12
- * (e.g. `['email', 'address.phone']`). An empty array means clean.
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>): string[];
19
+ export declare function findPiiPaths(checker: ts.TypeChecker, type: ts.Type, path?: string, depth?: number, seen?: Set<ts.Type>): ClassifiedField[];
@@ -1,27 +1,37 @@
1
1
  import * as ts from 'typescript';
2
2
  /**
3
- * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
4
- * the structural marker emitted by `Private<T>` and `Secret<T>`.
3
+ * Recursively walks a resolved TypeScript type looking for `__classification__` brands —
4
+ * the structural marker emitted by `Private<T>`, `Pii<T>`, and `Secret<T>`.
5
5
  *
6
- * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
7
- * system as an intersection whose constituents include a type with a `__pii__`
6
+ * `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
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
- * intersection exposes a property named `__pii__`.
9
+ * intersection exposes a property named `__classification__`.
10
10
  *
11
- * Returns the list of dotted field paths where a brand was found
12
- * (e.g. `['email', 'address.phone']`). An empty array means clean.
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))
16
17
  return [];
17
18
  seen.add(type);
18
19
  // ── Is this type itself branded? ─────────────────────────────────────────
19
- // Private<T> = T & { readonly __pii__: 'private' } → isIntersection()
20
- // where one constituent has a `__pii__` property.
20
+ // Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
21
+ // where one constituent has a `__classification__` property whose type is a string literal.
21
22
  if (type.isIntersection()) {
22
- const branded = type.types.some((t) => t.getProperties().some((p) => p.name === '__pii__'));
23
- if (branded) {
24
- return [path || '<return value>'];
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, _checker: ts.TypeChecker): boolean;
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, _checker) {
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' && ts.isStringLiteral(prop.initializer)) {
108
- metadata.title = prop.initializer.text;
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
- ts.isStringLiteral(prop.initializer)) {
112
- metadata.summary = prop.initializer.text;
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
- ts.isStringLiteral(prop.initializer)) {
116
- metadata.description = prop.initializer.text;
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)) {
@@ -203,6 +203,10 @@ export interface SerializableInspectorState {
203
203
  meta: InspectorState['nodes']['meta'];
204
204
  files: string[];
205
205
  };
206
+ auth: {
207
+ providers: string[];
208
+ files: string[];
209
+ };
206
210
  secrets: {
207
211
  definitions: InspectorState['secrets']['definitions'];
208
212
  files: string[];