@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.
- package/CHANGELOG.md +98 -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 +3 -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-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/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/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/extract-dsl-workflow.js +56 -1
- 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 +14 -0
- 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 +6 -2
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- 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 +6 -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-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-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/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/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/extract-dsl-workflow.ts +66 -0
- package/src/utils/workflow/dsl/patterns.ts +18 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -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 +8 -0
- package/src/visit.ts +4 -0
- 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
|
+
})
|
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
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { test, describe } from 'node:test'
|
|
2
2
|
import { strict as assert } from 'node:assert'
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
+
})
|