@pikku/inspector 0.12.59 → 0.12.61
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 +373 -0
- package/OPTIMIZATION-PLAN.md +19 -19
- package/dist/add/add-agent-scorer.d.ts +2 -0
- package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
- package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
- package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +5 -0
- package/dist/add/add-functions.js +10 -1
- package/dist/add/add-http-route.js +18 -18
- package/dist/add/add-keyed-wiring.d.ts +2 -0
- package/dist/add/add-keyed-wiring.js +3 -1
- package/dist/add/add-middleware.js +5 -5
- package/dist/add/add-personas.js +3 -0
- package/dist/add/add-secret.js +1 -0
- package/dist/add/add-variable.js +1 -0
- package/dist/add/add-wire-remote-addon.js +2 -1
- package/dist/add/add-workflow-graph.js +63 -6
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/inspector.js +13 -3
- package/dist/types.d.ts +10 -5
- package/dist/utils/annotate-http-route-auth.js +2 -1
- package/dist/utils/check-secret-sinks.js +2 -1
- package/dist/utils/compute-required-schemas.d.ts +1 -1
- package/dist/utils/contract-hashes.d.ts +2 -1
- package/dist/utils/contract-hashes.js +1 -1
- package/dist/utils/extract-function-name.js +1 -1
- package/dist/utils/extract-services.d.ts +1 -1
- package/dist/utils/extract-surface-usage.d.ts +29 -0
- package/dist/utils/extract-surface-usage.js +97 -0
- package/dist/utils/filter-inspector-state.js +53 -16
- package/dist/utils/filter-utils.d.ts +1 -1
- package/dist/utils/get-property-value.js +42 -3
- package/dist/utils/load-addon-functions-meta.js +31 -0
- package/dist/utils/middleware.d.ts +2 -2
- package/dist/utils/middleware.js +5 -5
- package/dist/utils/permissions.d.ts +1 -1
- package/dist/utils/post-process.d.ts +28 -8
- package/dist/utils/post-process.js +83 -18
- package/dist/utils/resolve-deploy-target.d.ts +1 -1
- package/dist/utils/resolve-function-meta.d.ts +1 -1
- package/dist/utils/resolve-function-types.d.ts +1 -1
- package/dist/utils/resolve-versions.js +1 -1
- package/dist/utils/schema-generator.d.ts +1 -1
- package/dist/utils/serialize-inspector-state.d.ts +4 -3
- package/dist/utils/serialize-inspector-state.js +6 -4
- package/dist/utils/serialize-openapi-json.d.ts +1 -1
- package/dist/utils/validate-auth-sessionless.js +2 -1
- package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
- package/dist/visit.js +4 -4
- package/package.json +10 -10
- package/run-tests.sh +0 -0
- package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
- package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
- package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
- package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
- package/src/add/add-auth.ts +2 -2
- package/src/add/add-cli.ts +6 -0
- package/src/add/add-credential.ts +3 -6
- package/src/add/add-file-with-factory.ts +2 -6
- package/src/add/add-functions.ts +23 -25
- package/src/add/add-http-route-ref.test.ts +149 -0
- package/src/add/add-http-route.ts +25 -28
- package/src/add/add-keyed-wiring.ts +8 -7
- package/src/add/add-middleware.ts +7 -5
- package/src/add/add-permission.ts +1 -1
- package/src/add/add-personas.test.ts +24 -0
- package/src/add/add-personas.ts +3 -1
- package/src/add/add-scenario-step.test.ts +35 -0
- package/src/add/add-scope.test.ts +4 -4
- package/src/add/add-scope.ts +4 -8
- package/src/add/add-secret.ts +1 -0
- package/src/add/add-system-role.test.ts +8 -8
- package/src/add/add-system-role.ts +2 -4
- package/src/add/add-variable.ts +1 -0
- package/src/add/add-wire-remote-addon.ts +4 -1
- package/src/add/add-workflow-graph-foreach.test.ts +174 -0
- package/src/add/add-workflow-graph.ts +78 -6
- package/src/add/add-workflow-sleep-expression.test.ts +3 -4
- package/src/add/addon-contracts.test.ts +1 -1
- package/src/add/config-value-metadata.test.ts +94 -0
- package/src/add/secret-sink-check.test.ts +4 -1
- package/src/error-codes.ts +3 -1
- package/src/index.ts +3 -4
- package/src/inspector.ts +20 -3
- package/src/types.ts +10 -7
- package/src/utils/annotate-http-route-auth.test.ts +5 -7
- package/src/utils/annotate-http-route-auth.ts +2 -1
- package/src/utils/check-secret-sinks.ts +4 -1
- package/src/utils/compute-required-schemas.ts +1 -1
- package/src/utils/contract-hashes.test.ts +1 -1
- package/src/utils/contract-hashes.ts +3 -2
- package/src/utils/extract-function-name.ts +1 -1
- package/src/utils/extract-services.ts +4 -2
- package/src/utils/extract-surface-usage.test.ts +147 -0
- package/src/utils/extract-surface-usage.ts +121 -0
- package/src/utils/filter-inspector-state.test.ts +138 -12
- package/src/utils/filter-inspector-state.ts +82 -21
- package/src/utils/filter-utils.ts +1 -1
- package/src/utils/get-property-value.ts +43 -5
- package/src/utils/load-addon-functions-meta.ts +40 -0
- package/src/utils/middleware.test.ts +23 -23
- package/src/utils/middleware.ts +6 -6
- package/src/utils/permissions.ts +1 -1
- package/src/utils/post-process.test.ts +68 -11
- package/src/utils/post-process.ts +91 -28
- package/src/utils/resolve-deploy-target.ts +1 -1
- package/src/utils/resolve-function-meta.ts +1 -1
- package/src/utils/resolve-function-types.ts +1 -1
- package/src/utils/resolve-versions.ts +1 -1
- package/src/utils/schema-generator.ts +2 -1
- package/src/utils/serialize-inspector-state.ts +10 -7
- package/src/utils/serialize-mcp-json.ts +1 -1
- package/src/utils/serialize-openapi-json.ts +1 -1
- package/src/utils/validate-auth-sessionless.ts +2 -1
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
- package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
- package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
- package/src/visit.ts +4 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add/add-ai-scorer.d.ts +0 -2
- package/dist/utils/secret-brokers.d.ts +0 -2
- package/dist/utils/secret-brokers.js +0 -9
package/src/add/add-functions.ts
CHANGED
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
} from '../utils/extract-function-name.js'
|
|
9
9
|
import { extractFunctionNode } from '../utils/extract-function-node.js'
|
|
10
10
|
import { extractUsedWires } from '../utils/extract-services.js'
|
|
11
|
-
import type { AuditDurability
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
11
|
+
import type { AuditDurability } from '@pikku/core/services'
|
|
12
|
+
import type { FunctionServicesMeta } from '@pikku/core/function'
|
|
13
|
+
import type { ScenarioSurface, ScenarioStepKind } from '@pikku/core/scenario'
|
|
14
|
+
import { formatVersionedId, parseVersionedId } from '@pikku/core/utils'
|
|
14
15
|
|
|
15
16
|
/** Binding keys a `pikkuScenarioStep` may declare, in report order. */
|
|
16
17
|
const SCENARIO_SURFACE_KEYS: readonly ScenarioSurface[] = [
|
|
@@ -592,6 +593,7 @@ export const addFunctions: AddWiring = (
|
|
|
592
593
|
let workflowRetries: number | undefined
|
|
593
594
|
let workflowTimeout: string | undefined
|
|
594
595
|
let scenarioStepSurfaces: ScenarioSurface[] | undefined
|
|
596
|
+
let scenarioStepRequiresActor: boolean | undefined
|
|
595
597
|
let scenarioStepAddon: string | undefined
|
|
596
598
|
let scenarioStepTemplate: string | undefined
|
|
597
599
|
let scopes: string[] | undefined
|
|
@@ -683,8 +685,7 @@ export const addFunctions: AddWiring = (
|
|
|
683
685
|
mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
|
|
684
686
|
auth = getPropertyValue(firstArg, 'auth') as boolean | undefined
|
|
685
687
|
permissionsInBody = getPropertyValue(firstArg, 'permissionsInBody') as
|
|
686
|
-
|
|
|
687
|
-
| undefined
|
|
688
|
+
boolean | undefined
|
|
688
689
|
if (permissionsInBody && !options.allow?.permissionsInBody) {
|
|
689
690
|
logger.critical(
|
|
690
691
|
ErrorCode.PERMISSIONS_IN_BODY_NOT_ALLOWED,
|
|
@@ -721,22 +722,15 @@ export const addFunctions: AddWiring = (
|
|
|
721
722
|
}
|
|
722
723
|
readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
|
|
723
724
|
deploy = getPropertyValue(firstArg, 'deploy') as
|
|
724
|
-
| '
|
|
725
|
-
| 'server'
|
|
726
|
-
| 'auto'
|
|
727
|
-
| undefined
|
|
725
|
+
'serverless' | 'server' | 'auto' | undefined
|
|
728
726
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
729
|
-
|
|
|
730
|
-
| undefined
|
|
727
|
+
boolean | undefined
|
|
731
728
|
workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
|
|
732
|
-
|
|
|
733
|
-
| undefined
|
|
729
|
+
boolean | undefined
|
|
734
730
|
workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as
|
|
735
|
-
|
|
|
736
|
-
| undefined
|
|
731
|
+
number | undefined
|
|
737
732
|
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as
|
|
738
|
-
|
|
|
739
|
-
| undefined
|
|
733
|
+
string | undefined
|
|
740
734
|
// A step declares one implementation per surface it supports; the binding
|
|
741
735
|
// keys that are present ARE the declaration, so there is nothing to read a
|
|
742
736
|
// value from — only names to collect.
|
|
@@ -767,8 +761,7 @@ export const addFunctions: AddWiring = (
|
|
|
767
761
|
scenarioStepSurfaces = ['default']
|
|
768
762
|
if (scenarioStepKind === 'addon') {
|
|
769
763
|
scenarioStepAddon = getPropertyValue(firstArg, 'addon') as
|
|
770
|
-
|
|
|
771
|
-
| undefined
|
|
764
|
+
string | undefined
|
|
772
765
|
if (!scenarioStepAddon) {
|
|
773
766
|
logger.error(
|
|
774
767
|
`• pikkuAddonScenarioStep('${name}') needs a string literal \`addon\` naming the addon whose system acts — the same name its wireAddon declares.`
|
|
@@ -780,10 +773,16 @@ export const addFunctions: AddWiring = (
|
|
|
780
773
|
scenarioStepSurfaces = declaredSurfaces.length
|
|
781
774
|
? declaredSurfaces
|
|
782
775
|
: undefined
|
|
776
|
+
// A browser binding cannot be provisioned without a window to open, so it
|
|
777
|
+
// requires an actor whether or not the step said so.
|
|
778
|
+
scenarioStepRequiresActor =
|
|
779
|
+
getPropertyValue(firstArg, 'actor') === true ||
|
|
780
|
+
declaredSurfaces.includes('browser')
|
|
781
|
+
? true
|
|
782
|
+
: undefined
|
|
783
783
|
}
|
|
784
784
|
scenarioStepTemplate = getPropertyValue(firstArg, 'template') as
|
|
785
|
-
|
|
|
786
|
-
| undefined
|
|
785
|
+
string | undefined
|
|
787
786
|
scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined
|
|
788
787
|
|
|
789
788
|
// Extract approvalDescription identifier reference
|
|
@@ -827,13 +826,11 @@ export const addFunctions: AddWiring = (
|
|
|
827
826
|
) {
|
|
828
827
|
const nodeObj = prop.initializer
|
|
829
828
|
nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
|
|
830
|
-
|
|
|
831
|
-
| null
|
|
829
|
+
string | null
|
|
832
830
|
nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
|
|
833
831
|
nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
|
|
834
832
|
nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
|
|
835
|
-
|
|
|
836
|
-
| null
|
|
833
|
+
boolean | null
|
|
837
834
|
|
|
838
835
|
if (!nodeDisplayName) {
|
|
839
836
|
logger.critical(
|
|
@@ -1376,6 +1373,7 @@ export const addFunctions: AddWiring = (
|
|
|
1376
1373
|
workflowRetries: workflowRetries ?? undefined,
|
|
1377
1374
|
workflowTimeout: workflowTimeout ?? undefined,
|
|
1378
1375
|
scenarioStepSurfaces,
|
|
1376
|
+
scenarioStepRequiresActor,
|
|
1379
1377
|
// `persona` is the default reading of an unmarked step, so it is left off
|
|
1380
1378
|
// rather than written into every step meta a project already has.
|
|
1381
1379
|
scenarioStepKind:
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test, before, after } from 'node:test'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { inspect } from '../inspector.js'
|
|
7
|
+
import { filterInspectorState } from '../utils/filter-inspector-state.js'
|
|
8
|
+
import type { InspectorLogger, InspectorState } from '../types.js'
|
|
9
|
+
|
|
10
|
+
const ADDON_PACKAGE = '@addon/greeter'
|
|
11
|
+
const NAMESPACE = 'ext'
|
|
12
|
+
const TARGET = `${NAMESPACE}:greet`
|
|
13
|
+
|
|
14
|
+
const logger: InspectorLogger = {
|
|
15
|
+
debug: () => {},
|
|
16
|
+
info: () => {},
|
|
17
|
+
warn: () => {},
|
|
18
|
+
error: () => {},
|
|
19
|
+
diagnostic: () => {},
|
|
20
|
+
critical: () => {},
|
|
21
|
+
hasCriticalErrors: () => false,
|
|
22
|
+
} as unknown as InspectorLogger
|
|
23
|
+
|
|
24
|
+
const writeAddonFixture = (rootDir: string) => {
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(rootDir, 'package.json'),
|
|
27
|
+
JSON.stringify({ name: 'consumer' })
|
|
28
|
+
)
|
|
29
|
+
const addonDir = join(rootDir, 'node_modules', ADDON_PACKAGE)
|
|
30
|
+
const pikku = join(addonDir, '.pikku')
|
|
31
|
+
mkdirSync(join(pikku, 'function'), { recursive: true })
|
|
32
|
+
mkdirSync(join(pikku, 'schemas', 'schemas'), { recursive: true })
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(addonDir, 'package.json'),
|
|
35
|
+
JSON.stringify({ name: ADDON_PACKAGE })
|
|
36
|
+
)
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(pikku, 'function', 'pikku-functions-meta.gen.json'),
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
greet: {
|
|
41
|
+
pikkuFuncId: 'greet',
|
|
42
|
+
name: 'greet',
|
|
43
|
+
sessionless: true,
|
|
44
|
+
inputSchemaName: 'GreetInput',
|
|
45
|
+
outputSchemaName: 'GreetOutput',
|
|
46
|
+
inputs: ['GreetInput'],
|
|
47
|
+
outputs: ['GreetOutput'],
|
|
48
|
+
services: { optimized: true, services: ['logger'] },
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
writeFileSync(
|
|
53
|
+
join(pikku, 'schemas', 'schemas', 'GreetInput.schema.json'),
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
id: { type: 'string' },
|
|
58
|
+
loud: { type: 'boolean' },
|
|
59
|
+
name: { type: 'string' },
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const writeWiring = (rootDir: string): string => {
|
|
66
|
+
const file = join(rootDir, 'greet.wiring.ts')
|
|
67
|
+
writeFileSync(
|
|
68
|
+
file,
|
|
69
|
+
[
|
|
70
|
+
`import { ref, wireAddon } from '@pikku/core'`,
|
|
71
|
+
`import { wireHTTP } from '@pikku/core/http'`,
|
|
72
|
+
``,
|
|
73
|
+
`wireAddon({ name: '${NAMESPACE}', package: '${ADDON_PACKAGE}' })`,
|
|
74
|
+
``,
|
|
75
|
+
`wireHTTP({`,
|
|
76
|
+
` auth: false,`,
|
|
77
|
+
` method: 'get',`,
|
|
78
|
+
` route: '/greet/:id',`,
|
|
79
|
+
` func: ref('${TARGET}'),`,
|
|
80
|
+
`})`,
|
|
81
|
+
``,
|
|
82
|
+
`wireHTTP({`,
|
|
83
|
+
` auth: false,`,
|
|
84
|
+
` method: 'post',`,
|
|
85
|
+
` route: '/greet',`,
|
|
86
|
+
` query: ['loud'],`,
|
|
87
|
+
` func: ref('${TARGET}'),`,
|
|
88
|
+
`})`,
|
|
89
|
+
].join('\n')
|
|
90
|
+
)
|
|
91
|
+
return file
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('an HTTP route wired with ref() to an addon function', () => {
|
|
95
|
+
let rootDir: string
|
|
96
|
+
let state: InspectorState
|
|
97
|
+
|
|
98
|
+
before(async () => {
|
|
99
|
+
rootDir = mkdtempSync(join(tmpdir(), 'pikku-http-ref-'))
|
|
100
|
+
writeAddonFixture(rootDir)
|
|
101
|
+
const file = writeWiring(rootDir)
|
|
102
|
+
state = await inspect(logger, [file], { rootDir })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
after(() => {
|
|
106
|
+
rmSync(rootDir, { recursive: true, force: true })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('records the ref target as the route function id', () => {
|
|
110
|
+
assert.equal(state.http.meta.get['/greet/:id']?.pikkuFuncId, TARGET)
|
|
111
|
+
assert.equal(state.http.meta.post['/greet']?.pikkuFuncId, TARGET)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// packageName names the package a wire runs in — its middleware, its
|
|
115
|
+
// services, its scoped credentials. The route belongs to the consuming app
|
|
116
|
+
// however its function is reached, so tagging it with the addon's package
|
|
117
|
+
// would silently swap the app's global middleware for the addon's.
|
|
118
|
+
test("leaves the route in the consuming app's package", () => {
|
|
119
|
+
assert.equal(state.http.meta.get['/greet/:id']?.packageName, undefined)
|
|
120
|
+
assert.equal(state.http.meta.post['/greet']?.packageName, undefined)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('mints no wrapper function metadata for the route', () => {
|
|
124
|
+
const wrapperIds = Object.keys(state.functions.meta).filter((id) =>
|
|
125
|
+
id.startsWith('http:')
|
|
126
|
+
)
|
|
127
|
+
assert.deepEqual(wrapperIds, [])
|
|
128
|
+
assert.equal(state.functions.meta[TARGET], undefined)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('keeps each route its own params and query', () => {
|
|
132
|
+
assert.deepEqual(state.http.meta.get['/greet/:id']?.params, ['id'])
|
|
133
|
+
assert.equal(state.http.meta.get['/greet/:id']?.query, undefined)
|
|
134
|
+
assert.equal(state.http.meta.post['/greet']?.params, undefined)
|
|
135
|
+
assert.deepEqual(state.http.meta.post['/greet']?.query, ['loud'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('marks the addon target as used so tree-shaking keeps it', () => {
|
|
139
|
+
assert.ok(state.serviceAggregation.usedFunctions.has(TARGET))
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('survives a filtered bundle together with its addon declaration', () => {
|
|
143
|
+
const filtered = filterInspectorState(state, { names: [TARGET] }, logger)
|
|
144
|
+
assert.equal(filtered.http.meta.get['/greet/:id']?.pikkuFuncId, TARGET)
|
|
145
|
+
assert.equal(filtered.http.meta.post['/greet']?.pikkuFuncId, TARGET)
|
|
146
|
+
assert.ok(filtered.rpc.wireAddonDeclarations.has(NAMESPACE))
|
|
147
|
+
assert.ok(filtered.serviceAggregation.usedFunctions.has(TARGET))
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -243,6 +243,17 @@ export function registerHTTPRoute({
|
|
|
243
243
|
)
|
|
244
244
|
: null
|
|
245
245
|
|
|
246
|
+
// A ref()-wired route points straight at the addon function, the same way the
|
|
247
|
+
// CLI and channel adders record one. The addon's own metadata is the contract
|
|
248
|
+
// — and it is authoritative where the checker is not: `ref('ns:fn')` only
|
|
249
|
+
// types precisely once the consumer's own rpc map already lists the addon's
|
|
250
|
+
// functions, so a cold run inferred the whole `FlattenedRPCMap` and a warm one
|
|
251
|
+
// inferred the real input. The addon's metadata says which it is either way.
|
|
252
|
+
//
|
|
253
|
+
// The route stays the consuming app's all the same, so it records no
|
|
254
|
+
// packageName: that names the package a wire *runs in*, and running this one
|
|
255
|
+
// inside the addon would hand it the addon's middleware and services instead
|
|
256
|
+
// of the app's. `ref` reaches the addon over RPC, which enters it properly.
|
|
246
257
|
if (refAddonTarget) {
|
|
247
258
|
const targetMeta = resolveFunctionMeta(state, refAddonTarget)
|
|
248
259
|
if (!targetMeta) {
|
|
@@ -251,25 +262,16 @@ export function registerHTTPRoute({
|
|
|
251
262
|
)
|
|
252
263
|
return
|
|
253
264
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Propagate sessionless from the addon target so the runtime can correctly
|
|
266
|
-
// determine whether a session is required for ref()-based routes.
|
|
267
|
-
if (refAddonTarget) {
|
|
268
|
-
const targetMeta = resolveFunctionMeta(state, refAddonTarget)
|
|
269
|
-
const inlineMeta = state.functions.meta[funcName]
|
|
270
|
-
if (targetMeta && inlineMeta && targetMeta.sessionless !== undefined) {
|
|
271
|
-
inlineMeta.sessionless = targetMeta.sessionless
|
|
272
|
-
}
|
|
265
|
+
funcName = refAddonTarget
|
|
266
|
+
} else {
|
|
267
|
+
ensureFunctionMetadata(
|
|
268
|
+
state,
|
|
269
|
+
funcName,
|
|
270
|
+
fullRoute,
|
|
271
|
+
funcInitializer,
|
|
272
|
+
checker,
|
|
273
|
+
extracted.isHelper
|
|
274
|
+
)
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
// The route's own `auth`, falling back to the group's. Recorded on the meta
|
|
@@ -322,13 +324,12 @@ export function registerHTTPRoute({
|
|
|
322
324
|
const input = fnMeta.inputs?.[0] || null
|
|
323
325
|
|
|
324
326
|
const getRouteInputKeys = (): string[] | null => {
|
|
325
|
-
const
|
|
326
|
-
const inputTypes = state.typesLookup.get(targetFuncName)
|
|
327
|
+
const inputTypes = state.typesLookup.get(funcName)
|
|
327
328
|
if (inputTypes && inputTypes.length > 0) {
|
|
328
329
|
return extractTypeKeys(inputTypes[0])
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
const targetMeta = resolveFunctionMeta(state,
|
|
332
|
+
const targetMeta = resolveFunctionMeta(state, funcName)
|
|
332
333
|
if (targetMeta?.inputSchemaName) {
|
|
333
334
|
const schema = state.schemas[targetMeta.inputSchemaName] as any
|
|
334
335
|
const properties = schema?.properties
|
|
@@ -354,7 +355,7 @@ export function registerHTTPRoute({
|
|
|
354
355
|
logger.critical(
|
|
355
356
|
ErrorCode.ROUTE_PARAM_MISMATCH,
|
|
356
357
|
`Route '${fullRoute}' has path parameter(s) [${missingParams.join(', ')}] ` +
|
|
357
|
-
`not found in function '${
|
|
358
|
+
`not found in function '${funcName}' input type. ` +
|
|
358
359
|
`Input type has: [${inputKeys.join(', ')}]`
|
|
359
360
|
)
|
|
360
361
|
return
|
|
@@ -368,7 +369,7 @@ export function registerHTTPRoute({
|
|
|
368
369
|
logger.critical(
|
|
369
370
|
ErrorCode.ROUTE_QUERY_MISMATCH,
|
|
370
371
|
`Route '${fullRoute}' has query parameter(s) [${missingQuery.join(', ')}] ` +
|
|
371
|
-
`not found in function '${
|
|
372
|
+
`not found in function '${funcName}' input type. ` +
|
|
372
373
|
`Input type has: [${inputKeys.join(', ')}]`
|
|
373
374
|
)
|
|
374
375
|
return
|
|
@@ -393,9 +394,6 @@ export function registerHTTPRoute({
|
|
|
393
394
|
const permissions = resolvePermissions(state, obj, tags, checker)
|
|
394
395
|
|
|
395
396
|
state.serviceAggregation.usedFunctions.add(funcName)
|
|
396
|
-
if (refAddonTarget) {
|
|
397
|
-
state.serviceAggregation.usedFunctions.add(refAddonTarget)
|
|
398
|
-
}
|
|
399
397
|
extractWireNames(middleware).forEach((name) =>
|
|
400
398
|
state.serviceAggregation.usedMiddleware.add(name)
|
|
401
399
|
)
|
|
@@ -420,7 +418,6 @@ export function registerHTTPRoute({
|
|
|
420
418
|
state.http.files.add(sourceFile.fileName)
|
|
421
419
|
state.http.meta[method][fullRoute] = {
|
|
422
420
|
pikkuFuncId: funcName,
|
|
423
|
-
...(refAddonTarget && { refTarget: refAddonTarget }),
|
|
424
421
|
...(packageName && { packageName }),
|
|
425
422
|
route: fullRoute,
|
|
426
423
|
sourceFile: sourceFile.fileName,
|
|
@@ -7,13 +7,15 @@ import {
|
|
|
7
7
|
import type { AddWiring, InspectorState } from '../types.js'
|
|
8
8
|
import { ErrorCode } from '../error-codes.js'
|
|
9
9
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
10
|
-
import { parseDurationString } from '@pikku/core'
|
|
10
|
+
import { parseDurationString } from '@pikku/core/utils'
|
|
11
11
|
|
|
12
12
|
export interface KeyedWiringConfig {
|
|
13
13
|
functionName: string
|
|
14
14
|
idField: string
|
|
15
15
|
label: string
|
|
16
16
|
schemaPrefix: string
|
|
17
|
+
/** Boolean property carrying this wiring kind's deploy-gate opt-in/opt-out. */
|
|
18
|
+
flagField: string
|
|
17
19
|
getState: (state: InspectorState) => {
|
|
18
20
|
definitions: any[]
|
|
19
21
|
files: Set<string>
|
|
@@ -49,17 +51,15 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
49
51
|
|
|
50
52
|
const nameValue = getPropertyValue(obj, 'name') as string | null
|
|
51
53
|
const displayNameValue = getPropertyValue(obj, 'displayName') as
|
|
52
|
-
|
|
|
53
|
-
| null
|
|
54
|
+
string | null
|
|
54
55
|
const descriptionValue = getPropertyValue(obj, 'description') as
|
|
55
|
-
|
|
|
56
|
-
| null
|
|
56
|
+
string | null
|
|
57
57
|
const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod') as
|
|
58
|
-
|
|
|
59
|
-
| null
|
|
58
|
+
string | null
|
|
60
59
|
const docsUrlValue = getPropertyValue(obj, 'docsUrl') as string | null
|
|
61
60
|
const allowedHostsValue = getArrayPropertyValue(obj, 'allowedHosts')
|
|
62
61
|
const idValue = getPropertyValue(obj, config.idField) as string | null
|
|
62
|
+
const flagValue = getPropertyValue(obj, config.flagField) === true
|
|
63
63
|
|
|
64
64
|
let schemaVariableName: string | null = null
|
|
65
65
|
let schemaSourceFile: string | null = null
|
|
@@ -171,6 +171,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
171
171
|
docsUrl: docsUrlValue || undefined,
|
|
172
172
|
allowedHosts: allowedHostsValue || undefined,
|
|
173
173
|
[config.idField]: idValue,
|
|
174
|
+
[config.flagField]: flagValue || undefined,
|
|
174
175
|
schema: schemaLookupName,
|
|
175
176
|
sourceFile,
|
|
176
177
|
})
|
|
@@ -698,12 +698,14 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
698
698
|
return
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
if (expression.text === '
|
|
701
|
+
if (expression.text === 'pikkuAgentMiddleware') {
|
|
702
702
|
const arg = args[0]
|
|
703
703
|
if (!arg) return
|
|
704
704
|
|
|
705
705
|
if (!ts.isObjectLiteralExpression(arg)) {
|
|
706
|
-
logger.error(
|
|
706
|
+
logger.error(
|
|
707
|
+
`• pikkuAgentMiddleware() requires an object literal argument.`
|
|
708
|
+
)
|
|
707
709
|
return
|
|
708
710
|
}
|
|
709
711
|
|
|
@@ -744,13 +746,13 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
|
|
|
744
746
|
pikkuFuncId = node.parent.name.text
|
|
745
747
|
} else {
|
|
746
748
|
logger.error(
|
|
747
|
-
`•
|
|
748
|
-
`Extract it to a const: const myMiddleware =
|
|
749
|
+
`• pikkuAgentMiddleware() must be assigned to a variable or object property. ` +
|
|
750
|
+
`Extract it to a const: const myMiddleware = pikkuAgentMiddleware(...)`
|
|
749
751
|
)
|
|
750
752
|
return
|
|
751
753
|
}
|
|
752
754
|
}
|
|
753
|
-
state.
|
|
755
|
+
state.agentMiddleware.definitions[pikkuFuncId] = {
|
|
754
756
|
services,
|
|
755
757
|
sourceFile: node.getSourceFile().fileName,
|
|
756
758
|
position: node.getStart(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import type { FunctionWiresMeta } from '@pikku/core'
|
|
2
|
+
import type { FunctionWiresMeta } from '@pikku/core/function'
|
|
3
3
|
import type { AddWiring } from '../types.js'
|
|
4
4
|
import { extractFunctionName } from '../utils/extract-function-name.js'
|
|
5
5
|
import {
|
|
@@ -108,6 +108,30 @@ describe('addPersonas inspector', () => {
|
|
|
108
108
|
assert.equal(susan.runnable, true)
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
test('the app a person signs into survives into the meta', async () => {
|
|
112
|
+
const { state, criticals } = await inspectSource(
|
|
113
|
+
withRoles(
|
|
114
|
+
'definePersonas({',
|
|
115
|
+
' susan: {',
|
|
116
|
+
" name: 'Susan',",
|
|
117
|
+
" app: 'storefront',",
|
|
118
|
+
' },',
|
|
119
|
+
'})'
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(criticals, [])
|
|
124
|
+
assert.equal(state.personas.definitions[0]!.app, 'storefront')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('a person who names no app has none, rather than a guessed one', async () => {
|
|
128
|
+
const { state } = await inspectSource(
|
|
129
|
+
withRoles('definePersonas({', " susan: { name: 'Susan' },", '})')
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
assert.equal(state.personas.definitions[0]!.app, undefined)
|
|
133
|
+
})
|
|
134
|
+
|
|
111
135
|
test('extracts a declared avatar', async () => {
|
|
112
136
|
const { state } = await inspectSource(
|
|
113
137
|
withRoles(
|
package/src/add/add-personas.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
2
|
import { DISPOSITIONS } from '@pikku/core/virtual-user'
|
|
3
3
|
import { isRunnablePersona } from '@pikku/core/persona'
|
|
4
|
-
import type {
|
|
4
|
+
import type { PersonaMeta, PersonaAccountMeta } from '@pikku/core/persona'
|
|
5
5
|
import { ErrorCode } from '../error-codes.js'
|
|
6
6
|
import { claimSingleDeclaration } from '../utils/single-declaration.js'
|
|
7
7
|
import type { AddWiring, InspectorLogger } from '../types.js'
|
|
@@ -265,6 +265,8 @@ export const addPersonas: AddWiring = (logger, node, _checker, state) => {
|
|
|
265
265
|
if (avatarUrl !== undefined) persona.avatarUrl = avatarUrl
|
|
266
266
|
const personality = stringProperty(config, 'personality')
|
|
267
267
|
if (personality !== undefined) persona.personality = personality
|
|
268
|
+
const app = stringProperty(config, 'app')
|
|
269
|
+
if (app !== undefined) persona.app = app
|
|
268
270
|
if (disposition !== undefined) {
|
|
269
271
|
persona.disposition = disposition as PersonaMeta['disposition']
|
|
270
272
|
}
|
|
@@ -39,6 +39,12 @@ const STEPS = [
|
|
|
39
39
|
" description: 'sees a receipt',",
|
|
40
40
|
' default: async ({ logger }) => ({ ok: true }),',
|
|
41
41
|
'})',
|
|
42
|
+
'export const asksForTheReceipt = pikkuScenarioStep({',
|
|
43
|
+
" name: 'asksForTheReceipt',",
|
|
44
|
+
" description: 'asks for the receipt',",
|
|
45
|
+
' actor: true,',
|
|
46
|
+
' default: async ({ logger }) => ({ ok: true }),',
|
|
47
|
+
'})',
|
|
42
48
|
].join('\n')
|
|
43
49
|
|
|
44
50
|
const scenarioSource = (body: string[], preamble: string[] = []) =>
|
|
@@ -210,6 +216,35 @@ describe('pikkuScenarioStep', () => {
|
|
|
210
216
|
}
|
|
211
217
|
})
|
|
212
218
|
|
|
219
|
+
test('a step declaring `actor: true` called without one is a PKU677 critical', async () => {
|
|
220
|
+
const { criticals, cleanup } = await run([
|
|
221
|
+
"await scenario.then('asks for the receipt', 'asksForTheReceipt', {})",
|
|
222
|
+
])
|
|
223
|
+
try {
|
|
224
|
+
assert.ok(
|
|
225
|
+
criticals.find((c) => c.code === 'PKU677'),
|
|
226
|
+
`a step that says it runs as somebody must be given somebody, browser binding or not, got: ${JSON.stringify(criticals)}`
|
|
227
|
+
)
|
|
228
|
+
} finally {
|
|
229
|
+
await cleanup()
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('a step that declares no actor is not flagged for running without one', async () => {
|
|
234
|
+
const { criticals, cleanup } = await run([
|
|
235
|
+
"await scenario.then('sees a receipt', 'seesAReceipt', {})",
|
|
236
|
+
])
|
|
237
|
+
try {
|
|
238
|
+
assert.equal(
|
|
239
|
+
criticals.filter((c) => c.code === 'PKU677').length,
|
|
240
|
+
0,
|
|
241
|
+
`an assertion over an earlier step's value has nobody to be, got: ${JSON.stringify(criticals)}`
|
|
242
|
+
)
|
|
243
|
+
} finally {
|
|
244
|
+
await cleanup()
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
213
248
|
test('a scenario that never asserts is a PKU680 critical', async () => {
|
|
214
249
|
const { criticals, cleanup } = await run([
|
|
215
250
|
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
@@ -202,10 +202,10 @@ describe('addScope inspector', () => {
|
|
|
202
202
|
[],
|
|
203
203
|
`expected no duplicate critical, got ${JSON.stringify(criticals)}`
|
|
204
204
|
)
|
|
205
|
-
assert.deepEqual(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
205
|
+
assert.deepEqual(state.scopes.definitions.map((d) => d.name).sort(), [
|
|
206
|
+
'admin',
|
|
207
|
+
'billing',
|
|
208
|
+
])
|
|
209
209
|
})
|
|
210
210
|
|
|
211
211
|
// The CLI scaffolds a scope tree of its own — `user-admin.gen.ts` declares the
|
package/src/add/add-scope.ts
CHANGED
|
@@ -88,14 +88,12 @@ const extractScopeNodes = (
|
|
|
88
88
|
|
|
89
89
|
const node: ScopeNodeMeta = {}
|
|
90
90
|
const displayName = getPropertyValue(prop.initializer, 'displayName') as
|
|
91
|
-
|
|
|
92
|
-
| null
|
|
91
|
+
string | null
|
|
93
92
|
if (displayName) {
|
|
94
93
|
node.displayName = displayName
|
|
95
94
|
}
|
|
96
95
|
const description = getPropertyValue(prop.initializer, 'description') as
|
|
97
|
-
|
|
|
98
|
-
| null
|
|
96
|
+
string | null
|
|
99
97
|
if (description) {
|
|
100
98
|
node.description = description
|
|
101
99
|
}
|
|
@@ -192,11 +190,9 @@ export const addScope: AddWiring = (logger, node, checker, state, _options) => {
|
|
|
192
190
|
|
|
193
191
|
const root = prop.initializer
|
|
194
192
|
const displayNameValue = getPropertyValue(root, 'displayName') as
|
|
195
|
-
|
|
|
196
|
-
| null
|
|
193
|
+
string | null
|
|
197
194
|
const descriptionValue = getPropertyValue(root, 'description') as
|
|
198
|
-
|
|
|
199
|
-
| null
|
|
195
|
+
string | null
|
|
200
196
|
|
|
201
197
|
let scopes: Record<string, ScopeNodeMeta> | undefined
|
|
202
198
|
const scopesProp = root.properties.find(
|
package/src/add/add-secret.ts
CHANGED
|
@@ -118,10 +118,10 @@ describe('addSystemRole inspector', () => {
|
|
|
118
118
|
)
|
|
119
119
|
)
|
|
120
120
|
|
|
121
|
-
assert.deepEqual(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
121
|
+
assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
|
|
122
|
+
'admin',
|
|
123
|
+
'buyer',
|
|
124
|
+
])
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
// Roles follow scopes: a project scaffolding user-admin has a generated
|
|
@@ -142,10 +142,10 @@ describe('addSystemRole inspector', () => {
|
|
|
142
142
|
[],
|
|
143
143
|
`expected no duplicate critical, got ${JSON.stringify(criticals)}`
|
|
144
144
|
)
|
|
145
|
-
assert.deepEqual(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
145
|
+
assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
|
|
146
|
+
'admin',
|
|
147
|
+
'buyer',
|
|
148
|
+
])
|
|
149
149
|
})
|
|
150
150
|
|
|
151
151
|
// A generated file is not the one place a person adds a role to, so it neither
|
|
@@ -146,11 +146,9 @@ export const addSystemRole: AddWiring = (
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const displayName = getPropertyValue(prop.initializer, 'displayName') as
|
|
149
|
-
|
|
|
150
|
-
| null
|
|
149
|
+
string | null
|
|
151
150
|
const description = getPropertyValue(prop.initializer, 'description') as
|
|
152
|
-
|
|
|
153
|
-
| null
|
|
151
|
+
string | null
|
|
154
152
|
|
|
155
153
|
state.systemRoles.files.add(sourceFile)
|
|
156
154
|
state.systemRoles.definitions.push({
|