@pikku/inspector 0.12.42 → 0.12.44

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 (126) hide show
  1. package/CHANGELOG.md +266 -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 +5 -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-keyed-wiring.js +2 -0
  10. package/dist/add/add-mcp-prompt.js +0 -1
  11. package/dist/add/add-mcp-resource.js +0 -1
  12. package/dist/add/add-permission.d.ts +1 -1
  13. package/dist/add/add-permission.js +3 -177
  14. package/dist/add/add-queue-worker.js +3 -0
  15. package/dist/add/add-schedule.js +3 -0
  16. package/dist/add/add-scope.d.ts +2 -0
  17. package/dist/add/add-scope.js +146 -0
  18. package/dist/add/add-trigger.js +3 -0
  19. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  20. package/dist/add/add-wire-remote-addon.js +66 -0
  21. package/dist/add/add-workflow-graph.js +32 -3
  22. package/dist/add/add-workflow.js +9 -4
  23. package/dist/error-codes.d.ts +3 -0
  24. package/dist/error-codes.js +4 -0
  25. package/dist/inspector.js +11 -5
  26. package/dist/types.d.ts +19 -13
  27. package/dist/utils/custom-types-generator.js +2 -1
  28. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  29. package/dist/utils/ensure-function-metadata.js +24 -0
  30. package/dist/utils/filter-inspector-state.js +9 -0
  31. package/dist/utils/get-property-value.d.ts +7 -2
  32. package/dist/utils/get-property-value.js +48 -1
  33. package/dist/utils/load-addon-functions-meta.js +81 -6
  34. package/dist/utils/permissions.d.ts +1 -21
  35. package/dist/utils/permissions.js +17 -108
  36. package/dist/utils/post-process.d.ts +30 -1
  37. package/dist/utils/post-process.js +150 -72
  38. package/dist/utils/schema-generator.js +19 -4
  39. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  40. package/dist/utils/serialize-inspector-state.js +12 -6
  41. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  42. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  43. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  44. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
  45. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +274 -100
  46. package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
  47. package/dist/utils/workflow/dsl/patterns.js +11 -0
  48. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +76 -5
  49. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  50. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  51. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  52. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  53. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -4
  54. package/dist/visit.js +4 -0
  55. package/package.json +3 -3
  56. package/run-tests.sh +1 -1
  57. package/src/add/add-addon-bans.ts +1 -0
  58. package/src/add/add-ai-agent.test.ts +87 -0
  59. package/src/add/add-ai-agent.ts +122 -0
  60. package/src/add/add-credential.ts +8 -0
  61. package/src/add/add-functions.ts +10 -12
  62. package/src/add/add-gateway.ts +11 -0
  63. package/src/add/add-http-route.ts +2 -8
  64. package/src/add/add-keyed-wiring.ts +2 -0
  65. package/src/add/add-mcp-prompt.ts +0 -1
  66. package/src/add/add-mcp-resource.ts +0 -1
  67. package/src/add/add-permission.ts +4 -242
  68. package/src/add/add-queue-worker.ts +11 -0
  69. package/src/add/add-schedule.ts +11 -0
  70. package/src/add/add-scope.test.ts +346 -0
  71. package/src/add/add-scope.ts +225 -0
  72. package/src/add/add-trigger.ts +11 -0
  73. package/src/add/add-wire-remote-addon.ts +77 -0
  74. package/src/add/add-workflow-edge-cases.test.ts +201 -0
  75. package/src/add/add-workflow-fanout.test.ts +60 -0
  76. package/src/add/add-workflow-graph-input.test.ts +94 -0
  77. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  78. package/src/add/add-workflow-graph.ts +35 -3
  79. package/src/add/add-workflow-on-error.test.ts +118 -0
  80. package/src/add/add-workflow-return-switch.test.ts +121 -0
  81. package/src/add/add-workflow-silent-drops.test.ts +163 -0
  82. package/src/add/add-workflow-sleep-expression.test.ts +97 -0
  83. package/src/add/add-workflow.ts +9 -4
  84. package/src/add/config-value-metadata.test.ts +169 -0
  85. package/src/add/inline-wiring-function.test.ts +104 -0
  86. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  87. package/src/error-codes.ts +5 -0
  88. package/src/inspector.ts +14 -4
  89. package/src/types.ts +16 -17
  90. package/src/utils/custom-types-generator.test.ts +26 -1
  91. package/src/utils/custom-types-generator.ts +2 -1
  92. package/src/utils/ensure-function-metadata.ts +42 -0
  93. package/src/utils/filter-inspector-state.test.ts +0 -2
  94. package/src/utils/filter-inspector-state.ts +9 -0
  95. package/src/utils/get-property-value.test.ts +141 -0
  96. package/src/utils/get-property-value.ts +62 -1
  97. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  98. package/src/utils/load-addon-functions-meta.ts +94 -6
  99. package/src/utils/load-addon-scopes.test.ts +138 -0
  100. package/src/utils/permissions.test.ts +7 -232
  101. package/src/utils/permissions.ts +20 -160
  102. package/src/utils/post-process.test.ts +269 -4
  103. package/src/utils/post-process.ts +216 -95
  104. package/src/utils/schema-generator.test.ts +164 -0
  105. package/src/utils/schema-generator.ts +26 -3
  106. package/src/utils/serialize-inspector-state.ts +22 -25
  107. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  108. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  109. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  110. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
  111. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
  112. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
  113. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
  114. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
  115. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
  116. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
  117. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +311 -105
  118. package/src/utils/workflow/dsl/patterns.ts +18 -0
  119. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +82 -10
  120. package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
  121. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  122. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  123. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  124. package/src/utils/workflow/graph/workflow-graph.types.ts +11 -2
  125. package/src/visit.ts +4 -0
  126. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,97 @@
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
+
9
+ const STEPS = [
10
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
11
+ "export const notify = pikkuSessionlessFunc({ func: async () => ({ id: 'n' }) })",
12
+ ].join('\n')
13
+
14
+ async function run(body: string) {
15
+ const diags: string[] = []
16
+ const logger = {
17
+ debug: () => {},
18
+ info: () => {},
19
+ warn: () => {},
20
+ error: () => {},
21
+ diagnostic: (...a: any[]) => diags.push(a.map(String).join(' ')),
22
+ critical: (...a: any[]) => diags.push(a.map(String).join(' ')),
23
+ hasCriticalErrors: () => false,
24
+ } as unknown as InspectorLogger
25
+
26
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-sleep-'))
27
+ const stepFile = join(rootDir, 'w.steps.ts')
28
+ const wfFile = join(rootDir, 'w.workflow.ts')
29
+ await writeFile(stepFile, STEPS)
30
+ await writeFile(
31
+ wfFile,
32
+ [
33
+ "import { pikkuWorkflowFunc } from '@pikku/core/workflow'",
34
+ 'export const wf = pikkuWorkflowFunc(async (_, data: any, { workflow }) => {',
35
+ body,
36
+ '})',
37
+ ].join('\n')
38
+ )
39
+ try {
40
+ const state: any = await inspect(logger, [stepFile, wfFile], { rootDir })
41
+ const gm = state.workflows.graphMeta
42
+ const graph = (gm instanceof Map ? [...gm.values()] : Object.values(gm))[0]
43
+ return { graph: graph as any, diags }
44
+ } finally {
45
+ await rm(rootDir, { recursive: true, force: true })
46
+ }
47
+ }
48
+
49
+ describe('sleep with a duration that is not a literal', () => {
50
+ test('a variable duration inside a loop is kept, not rejected', async () => {
51
+ const { graph, diags } = await run(
52
+ [
53
+ ' for (const interval of data.intervals) {',
54
+ ' await workflow.sleep(`Wait ${interval}`, interval)',
55
+ " await workflow.do('Notify', 'notify', {})",
56
+ ' }',
57
+ ' return { sent: true }',
58
+ ].join('\n')
59
+ )
60
+
61
+ assert.deepEqual(
62
+ diags.filter((d) => d.includes('sleep')),
63
+ [],
64
+ 'a duration only known at runtime is legal — the closure evaluates it'
65
+ )
66
+
67
+ const sleepNode = Object.values(graph?.nodes ?? {}).find(
68
+ (n: any) => n.flow === 'sleep'
69
+ ) as any
70
+ assert.ok(sleepNode, 'the sleep must still appear in the graph')
71
+ assert.equal(
72
+ sleepNode.expression,
73
+ 'interval',
74
+ 'its source text is recorded so the graph can show what it waits on'
75
+ )
76
+ })
77
+
78
+ test('a literal duration still round-trips as a value', async () => {
79
+ const { graph, diags } = await run(
80
+ [
81
+ " await workflow.sleep('Wait', '5s')",
82
+ ' return { sent: true }',
83
+ ].join('\n')
84
+ )
85
+
86
+ assert.deepEqual(diags, [])
87
+ const sleepNode = Object.values(graph?.nodes ?? {}).find(
88
+ (n: any) => n.flow === 'sleep'
89
+ ) as any
90
+ assert.equal(sleepNode?.duration, '5s')
91
+ assert.equal(
92
+ sleepNode?.expression,
93
+ undefined,
94
+ 'a literal is a value, not an expression'
95
+ )
96
+ })
97
+ })
@@ -73,8 +73,8 @@ function hasInlineSteps(steps: WorkflowStepMeta[]): boolean {
73
73
  if (c.steps && hasInlineSteps(c.steps)) return true
74
74
  }
