@pikku/inspector 0.12.42 → 0.12.43

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 (102) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/add/add-addon-bans.js +1 -0
  3. package/dist/add/add-ai-agent.d.ts +3 -1
  4. package/dist/add/add-ai-agent.js +82 -0
  5. package/dist/add/add-credential.js +3 -1
  6. package/dist/add/add-functions.js +10 -9
  7. package/dist/add/add-gateway.js +3 -0
  8. package/dist/add/add-http-route.js +2 -2
  9. package/dist/add/add-mcp-prompt.js +0 -1
  10. package/dist/add/add-mcp-resource.js +0 -1
  11. package/dist/add/add-permission.d.ts +1 -1
  12. package/dist/add/add-permission.js +3 -177
  13. package/dist/add/add-queue-worker.js +3 -0
  14. package/dist/add/add-schedule.js +3 -0
  15. package/dist/add/add-scope.d.ts +2 -0
  16. package/dist/add/add-scope.js +146 -0
  17. package/dist/add/add-trigger.js +3 -0
  18. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  19. package/dist/add/add-wire-remote-addon.js +66 -0
  20. package/dist/add/add-workflow-graph.js +32 -3
  21. package/dist/error-codes.d.ts +3 -0
  22. package/dist/error-codes.js +4 -0
  23. package/dist/inspector.js +11 -5
  24. package/dist/types.d.ts +19 -13
  25. package/dist/utils/custom-types-generator.js +2 -1
  26. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  27. package/dist/utils/ensure-function-metadata.js +24 -0
  28. package/dist/utils/filter-inspector-state.js +9 -0
  29. package/dist/utils/get-property-value.d.ts +7 -2
  30. package/dist/utils/get-property-value.js +48 -1
  31. package/dist/utils/load-addon-functions-meta.js +81 -6
  32. package/dist/utils/permissions.d.ts +1 -21
  33. package/dist/utils/permissions.js +17 -108
  34. package/dist/utils/post-process.d.ts +30 -1
  35. package/dist/utils/post-process.js +150 -72
  36. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  37. package/dist/utils/serialize-inspector-state.js +12 -6
  38. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  39. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  40. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  41. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +56 -1
  42. package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
  43. package/dist/utils/workflow/dsl/patterns.js +11 -0
  44. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +14 -0
  45. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  46. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  47. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  48. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  49. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +6 -2
  50. package/dist/visit.js +4 -0
  51. package/package.json +3 -3
  52. package/src/add/add-addon-bans.ts +1 -0
  53. package/src/add/add-ai-agent.test.ts +87 -0
  54. package/src/add/add-ai-agent.ts +122 -0
  55. package/src/add/add-credential.ts +6 -0
  56. package/src/add/add-functions.ts +10 -12
  57. package/src/add/add-gateway.ts +11 -0
  58. package/src/add/add-http-route.ts +2 -8
  59. package/src/add/add-mcp-prompt.ts +0 -1
  60. package/src/add/add-mcp-resource.ts +0 -1
  61. package/src/add/add-permission.ts +4 -242
  62. package/src/add/add-queue-worker.ts +11 -0
  63. package/src/add/add-schedule.ts +11 -0
  64. package/src/add/add-scope.test.ts +346 -0
  65. package/src/add/add-scope.ts +225 -0
  66. package/src/add/add-trigger.ts +11 -0
  67. package/src/add/add-wire-remote-addon.ts +77 -0
  68. package/src/add/add-workflow-graph-input.test.ts +94 -0
  69. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  70. package/src/add/add-workflow-graph.ts +35 -3
  71. package/src/add/inline-wiring-function.test.ts +104 -0
  72. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  73. package/src/error-codes.ts +5 -0
  74. package/src/inspector.ts +14 -4
  75. package/src/types.ts +16 -17
  76. package/src/utils/custom-types-generator.test.ts +26 -1
  77. package/src/utils/custom-types-generator.ts +2 -1
  78. package/src/utils/ensure-function-metadata.ts +42 -0
  79. package/src/utils/filter-inspector-state.test.ts +0 -2
  80. package/src/utils/filter-inspector-state.ts +9 -0
  81. package/src/utils/get-property-value.test.ts +141 -0
  82. package/src/utils/get-property-value.ts +62 -1
  83. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  84. package/src/utils/load-addon-functions-meta.ts +94 -6
  85. package/src/utils/load-addon-scopes.test.ts +138 -0
  86. package/src/utils/permissions.test.ts +7 -232
  87. package/src/utils/permissions.ts +20 -160
  88. package/src/utils/post-process.test.ts +269 -4
  89. package/src/utils/post-process.ts +216 -95
  90. package/src/utils/serialize-inspector-state.ts +22 -25
  91. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  92. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  93. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  94. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +66 -0
  95. package/src/utils/workflow/dsl/patterns.ts +18 -0
  96. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -0
  97. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  98. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  99. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  100. package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
  101. package/src/visit.ts +4 -0
  102. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,104 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import type { ErrorCode, InspectorLogger } from '../types.js'
