@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.
- package/CHANGELOG.md +266 -0
- package/dist/add/add-addon-bans.js +1 -0
- package/dist/add/add-ai-agent.d.ts +3 -1
- package/dist/add/add-ai-agent.js +82 -0
- package/dist/add/add-credential.js +5 -1
- package/dist/add/add-functions.js +10 -9
- package/dist/add/add-gateway.js +3 -0
- package/dist/add/add-http-route.js +2 -2
- package/dist/add/add-keyed-wiring.js +2 -0
- package/dist/add/add-mcp-prompt.js +0 -1
- package/dist/add/add-mcp-resource.js +0 -1
- package/dist/add/add-permission.d.ts +1 -1
- package/dist/add/add-permission.js +3 -177
- package/dist/add/add-queue-worker.js +3 -0
- package/dist/add/add-schedule.js +3 -0
- package/dist/add/add-scope.d.ts +2 -0
- package/dist/add/add-scope.js +146 -0
- package/dist/add/add-trigger.js +3 -0
- package/dist/add/add-wire-remote-addon.d.ts +10 -0
- package/dist/add/add-wire-remote-addon.js +66 -0
- package/dist/add/add-workflow-graph.js +32 -3
- package/dist/add/add-workflow.js +9 -4
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +4 -0
- package/dist/inspector.js +11 -5
- package/dist/types.d.ts +19 -13
- package/dist/utils/custom-types-generator.js +2 -1
- package/dist/utils/ensure-function-metadata.d.ts +15 -0
- package/dist/utils/ensure-function-metadata.js +24 -0
- package/dist/utils/filter-inspector-state.js +9 -0
- package/dist/utils/get-property-value.d.ts +7 -2
- package/dist/utils/get-property-value.js +48 -1
- package/dist/utils/load-addon-functions-meta.js +81 -6
- package/dist/utils/permissions.d.ts +1 -21
- package/dist/utils/permissions.js +17 -108
- package/dist/utils/post-process.d.ts +30 -1
- package/dist/utils/post-process.js +150 -72
- package/dist/utils/schema-generator.js +19 -4
- package/dist/utils/serialize-inspector-state.d.ts +13 -8
- package/dist/utils/serialize-inspector-state.js +12 -6
- package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
- package/dist/utils/serialize-permissions-groups-meta.js +0 -26
- package/dist/utils/workflow/derive-workflow-plan.js +9 -3
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +274 -100
- package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
- package/dist/utils/workflow/dsl/patterns.js +11 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +76 -5
- package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -4
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/run-tests.sh +1 -1
- package/src/add/add-addon-bans.ts +1 -0
- package/src/add/add-ai-agent.test.ts +87 -0
- package/src/add/add-ai-agent.ts +122 -0
- package/src/add/add-credential.ts +8 -0
- package/src/add/add-functions.ts +10 -12
- package/src/add/add-gateway.ts +11 -0
- package/src/add/add-http-route.ts +2 -8
- package/src/add/add-keyed-wiring.ts +2 -0
- package/src/add/add-mcp-prompt.ts +0 -1
- package/src/add/add-mcp-resource.ts +0 -1
- package/src/add/add-permission.ts +4 -242
- package/src/add/add-queue-worker.ts +11 -0
- package/src/add/add-schedule.ts +11 -0
- package/src/add/add-scope.test.ts +346 -0
- package/src/add/add-scope.ts +225 -0
- package/src/add/add-trigger.ts +11 -0
- package/src/add/add-wire-remote-addon.ts +77 -0
- package/src/add/add-workflow-edge-cases.test.ts +201 -0
- package/src/add/add-workflow-fanout.test.ts +60 -0
- package/src/add/add-workflow-graph-input.test.ts +94 -0
- package/src/add/add-workflow-graph-notes.test.ts +74 -0
- package/src/add/add-workflow-graph.ts +35 -3
- package/src/add/add-workflow-on-error.test.ts +118 -0
- package/src/add/add-workflow-return-switch.test.ts +121 -0
- package/src/add/add-workflow-silent-drops.test.ts +163 -0
- package/src/add/add-workflow-sleep-expression.test.ts +97 -0
- package/src/add/add-workflow.ts +9 -4
- package/src/add/config-value-metadata.test.ts +169 -0
- package/src/add/inline-wiring-function.test.ts +104 -0
- package/src/add/validate-workflow-graph-addons.test.ts +102 -0
- package/src/error-codes.ts +5 -0
- package/src/inspector.ts +14 -4
- package/src/types.ts +16 -17
- package/src/utils/custom-types-generator.test.ts +26 -1
- package/src/utils/custom-types-generator.ts +2 -1
- package/src/utils/ensure-function-metadata.ts +42 -0
- package/src/utils/filter-inspector-state.test.ts +0 -2
- package/src/utils/filter-inspector-state.ts +9 -0
- package/src/utils/get-property-value.test.ts +141 -0
- package/src/utils/get-property-value.ts +62 -1
- package/src/utils/load-addon-functions-meta.test.ts +157 -0
- package/src/utils/load-addon-functions-meta.ts +94 -6
- package/src/utils/load-addon-scopes.test.ts +138 -0
- package/src/utils/permissions.test.ts +7 -232
- package/src/utils/permissions.ts +20 -160
- package/src/utils/post-process.test.ts +269 -4
- package/src/utils/post-process.ts +216 -95
- package/src/utils/schema-generator.test.ts +164 -0
- package/src/utils/schema-generator.ts +26 -3
- package/src/utils/serialize-inspector-state.ts +22 -25
- package/src/utils/serialize-permissions-groups-meta.ts +0 -28
- package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
- package/src/utils/workflow/derive-workflow-plan.ts +11 -4
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +311 -105
- package/src/utils/workflow/dsl/patterns.ts +18 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +82 -10
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
- package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +11 -2
- package/src/visit.ts +4 -0
- 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
|
+
})
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -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.
|
|
77
|
-
if (hasInlineSteps(
|
|
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.
|
|
106
|
-
collectInvokedRPCs(
|
|
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
|
+
})
|
package/src/error-codes.ts
CHANGED
|
@@ -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
|