@pikku/inspector 0.12.75 → 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 +330 -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 +14 -0
- 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 -2232
- package/src/utils/filter-inspector-state.ts +0 -1298
- 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,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two step kinds whose subject is not a person: the app acting on itself,
|
|
3
|
-
* and a third-party system acting on it.
|
|
4
|
-
*
|
|
5
|
-
* What is being pinned here is mostly negative — that neither can declare a
|
|
6
|
-
* surface, and that both are marked in a way the virtual-user catalogue can
|
|
7
|
-
* refuse. The second is not tidiness: a virtual user that could invoke "Stripe's
|
|
8
|
-
* webhook arrives" would forge its own payment success, and every finding
|
|
9
|
-
* downstream of that is worthless.
|
|
10
|
-
*/
|
|
11
|
-
import { strict as assert } from 'assert'
|
|
12
|
-
import { describe, test } from 'node:test'
|
|
13
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
14
|
-
import { tmpdir } from 'node:os'
|
|
15
|
-
import { join } from 'node:path'
|
|
16
|
-
import { inspect } from '../inspector.js'
|
|
17
|
-
import type { InspectorLogger } from '../types.js'
|
|
18
|
-
|
|
19
|
-
async function run(source: string) {
|
|
20
|
-
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-subject-step-'))
|
|
21
|
-
const file = join(rootDir, 'subjects.steps.ts')
|
|
22
|
-
await writeFile(file, source)
|
|
23
|
-
const errors: string[] = []
|
|
24
|
-
const logger = {
|
|
25
|
-
debug: () => {},
|
|
26
|
-
info: () => {},
|
|
27
|
-
warn: () => {},
|
|
28
|
-
error: (message: string) => {
|
|
29
|
-
errors.push(message)
|
|
30
|
-
},
|
|
31
|
-
diagnostic: () => {},
|
|
32
|
-
critical: (_code: any, message: string) => {
|
|
33
|
-
errors.push(message)
|
|
34
|
-
},
|
|
35
|
-
hasCriticalErrors: () => false,
|
|
36
|
-
} as unknown as InspectorLogger
|
|
37
|
-
|
|
38
|
-
const state = await inspect(logger, [file], { rootDir })
|
|
39
|
-
return {
|
|
40
|
-
state,
|
|
41
|
-
errors,
|
|
42
|
-
cleanup: () => rm(rootDir, { recursive: true, force: true }),
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const IMPORTS =
|
|
47
|
-
"import { pikkuPlatformScenarioStep, pikkuAddonScenarioStep } from '@pikku/core/workflow'"
|
|
48
|
-
|
|
49
|
-
describe('pikkuPlatformScenarioStep', () => {
|
|
50
|
-
test('registers under its declared name, marked as the platform acting', async () => {
|
|
51
|
-
const { state, errors, cleanup } = await run(
|
|
52
|
-
[
|
|
53
|
-
IMPORTS,
|
|
54
|
-
'export const trialHasExpired = pikkuPlatformScenarioStep({',
|
|
55
|
-
" name: 'trialHasExpired',",
|
|
56
|
-
" description: 'the platform has expired the trial',",
|
|
57
|
-
' func: async ({ logger }, data: { orgId: string }) => ({ ok: !!data.orgId }),',
|
|
58
|
-
'})',
|
|
59
|
-
].join('\n')
|
|
60
|
-
)
|
|
61
|
-
try {
|
|
62
|
-
assert.deepEqual(errors, [])
|
|
63
|
-
const meta = state.functions.meta['trialHasExpired']
|
|
64
|
-
assert.ok(meta, 'expected a function registered as trialHasExpired')
|
|
65
|
-
assert.equal(meta.scenarioStepKind, 'platform')
|
|
66
|
-
// Held out of every deployed unit, exactly like a persona step.
|
|
67
|
-
assert.equal(meta.scenarioStep, true)
|
|
68
|
-
assert.equal(state.rpc.exposedMeta['trialHasExpired'], undefined)
|
|
69
|
-
assert.equal(state.rpc.internalMeta['trialHasExpired'], undefined)
|
|
70
|
-
} finally {
|
|
71
|
-
await cleanup()
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('has exactly one witness: a surface binding is refused', async () => {
|
|
76
|
-
const { state, errors, cleanup } = await run(
|
|
77
|
-
[
|
|
78
|
-
IMPORTS,
|
|
79
|
-
'export const trialHasExpired = pikkuPlatformScenarioStep({',
|
|
80
|
-
" name: 'trialHasExpired',",
|
|
81
|
-
' browser: async ({ logger }) => ({ ok: true }),',
|
|
82
|
-
'})',
|
|
83
|
-
].join('\n')
|
|
84
|
-
)
|
|
85
|
-
try {
|
|
86
|
-
assert.ok(
|
|
87
|
-
errors.some((message) => message.includes('browser')),
|
|
88
|
-
`expected the browser binding to be refused, got: ${errors.join(' | ')}`
|
|
89
|
-
)
|
|
90
|
-
assert.equal(state.functions.meta['trialHasExpired'], undefined)
|
|
91
|
-
} finally {
|
|
92
|
-
await cleanup()
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('declares one surface, so a browser run never demands an actor for it', async () => {
|
|
97
|
-
const { state, cleanup } = await run(
|
|
98
|
-
[
|
|
99
|
-
IMPORTS,
|
|
100
|
-
'export const trialHasExpired = pikkuPlatformScenarioStep({',
|
|
101
|
-
" name: 'trialHasExpired',",
|
|
102
|
-
' func: async ({ logger }) => ({ ok: true }),',
|
|
103
|
-
'})',
|
|
104
|
-
].join('\n')
|
|
105
|
-
)
|
|
106
|
-
try {
|
|
107
|
-
assert.deepEqual(
|
|
108
|
-
state.functions.meta['trialHasExpired']?.scenarioStepSurfaces,
|
|
109
|
-
['default']
|
|
110
|
-
)
|
|
111
|
-
} finally {
|
|
112
|
-
await cleanup()
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
describe('pikkuAddonScenarioStep', () => {
|
|
118
|
-
test('records the addon whose system acts', async () => {
|
|
119
|
-
const { state, errors, cleanup } = await run(
|
|
120
|
-
[
|
|
121
|
-
IMPORTS,
|
|
122
|
-
'export const stripeWebhookArrives = pikkuAddonScenarioStep({',
|
|
123
|
-
" name: 'stripeWebhookArrives',",
|
|
124
|
-
" addon: 'stripe',",
|
|
125
|
-
" template: 'Stripe sends {event}',",
|
|
126
|
-
' func: async ({ logger }, data: { event: string }) => ({ ok: !!data.event }),',
|
|
127
|
-
'})',
|
|
128
|
-
].join('\n')
|
|
129
|
-
)
|
|
130
|
-
try {
|
|
131
|
-
assert.deepEqual(errors, [])
|
|
132
|
-
const meta = state.functions.meta['stripeWebhookArrives']
|
|
133
|
-
assert.equal(meta?.scenarioStepKind, 'addon')
|
|
134
|
-
assert.equal(meta?.scenarioStepAddon, 'stripe')
|
|
135
|
-
assert.equal(meta?.scenarioStepTemplate, 'Stripe sends {event}')
|
|
136
|
-
} finally {
|
|
137
|
-
await cleanup()
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
test('without an addon it is refused — nobody can say whose system acted', async () => {
|
|
142
|
-
const { state, errors, cleanup } = await run(
|
|
143
|
-
[
|
|
144
|
-
IMPORTS,
|
|
145
|
-
'export const stripeWebhookArrives = pikkuAddonScenarioStep({',
|
|
146
|
-
" name: 'stripeWebhookArrives',",
|
|
147
|
-
' func: async ({ logger }) => ({ ok: true }),',
|
|
148
|
-
'})',
|
|
149
|
-
].join('\n')
|
|
150
|
-
)
|
|
151
|
-
try {
|
|
152
|
-
assert.ok(
|
|
153
|
-
errors.some((message) => message.includes('addon')),
|
|
154
|
-
`expected a missing-addon error, got: ${errors.join(' | ')}`
|
|
155
|
-
)
|
|
156
|
-
assert.equal(state.functions.meta['stripeWebhookArrives'], undefined)
|
|
157
|
-
} finally {
|
|
158
|
-
await cleanup()
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
})
|
|
@@ -1,297 +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 type { 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-role-'))
|
|
27
|
-
const file = join(rootDir, 'roles.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-role-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
|
-
const IMPORTS = [
|
|
58
|
-
"import { defineScope } from '@pikku/core/scope'",
|
|
59
|
-
"import { defineSystemRole } from '@pikku/core/role'",
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
const withScopes = (...lines: string[]) =>
|
|
63
|
-
[
|
|
64
|
-
...IMPORTS,
|
|
65
|
-
'defineScope({',
|
|
66
|
-
' catalogue: { scopes: { read: {} } },',
|
|
67
|
-
' orders: { scopes: { create: {} } },',
|
|
68
|
-
' admin: { scopes: { users: { scopes: { ban: {} } } } },',
|
|
69
|
-
'})',
|
|
70
|
-
...lines,
|
|
71
|
-
].join('\n')
|
|
72
|
-
|
|
73
|
-
describe('addSystemRole inspector', () => {
|
|
74
|
-
test('extracts a role with its scopes', async () => {
|
|
75
|
-
const { state, criticals } = await inspectSource(
|
|
76
|
-
withScopes(
|
|
77
|
-
'defineSystemRole({',
|
|
78
|
-
' buyer: {',
|
|
79
|
-
" displayName: 'Buyer',",
|
|
80
|
-
" description: 'Browses and orders',",
|
|
81
|
-
" scopes: ['catalogue:read', 'orders:create'],",
|
|
82
|
-
' },',
|
|
83
|
-
'})'
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
assert.deepEqual(criticals, [])
|
|
88
|
-
assert.equal(state.systemRoles.definitions.length, 1)
|
|
89
|
-
const role = state.systemRoles.definitions[0]!
|
|
90
|
-
assert.equal(role.name, 'buyer')
|
|
91
|
-
assert.equal(role.displayName, 'Buyer')
|
|
92
|
-
assert.equal(role.description, 'Browses and orders')
|
|
93
|
-
assert.deepEqual(role.scopes, ['catalogue:read', 'orders:create'])
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('extracts several roles from one call', async () => {
|
|
97
|
-
const { state, criticals } = await inspectSource(
|
|
98
|
-
withScopes(
|
|
99
|
-
'defineSystemRole({',
|
|
100
|
-
" buyer: { scopes: ['catalogue:read'] },",
|
|
101
|
-
" admin: { scopes: ['admin'] },",
|
|
102
|
-
'})'
|
|
103
|
-
)
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
assert.deepEqual(criticals, [])
|
|
107
|
-
assert.deepEqual(
|
|
108
|
-
state.systemRoles.definitions.map((r) => r.name),
|
|
109
|
-
['buyer', 'admin']
|
|
110
|
-
)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test('extracts roles from several calls in one file', async () => {
|
|
114
|
-
const { state } = await inspectSource(
|
|
115
|
-
withScopes(
|
|
116
|
-
"defineSystemRole({ buyer: { scopes: ['catalogue:read'] } })",
|
|
117
|
-
"defineSystemRole({ admin: { scopes: ['admin'] } })"
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
|
|
122
|
-
'admin',
|
|
123
|
-
'buyer',
|
|
124
|
-
])
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
// Roles follow scopes: a project scaffolding user-admin has a generated
|
|
128
|
-
// declaration beside its own, so every file's roles have to survive.
|
|
129
|
-
test('declarations in separate files all survive', async () => {
|
|
130
|
-
const { state, criticals } = await inspectSources({
|
|
131
|
-
'roles.ts': withScopes(
|
|
132
|
-
"defineSystemRole({ buyer: { scopes: ['catalogue:read'] } })"
|
|
133
|
-
),
|
|
134
|
-
'more-roles.ts': [
|
|
135
|
-
"import { defineSystemRole } from '@pikku/core/role'",
|
|
136
|
-
"defineSystemRole({ admin: { scopes: ['admin'] } })",
|
|
137
|
-
].join('\n'),
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
assert.deepEqual(
|
|
141
|
-
criticals.filter((c) => /Only one defineSystemRole/.test(c.message)),
|
|
142
|
-
[],
|
|
143
|
-
`expected no duplicate critical, got ${JSON.stringify(criticals)}`
|
|
144
|
-
)
|
|
145
|
-
assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
|
|
146
|
-
'admin',
|
|
147
|
-
'buyer',
|
|
148
|
-
])
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// A generated file is not the one place a person adds a role to, so it neither
|
|
152
|
-
// takes the single slot nor collides with the app's own declaration.
|
|
153
|
-
test('a generated declaration neither claims the slot nor conflicts', async () => {
|
|
154
|
-
const { state, criticals } = await inspectSources({
|
|
155
|
-
'roles.gen.ts': [
|
|
156
|
-
"import { defineSystemRole } from '@pikku/core/role'",
|
|
157
|
-
'defineSystemRole({ admin: { scopes: [] } })',
|
|
158
|
-
].join('\n'),
|
|
159
|
-
'roles.ts': withScopes(
|
|
160
|
-
"defineSystemRole({ buyer: { scopes: ['catalogue:read'] } })"
|
|
161
|
-
),
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
assert.deepEqual(
|
|
165
|
-
criticals.filter(
|
|
166
|
-
(c) => c.code === ErrorCode.DUPLICATE_SYSTEM_ROLE_DEFINITION
|
|
167
|
-
),
|
|
168
|
-
[],
|
|
169
|
-
`expected no duplicate critical, got ${JSON.stringify(criticals)}`
|
|
170
|
-
)
|
|
171
|
-
assert.deepEqual(state.systemRoles.definitions.map((r) => r.name).sort(), [
|
|
172
|
-
'admin',
|
|
173
|
-
'buyer',
|
|
174
|
-
])
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
test('a role granting nothing is legitimate, said explicitly', async () => {
|
|
178
|
-
const { state, criticals } = await inspectSource(
|
|
179
|
-
withScopes('defineSystemRole({', ' guest: { scopes: [] },', '})')
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
assert.deepEqual(criticals, [])
|
|
183
|
-
assert.deepEqual(state.systemRoles.definitions[0]!.scopes, [])
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// Omitting `scopes` is not the same as declaring none — one is a decision,
|
|
187
|
-
// the other is a role nobody finished writing.
|
|
188
|
-
test('omitting scopes entirely is refused', async () => {
|
|
189
|
-
const { state, criticals } = await inspectSource(
|
|
190
|
-
withScopes('defineSystemRole({', " guest: { description: 'x' },", '})')
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
assert.equal(state.systemRoles.definitions.length, 0)
|
|
194
|
-
assert.ok(criticals.some((c) => /has no 'scopes'/.test(c.message)))
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
test('a name containing the scope separator is refused', async () => {
|
|
198
|
-
const { state, criticals } = await inspectSource(
|
|
199
|
-
withScopes('defineSystemRole({', " 'admin:users': { scopes: [] },", '})')
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
assert.equal(state.systemRoles.definitions.length, 0)
|
|
203
|
-
assert.ok(criticals.some((c) => /separator/.test(c.message)))
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// Scopes are read by AST, so a computed entry cannot be checked against the
|
|
207
|
-
// declared set — accepting it silently would let a role grant anything.
|
|
208
|
-
test('a computed scope entry is refused', async () => {
|
|
209
|
-
const { criticals } = await inspectSource(
|
|
210
|
-
withScopes(
|
|
211
|
-
'const s = "catalogue:read"',
|
|
212
|
-
'defineSystemRole({',
|
|
213
|
-
' buyer: { scopes: [s] },',
|
|
214
|
-
'})'
|
|
215
|
-
)
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
assert.ok(criticals.some((c) => /not a string literal/.test(c.message)))
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
test('`as const` does not hide the declaration', async () => {
|
|
222
|
-
const { state, criticals } = await inspectSource(
|
|
223
|
-
withScopes(
|
|
224
|
-
'defineSystemRole({',
|
|
225
|
-
" buyer: { scopes: ['catalogue:read'] },",
|
|
226
|
-
'} as const)'
|
|
227
|
-
)
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
assert.deepEqual(criticals, [])
|
|
231
|
-
assert.equal(state.systemRoles.definitions.length, 1)
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
describe('validateSystemRoleScopes', () => {
|
|
236
|
-
test('accepts declared scopes, including an intermediate node', async () => {
|
|
237
|
-
const { criticals } = await inspectSource(
|
|
238
|
-
withScopes(
|
|
239
|
-
'defineSystemRole({',
|
|
240
|
-
" admin: { scopes: ['admin', 'admin:users:ban'] },",
|
|
241
|
-
'})'
|
|
242
|
-
)
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
assert.deepEqual(criticals, [])
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
test('accepts a subtree wildcard whose node exists', async () => {
|
|
249
|
-
const { criticals } = await inspectSource(
|
|
250
|
-
withScopes(
|
|
251
|
-
'defineSystemRole({',
|
|
252
|
-
" admin: { scopes: ['admin:*'] },",
|
|
253
|
-
'})'
|
|
254
|
-
)
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
assert.deepEqual(criticals, [])
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
// The failure this exists to prevent: no function checks `orders:refund`, so
|
|
261
|
-
// a persona holding the role passes every call and the boundary the role
|
|
262
|
-
// describes is never exercised.
|
|
263
|
-
test('an undeclared scope fails the build and lists what is available', async () => {
|
|
264
|
-
const { criticals } = await inspectSource(
|
|
265
|
-
withScopes(
|
|
266
|
-
'defineSystemRole({',
|
|
267
|
-
" buyer: { scopes: ['orders:refund'] },",
|
|
268
|
-
'})'
|
|
269
|
-
)
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
const critical = criticals.find((c) => /orders:refund/.test(c.message))
|
|
273
|
-
assert.ok(critical, `expected a critical, got ${JSON.stringify(criticals)}`)
|
|
274
|
-
assert.match(critical!.message, /is not declared/)
|
|
275
|
-
assert.match(critical!.message, /catalogue:read/)
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
test('a subtree wildcard on an undeclared node fails', async () => {
|
|
279
|
-
const { criticals } = await inspectSource(
|
|
280
|
-
withScopes(
|
|
281
|
-
'defineSystemRole({',
|
|
282
|
-
" buyer: { scopes: ['nonsense:*'] },",
|
|
283
|
-
'})'
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
assert.ok(criticals.some((c) => /nonsense:\*/.test(c.message)))
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
test('the bare wildcard is refused — it hides what the role confers', async () => {
|
|
291
|
-
const { criticals } = await inspectSource(
|
|
292
|
-
withScopes('defineSystemRole({', " god: { scopes: ['*'] },", '})')
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
assert.ok(criticals.some((c) => /bare wildcard/.test(c.message)))
|
|
296
|
-
})
|
|
297
|
-
})
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import { getPropertyValue } from '../utils/get-property-value.js'
|
|
3
|
-
import type { AddWiring } from '../types.js'
|
|
4
|
-
import { ErrorCode } from '../error-codes.js'
|
|
5
|
-
|
|
6
|
-
const SEPARATOR = ':'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Unwraps `x as const` / `x satisfies T` so a cast declaration is still
|
|
10
|
-
* extracted rather than silently skipped.
|
|
11
|
-
*/
|
|
12
|
-
const unwrapAs = (node: ts.Expression): ts.Expression =>
|
|
13
|
-
ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
|
|
14
|
-
? unwrapAs(node.expression)
|
|
15
|
-
: node
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Reads a `scopes: [...]` array of string literals.
|
|
19
|
-
*
|
|
20
|
-
* Returns `undefined` rather than an empty array when the property is absent
|
|
21
|
-
* or unreadable, so the caller can tell "declared no scopes" from "we could
|
|
22
|
-
* not see the scopes" — the first is a legitimate placeholder role, the second
|
|
23
|
-
* would silently produce one.
|
|
24
|
-
*/
|
|
25
|
-
const extractScopes = (
|
|
26
|
-
obj: ts.ObjectLiteralExpression,
|
|
27
|
-
roleName: string,
|
|
28
|
-
logger: Parameters<AddWiring>[0]
|
|
29
|
-
): string[] | undefined => {
|
|
30
|
-
const prop = obj.properties.find(
|
|
31
|
-
(p) =>
|
|
32
|
-
ts.isPropertyAssignment(p) &&
|
|
33
|
-
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
34
|
-
p.name.text === 'scopes'
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
if (!prop) {
|
|
38
|
-
logger.critical(
|
|
39
|
-
ErrorCode.INVALID_VALUE,
|
|
40
|
-
`System role '${roleName}' has no 'scopes'. A role that grants nothing should say so with 'scopes: []'.`
|
|
41
|
-
)
|
|
42
|
-
return undefined
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
!ts.isPropertyAssignment(prop) ||
|
|
47
|
-
!ts.isArrayLiteralExpression(prop.initializer)
|
|
48
|
-
) {
|
|
49
|
-
logger.critical(
|
|
50
|
-
ErrorCode.INVALID_VALUE,
|
|
51
|
-
`System role '${roleName}' must declare 'scopes' as an array literal.`
|
|
52
|
-
)
|
|
53
|
-
return undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const scopes: string[] = []
|
|
57
|
-
for (const element of prop.initializer.elements) {
|
|
58
|
-
const unwrapped = unwrapAs(element)
|
|
59
|
-
if (!ts.isStringLiteral(unwrapped)) {
|
|
60
|
-
logger.critical(
|
|
61
|
-
ErrorCode.INVALID_VALUE,
|
|
62
|
-
`System role '${roleName}' lists a scope that is not a string literal. ` +
|
|
63
|
-
`Scopes are extracted by AST, so a computed value cannot be checked against the declared set.`
|
|
64
|
-
)
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
scopes.push(unwrapped.text)
|
|
68
|
-
}
|
|
69
|
-
return scopes
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const addSystemRole: AddWiring = (
|
|
73
|
-
logger,
|
|
74
|
-
node,
|
|
75
|
-
_checker,
|
|
76
|
-
state,
|
|
77
|
-
_options
|
|
78
|
-
) => {
|
|
79
|
-
if (!ts.isCallExpression(node)) {
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const expression = node.expression
|
|
84
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'defineSystemRole') {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const firstArg = node.arguments[0]
|
|
89
|
-
if (!firstArg) {
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const unwrapped = unwrapAs(firstArg)
|
|
94
|
-
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const sourceFile = node.getSourceFile().fileName
|
|
99
|
-
|
|
100
|
-
for (const prop of unwrapped.properties) {
|
|
101
|
-
if (!ts.isPropertyAssignment(prop)) {
|
|
102
|
-
continue
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let name: string | undefined
|
|
106
|
-
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
|
|
107
|
-
name = prop.name.text
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (name === undefined) {
|
|
111
|
-
logger.critical(
|
|
112
|
-
ErrorCode.NON_LITERAL_WIRE_NAME,
|
|
113
|
-
'A system role is declared with a key that is not a literal.'
|
|
114
|
-
)
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (name.length === 0) {
|
|
119
|
-
logger.critical(
|
|
120
|
-
ErrorCode.INVALID_VALUE,
|
|
121
|
-
'A system role is declared with an empty name.'
|
|
122
|
-
)
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (name.includes(SEPARATOR)) {
|
|
127
|
-
logger.critical(
|
|
128
|
-
ErrorCode.INVALID_VALUE,
|
|
129
|
-
`System role '${name}' contains the '${SEPARATOR}' separator. ` +
|
|
130
|
-
`'${SEPARATOR}' delimits scope ids; a role named like a scope reads as one.`
|
|
131
|
-
)
|
|
132
|
-
continue
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
136
|
-
logger.critical(
|
|
137
|
-
ErrorCode.INVALID_VALUE,
|
|
138
|
-
`System role '${name}' must be an object literal.`
|
|
139
|
-
)
|
|
140
|
-
continue
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const scopes = extractScopes(prop.initializer, name, logger)
|
|
144
|
-
if (scopes === undefined) {
|
|
145
|
-
continue
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const displayName = getPropertyValue(prop.initializer, 'displayName') as
|
|
149
|
-
string | null
|
|
150
|
-
const description = getPropertyValue(prop.initializer, 'description') as
|
|
151
|
-
string | null
|
|
152
|
-
|
|
153
|
-
state.systemRoles.files.add(sourceFile)
|
|
154
|
-
state.systemRoles.definitions.push({
|
|
155
|
-
name,
|
|
156
|
-
displayName: displayName || undefined,
|
|
157
|
-
description: description || undefined,
|
|
158
|
-
scopes,
|
|
159
|
-
sourceFile,
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
}
|