8
+
9
+ const silentLogger = (): InspectorLogger => ({
10
+ debug: () => {},
11
+ info: () => {},
12
+ warn: () => {},
13
+ error: () => {},
14
+ diagnostic: () => {},
15
+ critical: (_code: ErrorCode, _message: string) => {},
16
+ hasCriticalErrors: () => false,
17
+ })
18
+
19
+ // A `func:` inlined into a wiring has no exported name, so addFunctions skips
20
+ // it and only the wiring's context-based id exists. The wiring visitor must
21
+ // register the function metadata under that id (via ensureInlineWiringFunction)
22
+ // or the transport resolves to nothing at runtime ("Missing generated metadata
23
+ // for ...") and, once registered, every invocation would 403 unless the
24
+ // sessionless flag is carried across from the helper used.
25
+ const WIRINGS = [
26
+ {
27
+ label: 'wireQueueWorker',
28
+ wire: 'wireQueueWorker',
29
+ props: "name: 'inline-queue',",
30
+ expectedId: 'queue:inline-queue',
31
+ },
32
+ {
33
+ label: 'wireScheduler',
34
+ wire: 'wireScheduler',
35
+ props: "name: 'inline-cron', schedule: '* * * * *',",
36
+ expectedId: 'scheduler:inline-cron',
37
+ },
38
+ {
39
+ label: 'wireTrigger',
40
+ wire: 'wireTrigger',
41
+ props: "name: 'inline-trigger',",
42
+ expectedId: 'trigger:inline-trigger',
43
+ },
44
+ {
45
+ label: 'wireGateway',
46
+ wire: 'wireGateway',
47
+ props: "name: 'inline-gw', type: 'slack',",
48
+ expectedId: 'gateway:inline-gw',
49
+ },
50
+ ]
51
+
52
+ const write = (wire: string, props: string, helper: string) =>
53
+ [
54
+ `import { ${helper}, ${wire} } from '@pikku/core'`,
55
+ `${wire}({`,
56
+ ` ${props}`,
57
+ ` func: ${helper}<{ url: string }, void>({ func: async () => {} }),`,
58
+ '})',
59
+ ].join('\n')
60
+
61
+ describe('inline wiring functions register metadata', () => {
62
+ for (const { label, wire, props, expectedId } of WIRINGS) {
63
+ test(`${label} registers a sessionless func inlined into it`, async () => {
64
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-inline-'))
65
+ const file = join(rootDir, 'wire.ts')
66
+ await writeFile(file, write(wire, props, 'pikkuSessionlessFunc'))
67
+
68
+ try {
69
+ const state = await inspect(silentLogger(), [file], { rootDir })
70
+ const funcMeta = state.functions.meta[expectedId]
71
+ assert.ok(
72
+ funcMeta,
73
+ `${label}: metadata must exist under the id the wiring references`
74
+ )
75
+ assert.equal(
76
+ funcMeta!.sessionless,
77
+ true,
78
+ `${label}: pikkuSessionlessFunc must stay sessionless when inlined`
79
+ )
80
+ } finally {
81
+ await rm(rootDir, { recursive: true, force: true })
82
+ }
83
+ })
84
+
85
+ test(`${label} keeps an inlined pikkuFunc session-required`, async () => {
86
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-inline-auth-'))
87
+ const file = join(rootDir, 'wire.ts')
88
+ await writeFile(file, write(wire, props, 'pikkuFunc'))
89
+
90
+ try {
91
+ const state = await inspect(silentLogger(), [file], { rootDir })
92
+ const funcMeta = state.functions.meta[expectedId]
93
+ assert.ok(funcMeta, `${label}: metadata must exist`)
94
+ assert.equal(
95
+ funcMeta!.sessionless,
96
+ false,
97
+ `${label}: pikkuFunc must not be marked sessionless`
98
+ )
99
+ } finally {
100
+ await rm(rootDir, { recursive: true, force: true })
101
+ }
102
+ })
103
+ }
104
+ })
@@ -0,0 +1,102 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import type { InspectorLogger } from '../types.js'
8
+ import { ErrorCode } from '../error-codes.js'
9
+
10
+ function makeLogger(
11
+ criticals: Array<{ code: string; message: string }>
12
+ ): InspectorLogger {
13
+ return {
14
+ debug: () => {},
15
+ info: () => {},
16
+ warn: () => {},
17
+ error: () => {},
18
+ diagnostic: () => {},
19
+ critical: (code, message) => criticals.push({ code, message }),
20
+ hasCriticalErrors: () => criticals.length > 0,
21
+ }
22
+ }
23
+
24
+ describe('validateWorkflowGraphAddons', () => {
25
+ test('a graph node referencing graph: without @pikku/addon-graph wired raises PKU642', async () => {
26
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-graph-addon-'))
27
+ const stepFile = join(rootDir, 'my.steps.ts')
28
+ const graphFile = join(rootDir, 'my.graph.ts')
29
+
30
+ await writeFile(
31
+ stepFile,
32
+ [
33
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
34
+ 'export const notify = pikkuSessionlessFunc({ func: async () => ({ sent: true }) })',
35
+ ].join('\n')
36
+ )
37
+ await writeFile(
38
+ graphFile,
39
+ [
40
+ "import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
41
+ 'export const myGraph = pikkuWorkflowGraph({',
42
+ " name: 'my-graph',",
43
+ ' nodes: {',
44
+ " setFields: 'graph:editFields',",
45
+ " notify: 'notify',",
46
+ ' },',
47
+ " config: { setFields: { next: 'notify' }, notify: {} },",
48
+ '})',
49
+ ].join('\n')
50
+ )
51
+
52
+ try {
53
+ const criticals: Array<{ code: string; message: string }> = []
54
+ await inspect(makeLogger(criticals), [stepFile, graphFile], { rootDir })
55
+ const hit = criticals.find(
56
+ (c) => c.code === ErrorCode.WORKFLOW_GRAPH_ADDON_NOT_WIRED
57
+ )
58
+ assert.ok(hit, 'expected a WORKFLOW_GRAPH_ADDON_NOT_WIRED critical')
59
+ assert.match(hit!.message, /@pikku\/addon-graph is not wired/)
60
+ assert.match(hit!.message, /scaffold/)
61
+ } finally {
62
+ await rm(rootDir, { recursive: true, force: true })
63
+ }
64
+ })
65
+
66
+ test('a graph with only user RPCs raises no addon-graph critical', async () => {
67
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-graph-addon-ok-'))
68
+ const stepFile = join(rootDir, 'my.steps.ts')
69
+ const graphFile = join(rootDir, 'my.graph.ts')
70
+
71
+ await writeFile(
72
+ stepFile,
73
+ [
74
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
75
+ 'export const assess = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
76
+ 'export const notify = pikkuSessionlessFunc({ func: async () => ({ sent: true }) })',
77
+ ].join('\n')
78
+ )
79
+ await writeFile(
80
+ graphFile,
81
+ [
82
+ "import { pikkuWorkflowGraph } from '@pikku/core/workflow'",
83
+ 'export const myGraph = pikkuWorkflowGraph({',
84
+ " name: 'my-graph',",
85
+ " nodes: { assess: 'assess', notify: 'notify' },",
86
+ " config: { assess: { next: 'notify' }, notify: {} },",
87
+ '})',
88
+ ].join('\n')
89
+ )
90
+
91
+ try {
92
+ const criticals: Array<{ code: string; message: string }> = []
93
+ await inspect(makeLogger(criticals), [stepFile, graphFile], { rootDir })
94
+ const hit = criticals.find(
95
+ (c) => c.code === ErrorCode.WORKFLOW_GRAPH_ADDON_NOT_WIRED
96
+ )
97
+ assert.equal(hit, undefined, 'no addon-graph critical expected')
98
+ } finally {
99
+ await rm(rootDir, { recursive: true, force: true })
100
+ }
101
+ })
102
+ })
@@ -24,6 +24,11 @@ export enum ErrorCode {
24
24
  EXPECT_EVENTUALLY_SCENARIO_ONLY = 'PKU675',
25
25
  WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = 'PKU600',
26
26
  INVALID_DSL_WORKFLOW = 'PKU641',
27
+ WORKFLOW_GRAPH_ADDON_NOT_WIRED = 'PKU642',
28
+
29
+ // Remote addon (wireRemoteAddon) validation
30
+ REMOTE_ADDON_NOT_DEV_DEPENDENCY = 'PKU338',
31
+ REMOTE_ADDON_AUTH_UNRESOLVED = 'PKU339',
27
32
 
28
33
  // Database schema codegen warnings
29
34
  DB_COLUMN_NAME_TYPE_CONTRADICTION = 'PKU480',
package/src/inspector.ts CHANGED
@@ -18,12 +18,16 @@ import {
18
18
  validateSecretOverrides,
19
19
  validateVariableOverrides,
20
20
  validateCredentialOverrides,
21
+ validateRemoteAddonDependencies,
22
+ validateRemoteAddonAuth,
23
+ validateScopeReferences,
21
24
  computeResolvedIOTypes,
22
25
  computeMiddlewareGroupsMeta,
23
26
  computePermissionsGroupsMeta,
24
27
  computeRequiredSchemas,
25
28
  computeDiagnostics,
26
29
  validateSchemaWiringSeparation,
30
+ validateWorkflowGraphAddons,
27
31
  } from './utils/post-process.js'
28
32
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js'
29
33
  import { pikkuState } from '@pikku/core/internal'
@@ -90,7 +94,6 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
90
94
  },
