@pikku/inspector 0.12.22 → 0.12.24

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/add/add-addon-bans.d.ts +7 -0
  3. package/dist/add/add-addon-bans.js +65 -0
  4. package/dist/add/add-auth.js +43 -0
  5. package/dist/add/add-channel.js +47 -6
  6. package/dist/add/add-cli.js +17 -0
  7. package/dist/add/add-http-route.d.ts +11 -1
  8. package/dist/add/add-http-route.js +37 -0
  9. package/dist/add/add-http-routes.d.ts +0 -3
  10. package/dist/add/add-http-routes.js +179 -36
  11. package/dist/error-codes.d.ts +3 -1
  12. package/dist/error-codes.js +3 -0
  13. package/dist/inspector.js +17 -5
  14. package/dist/types.d.ts +48 -1
  15. package/dist/utils/get-exported-variable-name.d.ts +2 -0
  16. package/dist/utils/get-exported-variable-name.js +34 -0
  17. package/dist/utils/load-addon-functions-meta.js +98 -0
  18. package/dist/utils/post-process.js +16 -3
  19. package/dist/utils/resolve-addon-package.js +3 -1
  20. package/dist/utils/resolve-ref-contract.d.ts +21 -0
  21. package/dist/utils/resolve-ref-contract.js +46 -0
  22. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  23. package/dist/utils/serialize-inspector-state.js +9 -0
  24. package/dist/visit.js +24 -19
  25. package/package.json +1 -1
  26. package/src/add/add-addon-bans.ts +84 -0
  27. package/src/add/add-auth.test.ts +94 -0
  28. package/src/add/add-auth.ts +46 -0
  29. package/src/add/add-channel.ts +66 -7
  30. package/src/add/add-cli.ts +30 -0
  31. package/src/add/add-http-route.ts +75 -1
  32. package/src/add/add-http-routes.ts +283 -41
  33. package/src/add/addon-bans.test.ts +121 -0
  34. package/src/add/addon-contracts.test.ts +221 -0
  35. package/src/error-codes.ts +4 -0
  36. package/src/inspector.ts +17 -5
  37. package/src/types.ts +70 -1
  38. package/src/utils/get-exported-variable-name.ts +48 -0
  39. package/src/utils/load-addon-functions-meta.ts +164 -0
  40. package/src/utils/post-process.ts +17 -3
  41. package/src/utils/resolve-addon-package.ts +6 -1
  42. package/src/utils/resolve-ref-contract.ts +71 -0
  43. package/src/utils/serialize-inspector-state.ts +10 -0
  44. package/src/visit.ts +26 -19
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -1,45 +1,63 @@
1
1
  import * as ts from 'typescript'
2
2
  import { getPropertyValue } from '../utils/get-property-value.js'
3
- import type { AddWiring, InspectorState, InspectorLogger } from '../types.js'
4
- import { registerHTTPRoute } from './add-http-route.js'
3
+ import type {
4
+ AddWiring,
5
+ ExportedHTTPRouteConfigMeta,
6
+ ExportedHTTPRouteMapMeta,
7
+ ExportedHTTPRoutesGroupMeta,
8
+ InspectorLogger,
9
+ InspectorState,
10
+ } from '../types.js'
11
+ import { registerHTTPRoute, registerHTTPRouteMeta } from './add-http-route.js'
5
12
  import { resolveIdentifier } from '../utils/resolve-identifier.js'
13
+ import { extractFunctionName } from '../utils/extract-function-name.js'
14
+ import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
15
+ import { resolveAddonName } from '../utils/resolve-addon-package.js'
16
+ import {
17
+ resolveRefContract,
18
+ type RefContractResolution,
19
+ } from '../utils/resolve-ref-contract.js'
20
+ import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
6
21
 
7
- /**
8
- * Group configuration extracted from wireHTTPRoutes or defineHTTPRoutes
9
- */
10
22
  interface GroupConfig {
11
23
  basePath: string
12
24
  tags: string[]
13
25
  auth?: boolean
14
26
  }
