@pikku/inspector 0.12.50 → 0.12.52
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 +441 -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 +156 -7
- 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 +210 -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/error-codes.d.ts +10 -1
- package/dist/error-codes.js +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +33 -1
- package/dist/types.d.ts +46 -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/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 +243 -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/validate-exposed-functions-gated.d.ts +14 -0
- package/dist/utils/validate-exposed-functions-gated.js +85 -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/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 +219 -9
- 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 +268 -0
- package/src/add/add-personas.ts +276 -0
- package/src/add/add-scenario-step.test.ts +49 -3
- package/src/add/add-scope.test.ts +30 -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 +214 -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/addon-bans.test.ts +4 -4
- package/src/add/config-value-metadata.test.ts +11 -11
- package/src/add/wire-name-literal.test.ts +2 -2
- package/src/error-codes.ts +13 -0
- package/src/index.ts +4 -1
- package/src/inspector.ts +39 -0
- package/src/types.ts +47 -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/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 +310 -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/validate-exposed-functions-gated.test.ts +230 -0
- package/src/utils/validate-exposed-functions-gated.ts +94 -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/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -39,8 +39,8 @@ describe('addScope inspector', () => {
|
|
|
39
39
|
test('extracts a flat scope', async () => {
|
|
40
40
|
const { state, criticals } = await inspectSource(
|
|
41
41
|
[
|
|
42
|
-
"import {
|
|
43
|
-
'
|
|
42
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
43
|
+
'defineScope({',
|
|
44
44
|
' admin: {',
|
|
45
45
|
" displayName: 'Administration',",
|
|
46
46
|
" description: 'Administrative access',",
|
|
@@ -62,8 +62,8 @@ describe('addScope inspector', () => {
|
|
|
62
62
|
test('extracts a displayName at every depth', async () => {
|
|
63
63
|
const { state, criticals } = await inspectSource(
|
|
64
64
|
[
|
|
65
|
-
"import {
|
|
66
|
-
'
|
|
65
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
66
|
+
'defineScope({',
|
|
67
67
|
' admin: {',
|
|
68
68
|
" displayName: 'Administration',",
|
|
69
69
|
' scopes: {',
|
|
@@ -90,8 +90,8 @@ describe('addScope inspector', () => {
|
|
|
90
90
|
test('extracts a nested scope tree', async () => {
|
|
91
91
|
const { state, criticals } = await inspectSource(
|
|
92
92
|
[
|
|
93
|
-
"import {
|
|
94
|
-
'
|
|
93
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
94
|
+
'defineScope({',
|
|
95
95
|
' admin: {',
|
|
96
96
|
' scopes: {',
|
|
97
97
|
' invoices: {',
|
|
@@ -120,8 +120,8 @@ describe('addScope inspector', () => {
|
|
|
120
120
|
test('records the source file', async () => {
|
|
121
121
|
const { state, file } = await inspectSource(
|
|
122
122
|
[
|
|
123
|
-
"import {
|
|
124
|
-
'
|
|
123
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
124
|
+
'defineScope({ admin: {} })',
|
|
125
125
|
].join('\n')
|
|
126
126
|
)
|
|
127
127
|
|
|
@@ -134,8 +134,8 @@ describe('addScope inspector', () => {
|
|
|
134
134
|
test('extracts several roots from one declaration', async () => {
|
|
135
135
|
const { state, criticals } = await inspectSource(
|
|
136
136
|
[
|
|
137
|
-
"import {
|
|
138
|
-
'
|
|
137
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
138
|
+
'defineScope({ admin: {}, billing: { scopes: { read: {} } } })',
|
|
139
139
|
].join('\n')
|
|
140
140
|
)
|
|
141
141
|
|
|
@@ -150,9 +150,9 @@ describe('addScope inspector', () => {
|
|
|
150
150
|
test('extracts several declarations', async () => {
|
|
151
151
|
const { state } = await inspectSource(
|
|
152
152
|
[
|
|
153
|
-
"import {
|
|
154
|
-
'
|
|
155
|
-
'
|
|
153
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
154
|
+
'defineScope({ admin: {} })',
|
|
155
|
+
'defineScope({ billing: {} })',
|
|
156
156
|
].join('\n')
|
|
157
157
|
)
|
|
158
158
|
|
|
@@ -165,8 +165,8 @@ describe('addScope inspector', () => {
|
|
|
165
165
|
test('is critical when a root embeds the separator', async () => {
|
|
166
166
|
const { criticals } = await inspectSource(
|
|
167
167
|
[
|
|
168
|
-
"import {
|
|
169
|
-
"
|
|
168
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
169
|
+
"defineScope({ 'admin:users': {} })",
|
|
170
170
|
].join('\n')
|
|
171
171
|
)
|
|
172
172
|
|
|
@@ -179,8 +179,8 @@ describe('addScope inspector', () => {
|
|
|
179
179
|
test('is critical when a root is the wildcard', async () => {
|
|
180
180
|
const { criticals } = await inspectSource(
|
|
181
181
|
[
|
|
182
|
-
"import {
|
|
183
|
-
"
|
|
182
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
183
|
+
"defineScope({ '*': {} })",
|
|
184
184
|
].join('\n')
|
|
185
185
|
)
|
|
186
186
|
|
|
@@ -193,9 +193,9 @@ describe('addScope inspector', () => {
|
|
|
193
193
|
test('is critical when a root key is not a literal', async () => {
|
|
194
194
|
const { criticals } = await inspectSource(
|
|
195
195
|
[
|
|
196
|
-
"import {
|
|
196
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
197
197
|
'const k = String(1)',
|
|
198
|
-
'
|
|
198
|
+
'defineScope({ [k]: {} } as any)',
|
|
199
199
|
].join('\n')
|
|
200
200
|
)
|
|
201
201
|
|
|
@@ -208,8 +208,8 @@ describe('addScope inspector', () => {
|
|
|
208
208
|
test('is critical when a root is not an object literal', async () => {
|
|
209
209
|
const { criticals } = await inspectSource(
|
|
210
210
|
[
|
|
211
|
-
"import {
|
|
212
|
-
"
|
|
211
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
212
|
+
"defineScope({ admin: 'nope' } as any)",
|
|
213
213
|
].join('\n')
|
|
214
214
|
)
|
|
215
215
|
|
|
@@ -223,7 +223,7 @@ describe('addScope inspector', () => {
|
|
|
223
223
|
describe('validateScopeReferences', () => {
|
|
224
224
|
const funcSource = (scopes: string, decls: string[] = []) =>
|
|
225
225
|
[
|
|
226
|
-
"import {
|
|
226
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
227
227
|
"import { pikkuSessionlessFunc } from '@pikku/core'",
|
|
228
228
|
...decls,
|
|
229
229
|
'export const f = pikkuSessionlessFunc({',
|
|
@@ -235,7 +235,7 @@ describe('validateScopeReferences', () => {
|
|
|
235
235
|
test('accepts a declared scope', async () => {
|
|
236
236
|
const { criticals } = await inspectSource(
|
|
237
237
|
funcSource("['admin:invoices:create']", [
|
|
238
|
-
'
|
|
238
|
+
'defineScope({',
|
|
239
239
|
' admin: {',
|
|
240
240
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
241
241
|
' },',
|
|
@@ -249,7 +249,7 @@ describe('validateScopeReferences', () => {
|
|
|
249
249
|
test('accepts an intermediate node of a declared tree', async () => {
|
|
250
250
|
const { criticals } = await inspectSource(
|
|
251
251
|
funcSource("['admin:invoices']", [
|
|
252
|
-
'
|
|
252
|
+
'defineScope({',
|
|
253
253
|
' admin: {',
|
|
254
254
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
255
255
|
' },',
|
|
@@ -262,7 +262,7 @@ describe('validateScopeReferences', () => {
|
|
|
262
262
|
|
|
263
263
|
test('rejects an undeclared scope', async () => {
|
|
264
264
|
const { criticals } = await inspectSource(
|
|
265
|
-
funcSource("['billing:read']", ['
|
|
265
|
+
funcSource("['billing:read']", ['defineScope({ admin: {} })'])
|
|
266
266
|
)
|
|
267
267
|
|
|
268
268
|
assert.ok(
|
|
@@ -277,7 +277,7 @@ describe('validateScopeReferences', () => {
|
|
|
277
277
|
|
|
278
278
|
test('lists the available scopes when one is undeclared', async () => {
|
|
279
279
|
const { criticals } = await inspectSource(
|
|
280
|
-
funcSource("['nope']", ['
|
|
280
|
+
funcSource("['nope']", ['defineScope({ admin: {} })'])
|
|
281
281
|
)
|
|
282
282
|
|
|
283
283
|
assert.ok(
|
|
@@ -289,7 +289,7 @@ describe('validateScopeReferences', () => {
|
|
|
289
289
|
test('rejects a typo in a nested scope', async () => {
|
|
290
290
|
const { criticals } = await inspectSource(
|
|
291
291
|
funcSource("['admin:invoice:create']", [
|
|
292
|
-
'
|
|
292
|
+
'defineScope({',
|
|
293
293
|
' admin: {',
|
|
294
294
|
' scopes: { invoices: { scopes: { create: {} } } },',
|
|
295
295
|
' },',
|
|
@@ -305,7 +305,7 @@ describe('validateScopeReferences', () => {
|
|
|
305
305
|
|
|
306
306
|
test('accepts a wildcard requirement whose node is declared', async () => {
|
|
307
307
|
const { criticals } = await inspectSource(
|
|
308
|
-
funcSource("['admin:*']", ['
|
|
308
|
+
funcSource("['admin:*']", ['defineScope({ admin: {} })'])
|
|
309
309
|
)
|
|
310
310
|
|
|
311
311
|
assert.deepEqual(criticals, [])
|
|
@@ -313,7 +313,7 @@ describe('validateScopeReferences', () => {
|
|
|
313
313
|
|
|
314
314
|
test('rejects a wildcard requirement whose node is undeclared', async () => {
|
|
315
315
|
const { criticals } = await inspectSource(
|
|
316
|
-
funcSource("['billing:*']", ['
|
|
316
|
+
funcSource("['billing:*']", ['defineScope({ admin: {} })'])
|
|
317
317
|
)
|
|
318
318
|
|
|
319
319
|
assert.ok(
|
|
@@ -324,7 +324,7 @@ describe('validateScopeReferences', () => {
|
|
|
324
324
|
|
|
325
325
|
test('rejects a bare wildcard requirement', async () => {
|
|
326
326
|
const { criticals } = await inspectSource(
|
|
327
|
-
funcSource("['*']", ['
|
|
327
|
+
funcSource("['*']", ['defineScope({ admin: {} })'])
|
|
328
328
|
)
|
|
329
329
|
|
|
330
330
|
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
|
+
})
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
const IMPORTS = [
|
|
39
|
+
"import { defineScope } from '@pikku/core/scope'",
|
|
40
|
+
"import { defineSystemRole } from '@pikku/core/role'",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const withScopes = (...lines: string[]) =>
|
|
44
|
+
[
|
|
45
|
+
...IMPORTS,
|
|
46
|
+
'defineScope({',
|
|
47
|
+
' catalogue: { scopes: { read: {} } },',
|
|
48
|
+
' orders: { scopes: { create: {} } },',
|
|
49
|
+
' admin: { scopes: { users: { scopes: { ban: {} } } } },',
|
|
50
|
+
'})',
|
|
51
|
+
...lines,
|
|
52
|
+
].join('\n')
|
|
53
|
+
|
|
54
|
+
describe('addSystemRole inspector', () => {
|
|
55
|
+
test('extracts a role with its scopes', async () => {
|
|
56
|
+
const { state, criticals } = await inspectSource(
|
|
57
|
+
withScopes(
|
|
58
|
+
'defineSystemRole({',
|
|
59
|
+
' buyer: {',
|
|
60
|
+
" displayName: 'Buyer',",
|
|
61
|
+
" description: 'Browses and orders',",
|
|
62
|
+
" scopes: ['catalogue:read', 'orders:create'],",
|
|
63
|
+
' },',
|
|
64
|
+
'})'
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(criticals, [])
|
|
69
|
+
assert.equal(state.systemRoles.definitions.length, 1)
|
|
70
|
+
const role = state.systemRoles.definitions[0]!
|
|
71
|
+
assert.equal(role.name, 'buyer')
|
|
72
|
+
assert.equal(role.displayName, 'Buyer')
|
|
73
|
+
assert.equal(role.description, 'Browses and orders')
|
|
74
|
+
assert.deepEqual(role.scopes, ['catalogue:read', 'orders:create'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('extracts several roles from one call', async () => {
|
|
78
|
+
const { state, criticals } = await inspectSource(
|
|
79
|
+
withScopes(
|
|
80
|
+
'defineSystemRole({',
|
|
81
|
+
" buyer: { scopes: ['catalogue:read'] },",
|
|
82
|
+
" admin: { scopes: ['admin'] },",
|
|
83
|
+
'})'
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert.deepEqual(criticals, [])
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
state.systemRoles.definitions.map((r) => r.name),
|
|
90
|
+
['buyer', 'admin']
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('a role granting nothing is legitimate, said explicitly', async () => {
|
|
95
|
+
const { state, criticals } = await inspectSource(
|
|
96
|
+
withScopes('defineSystemRole({', ' guest: { scopes: [] },', '})')
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(criticals, [])
|
|
100
|
+
assert.deepEqual(state.systemRoles.definitions[0]!.scopes, [])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Omitting `scopes` is not the same as declaring none — one is a decision,
|
|
104
|
+
// the other is a role nobody finished writing.
|
|
105
|
+
test('omitting scopes entirely is refused', async () => {
|
|
106
|
+
const { state, criticals } = await inspectSource(
|
|
107
|
+
withScopes('defineSystemRole({', " guest: { description: 'x' },", '})')
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
assert.equal(state.systemRoles.definitions.length, 0)
|
|
111
|
+
assert.ok(criticals.some((c) => /has no 'scopes'/.test(c.message)))
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('a name containing the scope separator is refused', async () => {
|
|
115
|
+
const { state, criticals } = await inspectSource(
|
|
116
|
+
withScopes('defineSystemRole({', " 'admin:users': { scopes: [] },", '})')
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert.equal(state.systemRoles.definitions.length, 0)
|
|
120
|
+
assert.ok(criticals.some((c) => /separator/.test(c.message)))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Scopes are read by AST, so a computed entry cannot be checked against the
|
|
124
|
+
// declared set — accepting it silently would let a role grant anything.
|
|
125
|
+
test('a computed scope entry is refused', async () => {
|
|
126
|
+
const { criticals } = await inspectSource(
|
|
127
|
+
withScopes(
|
|
128
|
+
'const s = "catalogue:read"',
|
|
129
|
+
'defineSystemRole({',
|
|
130
|
+
' buyer: { scopes: [s] },',
|
|
131
|
+
'})'
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
assert.ok(criticals.some((c) => /not a string literal/.test(c.message)))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('`as const` does not hide the declaration', async () => {
|
|
139
|
+
const { state, criticals } = await inspectSource(
|
|
140
|
+
withScopes(
|
|
141
|
+
'defineSystemRole({',
|
|
142
|
+
" buyer: { scopes: ['catalogue:read'] },",
|
|
143
|
+
'} as const)'
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert.deepEqual(criticals, [])
|
|
148
|
+
assert.equal(state.systemRoles.definitions.length, 1)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('validateSystemRoleScopes', () => {
|
|
153
|
+
test('accepts declared scopes, including an intermediate node', async () => {
|
|
154
|
+
const { criticals } = await inspectSource(
|
|
155
|
+
withScopes(
|
|
156
|
+
'defineSystemRole({',
|
|
157
|
+
" admin: { scopes: ['admin', 'admin:users:ban'] },",
|
|
158
|
+
'})'
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert.deepEqual(criticals, [])
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('accepts a subtree wildcard whose node exists', async () => {
|
|
166
|
+
const { criticals } = await inspectSource(
|
|
167
|
+
withScopes(
|
|
168
|
+
'defineSystemRole({',
|
|
169
|
+
" admin: { scopes: ['admin:*'] },",
|
|
170
|
+
'})'
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert.deepEqual(criticals, [])
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// The failure this exists to prevent: no function checks `orders:refund`, so
|
|
178
|
+
// a persona holding the role passes every call and the boundary the role
|
|
179
|
+
// describes is never exercised.
|
|
180
|
+
test('an undeclared scope fails the build and lists what is available', async () => {
|
|
181
|
+
const { criticals } = await inspectSource(
|
|
182
|
+
withScopes(
|
|
183
|
+
'defineSystemRole({',
|
|
184
|
+
" buyer: { scopes: ['orders:refund'] },",
|
|
185
|
+
'})'
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const critical = criticals.find((c) => /orders:refund/.test(c.message))
|
|
190
|
+
assert.ok(critical, `expected a critical, got ${JSON.stringify(criticals)}`)
|
|
191
|
+
assert.match(critical!.message, /is not declared/)
|
|
192
|
+
assert.match(critical!.message, /catalogue:read/)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('a subtree wildcard on an undeclared node fails', async () => {
|
|
196
|
+
const { criticals } = await inspectSource(
|
|
197
|
+
withScopes(
|
|
198
|
+
'defineSystemRole({',
|
|
199
|
+
" buyer: { scopes: ['nonsense:*'] },",
|
|
200
|
+
'})'
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
assert.ok(criticals.some((c) => /nonsense:\*/.test(c.message)))
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('the bare wildcard is refused — it hides what the role confers', async () => {
|
|
208
|
+
const { criticals } = await inspectSource(
|
|
209
|
+
withScopes('defineSystemRole({', " god: { scopes: ['*'] },", '})')
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
assert.ok(criticals.some((c) => /bare wildcard/.test(c.message)))
|
|
213
|
+
})
|
|
214
|
+
})
|