91
95
  files: new Set(),
92
96
  routeMiddleware: new Map(),
93
- routePermissions: new Map(),
94
97
  },
95
98
  channels: {
96
99
  files: new Set(),
@@ -125,6 +128,8 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
125
128
  internalFiles: new Map(),
126
129
  exposedMeta: {},
127
130
  exposedFiles: new Map(),
131
+ remoteMeta: {},
132
+ remoteFiles: new Map(),
128
133
  invokedFunctions: new Set(),
129
134
  invokedFunctionsByFile: new Map(),
130
135
  usedAddons: new Set(),
@@ -166,6 +171,10 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
166
171
  definitions: [],
167
172
  files: new Set(),
168
173
  },
174
+ scopes: {
175
+ definitions: [],
176
+ files: new Set(),
177
+ },
169
178
  variables: {
170
179
  definitions: [],
171
180
  files: new Set(),
@@ -191,7 +200,6 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
191
200
  permissions: {
192
201
  definitions: {},
193
202
  instances: {},
194
- tagPermissions: new Map(),
195
203
  },
196
204
  serviceAggregation: {
197
205
  requiredServices: new Set(),
@@ -215,8 +223,6 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
215
223
  },
216
224
  permissionsGroupsMeta: {
217
225
  definitions: {},
218
- httpGroups: {},
219
- tagGroups: {},
220
226
  },
221
227
  requiredSchemas: new Set(),
222
228
  openAPISpec: null,
@@ -400,6 +406,7 @@ export const inspect = async (
400
406
  computePermissionsGroupsMeta(state)
401
407
  computeDiagnostics(state)
402
408
  validateSchemaWiringSeparation(logger, state)
409
+ validateWorkflowGraphAddons(logger, state)
403
410
 
404
411
  if (options.openAPI) {
405
412
  state.openAPISpec = await generateOpenAPISpec(
@@ -416,6 +423,9 @@ export const inspect = async (
416
423
  validateSecretOverrides(logger, state)
417
424
  validateVariableOverrides(logger, state)
418
425
  validateCredentialOverrides(logger, state)
426
+ validateRemoteAddonDependencies(logger, state)
427
+ validateRemoteAddonAuth(logger, state)
428
+ validateScopeReferences(logger, state)
419
429
  }
420
430
 
421
431
  state.program = program
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ import type { CLICommandMeta } from '@pikku/core/cli'
17
17
  import type { NodesMeta } from '@pikku/core/node'
18
18
  import type { SecretDefinitions } from '@pikku/core/secret'
19
19
  import type { CredentialDefinitions } from '@pikku/core/credential'
20
+ import type { ScopeDefinitions } from '@pikku/core/scope'
20
21
  import type { VariableDefinitions } from '@pikku/core/variable'
21
22
  import type { TypesMap } from './types-map.js'
22
23
  import type {
@@ -57,16 +58,6 @@ export interface MiddlewareGroupMeta {
57
58
  isFactory: boolean // true if wrapped in () => add...()
58
59
  }
59
60
 
60
- export interface PermissionGroupMeta {
61
- exportName: string | null // null if not exported
62
- sourceFile: string
63
- position: number
64
- services: FunctionServicesMeta
65
- count: number
66
- instanceIds: string[]
67
- isFactory: boolean // true if wrapped in () => add...()
68
- }
69
-
70
61
  export interface InspectorHTTPState {
71
62
  metaInputTypes: MetaInputTypes
72
63
  meta: HTTPWiringsMeta
@@ -75,10 +66,6 @@ export interface InspectorHTTPState {
75
66
  // Pattern '*' matches all routes (from addHTTPMiddleware('*', [...]))
76
67
  // Pattern '/api/*' matches specific routes (from addHTTPMiddleware('/api/*', [...]))
77
68
  routeMiddleware: Map<string, MiddlewareGroupMeta>
78
- // HTTP permission calls tracking - route pattern -> group metadata
79
- // Pattern '*' matches all routes (from addHTTPPermission('*', [...]))
80
- // Pattern '/api/*' matches specific routes (from addHTTPPermission('/api/*', [...]))
81
- routePermissions: Map<string, PermissionGroupMeta>
82
69
  }
83
70
 
84
71
  /**
@@ -241,7 +228,6 @@ export interface InspectorPermissionInstance {
241
228
  export interface InspectorPermissionState {
242
229
  definitions: Record<string, InspectorPermissionDefinition>
243
230
  instances: Record<string, InspectorPermissionInstance>
244
- tagPermissions: Map<string, PermissionGroupMeta>
245
231
  }
246
232
 
247
233
  export type InspectorFilters = {
@@ -487,6 +473,9 @@ export interface InspectorState {
487
473
  internalFiles: Map<string, { path: string; exportedName: string }>
488
474
  exposedMeta: Record<string, string>
489
475
  exposedFiles: Map<string, { path: string; exportedName: string }>
476
+ /** Functions marked `remote: true` — the surface a wireRemoteAddon consumer imports */
477
+ remoteMeta: Record<string, string>
478
+ remoteFiles: Map<string, { path: string; exportedName: string }>
490
479
  invokedFunctions: Set<string>
491
480
  invokedFunctionsByFile: Map<string, Set<string>>
492
481
  usedAddons: Set<string>
@@ -496,6 +485,14 @@ export interface InspectorState {
496
485
  package: string
497
486
  rpcEndpoint?: string
498
487
  mcp?: boolean
488
+ /** True when declared via `wireRemoteAddon` — import the addon's `.remote.gen` map, not `.internal.gen` */
489
+ remote?: boolean
490
+ /** wireRemoteAddon: whether an `auth` binding was supplied (vs a public surface) */
491
+ hasAuth?: boolean
492
+ /** wireRemoteAddon: statically-known `auth.credentialId`, for verify */
493
+ authCredentialId?: string
494
+ /** wireRemoteAddon: statically-known `auth.secretId`, for verify */
495
+ authSecretId?: string
499
496
  secretOverrides?: Record<string, string>
500
497
  variableOverrides?: Record<string, string>
501
498
  credentialOverrides?: Record<string, string>
@@ -549,6 +546,10 @@ export interface InspectorState {
549
546
  definitions: CredentialDefinitions
550
547
  files: Set<string>
551
548
  }
549
+ scopes: {
550
+ definitions: ScopeDefinitions
551
+ files: Set<string>
552
+ }
552
553
  variables: {
553
554
  definitions: VariableDefinitions
554
555
  files: Set<string>
@@ -584,8 +585,6 @@ export interface InspectorState {
584
585
  }
585
586
  permissionsGroupsMeta: {
586
587
  definitions: Record<string, InspectorPermissionDefinition>
587
- httpGroups: Record<string, PermissionGroupMeta>
588
- tagGroups: Record<string, PermissionGroupMeta>
589
588
  }
590
589
  requiredSchemas: Set<string>
591
590
  openAPISpec: Record<string, any> | null
@@ -1,6 +1,9 @@
1
1
  import { test, describe } from 'node:test'
2
2
  import { strict as assert } from 'node:assert'
3
- import { generateCustomTypes } from './custom-types-generator.js'
3
+ import {
4
+ generateCustomTypes,
5
+ sanitizeTypeName,
6
+ } from './custom-types-generator.js'
4
7
  import { TypesMap } from '../types-map.js'
5
8
 
6
9
  function makeTypesMap(
@@ -97,3 +100,25 @@ describe('generateCustomTypes — classification wrapper stripping', () => {
97
100
  )
98
101
  })
99
102
  })
103
+
104
+ describe('sanitizeTypeName — valid JS identifier', () => {
105
+ test('replaces disallowed characters with underscores', () => {
106
+ assert.equal(
107
+ sanitizeTypeName('Add Beehiiv subscribers'),
108
+ 'Add_Beehiiv_subscribers'
109
+ )
110
+ })
111
+
112
+ test('prefixes an underscore when the name starts with a digit', () => {
113
+ // A name like "2. Add Beehiiv newsletter subscribers" otherwise produces
114
+ // `2__Add_...`, an invalid JS identifier that breaks the generated
115
+ // `import <ident> from ...` in pikku-workflow-wirings-meta.gen.ts.
116
+ const id = sanitizeTypeName('2. Add Beehiiv newsletter subscribers')
117
+ assert.equal(id, '_2__Add_Beehiiv_newsletter_subscribers')
118
+ assert.match(id, /^[A-Za-z_$]/, 'must start with a valid identifier char')
119
+ })
120
+
121
+ test('leaves an already-valid identifier unchanged', () => {
122
+ assert.equal(sanitizeTypeName('UserEmail'), 'UserEmail')
123
+ })
124
+ })
@@ -8,7 +8,8 @@ import type { TypesMap } from '../types-map.js'
8
8
  */
9
9
 
10
10
  export function sanitizeTypeName(name: string): string {
11
- return name.replace(/[^a-zA-Z0-9_$]/g, '_')
11
+ const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_')
12
+ return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized
12
13
  }
13
14
 
14
15
  const CLASSIFICATION_WRAPPERS = new Set(['Private', 'Pii', 'Secret'])
@@ -309,6 +309,48 @@ export function ensureFunctionMetadata(
309
309
  }
310
310
  }
311
311
 
312
+ /**
313
+ * Register metadata for a `func:` inlined into a wiring (queue/scheduler/
314
+ * trigger/gateway/…).
315
+ *
316
+ * An inline function has no exported name, so `addFunctions` skips it (it bails
317
+ * on `__temp_` ids) and only the wiring's context-based id exists. Without this
318
+ * the wiring points at a function that was never registered and the worker
319
+ * resolves to nothing at runtime ("Missing generated metadata for ...").
320
+ *
321
+ * The stub also carries no session info, so the runtime would treat it as
322
+ * session-required and every invocation would fail with "Authentication
323
+ * required". Derive `sessionless` from the helper the function was built with —
324
+ * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
325
+ */
326
+ export function ensureInlineWiringFunction(
327
+ state: InspectorState,
328
+ pikkuFuncId: string,
329
+ fallbackName: string | undefined,
330
+ funcInitializer: ts.Node,
331
+ checker: ts.TypeChecker,
332
+ isHelper?: boolean
333
+ ): void {
334
+ ensureFunctionMetadata(
335
+ state,
336
+ pikkuFuncId,
337
+ fallbackName,
338
+ funcInitializer,
339
+ checker,
340
+ isHelper
341
+ )
342
+
343
+ const meta = state.functions.meta[pikkuFuncId]
344
+ if (
345
+ meta &&
346
+ meta.sessionless === undefined &&
347
+ ts.isCallExpression(funcInitializer) &&
348
+ ts.isIdentifier(funcInitializer.expression)
349
+ ) {
350
+ meta.sessionless = funcInitializer.expression.text !== 'pikkuFunc'
351
+ }
352
+ }
353
+
312
354
  function populateTypesLookup(
313
355
  state: InspectorState,
314
356
  pikkuFuncId: string,
@@ -82,7 +82,6 @@ function createMockInspectorState(): Omit<InspectorState, 'typesLookup'> {
82
82
  },
83
83
  files: new Set(['/test/project/src/api/users.ts']),
84
84
  routeMiddleware: new Map(),
85
- routePermissions: new Map(),
86
85
  },
87
86
  functions: {
88
87
  typesMap: new Map(),
@@ -332,7 +331,6 @@ function createMockInspectorState(): Omit<InspectorState, 'typesLookup'> {
332
331
  permissions: {
333
332
  definitions: {},
334
333
  instances: {},
335
- tagPermissions: new Map(),
336
334
  },
337
335
  serviceAggregation: {
338
336
  requiredServices: new Set(),
@@ -425,6 +425,8 @@ export function filterInspectorState(
425
425
  internalFiles: new Map(state.rpc.internalFiles),
426
426
  exposedMeta: { ...state.rpc.exposedMeta },
427
427
  exposedFiles: new Map(state.rpc.exposedFiles),
428
+ remoteMeta: { ...state.rpc.remoteMeta },
429
+ remoteFiles: new Map(state.rpc.remoteFiles),
428
430
  invokedFunctions: new Set(state.rpc.invokedFunctions),
429
431
  },
430
432
  cli: {
@@ -1052,6 +1054,13 @@ export function filterInspectorState(
1052
1054
  filteredState.rpc.exposedFiles.delete(key)
1053
1055
  }
1054
1056
  }
1057
+ for (const key of Object.keys(filteredState.rpc.remoteMeta)) {
1058
+ const targetFuncId = filteredState.rpc.remoteMeta[key]
1059
+ if (!survivingFuncIds.has(targetFuncId) && !survivingFuncIds.has(key)) {
1060
+ delete filteredState.rpc.remoteMeta[key]
1061
+ filteredState.rpc.remoteFiles.delete(key)
1062
+ }
1063
+ }
1055
1064
  // Prune invokedFunctions to match surviving functions
1056
1065
  for (const funcId of filteredState.rpc.invokedFunctions) {
1057
1066
  if (!survivingFuncIds.has(funcId)) {
@@ -0,0 +1,141 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import * as ts from 'typescript'
4
+ import {
5
+ getArrayPropertyValue,
6
+ getRecordPropertyValue,
7
+ } from './get-property-value.js'
8
+
9
+ /**
10
+ * Parses `const x = { ... }` and hands back the object literal.
11
+ */
12
+ const objectLiteral = (source: string): ts.ObjectLiteralExpression => {
13
+ const file = ts.createSourceFile(
14
+ 'test.ts',
15
+ `const x = ${source}`,
16
+ ts.ScriptTarget.Latest,
17
+ true
18
+ )
19
+ const statement = file.statements[0] as ts.VariableStatement
20
+ const initializer = statement.declarationList.declarations[0]!.initializer
21
+ if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
22
+ throw new Error('expected an object literal')
23
+ }
24
+ return initializer
25
+ }
26
+
27
+ const firstObjectLiteral = (src: string): ts.ObjectLiteralExpression => {
28
+ const sourceFile = ts.createSourceFile(
29
+ 'test.ts',
30
+ src,
31
+ ts.ScriptTarget.Latest,
32
+ true
33
+ )
34
+ let found: ts.ObjectLiteralExpression | undefined
35
+ const walk = (node: ts.Node) => {
36
+ if (!found && ts.isObjectLiteralExpression(node)) {
37
+ found = node
38
+ }
39
+ ts.forEachChild(node, walk)
40
+ }
41
+ walk(sourceFile)
42
+ if (!found) {
43
+ throw new Error('no object literal in source')
44
+ }
45
+ return found
46
+ }
47
+
48
+ describe('getArrayPropertyValue', () => {
49
+ test('reads a plain array literal', () => {
50
+ const obj = objectLiteral(`{ tags: ['a', 'b'] }`)
51
+ assert.deepEqual(getArrayPropertyValue(obj, 'tags'), ['a', 'b'])
52
+ })
53
+
54
+ test('returns null when the property is absent', () => {
55
+ const obj = objectLiteral(`{ other: ['a'] }`)
56
+ assert.equal(getArrayPropertyValue(obj, 'tags'), null)
57
+ })
58
+
59
+ test('reads an array widened with `as const`', () => {
60
+ const obj = objectLiteral(`{ tags: ['a', 'b'] as const }`)
61
+ assert.deepEqual(
62
+ getArrayPropertyValue(obj, 'tags'),
63
+ ['a', 'b'],
64
+ '`as const` on an array is idiomatic and must not drop the value'
65
+ )
66
+ })
67
+
68
+ test('reads an array cast with `as any`', () => {
69
+ const obj = objectLiteral(`{ tags: ['a'] as any }`)
70
+ assert.deepEqual(
71
+ getArrayPropertyValue(obj, 'tags'),
72
+ ['a'],
73
+ 'a cast must not let a value escape static validation'
74
+ )
75
+ })
76
+
77
+ test('reads an array through `satisfies`', () => {
78
+ const obj = objectLiteral(`{ tags: ['a'] satisfies string[] }`)
79
+ assert.deepEqual(getArrayPropertyValue(obj, 'tags'), ['a'])
80
+ })
81
+
82
+ test('reads an array through nested casts', () => {
83
+ const obj = objectLiteral(`{ tags: ['a'] as const as any }`)
84
+ assert.deepEqual(getArrayPropertyValue(obj, 'tags'), ['a'])
85
+ })
86
+
87
+ test('ignores non-string elements', () => {
88
+ const obj = objectLiteral(`{ tags: ['a', 1, foo] }`)
89
+ assert.deepEqual(getArrayPropertyValue(obj, 'tags'), ['a'])
90
+ })
91
+
92
+ test('returns null for a non-array initializer', () => {
93
+ const obj = objectLiteral(`{ tags: 'a' }`)
94
+ assert.equal(getArrayPropertyValue(obj, 'tags'), null)
95
+ })
96
+ })
97
+
98
+ describe('getRecordPropertyValue', () => {
99
+ test('extracts a string record', () => {
100
+ const obj = firstObjectLiteral(
101
+ `const x = { additionalParams: { access_type: 'offline', prompt: 'consent' } }`
102
+ )
103
+ assert.deepStrictEqual(getRecordPropertyValue(obj, 'additionalParams'), {
104
+ access_type: 'offline',
105
+ prompt: 'consent',
106
+ })
107
+ })
108
+
109
+ test('handles quoted keys', () => {
110
+ const obj = firstObjectLiteral(
111
+ `const x = { additionalParams: { 'token_access_type': 'offline' } }`
112
+ )
113
+ assert.deepStrictEqual(getRecordPropertyValue(obj, 'additionalParams'), {
114
+ token_access_type: 'offline',
115
+ })
116
+ })
117
+
118
+ test('returns null when the property is absent', () => {
119
+ const obj = firstObjectLiteral(`const x = { scopes: ['a'] }`)
120
+ assert.strictEqual(getRecordPropertyValue(obj, 'additionalParams'), null)
121
+ })
122
+
123
+ test('returns null for a non-literal initializer', () => {
124
+ const obj = firstObjectLiteral(`const x = { additionalParams: SOME_CONST }`)
125
+ assert.strictEqual(getRecordPropertyValue(obj, 'additionalParams'), null)
126
+ })
127
+
128
+ test('skips non-string values rather than coercing them', () => {
129
+ const obj = firstObjectLiteral(
130
+ `const x = { additionalParams: { a: 'keep', b: 42, c: true } }`
131
+ )
132
+ assert.deepStrictEqual(getRecordPropertyValue(obj, 'additionalParams'), {
133
+ a: 'keep',
134
+ })
135
+ })
136
+
137
+ test('returns null for an empty record', () => {
138
+ const obj = firstObjectLiteral(`const x = { additionalParams: {} }`)
139
+ assert.strictEqual(getRecordPropertyValue(obj, 'additionalParams'), null)
140
+ })
141
+ })