75
75
  if (step.defaultSteps && hasInlineSteps(step.defaultSteps)) return true
76
- } else if (step.type === 'fanout' && step.child) {
77
- if (hasInlineSteps([step.child])) return true
76
+ } else if (step.type === 'fanout' && step.body) {
77
+ if (hasInlineSteps(step.body)) return true
78
78
  } else if (step.type === 'parallel' && step.children) {
79
79
  if (hasInlineSteps(step.children)) return true
80
80
  }
@@ -92,6 +92,11 @@ export function collectInvokedRPCs(
92
92
  for (const step of steps) {
93
93
  if (step.type === 'rpc' && step.rpcName) {
94
94
  rpcs.add(step.rpcName)
95
+ // The compensation handler is invoked at runtime too, so it has to be
96
+ // wired like any other step.
97
+ if (step.options?.onError) {
98
+ rpcs.add(step.options.onError)
99
+ }
95
100
  } else if (step.type === 'branch') {
96
101
  for (const branch of step.branches) {
97
102
  collectInvokedRPCs(branch.steps, rpcs)
@@ -102,8 +107,8 @@ export function collectInvokedRPCs(
102
107
  if (c.steps) collectInvokedRPCs(c.steps, rpcs)
103
108
  }
104
109
  if (step.defaultSteps) collectInvokedRPCs(step.defaultSteps, rpcs)
105
- } else if (step.type === 'fanout' && step.child) {
106
- collectInvokedRPCs([step.child], rpcs)
110
+ } else if (step.type === 'fanout' && step.body) {
111
+ collectInvokedRPCs(step.body, rpcs)
107
112
  } else if (step.type === 'parallel' && step.children) {
108
113
  collectInvokedRPCs(step.children, rpcs)
109
114
  }
@@ -0,0 +1,169 @@
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 { join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { inspect } from '../inspector.js'
7
+ import type { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ // Fixtures must sit inside the package so `zod` resolves — the schema-vendor
11
+ // check rejects a schema it cannot trace to a supported library, and the
12
+ // definition is then never recorded at all.
13
+ const fixtureRoot = join(
14
+ fileURLToPath(new URL('.', import.meta.url)),
15
+ '__fixtures'
16
+ )
17
+
18
+ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
19
+ ({
20
+ debug: () => {},
21
+ info: () => {},
22
+ warn: () => {},
23
+ error: () => {},
24
+ diagnostic: ({ code, message }) => {
25
+ criticals.push({ code, message })
26
+ },
27
+ critical: (code: ErrorCode, message: string) => {
28
+ criticals.push({ code, message })
29
+ },
30
+ hasCriticalErrors: () => criticals.length > 0,
31
+ }) satisfies InspectorLogger
32
+
33
+ /**
34
+ * `docsUrl` is only useful if it survives inspection — a field declared on the
35
+ * wiring but dropped by the inspector is invisible to every consumer
36
+ * downstream, which is exactly how `displayName` went unused.
37
+ */
38
+ describe('config value metadata (docsUrl)', () => {
39
+ test('carries docsUrl from wireSecret into the definition', async () => {
40
+ const rootDir = await mkdtemp(`${fixtureRoot}-secret-`)
41
+ const file = join(rootDir, 'secret.ts')
42
+
43
+ await writeFile(
44
+ file,
45
+ [
46
+ "import { wireSecret } from '@pikku/core'",
47
+ "import { z } from 'zod'",
48
+ 'export const StripeKeySchema = z.string()',
49
+ 'wireSecret({',
50
+ " name: 'stripeKey',",
51
+ " displayName: 'Stripe Secret Key',",
52
+ " secretId: 'STRIPE_SECRET_KEY',",
53
+ " docsUrl: 'https://dashboard.stripe.com/apikeys',",
54
+ ' schema: StripeKeySchema,',
55
+ '})',
56
+ ].join('\n')
57
+ )
58
+
59
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
60
+ try {
61
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
62
+ const def = state.secrets.definitions.find(
63
+ (d) => d.secretId === 'STRIPE_SECRET_KEY'
64
+ )
65
+ assert.ok(def, 'expected the secret definition to be inspected')
66
+ assert.equal(def!.docsUrl, 'https://dashboard.stripe.com/apikeys')
67
+ } finally {
68
+ await rm(rootDir, { recursive: true, force: true })
69
+ }
70
+ })
71
+
72
+ test('leaves docsUrl undefined when not declared', async () => {
73
+ const rootDir = await mkdtemp(`${fixtureRoot}-bare-`)
74
+ const file = join(rootDir, 'secret.ts')
75
+
76
+ await writeFile(
77
+ file,
78
+ [
79
+ "import { wireSecret } from '@pikku/core'",
80
+ "import { z } from 'zod'",
81
+ 'export const ApiKeySchema = z.string()',
82
+ 'wireSecret({',
83
+ " name: 'apiKey',",
84
+ " displayName: 'API Key',",
85
+ " secretId: 'API_KEY',",
86
+ ' schema: ApiKeySchema,',
87
+ '})',
88
+ ].join('\n')
89
+ )
90
+
91
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
92
+ try {
93
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
94
+ const def = state.secrets.definitions.find(
95
+ (d) => d.secretId === 'API_KEY'
96
+ )
97
+ assert.ok(def, 'expected the secret definition to be inspected')
98
+ assert.equal(def!.docsUrl, undefined)
99
+ } finally {
100
+ await rm(rootDir, { recursive: true, force: true })
101
+ }
102
+ })
103
+
104
+ test('carries docsUrl from wireVariable into the definition', async () => {
105
+ const rootDir = await mkdtemp(`${fixtureRoot}-variable-`)
106
+ const file = join(rootDir, 'variable.ts')
107
+
108
+ await writeFile(
109
+ file,
110
+ [
111
+ "import { wireVariable } from '@pikku/core'",
112
+ "import { z } from 'zod'",
113
+ 'export const ConsoleUrlSchema = z.string()',
114
+ 'wireVariable({',
115
+ " name: 'consoleUrl',",
116
+ " displayName: 'Console URL',",
117
+ " variableId: 'CONSOLE_URL',",
118
+ " docsUrl: 'https://example.com/docs/console-url',",
119
+ ' schema: ConsoleUrlSchema,',
120
+ '})',
121
+ ].join('\n')
122
+ )
123
+
124
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
125
+ try {
126
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
127
+ const def = state.variables.definitions.find(
128
+ (d) => d.variableId === 'CONSOLE_URL'
129
+ )
130
+ assert.ok(def, 'expected the variable definition to be inspected')
131
+ assert.equal(def!.docsUrl, 'https://example.com/docs/console-url')
132
+ } finally {
133
+ await rm(rootDir, { recursive: true, force: true })
134
+ }
135
+ })
136
+
137
+ test('carries docsUrl from wireCredential into the definition', async () => {
138
+ const rootDir = await mkdtemp(`${fixtureRoot}-credential-`)
139
+ const file = join(rootDir, 'credential.ts')
140
+
141
+ await writeFile(
142
+ file,
143
+ [
144
+ "import { wireCredential } from '@pikku/core'",
145
+ "import { z } from 'zod'",
146
+ 'export const TokenSchema = z.object({ token: z.string() })',
147
+ 'wireCredential({',
148
+ " name: 'githubToken',",
149
+ " displayName: 'GitHub Token',",
150
+ " type: 'wire',",
151
+ " docsUrl: 'https://github.com/settings/tokens',",
152
+ ' schema: TokenSchema,',
153
+ '})',
154
+ ].join('\n')
155
+ )
156
+
157
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
158
+ try {
159
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
160
+ const def = state.credentials.definitions.find(
161
+ (d) => d.name === 'githubToken'
162
+ )
163
+ assert.ok(def, 'expected the credential definition to be inspected')
164
+ assert.equal(def!.docsUrl, 'https://github.com/settings/tokens')
165
+ } finally {
166
+ await rm(rootDir, { recursive: true, force: true })
167
+ }
168
+ })
169
+ })
@@ -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