@pikku/inspector 0.12.50 → 0.12.54
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 +743 -0
- package/OPTIMIZATION-PLAN.md +9 -3
- package/dist/add/add-auth.d.ts +1 -1
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +7 -0
- package/dist/add/add-credential.js +1 -1
- package/dist/add/add-functions.js +199 -8
- package/dist/add/add-http-route.js +16 -6
- package/dist/add/add-middleware.js +46 -3
- package/dist/add/add-personas.d.ts +13 -0
- package/dist/add/add-personas.js +216 -0
- package/dist/add/add-scope.js +1 -1
- package/dist/add/add-secret.js +1 -1
- package/dist/add/add-system-role.d.ts +2 -0
- package/dist/add/add-system-role.js +102 -0
- package/dist/add/add-variable.js +1 -1
- package/dist/add/add-wire-addon.js +31 -0
- package/dist/add/add-workflow.js +12 -1
- package/dist/error-codes.d.ts +17 -1
- package/dist/error-codes.js +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +33 -1
- package/dist/types.d.ts +55 -2
- package/dist/utils/annotate-http-route-auth.d.ts +22 -0
- package/dist/utils/annotate-http-route-auth.js +48 -0
- package/dist/utils/check-secret-sinks.d.ts +31 -0
- package/dist/utils/check-secret-sinks.js +117 -0
- package/dist/utils/extract-function-name.js +13 -4
- package/dist/utils/extract-function-node.js +18 -1
- package/dist/utils/extract-secret-usage.d.ts +9 -0
- package/dist/utils/extract-secret-usage.js +61 -0
- package/dist/utils/literal-properties.d.ts +22 -0
- package/dist/utils/literal-properties.js +69 -0
- package/dist/utils/load-addon-functions-meta.js +26 -1
- package/dist/utils/permissions.js +8 -0
- package/dist/utils/post-process.d.ts +68 -3
- package/dist/utils/post-process.js +246 -13
- package/dist/utils/secret-alias-services.d.ts +8 -0
- package/dist/utils/secret-alias-services.js +45 -0
- package/dist/utils/secret-brokers.d.ts +2 -0
- package/dist/utils/secret-brokers.js +9 -0
- package/dist/utils/serialize-inspector-state.d.ts +10 -0
- package/dist/utils/serialize-inspector-state.js +18 -0
- package/dist/utils/serialize-middleware-groups-meta.js +3 -0
- package/dist/utils/single-declaration.d.ts +32 -0
- package/dist/utils/single-declaration.js +43 -0
- package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
- package/dist/utils/validate-exposed-functions-gated.js +93 -0
- package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
- package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +6 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +48 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +9 -4
- package/dist/utils/workflow/dsl/patterns.js +26 -4
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-auth.test.ts +5 -3
- package/src/add/add-auth.ts +1 -1
- package/src/add/add-cli.ts +8 -0
- package/src/add/add-credential.ts +1 -1
- package/src/add/add-functions.test.ts +263 -1
- package/src/add/add-functions.ts +273 -11
- package/src/add/add-http-route.ts +20 -7
- package/src/add/add-middleware.test.ts +196 -0
- package/src/add/add-middleware.ts +64 -4
- package/src/add/add-personas.test.ts +382 -0
- package/src/add/add-personas.ts +285 -0
- package/src/add/add-scenario-step.test.ts +87 -5
- package/src/add/add-scope.test.ts +103 -30
- package/src/add/add-scope.ts +1 -1
- package/src/add/add-secret.ts +1 -1
- package/src/add/add-subject-scenario-steps.test.ts +161 -0
- package/src/add/add-system-role.test.ts +297 -0
- package/src/add/add-system-role.ts +164 -0
- package/src/add/add-variable.ts +1 -1
- package/src/add/add-wire-addon.test.ts +111 -0
- package/src/add/add-wire-addon.ts +29 -0
- package/src/add/add-workflow.ts +21 -1
- package/src/add/addon-bans.test.ts +4 -4
- package/src/add/allow-escape-hatches.test.ts +144 -0
- package/src/add/audit-meta.test.ts +75 -0
- package/src/add/config-value-metadata.test.ts +11 -11
- package/src/add/secret-sink-check.test.ts +211 -0
- package/src/add/wire-name-literal.test.ts +2 -2
- package/src/error-codes.ts +26 -0
- package/src/index.ts +4 -1
- package/src/inspector.ts +39 -0
- package/src/types.ts +56 -2
- package/src/utils/annotate-http-route-auth.test.ts +163 -0
- package/src/utils/annotate-http-route-auth.ts +52 -0
- package/src/utils/check-secret-sinks.ts +145 -0
- package/src/utils/extract-function-name.ts +14 -4
- package/src/utils/extract-function-node.ts +18 -6
- package/src/utils/extract-secret-usage.test.ts +81 -0
- package/src/utils/extract-secret-usage.ts +73 -0
- package/src/utils/literal-properties.ts +97 -0
- package/src/utils/load-addon-functions-meta.ts +32 -2
- package/src/utils/permissions-shorthand.test.ts +79 -0
- package/src/utils/permissions.ts +7 -0
- package/src/utils/post-process.test.ts +133 -0
- package/src/utils/post-process.ts +313 -12
- package/src/utils/secret-alias-services.test.ts +146 -0
- package/src/utils/secret-alias-services.ts +58 -0
- package/src/utils/secret-brokers.test.ts +53 -0
- package/src/utils/secret-brokers.ts +11 -0
- package/src/utils/serialize-inspector-state.ts +28 -0
- package/src/utils/serialize-middleware-groups-meta.ts +3 -0
- package/src/utils/single-declaration.ts +57 -0
- package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
- package/src/utils/validate-exposed-functions-gated.ts +103 -0
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
- package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +57 -0
- package/src/utils/workflow/dsl/patterns.ts +33 -4
- package/src/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -30,13 +30,13 @@ const STEPS = [
|
|
|
30
30
|
'export const buysAnApple = pikkuScenarioStep({',
|
|
31
31
|
" name: 'buysAnApple',",
|
|
32
32
|
" description: 'buys an apple',",
|
|
33
|
-
' browser:
|
|
34
|
-
'
|
|
33
|
+
' browser: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
34
|
+
' default: async ({ logger }, data: { qty: number }) => ({ ok: data.qty > 0 }),',
|
|
35
35
|
'})',
|
|
36
36
|
'export const seesAReceipt = pikkuScenarioStep({',
|
|
37
37
|
" name: 'seesAReceipt',",
|
|
38
38
|
" description: 'sees a receipt',",
|
|
39
|
-
'
|
|
39
|
+
' default: async ({ logger }) => ({ ok: true }),',
|
|
40
40
|
'})',
|
|
41
41
|
].join('\n')
|
|
42
42
|
|
|
@@ -144,7 +144,7 @@ describe('pikkuScenarioStep', () => {
|
|
|
144
144
|
|
|
145
145
|
test('the step target is bundled, so the step function is wired', async () => {
|
|
146
146
|
const { state, cleanup } = await run([
|
|
147
|
-
"await scenario.
|
|
147
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
148
148
|
])
|
|
149
149
|
try {
|
|
150
150
|
assert.ok(
|
|
@@ -183,7 +183,7 @@ describe('pikkuScenarioStep', () => {
|
|
|
183
183
|
test('a non-literal step target is a PKU678 critical', async () => {
|
|
184
184
|
const { criticals, cleanup } = await run([
|
|
185
185
|
'const target = Math.random() > 0.5 ? "buysAnApple" : "seesAReceipt"',
|
|
186
|
-
"await scenario.
|
|
186
|
+
"await scenario.when('buys something', target as any, {}, { actor: actors.shopper })",
|
|
187
187
|
])
|
|
188
188
|
try {
|
|
189
189
|
assert.ok(
|
|
@@ -209,6 +209,88 @@ describe('pikkuScenarioStep', () => {
|
|
|
209
209
|
}
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
+
test('a scenario that never asserts is a PKU680 critical', async () => {
|
|
213
|
+
const { criticals, cleanup } = await run([
|
|
214
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
215
|
+
"await scenario.when('buys an apple', 'buysAnApple', { qty: 2 }, { actor: actors.shopper })",
|
|
216
|
+
])
|
|
217
|
+
try {
|
|
218
|
+
assert.ok(
|
|
219
|
+
criticals.find((c) => c.code === 'PKU680'),
|
|
220
|
+
`a flow with no 'then' proves only that nothing threw, and contributes 0/0 to witness coverage, got: ${JSON.stringify(criticals)}`
|
|
221
|
+
)
|
|
222
|
+
} finally {
|
|
223
|
+
await cleanup()
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('a scenario whose witness is an expectation helper is not flagged', async () => {
|
|
228
|
+
// `expectService` is an inline step and carries no phase, so it reaches
|
|
229
|
+
// PKU680 as nothing at all — but a recorded service call is a witness, and
|
|
230
|
+
// a flow that has one is not the assertion-free flow the rule is for.
|
|
231
|
+
const { criticals, cleanup } = await run([
|
|
232
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
233
|
+
"await scenario.expectService('the receipt was emailed', 'emailService.send', { actor: actors.shopper })",
|
|
234
|
+
])
|
|
235
|
+
try {
|
|
236
|
+
assert.equal(
|
|
237
|
+
criticals.filter((c) => c.code === 'PKU680').length,
|
|
238
|
+
0,
|
|
239
|
+
`got: ${JSON.stringify(criticals)}`
|
|
240
|
+
)
|
|
241
|
+
} finally {
|
|
242
|
+
await cleanup()
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('an expectation parked in an uncalled helper is still no assertion', async () => {
|
|
247
|
+
const { criticals, cleanup } = await run([
|
|
248
|
+
'const neverCalled = async () => {',
|
|
249
|
+
" await scenario.expectService('the receipt was emailed', 'emailService.send')",
|
|
250
|
+
'}',
|
|
251
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
252
|
+
])
|
|
253
|
+
try {
|
|
254
|
+
assert.ok(
|
|
255
|
+
criticals.find((c) => c.code === 'PKU680'),
|
|
256
|
+
`expected PKU680 — the helper never runs, so nothing is asserted; got: ${JSON.stringify(criticals)}`
|
|
257
|
+
)
|
|
258
|
+
} finally {
|
|
259
|
+
await cleanup()
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('a scenario with an assertion is not flagged', async () => {
|
|
264
|
+
const { criticals, cleanup } = await run([
|
|
265
|
+
"await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })",
|
|
266
|
+
"await scenario.then('sees a receipt', 'seesAReceipt', {}, { actor: actors.shopper })",
|
|
267
|
+
])
|
|
268
|
+
try {
|
|
269
|
+
assert.equal(
|
|
270
|
+
criticals.filter((c) => c.code === 'PKU680').length,
|
|
271
|
+
0,
|
|
272
|
+
`got: ${JSON.stringify(criticals)}`
|
|
273
|
+
)
|
|
274
|
+
} finally {
|
|
275
|
+
await cleanup()
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('a scenario driving only rpc steps is not flagged — it has no step ladder to assert on', async () => {
|
|
280
|
+
const { criticals, cleanup } = await run([
|
|
281
|
+
"await scenario.do('reads the order', 'orderGet', { orderId: 'o1' })",
|
|
282
|
+
])
|
|
283
|
+
try {
|
|
284
|
+
assert.equal(
|
|
285
|
+
criticals.filter((c) => c.code === 'PKU680').length,
|
|
286
|
+
0,
|
|
287
|
+
`the rule is about scenarios written as given/when/then, got: ${JSON.stringify(criticals)}`
|
|
288
|
+
)
|
|
289
|
+
} finally {
|
|
290
|
+
await cleanup()
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
212
294
|
test('a non-browser step without an actor is allowed', async () => {
|
|
213
295
|
const { criticals, cleanup } = await run([
|
|
214
296
|
"await scenario.given('sees a receipt', 'seesAReceipt', {})",
|
|
@@ -35,12 +35,31 @@ const inspectSource = async (source: string) => {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
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
|
+
|
|
38
57
|
describe('addScope inspector', () => {
|
|
39
58
|
test('extracts a flat scope', async () => {
|
|
40
59
|
const { state, criticals } = await inspectSource(
|
|
41
60
|
[
|
|
42
|
-
"import {
|
|
43
|
-
'
|
|
61
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
62
|
+
'defineScope({',
|
|
44
63
|
' admin: {',
|
|
45
64
|
" displayName: 'Administration',",
|
|
46
65
|
" description: 'Administrative access',",
|
|
@@ -62,8 +81,8 @@ describe('addScope inspector', () => {
|
|
|
62
81
|
test('extracts a displayName at every depth', async () => {
|
|
63
82
|
const { state, criticals } = await inspectSource(
|
|
64
83
|
[
|
|
65
|
-
"import {
|
|
66
|
-
'
|
|
84
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
85
|
+
'defineScope({',
|
|
67
86
|
' admin: {',
|
|
68
87
|
" displayName: 'Administration',",
|
|
69
88
|
' scopes: {',
|
|
@@ -90,8 +109,8 @@ describe('addScope inspector', () => {
|
|
|
90
109
|
test('extracts a nested scope tree', async () => {
|
|
91
110
|
const { state, criticals } = await inspectSource(
|
|
92
111
|
[
|
|
93
|
-
"import {
|
|
94
|
-
'
|
|
112
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
113
|
+
'defineScope({',
|
|
95
114
|
' admin: {',
|
|
96
115
|
' scopes: {',
|
|
97
116
|
' invoices: {',
|
|
@@ -120,8 +139,8 @@ describe('addScope inspector', () => {
|
|
|
120
139
|
test('records the source file', async () => {
|
|
121
140
|
const { state, file } = await inspectSource(
|
|
122
141
|
[
|
|
123
|
-
"import {
|
|
124
|
-
'
|
|
142
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
143
|
+
'defineScope({ admin: {} })',
|
|
125
144
|
].join('\n')
|
|
126
145
|
)
|
|
127
146
|
|
|
@@ -134,8 +153,8 @@ describe('addScope inspector', () => {
|
|
|
134
153
|
test('extracts several roots from one declaration', async () => {
|
|
135
154
|
const { state, criticals } = await inspectSource(
|
|
136
155
|
[
|
|
137
|
-
"import {
|
|
138
|
-
'
|
|
156
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
157
|
+
'defineScope({ admin: {}, billing: { scopes: { read: {} } } })',
|
|
139
158
|
].join('\n')
|
|
140
159
|
)
|
|
141
160
|
|
|
@@ -150,9 +169,9 @@ describe('addScope inspector', () => {
|
|
|
150
169
|
test('extracts several declarations', async () => {
|
|
151
170
|
const { state } = await inspectSource(
|
|
152
171
|
[
|
|
153
|
-
"import {
|
|
154
|
-
'
|
|
155
|
-
'
|
|
172
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
173
|
+
'defineScope({ admin: {} })',
|
|
174
|
+
'defineScope({ billing: {} })',
|
|
156
175
|
].join('\n')
|
|
157
176
|
)
|
|
158
177
|
|
|
@@ -162,11 +181,65 @@ describe('addScope inspector', () => {
|
|
|
162
181
|
)
|
|
163
182
|
})
|
|
164
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(
|
|
206
|
+
state.scopes.definitions.map((d) => d.name).sort(),
|
|
207
|
+
['admin', '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
|
+
|
|
165
238
|
test('is critical when a root embeds the separator', async () => {
|
|
166
239
|
const { criticals } = await inspectSource(
|
|
167
240
|
[
|
|
168
|
-
"import {
|
|
169
|
-
"
|
|
241
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
242
|
+
"defineScope({ 'admin:users': {} })",
|
|
170
243
|
].join('\n')
|
|
171
244
|
)
|
|
172
245
|
|
|
@@ -179,8 +252,8 @@ describe('addScope inspector', () => {
|
|
|
179
252
|
test('is critical when a root is the wildcard', async () => {
|
|
180
253
|
const { criticals } = await inspectSource(
|
|
181
254
|
[
|
|
182
|
-
"import {
|
|
183
|
-
"
|
|
255
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
256
|
+
"defineScope({ '*': {} })",
|
|
184
257
|
].join('\n')
|
|
185
258
|
)
|
|
186
259
|
|
|
@@ -193,9 +266,9 @@ describe('addScope inspector', () => {
|
|
|
193
266
|
test('is critical when a root key is not a literal', async () => {
|
|
194
267
|
const { criticals } = await inspectSource(
|
|
195
268
|
[
|
|
196
|
-
"import {
|
|
269
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
197
270
|
'const k = String(1)',
|
|
198
|
-
'
|
|
271
|
+
'defineScope({ [k]: {} } as any)',
|
|
199
272
|
].join('\n')
|
|
200
273
|
)
|
|
201
274
|
|
|
@@ -208,8 +281,8 @@ describe('addScope inspector', () => {
|
|
|
208
281
|
test('is critical when a root is not an object literal', async () => {
|
|
209
282
|
const { criticals } = await inspectSource(
|
|
210
283
|
[
|
|
211
|
-
"import {
|
|
212
|
-
"
|
|
284
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
285
|
+
"defineScope({ admin: 'nope' } as any)",
|
|
213
286
|
].join('\n')
|
|
214
287
|
)
|
|
215
288
|
|
|
@@ -223,7 +296,7 @@ describe('addScope inspector', () => {
|
|
|
223
296
|
describe('validateScopeReferences', () => {
|
|
224
297
|
const funcSource = (scopes: string, decls: string[] = []) =>
|
|
225
298
|
[
|
|
226
|
-
"import {
|
|
299
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
227
300
|
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
228
301
|
...decls,
|
|
229
302
|
'export const f = pikkuSessionlessFunc({',
|
|
@@ -235,7 +308,7 @@ describe('validateScopeReferences', () => {
|
|
|
235
308
|
test('accepts a declared scope', async () => {
|
|
236
309
|
const { criticals } = await inspectSource(
|
|
237
310
|
funcSource("['admin:invoices:create']", [
|
|
238
|
-
'
|
|
311
|
+
'defineScope({',
|
|
239
312
|
' admin: {',
|
|
240
313
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
241
314
|
' },',
|
|
@@ -249,7 +322,7 @@ describe('validateScopeReferences', () => {
|
|
|
249
322
|
test('accepts an intermediate node of a declared tree', async () => {
|
|
250
323
|
const { criticals } = await inspectSource(
|
|
251
324
|
funcSource("['admin:invoices']", [
|
|
252
|
-
'
|
|
325
|
+
'defineScope({',
|
|
253
326
|
' admin: {',
|
|
254
327
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
255
328
|
' },',
|
|
@@ -262,7 +335,7 @@ describe('validateScopeReferences', () => {
|
|
|
262
335
|
|
|
263
336
|
test('rejects an undeclared scope', async () => {
|
|
264
337
|
const { criticals } = await inspectSource(
|
|
265
|
-
funcSource("['billing:read']", ['
|
|
338
|
+
funcSource("['billing:read']", ['defineScope({ admin: {} })'])
|
|
266
339
|
)
|
|
267
340
|
|
|
268
341
|
assert.ok(
|
|
@@ -277,7 +350,7 @@ describe('validateScopeReferences', () => {
|
|
|
277
350
|
|
|
278
351
|
test('lists the available scopes when one is undeclared', async () => {
|
|
279
352
|
const { criticals } = await inspectSource(
|
|
280
|
-
funcSource("['nope']", ['
|
|
353
|
+
funcSource("['nope']", ['defineScope({ admin: {} })'])
|
|
281
354
|
)
|
|
282
355
|
|
|
283
356
|
assert.ok(
|
|
@@ -289,7 +362,7 @@ describe('validateScopeReferences', () => {
|
|
|
289
362
|
test('rejects a typo in a nested scope', async () => {
|
|
290
363
|
const { criticals } = await inspectSource(
|
|
291
364
|
funcSource("['admin:invoice:create']", [
|
|
292
|
-
'
|
|
365
|
+
'defineScope({',
|
|
293
366
|
' admin: {',
|
|
294
367
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
295
368
|
' },',
|
|
@@ -305,7 +378,7 @@ describe('validateScopeReferences', () => {
|
|
|
305
378
|
|
|
306
379
|
test('accepts a wildcard requirement whose node is declared', async () => {
|
|
307
380
|
const { criticals } = await inspectSource(
|
|
308
|
-
funcSource("['admin:*']", ['
|
|
381
|
+
funcSource("['admin:*']", ['defineScope({ admin: {} })'])
|
|
309
382
|
)
|
|
310
383
|
|
|
311
384
|
assert.deepEqual(criticals, [])
|
|
@@ -313,7 +386,7 @@ describe('validateScopeReferences', () => {
|
|
|
313
386
|
|
|
314
387
|
test('rejects a wildcard requirement whose node is undeclared', async () => {
|
|
315
388
|
const { criticals } = await inspectSource(
|
|
316
|
-
funcSource("['billing:*']", ['
|
|
389
|
+
funcSource("['billing:*']", ['defineScope({ admin: {} })'])
|
|
317
390
|
)
|
|
318
391
|
|
|
319
392
|
assert.ok(
|
|
@@ -324,7 +397,7 @@ describe('validateScopeReferences', () => {
|
|
|
324
397
|
|
|
325
398
|
test('rejects a bare wildcard requirement', async () => {
|
|
326
399
|
const { criticals } = await inspectSource(
|
|
327
|
-
funcSource("['*']", ['
|
|
400
|
+
funcSource("['*']", ['defineScope({ admin: {} })'])
|
|
328
401
|
)
|
|
329
402
|
|
|
330
403
|
assert.ok(
|
package/src/add/add-scope.ts
CHANGED
|
@@ -142,7 +142,7 @@ export const addScope: AddWiring = (logger, node, checker, state, _options) => {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const expression = node.expression
|
|
145
|
-
if (!ts.isIdentifier(expression) || expression.text !== '
|
|
145
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
|
|
146
146
|
return
|
|
147
147
|
}
|
|
148
148
|
|
package/src/add/add-secret.ts
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
})
|