15
27
 
16
- /**
17
- * Process wireHTTPRoutes calls
18
- */
19
28
  export const addHTTPRoutes: AddWiring = (
20
29
  logger,
21
30
  node,
22
31
  checker,
23
32
  state,
24
- _options
33
+ options
25
34
  ) => {
26
35
  if (!ts.isCallExpression(node)) return
27
36
 
28
37
  const { expression, arguments: args } = node
29
- if (!ts.isIdentifier(expression) || expression.text !== 'wireHTTPRoutes')
38
+ if (!ts.isIdentifier(expression)) return
39
+
40
+ if (expression.text === 'defineHTTPRoutes') {
41
+ const exportName = getExportedVariableName(node, options.sourceFile)
42
+ const firstArg = args[0]
43
+ if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
44
+ const contract = serializeHTTPRoutesContract(firstArg, checker, state)
45
+ if (contract) {
46
+ state.exportedContracts.http[exportName] = contract
47
+ }
48
+ }
30
49
  return
50
+ }
51
+
52
+ if (expression.text !== 'wireHTTPRoutes') return
31
53
 
32
54
  const firstArg = args[0]
33
55
  if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
34
56
 
35
- // Extract group config
36
57
  const groupConfig = extractGroupConfig(firstArg)
37
-
38
- // Get routes property
39
58
  const routesProp = getPropertyAssignment(firstArg, 'routes')
40
59
  if (!routesProp) return
41
60
 
42
- // Process routes recursively
43
61
  processRoutes(
44
62
  routesProp.initializer,
45
63
  groupConfig,
@@ -50,9 +68,6 @@ export const addHTTPRoutes: AddWiring = (
50
68
  )
51
69
  }
52
70
 
53
- /**
54
- * Get a property assignment from an object literal
55
- */
56
71
  function getPropertyAssignment(
57
72
  obj: ts.ObjectLiteralExpression,
58
73
  propName: string
@@ -69,9 +84,6 @@ function getPropertyAssignment(
69
84
  return undefined
70
85
  }
71
86
 
72
- /**
73
- * Extract group configuration from an object literal
74
- */
75
87
  function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
76
88
  const basePath = (getPropertyValue(obj, 'basePath') as string) || ''
77
89
  const tags = (getPropertyValue(obj, 'tags') as string[]) || []
@@ -84,9 +96,6 @@ function extractGroupConfig(obj: ts.ObjectLiteralExpression): GroupConfig {
84
96
  }
85
97
  }
86
98
 
87
- /**
88
- * Merge two group configs following cascading rules
89
- */
90
99
  function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
91
100
  return {
92
101
  basePath: parent.basePath + child.basePath,
@@ -95,9 +104,6 @@ function mergeConfigs(parent: GroupConfig, child: GroupConfig): GroupConfig {
95
104
  }
96
105
  }
97
106
 
98
- /**
99
- * Check if a value is a route config (has method, func, and route)
100
- */
101
107
  function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
102
108
  let hasMethod = false
103
109
  let hasFunc = false
@@ -114,9 +120,6 @@ function isRouteConfig(obj: ts.ObjectLiteralExpression): boolean {
114
120
  return hasMethod && hasFunc && hasRoute
115
121
  }
116
122
 
117
- /**
118
- * Check if a value is a route contract (has routes property but no method/func)
119
- */
120
123
  function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
121
124
  let hasRoutes = false
122
125
  let hasMethod = false
@@ -133,9 +136,6 @@ function isRouteContract(obj: ts.ObjectLiteralExpression): boolean {
133
136
  return hasRoutes && !hasMethod && !hasFunc
134
137
  }
135
138
 
