@pikku/inspector 0.12.74 → 0.12.81
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 +347 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +23 -13
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2207
- package/src/utils/filter-inspector-state.ts +0 -1302
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,419 +0,0 @@
|
|
|
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 { ErrorCode } from '../error-codes.js'
|
|
8
|
-
import type { InspectorLogger } from '../types.js'
|
|
9
|
-
|
|
10
|
-
const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
11
|
-
({
|
|
12
|
-
debug: () => {},
|
|
13
|
-
info: () => {},
|
|
14
|
-
warn: () => {},
|
|
15
|
-
error: () => {},
|
|
16
|
-
diagnostic: ({ code, message }) => {
|
|
17
|
-
criticals.push({ code, message })
|
|
18
|
-
},
|
|
19
|
-
critical: (code: ErrorCode, message: string) => {
|
|
20
|
-
criticals.push({ code, message })
|
|
21
|
-
},
|
|
22
|
-
hasCriticalErrors: () => criticals.length > 0,
|
|
23
|
-
}) satisfies InspectorLogger
|
|
24
|
-
|
|
25
|
-
const inspectSource = async (source: string) => {
|
|
26
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-scope-'))
|
|
27
|
-
const file = join(rootDir, 'scopes.ts')
|
|
28
|
-
await writeFile(file, source)
|
|
29
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
30
|
-
try {
|
|
31
|
-
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
32
|
-
return { state, criticals, file }
|
|
33
|
-
} finally {
|
|
34
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Inspects several files at once, keyed by basename. */
|
|
39
|
-
const inspectSources = async (sources: Record<string, string>) => {
|
|
40
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-scope-multi-'))
|
|
41
|
-
const files: Record<string, string> = {}
|
|
42
|
-
for (const [name, source] of Object.entries(sources)) {
|
|
43
|
-
files[name] = join(rootDir, name)
|
|
44
|
-
await writeFile(files[name]!, source)
|
|
45
|
-
}
|
|
46
|
-
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
47
|
-
try {
|
|
48
|
-
const state = await inspect(makeLogger(criticals), Object.values(files), {
|
|
49
|
-
rootDir,
|
|
50
|
-
})
|
|
51
|
-
return { state, criticals, files }
|
|
52
|
-
} finally {
|
|
53
|
-
await rm(rootDir, { recursive: true, force: true })
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('addScope inspector', () => {
|
|
58
|
-
test('extracts a flat scope', async () => {
|
|
59
|
-
const { state, criticals } = await inspectSource(
|
|
60
|
-
[
|
|
61
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
62
|
-
'defineScope({',
|
|
63
|
-
' admin: {',
|
|
64
|
-
" displayName: 'Administration',",
|
|
65
|
-
" description: 'Administrative access',",
|
|
66
|
-
' },',
|
|
67
|
-
'})',
|
|
68
|
-
].join('\n')
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
assert.equal(criticals.length, 0)
|
|
72
|
-
assert.equal(state.scopes.definitions.length, 1)
|
|
73
|
-
assert.equal(state.scopes.definitions[0]!.name, 'admin')
|
|
74
|
-
assert.equal(state.scopes.definitions[0]!.displayName, 'Administration')
|
|
75
|
-
assert.equal(
|
|
76
|
-
state.scopes.definitions[0]!.description,
|
|
77
|
-
'Administrative access'
|
|
78
|
-
)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('extracts a displayName at every depth', async () => {
|
|
82
|
-
const { state, criticals } = await inspectSource(
|
|
83
|
-
[
|
|
84
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
85
|
-
'defineScope({',
|
|
86
|
-
' admin: {',
|
|
87
|
-
" displayName: 'Administration',",
|
|
88
|
-
' scopes: {',
|
|
89
|
-
' invoices: {',
|
|
90
|
-
" displayName: 'Invoice Management',",
|
|
91
|
-
' scopes: {',
|
|
92
|
-
" create: { displayName: 'Create Invoices' },",
|
|
93
|
-
' },',
|
|
94
|
-
' },',
|
|
95
|
-
' },',
|
|
96
|
-
' },',
|
|
97
|
-
'})',
|
|
98
|
-
].join('\n')
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
assert.equal(criticals.length, 0)
|
|
102
|
-
const admin = state.scopes.definitions[0]!
|
|
103
|
-
assert.equal(admin.displayName, 'Administration')
|
|
104
|
-
const invoices = admin.scopes!.invoices!
|
|
105
|
-
assert.equal(invoices.displayName, 'Invoice Management')
|
|
106
|
-
assert.equal(invoices.scopes!.create!.displayName, 'Create Invoices')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('extracts a nested scope tree', async () => {
|
|
110
|
-
const { state, criticals } = await inspectSource(
|
|
111
|
-
[
|
|
112
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
113
|
-
'defineScope({',
|
|
114
|
-
' admin: {',
|
|
115
|
-
' scopes: {',
|
|
116
|
-
' invoices: {',
|
|
117
|
-
" description: 'Invoice management',",
|
|
118
|
-
' scopes: {',
|
|
119
|
-
" create: { description: 'Create invoices' },",
|
|
120
|
-
' },',
|
|
121
|
-
' },',
|
|
122
|
-
' },',
|
|
123
|
-
' },',
|
|
124
|
-
'})',
|
|
125
|
-
].join('\n')
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
assert.equal(criticals.length, 0)
|
|
129
|
-
assert.deepEqual(state.scopes.definitions[0]!.scopes, {
|
|
130
|
-
invoices: {
|
|
131
|
-
description: 'Invoice management',
|
|
132
|
-
scopes: {
|
|
133
|
-
create: { description: 'Create invoices' },
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('records the source file', async () => {
|
|
140
|
-
const { state, file } = await inspectSource(
|
|
141
|
-
[
|
|
142
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
143
|
-
'defineScope({ admin: {} })',
|
|
144
|
-
].join('\n')
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
assert.equal(state.scopes.definitions[0]!.sourceFile, file)
|
|
148
|
-
assert.ok(state.scopes.files.has(file))
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// The point of the keyed form: one call declares as many roots as you like,
|
|
152
|
-
// and a root reads exactly like the nodes beneath it.
|
|
153
|
-
test('extracts several roots from one declaration', async () => {
|
|
154
|
-
const { state, criticals } = await inspectSource(
|
|
155
|
-
[
|
|
156
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
157
|
-
'defineScope({ admin: {}, billing: { scopes: { read: {} } } })',
|
|
158
|
-
].join('\n')
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
assert.equal(criticals.length, 0)
|
|
162
|
-
assert.deepEqual(
|
|
163
|
-
state.scopes.definitions.map((d) => d.name),
|
|
164
|
-
['admin', 'billing']
|
|
165
|
-
)
|
|
166
|
-
assert.deepEqual(state.scopes.definitions[1]!.scopes, { read: {} })
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('extracts several declarations', async () => {
|
|
170
|
-
const { state } = await inspectSource(
|
|
171
|
-
[
|
|
172
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
173
|
-
'defineScope({ admin: {} })',
|
|
174
|
-
'defineScope({ billing: {} })',
|
|
175
|
-
].join('\n')
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
assert.deepEqual(
|
|
179
|
-
state.scopes.definitions.map((d) => d.name),
|
|
180
|
-
['admin', 'billing']
|
|
181
|
-
)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
// The CLI generates a `defineScope` of its own — `user-admin.gen.ts` ships
|
|
185
|
-
// the whole `admin` tree, and `@pikku/addon-console` spells it out again — so
|
|
186
|
-
// a project that scaffolds user-admin declares scopes in more than one file
|
|
187
|
-
// and every one of them has to survive.
|
|
188
|
-
test('declarations in separate files all survive', async () => {
|
|
189
|
-
const { state, criticals } = await inspectSources({
|
|
190
|
-
'user-admin.gen.ts': [
|
|
191
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
192
|
-
'defineScope({ admin: {} })',
|
|
193
|
-
].join('\n'),
|
|
194
|
-
'scopes.ts': [
|
|
195
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
196
|
-
'defineScope({ billing: {} })',
|
|
197
|
-
].join('\n'),
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
assert.deepEqual(
|
|
201
|
-
criticals.filter((c) => /Only one defineScope/.test(c.message)),
|
|
202
|
-
[],
|
|
203
|
-
`expected no duplicate critical, got ${JSON.stringify(criticals)}`
|
|
204
|
-
)
|
|
205
|
-
assert.deepEqual(state.scopes.definitions.map((d) => d.name).sort(), [
|
|
206
|
-
'admin',
|
|
207
|
-
'billing',
|
|
208
|
-
])
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
// The CLI scaffolds a scope tree of its own — `user-admin.gen.ts` declares the
|
|
212
|
-
// whole `admin` tree — so a generated declaration cannot be made to compete
|
|
213
|
-
// with the app's own for the single slot.
|
|
214
|
-
test('a generated declaration neither claims the slot nor conflicts', async () => {
|
|
215
|
-
const { state, criticals } = await inspectSources({
|
|
216
|
-
'user-admin.gen.ts': [
|
|
217
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
218
|
-
'defineScope({ admin: {} })',
|
|
219
|
-
].join('\n'),
|
|
220
|
-
'scopes.ts': [
|
|
221
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
222
|
-
'defineScope({ billing: {} })',
|
|
223
|
-
].join('\n'),
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
assert.deepEqual(
|
|
227
|
-
criticals.filter((c) => c.code === ErrorCode.DUPLICATE_SCOPE_DEFINITION),
|
|
228
|
-
[],
|
|
229
|
-
`expected no duplicate critical, got ${JSON.stringify(criticals)}`
|
|
230
|
-
)
|
|
231
|
-
assert.deepEqual(
|
|
232
|
-
state.scopes.definitions.map((d) => d.name).sort(),
|
|
233
|
-
['admin', 'billing'],
|
|
234
|
-
"both the generated tree and the app's own must survive"
|
|
235
|
-
)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
test('is critical when a root embeds the separator', async () => {
|
|
239
|
-
const { criticals } = await inspectSource(
|
|
240
|
-
[
|
|
241
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
242
|
-
"defineScope({ 'admin:users': {} })",
|
|
243
|
-
].join('\n')
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
assert.ok(
|
|
247
|
-
criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
|
|
248
|
-
`expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
|
|
249
|
-
)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
test('is critical when a root is the wildcard', async () => {
|
|
253
|
-
const { criticals } = await inspectSource(
|
|
254
|
-
[
|
|
255
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
256
|
-
"defineScope({ '*': {} })",
|
|
257
|
-
].join('\n')
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
assert.ok(
|
|
261
|
-
criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
|
|
262
|
-
`expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
|
|
263
|
-
)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
test('is critical when a root key is not a literal', async () => {
|
|
267
|
-
const { criticals } = await inspectSource(
|
|
268
|
-
[
|
|
269
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
270
|
-
'const k = String(1)',
|
|
271
|
-
'defineScope({ [k]: {} } as any)',
|
|
272
|
-
].join('\n')
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
assert.ok(
|
|
276
|
-
criticals.some((c) => c.code === ErrorCode.NON_LITERAL_WIRE_NAME),
|
|
277
|
-
`expected a NON_LITERAL_WIRE_NAME critical, got ${JSON.stringify(criticals)}`
|
|
278
|
-
)
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
test('is critical when a root is not an object literal', async () => {
|
|
282
|
-
const { criticals } = await inspectSource(
|
|
283
|
-
[
|
|
284
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
285
|
-
"defineScope({ admin: 'nope' } as any)",
|
|
286
|
-
].join('\n')
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
assert.ok(
|
|
290
|
-
criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
|
|
291
|
-
`expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
|
|
292
|
-
)
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
describe('validateScopeReferences', () => {
|
|
297
|
-
const funcSource = (scopes: string, decls: string[] = []) =>
|
|
298
|
-
[
|
|
299
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
300
|
-
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
301
|
-
...decls,
|
|
302
|
-
'export const f = pikkuSessionlessFunc({',
|
|
303
|
-
` scopes: ${scopes},`,
|
|
304
|
-
" func: async () => 'ok',",
|
|
305
|
-
'})',
|
|
306
|
-
].join('\n')
|
|
307
|
-
|
|
308
|
-
test('accepts a declared scope', async () => {
|
|
309
|
-
const { criticals } = await inspectSource(
|
|
310
|
-
funcSource("['admin:invoices:create']", [
|
|
311
|
-
'defineScope({',
|
|
312
|
-
' admin: {',
|
|
313
|
-
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
314
|
-
' },',
|
|
315
|
-
'})',
|
|
316
|
-
])
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
assert.deepEqual(criticals, [])
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
test('accepts an intermediate node of a declared tree', async () => {
|
|
323
|
-
const { criticals } = await inspectSource(
|
|
324
|
-
funcSource("['admin:invoices']", [
|
|
325
|
-
'defineScope({',
|
|
326
|
-
' admin: {',
|
|
327
|
-
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
328
|
-
' },',
|
|
329
|
-
'})',
|
|
330
|
-
])
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
assert.deepEqual(criticals, [])
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
test('rejects an undeclared scope', async () => {
|
|
337
|
-
const { criticals } = await inspectSource(
|
|
338
|
-
funcSource("['billing:read']", ['defineScope({ admin: {} })'])
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
assert.ok(
|
|
342
|
-
criticals.some(
|
|
343
|
-
(c) =>
|
|
344
|
-
c.code === ErrorCode.INVALID_VALUE &&
|
|
345
|
-
c.message.includes('billing:read')
|
|
346
|
-
),
|
|
347
|
-
`expected an INVALID_VALUE critical for billing:read, got ${JSON.stringify(criticals)}`
|
|
348
|
-
)
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
test('lists the available scopes when one is undeclared', async () => {
|
|
352
|
-
const { criticals } = await inspectSource(
|
|
353
|
-
funcSource("['nope']", ['defineScope({ admin: {} })'])
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
assert.ok(
|
|
357
|
-
criticals.some((c) => c.message.includes('Available scopes: admin')),
|
|
358
|
-
`expected the message to list available scopes, got ${JSON.stringify(criticals)}`
|
|
359
|
-
)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
test('rejects a typo in a nested scope', async () => {
|
|
363
|
-
const { criticals } = await inspectSource(
|
|
364
|
-
funcSource("['admin:invoice:create']", [
|
|
365
|
-
'defineScope({',
|
|
366
|
-
' admin: {',
|
|
367
|
-
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
368
|
-
' },',
|
|
369
|
-
'})',
|
|
370
|
-
])
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
assert.ok(
|
|
374
|
-
criticals.some((c) => c.message.includes('admin:invoice:create')),
|
|
375
|
-
`expected a critical for the typo, got ${JSON.stringify(criticals)}`
|
|
376
|
-
)
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
test('accepts a wildcard requirement whose node is declared', async () => {
|
|
380
|
-
const { criticals } = await inspectSource(
|
|
381
|
-
funcSource("['admin:*']", ['defineScope({ admin: {} })'])
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
assert.deepEqual(criticals, [])
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
test('rejects a wildcard requirement whose node is undeclared', async () => {
|
|
388
|
-
const { criticals } = await inspectSource(
|
|
389
|
-
funcSource("['billing:*']", ['defineScope({ admin: {} })'])
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
assert.ok(
|
|
393
|
-
criticals.some((c) => c.message.includes('billing:*')),
|
|
394
|
-
`expected a critical for billing:*, got ${JSON.stringify(criticals)}`
|
|
395
|
-
)
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
test('rejects a bare wildcard requirement', async () => {
|
|
399
|
-
const { criticals } = await inspectSource(
|
|
400
|
-
funcSource("['*']", ['defineScope({ admin: {} })'])
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
assert.ok(
|
|
404
|
-
criticals.some((c) => c.message.includes('bare wildcard')),
|
|
405
|
-
`expected a critical for the bare wildcard, got ${JSON.stringify(criticals)}`
|
|
406
|
-
)
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
test('is silent for a function declaring no scopes', async () => {
|
|
410
|
-
const { criticals } = await inspectSource(
|
|
411
|
-
[
|
|
412
|
-
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
413
|
-
"export const f = pikkuSessionlessFunc({ func: async () => 'ok' })",
|
|
414
|
-
].join('\n')
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
assert.deepEqual(criticals, [])
|
|
418
|
-
})
|
|
419
|
-
})
|
package/src/add/add-scope.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
3
|
-
import type { AddWiring, InspectorLogger } from '../types.js'
|
|
4
|
-
import { ErrorCode } from '../error-codes.js'
|
|
5
|
-
import type { ScopeNodeMeta } from '@pikku/core/scope'
|
|
6
|
-
|
|
7
|
-
const SEPARATOR = ':'
|
|
8
|
-
const WILDCARD = '*'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Validates a single scope segment, reporting rather than throwing so the
|
|
12
|
-
* inspector can surface every problem in one pass.
|
|
13
|
-
*
|
|
14
|
-
* @returns true when the segment is usable.
|
|
15
|
-
*/
|
|
16
|
-
const isValidSegment = (
|
|
17
|
-
segment: string,
|
|
18
|
-
scopeName: string,
|
|
19
|
-
logger: InspectorLogger
|
|
20
|
-
): boolean => {
|
|
21
|
-
if (segment.length === 0) {
|
|
22
|
-
logger.critical(
|
|
23
|
-
ErrorCode.INVALID_VALUE,
|
|
24
|
-
`Scope '${scopeName}' contains an empty segment.`
|
|
25
|
-
)
|
|
26
|
-
return false
|
|
27
|
-
}
|
|
28
|
-
if (segment.includes(SEPARATOR)) {
|
|
29
|
-
logger.critical(
|
|
30
|
-
ErrorCode.INVALID_VALUE,
|
|
31
|
-
`Scope segment '${segment}' in '${scopeName}' contains the '${SEPARATOR}' separator. ` +
|
|
32
|
-
`Nest scopes with the 'scopes' property instead of embedding '${SEPARATOR}' in a name.`
|
|
33
|
-
)
|
|
34
|
-
return false
|
|
35
|
-
}
|
|
36
|
-
if (segment === WILDCARD) {
|
|
37
|
-
logger.critical(
|
|
38
|
-
ErrorCode.INVALID_VALUE,
|
|
39
|
-
`Scope segment '${segment}' in '${scopeName}' is the wildcard. ` +
|
|
40
|
-
`'${WILDCARD}' is reserved for granting a scope and its descendants, and cannot be declared.`
|
|
41
|
-
)
|
|
42
|
-
return false
|
|
43
|
-
}
|
|
44
|
-
return true
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Recursively extracts a `scopes: { ... }` object literal into nested metadata.
|
|
49
|
-
*/
|
|
50
|
-
const extractScopeNodes = (
|
|
51
|
-
obj: ts.ObjectLiteralExpression,
|
|
52
|
-
scopeName: string,
|
|
53
|
-
logger: InspectorLogger
|
|
54
|
-
): Record<string, ScopeNodeMeta> | undefined => {
|
|
55
|
-
const nodes: Record<string, ScopeNodeMeta> = {}
|
|
56
|
-
|
|
57
|
-
for (const prop of obj.properties) {
|
|
58
|
-
if (!ts.isPropertyAssignment(prop)) {
|
|
59
|
-
continue
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let segment: string | undefined
|
|
63
|
-
if (ts.isIdentifier(prop.name)) {
|
|
64
|
-
segment = prop.name.text
|
|
65
|
-
} else if (ts.isStringLiteral(prop.name)) {
|
|
66
|
-
segment = prop.name.text
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (segment === undefined) {
|
|
70
|
-
logger.critical(
|
|
71
|
-
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
72
|
-
`Scope '${scopeName}' has a nested scope whose key is not a literal.`
|
|
73
|
-
)
|
|
74
|
-
continue
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!isValidSegment(segment, scopeName, logger)) {
|
|
78
|
-
continue
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
82
|
-
logger.critical(
|
|
83
|
-
ErrorCode.INVALID_VALUE,
|
|
84
|
-
`Nested scope '${segment}' in '${scopeName}' must be an object literal.`
|
|
85
|
-
)
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const node: ScopeNodeMeta = {}
|
|
90
|
-
const displayName = getPropertyValue(prop.initializer, 'displayName') as
|
|
91
|
-
string | null
|
|
92
|
-
if (displayName) {
|
|
93
|
-
node.displayName = displayName
|
|
94
|
-
}
|
|
95
|
-
const description = getPropertyValue(prop.initializer, 'description') as
|
|
96
|
-
string | null
|
|
97
|
-
if (description) {
|
|
98
|
-
node.description = description
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const nestedProp = prop.initializer.properties.find(
|
|
102
|
-
(p) =>
|
|
103
|
-
ts.isPropertyAssignment(p) &&
|
|
104
|
-
ts.isIdentifier(p.name) &&
|
|
105
|
-
p.name.text === 'scopes'
|
|
106
|
-
)
|
|
107
|
-
if (
|
|
108
|
-
nestedProp &&
|
|
109
|
-
ts.isPropertyAssignment(nestedProp) &&
|
|
110
|
-
ts.isObjectLiteralExpression(nestedProp.initializer)
|
|
111
|
-
) {
|
|
112
|
-
const nested = extractScopeNodes(
|
|
113
|
-
nestedProp.initializer,
|
|
114
|
-
scopeName,
|
|
115
|
-
logger
|
|
116
|
-
)
|
|
117
|
-
if (nested) {
|
|
118
|
-
node.scopes = nested
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
nodes[segment] = node
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return Object.keys(nodes).length > 0 ? nodes : undefined
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Unwraps `x as const` / `x as any` so a cast declaration is still extracted
|
|
130
|
-
* rather than silently skipped.
|
|
131
|
-
*/
|
|
132
|
-
const unwrapAs = (node: ts.Expression): ts.Expression =>
|
|
133
|
-
ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
|
|
134
|
-
? unwrapAs(node.expression)
|
|
135
|
-
: node
|
|
136
|
-
|
|
137
|
-
export const addScope: AddWiring = (logger, node, checker, state, _options) => {
|
|
138
|
-
if (!ts.isCallExpression(node)) {
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const expression = node.expression
|
|
143
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const firstArg = node.arguments[0]
|
|
148
|
-
if (!firstArg) {
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const unwrapped = unwrapAs(firstArg)
|
|
153
|
-
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const sourceFile = node.getSourceFile().fileName
|
|
158
|
-
|
|
159
|
-
// Roots are keyed exactly like the nodes beneath them, so each property of
|
|
160
|
-
// the call's single argument is one tree.
|
|
161
|
-
for (const prop of unwrapped.properties) {
|
|
162
|
-
if (!ts.isPropertyAssignment(prop)) {
|
|
163
|
-
continue
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let name: string | undefined
|
|
167
|
-
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
|
|
168
|
-
name = prop.name.text
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (name === undefined) {
|
|
172
|
-
logger.critical(
|
|
173
|
-
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
174
|
-
'A scope is declared with a key that is not a literal.'
|
|
175
|
-
)
|
|
176
|
-
continue
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (!isValidSegment(name, name, logger)) {
|
|
180
|
-
continue
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
184
|
-
logger.critical(
|
|
185
|
-
ErrorCode.INVALID_VALUE,
|
|
186
|
-
`Scope '${name}' must be an object literal.`
|
|
187
|
-
)
|
|
188
|
-
continue
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const root = prop.initializer
|
|
192
|
-
const displayNameValue = getPropertyValue(root, 'displayName') as
|
|
193
|
-
string | null
|
|
194
|
-
const descriptionValue = getPropertyValue(root, 'description') as
|
|
195
|
-
string | null
|
|
196
|
-
|
|
197
|
-
let scopes: Record<string, ScopeNodeMeta> | undefined
|
|
198
|
-
const scopesProp = root.properties.find(
|
|
199
|
-
(p) =>
|
|
200
|
-
ts.isPropertyAssignment(p) &&
|
|
201
|
-
ts.isIdentifier(p.name) &&
|
|
202
|
-
p.name.text === 'scopes'
|
|
203
|
-
)
|
|
204
|
-
if (
|
|
205
|
-
scopesProp &&
|
|
206
|
-
ts.isPropertyAssignment(scopesProp) &&
|
|
207
|
-
ts.isObjectLiteralExpression(scopesProp.initializer)
|
|
208
|
-
) {
|
|
209
|
-
scopes = extractScopeNodes(scopesProp.initializer, name, logger)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
state.scopes.files.add(sourceFile)
|
|
213
|
-
state.scopes.definitions.push({
|
|
214
|
-
name,
|
|
215
|
-
displayName: displayNameValue || undefined,
|
|
216
|
-
description: descriptionValue || undefined,
|
|
217
|
-
scopes,
|
|
218
|
-
sourceFile,
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
}
|
package/src/add/add-secret.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { createAddKeyedWiring } from './add-keyed-wiring.js'
|
|
2
|
-
|
|
3
|
-
export const addSecret = createAddKeyedWiring({
|
|
4
|
-
functionName: 'defineSecret',
|
|
5
|
-
idField: 'secretId',
|
|
6
|
-
label: 'Secret',
|
|
7
|
-
schemaPrefix: 'SecretSchema',
|
|
8
|
-
flagField: 'optional',
|
|
9
|
-
getState: (state) => state.secrets,
|
|
10
|
-
})
|