136
- /**
137
- * Recursively process routes - handles nested maps, contracts, and identifiers
138
- */
139
139
  function processRoutes(
140
140
  node: ts.Node,
141
141
  parentConfig: GroupConfig,
@@ -144,7 +144,6 @@ function processRoutes(
144
144
  logger: InspectorLogger,
145
145
  sourceFile: ts.SourceFile
146
146
  ): void {
147
- // Handle array of routes
148
147
  if (ts.isArrayLiteralExpression(node)) {
149
148
  for (const element of node.elements) {
150
149
  if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
@@ -154,15 +153,12 @@ function processRoutes(
154
153
  return
155
154
  }
156
155
 
157
- // Handle object literal
158
156
  if (ts.isObjectLiteralExpression(node)) {
159
- // Check if this is a route config
160
157
  if (isRouteConfig(node)) {
161
158
  processRoute(node, parentConfig, state, checker, logger, sourceFile)
162
159
  return
163
160
  }
164
161
 
165
- // Check if this is a route contract
166
162
  if (isRouteContract(node)) {
167
163
  const contractConfig = extractGroupConfig(node)
168
164
  const mergedConfig = mergeConfigs(parentConfig, contractConfig)
@@ -180,9 +176,17 @@ function processRoutes(
180
176
  return
181
177
  }
182
178
 
183
- // Otherwise it's a nested map - process each property
184
179
  for (const prop of node.properties) {
185
180
  if (ts.isPropertyAssignment(prop)) {
181
+ const ref = resolveRefContract(
182
+ prop.initializer,
183
+ 'refHTTP',
184
+ state.exportedContracts.addonHttp
185
+ )
186
+ if (ref) {
187
+ processRefHTTPContract(ref, parentConfig, state, logger, sourceFile)
188
+ continue
189
+ }
186
190
  processRoutes(
187
191
  prop.initializer,
188
192
  parentConfig,
@@ -196,7 +200,18 @@ function processRoutes(
196
200
  return
197
201
  }
198
202
 
199
- // Handle identifier - resolve to its definition
203
+ if (ts.isCallExpression(node)) {
204
+ const ref = resolveRefContract(
205
+ node,
206
+ 'refHTTP',
207
+ state.exportedContracts.addonHttp
208
+ )
209
+ if (ref) {
210
+ processRefHTTPContract(ref, parentConfig, state, logger, sourceFile)
211
+ }
212
+ return
213
+ }
214
+
200
215
  if (ts.isIdentifier(node)) {
201
216
  const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes'])
202
217
  if (resolved) {
@@ -205,9 +220,236 @@ function processRoutes(
205
220
  }
206
221
  }
207
222
 
208
- /**
209
- * Register a single route using the shared registerHTTPRoute function
210
- */
223
+ function processRefHTTPContract(
224
+ ref: RefContractResolution<ExportedHTTPRoutesGroupMeta>,
225
+ parentConfig: GroupConfig,
226
+ state: InspectorState,
227
+ logger: InspectorLogger,
228
+ sourceFile: ts.SourceFile
229
+ ): void {
230
+ const basePath =
231
+ ref.basePath !== undefined ? ref.basePath : ref.contract.basePath || ''
232
+ processExportedRouteMap(
233
+ ref.contract.routes,
234
+ mergeConfigs(parentConfig, {
235
+ basePath,
236
+ tags: ref.contract.tags || [],
237
+ auth: ref.contract.auth,
238
+ }),
239
+ state,
240
+ logger,
241
+ sourceFile
242
+ )
243
+ }
244
+
245
+ function processExportedRouteMap(
246
+ routes: ExportedHTTPRouteMapMeta,
247
+ parentConfig: GroupConfig,
248
+ state: InspectorState,
249
+ logger: InspectorLogger,
250
+ sourceFile: ts.SourceFile
251
+ ): void {
252
+ for (const value of Object.values(routes)) {
253
+ if (isExportedRouteConfig(value)) {
254
+ registerHTTPRouteMeta({
255
+ route: value,
256
+ state,
257
+ logger,
258
+ sourceFile,
259
+ basePath: parentConfig.basePath,
260
+ inheritedTags: parentConfig.tags,
261
+ inheritedAuth: parentConfig.auth,
262
+ })
263
+ continue
264
+ }
265
+
266
+ if (isExportedRouteContract(value)) {
267
+ processExportedRouteMap(
268
+ value.routes,
269
+ mergeConfigs(parentConfig, {
270
+ basePath: value.basePath || '',
271
+ tags: value.tags || [],
272
+ auth: value.auth,
273
+ }),
274
+ state,
275
+ logger,
276
+ sourceFile
277
+ )
278
+ continue
279
+ }
280
+
281
+ processExportedRouteMap(value, parentConfig, state, logger, sourceFile)
282
+ }
283
+ }
284
+
285
+ function isExportedRouteConfig(
286
+ value: ExportedHTTPRouteMapMeta[string]
287
+ ): value is ExportedHTTPRouteConfigMeta {
288
+ return (
289
+ typeof value === 'object' &&
290
+ value !== null &&
291
+ 'method' in value &&
292
+ 'route' in value &&
293
+ 'func' in value
294
+ )
295
+ }
296
+
297
+ function isExportedRouteContract(
298
+ value: ExportedHTTPRouteMapMeta[string]
299
+ ): value is ExportedHTTPRoutesGroupMeta {
300
+ return (
301
+ typeof value === 'object' &&
302
+ value !== null &&
303
+ 'routes' in value &&
304
+ !('method' in value)
305
+ )
306
+ }
307
+
308
+ function serializeHTTPRoutesContract(
309
+ node: ts.ObjectLiteralExpression,
310
+ checker: ts.TypeChecker,
311
+ state: InspectorState
312
+ ): ExportedHTTPRoutesGroupMeta | null {
313
+ if (isRouteContract(node)) {
314
+ const routesProp = getPropertyAssignment(node, 'routes')
315
+ if (!routesProp || !ts.isObjectLiteralExpression(routesProp.initializer)) {
316
+ return null
317
+ }
318
+
319
+ return {
320
+ ...extractGroupConfig(node),
321
+ routes: serializeHTTPRouteMap(routesProp.initializer, checker, state),
322
+ }
323
+ }
324
+
325
+ return {
326
+ routes: serializeHTTPRouteMap(node, checker, state),
327
+ }
328
+ }
329
+
330
+ function serializeHTTPRouteMap(
331
+ node: ts.ObjectLiteralExpression,
332
+ checker: ts.TypeChecker,
333
+ state: InspectorState
334
+ ): ExportedHTTPRouteMapMeta {
335
+ const result: ExportedHTTPRouteMapMeta = {}
336
+
337
+ for (const prop of node.properties) {
338
+ if (!ts.isPropertyAssignment(prop)) continue
339
+
340
+ const key = prop.name.getText().replace(/^['"]|['"]$/g, '')
341
+ const value = prop.initializer
342
+
343
+ if (ts.isObjectLiteralExpression(value)) {
344
+ if (isRouteConfig(value)) {
345
+ const route = serializeHTTPRouteConfig(value, checker, state)
346
+ if (route) {
347
+ result[key] = route
348
+ }
349
+ continue
350
+ }
351
+
352
+ if (isRouteContract(value)) {
353
+ const routeContract = serializeHTTPRoutesContract(value, checker, state)
354
+ if (routeContract) {
355
+ result[key] = routeContract
356
+ }
357
+ continue
358
+ }
359
+
360
+ result[key] = serializeHTTPRouteMap(value, checker, state)
361
+ continue
362
+ }
363
+
364
+ if (ts.isIdentifier(value)) {
365
+ const resolved = resolveIdentifier(value, checker, ['defineHTTPRoutes'])
366
+ if (resolved && ts.isObjectLiteralExpression(resolved)) {
367
+ if (isRouteContract(resolved)) {
368
+ const routeContract = serializeHTTPRoutesContract(
369
+ resolved,
370
+ checker,
371
+ state
372
+ )
373
+ if (routeContract) {
374
+ result[key] = routeContract
375
+ }
376
+ } else {
377
+ result[key] = serializeHTTPRouteMap(resolved, checker, state)
378
+ }
379
+ }
380
+ }
381
+ }
382
+
383
+ return result
384
+ }
385
+
386
+ function serializeHTTPRouteConfig(
387
+ obj: ts.ObjectLiteralExpression,
388
+ checker: ts.TypeChecker,
389
+ state: InspectorState
390
+ ): ExportedHTTPRouteConfigMeta | null {
391
+ const method = getPropertyValue(obj, 'method') as string | null
392
+ const route = getPropertyValue(obj, 'route') as string | null
393
+ const funcInitializer = getPropertyAssignmentInitializer(
394
+ obj,
395
+ 'func',
396
+ true,
397
+ checker
398
+ )
399
+
400
+ if (!method || !route || !funcInitializer) {
401
+ return null
402
+ }
403
+
404
+ let pikkuFuncId = extractFunctionName(
405
+ funcInitializer,
406
+ checker,
407
+ state.rootDir
408
+ ).pikkuFuncId
409
+ let packageName: string | undefined
410
+
411
+ if (
412
+ ts.isCallExpression(funcInitializer) &&
413
+ ts.isIdentifier(funcInitializer.expression) &&
414
+ funcInitializer.expression.text === 'ref'
415
+ ) {
416
+ const [firstArg] = funcInitializer.arguments
417
+ if (firstArg && ts.isStringLiteral(firstArg)) {
418
+ pikkuFuncId = firstArg.text
419
+ const addonNamespace = pikkuFuncId.includes(':')
420
+ ? pikkuFuncId.split(':')[0]
421
+ : null
422
+ packageName = addonNamespace
423
+ ? state.rpc.wireAddonDeclarations.get(addonNamespace)?.package
424
+ : undefined
425
+ }
426
+ } else if (ts.isIdentifier(funcInitializer)) {
427
+ packageName =
428
+ resolveAddonName(
429
+ funcInitializer,
430
+ checker,
431
+ state.rpc.wireAddonDeclarations
432
+ ) || undefined
433
+ }
434
+
435
+ return {
436
+ auth: getPropertyValue(obj, 'auth') as boolean | undefined,
437
+ contentType: getPropertyValue(obj, 'contentType') as string | undefined,
438
+ headers:
439
+ (getPropertyValue(obj, 'headers') as unknown as Record<string, string>) ||
440
+ undefined,
441
+ method,
442
+ route,
443
+ sse: getPropertyValue(obj, 'sse') as boolean | undefined,
444
+ tags: (getPropertyValue(obj, 'tags') as string[]) || undefined,
445
+ timeout: getPropertyValue(obj, 'timeout') as number | undefined,
446
+ func: {
447
+ pikkuFuncId,
448
+ ...(packageName && { packageName }),
449
+ },
450
+ }
451
+ }
452
+
211
453
  function processRoute(
212
454
  obj: ts.ObjectLiteralExpression,
213
455
  groupConfig: GroupConfig,
@@ -0,0 +1,121 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ const recordingLogger = () => {
11
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
12
+ const logger: InspectorLogger = {
13
+ debug: () => {},
14
+ info: () => {},
15
+ warn: () => {},
16
+ error: () => {},
17
+ critical: (code, message) => {
18
+ criticals.push({ code, message })
19
+ },
20
+ hasCriticalErrors: () => criticals.length > 0,
21
+ }
22
+ return { logger, criticals }
23
+ }
24
+
25
+ const withTempApp = async (
26
+ source: string,
27
+ run: (file: string, rootDir: string) => Promise<void>
28
+ ) => {
29
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-addon-bans-'))
30
+ const file = join(rootDir, 'app.ts')
31
+ await writeFile(
32
+ join(rootDir, 'package.json'),
33
+ JSON.stringify({ name: 'test-addon', type: 'module' }, null, 2)
34
+ )
35
+ await writeFile(file, source)
36
+ try {
37
+ await run(file, rootDir)
38
+ } finally {
39
+ await rm(rootDir, { recursive: true, force: true })
40
+ }
41
+ }
42
+
43
+ describe('addon authoring bans', () => {
44
+ const wireHTTPSource = [
45
+ "import { wireHTTP } from '@pikku/core/http'",
46
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
47
+ 'const f = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
48
+ "wireHTTP({ method: 'get', route: '/a', func: f })",
49
+ ].join('\n')
50
+
51
+ const defineWithMiddlewareSource = [
52
+ "import { defineHTTPRoutes } from '@pikku/core/http'",
53
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
54
+ 'const f = pikkuSessionlessFunc({ func: async () => ({ ok: true }) })',
55
+ 'const mw = (async (_s: any, _w: any, next: any) => next()) as any',
56
+ "export const routes = defineHTTPRoutes({ basePath: '/x', routes: { a: { method: 'get', route: '/a', func: f, middleware: [mw] } } })",
57
+ ].join('\n')
58
+
59
+ test('wire* inside an addon is a critical error', async () => {
60
+ await withTempApp(wireHTTPSource, async (file, rootDir) => {
61
+ const { logger, criticals } = recordingLogger()
62
+ await inspect(logger, [file], { rootDir, isAddon: true })
63
+ assert.ok(
64
+ criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
65
+ `expected ADDON_WIRING_NOT_ALLOWED, got ${JSON.stringify(criticals)}`
66
+ )
67
+ })
68
+ })
69
+
70
+ test('wire* outside an addon is allowed', async () => {
71
+ await withTempApp(wireHTTPSource, async (file, rootDir) => {
72
+ const { logger, criticals } = recordingLogger()
73
+ await inspect(logger, [file], { rootDir })
74
+ assert.ok(
75
+ !criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
76
+ `expected no addon-wiring ban, got ${JSON.stringify(criticals)}`
77
+ )
78
+ })
79
+ })
80
+
81
+ test('define* carrying middleware inside an addon is a critical error', async () => {
82
+ await withTempApp(defineWithMiddlewareSource, async (file, rootDir) => {
83
+ const { logger, criticals } = recordingLogger()
84
+ await inspect(logger, [file], { rootDir, isAddon: true })
85
+ assert.ok(
86
+ criticals.some(
87
+ (c) => c.code === ErrorCode.ADDON_CONTRACT_HANDLERS_NOT_ALLOWED
88
+ ),
89
+ `expected ADDON_CONTRACT_HANDLERS_NOT_ALLOWED, got ${JSON.stringify(criticals)}`
90
+ )
91
+ })
92
+ })
93
+
94
+ test('define* carrying middleware outside an addon is allowed', async () => {
95
+ await withTempApp(defineWithMiddlewareSource, async (file, rootDir) => {
96
+ const { logger, criticals } = recordingLogger()
97
+ await inspect(logger, [file], { rootDir })
98
+ assert.ok(
99
+ !criticals.some(
100
+ (c) => c.code === ErrorCode.ADDON_CONTRACT_HANDLERS_NOT_ALLOWED
101
+ ),
102
+ `expected no contract-handlers ban, got ${JSON.stringify(criticals)}`
103
+ )
104
+ })
105
+ })
106
+
107
+ test('wireSecret inside an addon is allowed', async () => {
108
+ const source = [
109
+ "import { wireSecret } from '@pikku/core'",
110
+ "wireSecret({ name: 'example', secretId: 'EXAMPLE' } as any)",
111
+ ].join('\n')
112
+ await withTempApp(source, async (file, rootDir) => {
113
+ const { logger, criticals } = recordingLogger()
114
+ await inspect(logger, [file], { rootDir, isAddon: true })
115
+ assert.ok(
116
+ !criticals.some((c) => c.code === ErrorCode.ADDON_WIRING_NOT_ALLOWED),
117
+ `expected wireSecret to be allowed, got ${JSON.stringify(criticals)}`
118
+ )
119
+ })
120
+ })
121
+ })