@pikku/inspector 0.12.41 → 0.12.43

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 (108) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/add/add-addon-bans.js +1 -0
  3. package/dist/add/add-ai-agent.d.ts +3 -1
  4. package/dist/add/add-ai-agent.js +82 -0
  5. package/dist/add/add-credential.js +3 -1
  6. package/dist/add/add-functions.js +10 -9
  7. package/dist/add/add-gateway.js +3 -0
  8. package/dist/add/add-http-route.js +2 -2
  9. package/dist/add/add-mcp-prompt.js +0 -1
  10. package/dist/add/add-mcp-resource.js +0 -1
  11. package/dist/add/add-permission.d.ts +1 -1
  12. package/dist/add/add-permission.js +3 -177
  13. package/dist/add/add-queue-worker.js +3 -0
  14. package/dist/add/add-schedule.js +3 -0
  15. package/dist/add/add-scope.d.ts +2 -0
  16. package/dist/add/add-scope.js +146 -0
  17. package/dist/add/add-trigger.js +3 -0
  18. package/dist/add/add-wire-remote-addon.d.ts +10 -0
  19. package/dist/add/add-wire-remote-addon.js +66 -0
  20. package/dist/add/add-workflow-graph.js +32 -3
  21. package/dist/error-codes.d.ts +3 -0
  22. package/dist/error-codes.js +4 -0
  23. package/dist/inspector.js +19 -7
  24. package/dist/types.d.ts +19 -13
  25. package/dist/utils/custom-types-generator.js +2 -1
  26. package/dist/utils/ensure-function-metadata.d.ts +15 -0
  27. package/dist/utils/ensure-function-metadata.js +24 -0
  28. package/dist/utils/filter-inspector-state.js +9 -0
  29. package/dist/utils/get-property-value.d.ts +7 -2
  30. package/dist/utils/get-property-value.js +48 -1
  31. package/dist/utils/load-addon-functions-meta.js +81 -6
  32. package/dist/utils/nested-project-filter.d.ts +1 -0
  33. package/dist/utils/nested-project-filter.js +29 -0
  34. package/dist/utils/permissions.d.ts +1 -21
  35. package/dist/utils/permissions.js +17 -108
  36. package/dist/utils/post-process.d.ts +30 -1
  37. package/dist/utils/post-process.js +150 -72
  38. package/dist/utils/serialize-inspector-state.d.ts +13 -8
  39. package/dist/utils/serialize-inspector-state.js +12 -6
  40. package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
  41. package/dist/utils/serialize-permissions-groups-meta.js +0 -26
  42. package/dist/utils/workflow/derive-workflow-plan.js +9 -3
  43. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +56 -1
  44. package/dist/utils/workflow/dsl/patterns.d.ts +10 -0
  45. package/dist/utils/workflow/dsl/patterns.js +24 -8
  46. package/dist/utils/workflow/dsl/validation.js +3 -4
  47. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +14 -0
  48. package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
  49. package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
  50. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
  51. package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
  52. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +6 -2
  53. package/dist/visit.js +4 -0
  54. package/package.json +3 -3
  55. package/src/add/add-addon-bans.ts +1 -0
  56. package/src/add/add-ai-agent.test.ts +87 -0
  57. package/src/add/add-ai-agent.ts +122 -0
  58. package/src/add/add-credential.ts +6 -0
  59. package/src/add/add-functions.ts +10 -12
  60. package/src/add/add-gateway.ts +11 -0
  61. package/src/add/add-http-route.ts +2 -8
  62. package/src/add/add-mcp-prompt.ts +0 -1
  63. package/src/add/add-mcp-resource.ts +0 -1
  64. package/src/add/add-permission.ts +4 -242
  65. package/src/add/add-queue-worker.ts +11 -0
  66. package/src/add/add-schedule.ts +11 -0
  67. package/src/add/add-scope.test.ts +346 -0
  68. package/src/add/add-scope.ts +225 -0
  69. package/src/add/add-trigger.ts +11 -0
  70. package/src/add/add-wire-remote-addon.ts +77 -0
  71. package/src/add/add-workflow-graph-input.test.ts +94 -0
  72. package/src/add/add-workflow-graph-notes.test.ts +74 -0
  73. package/src/add/add-workflow-graph.ts +35 -3
  74. package/src/add/inline-wiring-function.test.ts +104 -0
  75. package/src/add/validate-workflow-graph-addons.test.ts +102 -0
  76. package/src/error-codes.ts +5 -0
  77. package/src/inspector.ts +22 -6
  78. package/src/types.ts +16 -17
  79. package/src/utils/custom-types-generator.test.ts +26 -1
  80. package/src/utils/custom-types-generator.ts +2 -1
  81. package/src/utils/ensure-function-metadata.ts +42 -0
  82. package/src/utils/filter-inspector-state.test.ts +0 -2
  83. package/src/utils/filter-inspector-state.ts +9 -0
  84. package/src/utils/get-property-value.test.ts +141 -0
  85. package/src/utils/get-property-value.ts +62 -1
  86. package/src/utils/load-addon-functions-meta.test.ts +157 -0
  87. package/src/utils/load-addon-functions-meta.ts +94 -6
  88. package/src/utils/load-addon-scopes.test.ts +138 -0
  89. package/src/utils/nested-project-filter.test.ts +53 -0
  90. package/src/utils/nested-project-filter.ts +28 -0
  91. package/src/utils/permissions.test.ts +7 -232
  92. package/src/utils/permissions.ts +20 -160
  93. package/src/utils/post-process.test.ts +269 -4
  94. package/src/utils/post-process.ts +216 -95
  95. package/src/utils/serialize-inspector-state.ts +22 -25
  96. package/src/utils/serialize-permissions-groups-meta.ts +0 -28
  97. package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
  98. package/src/utils/workflow/derive-workflow-plan.ts +11 -4
  99. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +66 -0
  100. package/src/utils/workflow/dsl/patterns.ts +34 -8
  101. package/src/utils/workflow/dsl/validation.ts +3 -4
  102. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +15 -0
  103. package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
  104. package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
  105. package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
  106. package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
  107. package/src/visit.ts +4 -0
  108. package/tsconfig.tsbuildinfo +1 -1
@@ -1,53 +1,21 @@
1
1
  import * as ts from 'typescript'
2
2
  import type { FunctionWiresMeta } from '@pikku/core'
3
- import type { AddWiring, InspectorState } from '../types.js'
4
- import {
5
- extractFunctionName,
6
- isNamedExport,
7
- makeContextBasedId,
8
- } from '../utils/extract-function-name.js'
3
+ import type { AddWiring } from '../types.js'
4
+ import { extractFunctionName } from '../utils/extract-function-name.js'
9
5
  import {
10
6
  extractServicesFromFunction,
11
7
  extractUsedWires,
12
8
  } from '../utils/extract-services.js'
13
- import { extractPermissionPikkuNames } from '../utils/permissions.js'
14
9
  import { getPropertyValue } from '../utils/get-property-value.js'
15
10
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
16
11
 
17
- function renameTempDefinitions(
18
- state: InspectorState,
19
- definitionIds: string[],
20
- groupType: string,
21
- groupKey: string
22
- ): void {
23
- const tempIndices = definitionIds
24
- .map((name, i) => (name.startsWith('__temp_') ? i : -1))
25
- .filter((i) => i >= 0)
26
-
27
- for (const idx of tempIndices) {
28
- const oldId = definitionIds[idx]
29
- const newId =
30
- tempIndices.length === 1
31
- ? makeContextBasedId(groupType, groupKey)
32
- : makeContextBasedId(groupType, groupKey, String(idx))
33
- const existing = state.permissions.definitions[oldId]
34
- if (existing) {
35
- delete state.permissions.definitions[oldId]
36
- state.permissions.definitions[newId] = existing
37
- }
38
- definitionIds[idx] = newId
39
- }
40
- }
41
-
42
12
  function isInsidePermissionContainer(node: ts.Node): boolean {
43
13
  let current = node.parent
44
14
  while (current) {
45
15
  if (
46
16
  ts.isCallExpression(current) &&
47
17
  ts.isIdentifier(current.expression) &&
48
- (current.expression.text === 'pikkuPermissionFactory' ||
49
- current.expression.text === 'addTagPermission' ||
50
- current.expression.text === 'addHTTPPermission')
18
+ current.expression.text === 'pikkuPermissionFactory'
51
19
  ) {
52
20
  return true
53
21
  }
@@ -57,7 +25,7 @@ function isInsidePermissionContainer(node: ts.Node): boolean {
57
25
  }
58
26
 
59
27
  /**
60
- * Inspect pikkuPermission calls, addPermission calls, and addHTTPPermission calls
28
+ * Inspect pikkuPermission, pikkuAuth, and pikkuPermissionFactory definitions.
61
29
  */
62
30
  export const addPermission: AddWiring = (logger, node, checker, state) => {
63
31
  if (!ts.isCallExpression(node)) return
@@ -340,210 +308,4 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
340
308
  )
341
309
  return
342
310
  }
343
-
344
- // Handle addPermission('tag', [permission1, permission2])
345
- // Supports two patterns:
346
- // 1. export const x = () => addTagPermission('tag', [...]) (factory - tree-shakeable)
347
- // 2. export const x = addTagPermission('tag', [...]) (direct - no tree-shaking)
348
- if (expression.text === 'addTagPermission') {
349
- const tagArg = args[0]
350
- const permissionsArrayArg = args[1]
351
-
352
- if (!tagArg || !permissionsArrayArg) return
353
-
354
- // Extract tag name
355
- let tag: string | undefined
356
- if (ts.isStringLiteral(tagArg)) {
357
- tag = tagArg.text
358
- }
359
-
360
- if (!tag) {
361
- logger.warn(`• addTagPermission call without valid tag string`)
362
- return
363
- }
364
-
365
- // Check if permissions is a literal array or object
366
- if (
367
- !ts.isArrayLiteralExpression(permissionsArrayArg) &&
368
- !ts.isObjectLiteralExpression(permissionsArrayArg)
369
- ) {
370
- logger.error(
371
- `• addTagPermission('${tag}', ...) must have a literal array or object as second argument`
372
- )
373
- return
374
- }
375
-
376
- // Extract permission pikkuFuncIds from array
377
- const permissionNames = extractPermissionPikkuNames(
378
- permissionsArrayArg,
379
- checker,
380
- state.rootDir
381
- )
382
-
383
- if (permissionNames.length > 0) {
384
- renameTempDefinitions(state, permissionNames, 'tag', tag)
385
- }
386
-
387
- const allServices = new Set<string>()
388
- for (const permissionName of permissionNames) {
389
- const permissionMeta = state.permissions.definitions[permissionName]
390
- if (permissionMeta && permissionMeta.services) {
391
- for (const service of permissionMeta.services.services) {
392
- allServices.add(service)
393
- }
394
- }
395
- }
396
-
397
- let isFactory = false
398
- let exportedName: string | null = null
399
- let parent = node.parent
400
-
401
- if (parent && ts.isArrowFunction(parent)) {
402
- if (parent.parameters.length === 0) {
403
- isFactory = true
404
-
405
- const arrowParent = parent.parent
406
- if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
407
- if (ts.isIdentifier(arrowParent.name)) {
408
- if (isNamedExport(arrowParent)) {
409
- exportedName = arrowParent.name.text
410
- }
411
- }
412
- }
413
- }
414
- }
415
-
416
- if (!isFactory) {
417
- const extracted = extractFunctionName(node, checker, state.rootDir)
418
- exportedName = extracted.exportedName
419
- }
420
-
421
- if (!isFactory && exportedName) {
422
- logger.warn(
423
- `• Permission group '${exportedName}' for tag '${tag}' is not wrapped in a factory function. ` +
424
- `For tree-shaking, use: export const ${exportedName} = () => addTagPermission('${tag}', [...])`
425
- )
426
- }
427
-
428
- state.permissions.tagPermissions.set(tag, {
429
- exportName: exportedName,
430
- sourceFile: node.getSourceFile().fileName,
431
- position: node.getStart(),
432
- services: {
433
- optimized: false,
434
- services: Array.from(allServices),
435
- },
436
- count: permissionNames.length,
437
- instanceIds: permissionNames,
438
- isFactory,
439
- })
440
-
441
- logger.debug(
442
- `• Found tag permission group: ${tag} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
443
- )
444
- return
445
- }
446
-
447
- // Handle addHTTPPermission(pattern, [permission1, permission2])
448
- // Supports two patterns:
449
- // 1. export const x = () => addHTTPPermission('*', [...]) (factory - tree-shakeable)
450
- // 2. export const x = addHTTPPermission('*', [...]) (direct - no tree-shaking)
451
- if (expression.text === 'addHTTPPermission') {
452
- const patternArg = args[0]
453
- const permissionsArrayArg = args[1]
454
-
455
- if (!patternArg || !permissionsArrayArg) return
456
-
457
- // Extract route pattern
458
- let pattern: string | undefined
459
- if (ts.isStringLiteral(patternArg)) {
460
- pattern = patternArg.text
461
- }
462
-
463
- if (!pattern) {
464
- logger.warn(`• addHTTPPermission call without valid pattern string`)
465
- return
466
- }
467
-
468
- // Check if permissions is a literal array or object
469
- if (
470
- !ts.isArrayLiteralExpression(permissionsArrayArg) &&
471
- !ts.isObjectLiteralExpression(permissionsArrayArg)
472
- ) {
473
- logger.error(
474
- `• addHTTPPermission('${pattern}', ...) must have a literal array or object as second argument`
475
- )
476
- return
477
- }
478
-
479
- // Extract permission pikkuFuncIds from array
480
- const permissionNames = extractPermissionPikkuNames(
481
- permissionsArrayArg,
482
- checker,
483
- state.rootDir
484
- )
485
-
486
- if (permissionNames.length > 0) {
487
- renameTempDefinitions(state, permissionNames, 'http', pattern)
488
- }
489
-
490
- const allServices = new Set<string>()
491
- for (const permissionName of permissionNames) {
492
- const permissionMeta = state.permissions.definitions[permissionName]
493
- if (permissionMeta && permissionMeta.services) {
494
- for (const service of permissionMeta.services.services) {
495
- allServices.add(service)
496
- }
497
- }
498
- }
499
-
500
- let isFactory = false
501
- let exportedName: string | null = null
502
- let parent = node.parent
503
-
504
- if (parent && ts.isArrowFunction(parent)) {
505
- if (parent.parameters.length === 0) {
506
- isFactory = true
507
-
508
- const arrowParent = parent.parent
509
- if (arrowParent && ts.isVariableDeclaration(arrowParent)) {
510
- if (ts.isIdentifier(arrowParent.name)) {
511
- if (isNamedExport(arrowParent)) {
512
- exportedName = arrowParent.name.text
513
- }
514
- }
515
- }
516
- }
517
- }
518
-
519
- if (!isFactory) {
520
- const extracted = extractFunctionName(node, checker, state.rootDir)
521
- exportedName = extracted.exportedName
522
- }
523
-
524
- if (!isFactory && exportedName) {
525
- logger.warn(
526
- `• HTTP permission group '${exportedName}' for pattern '${pattern}' is not wrapped in a factory function. ` +
527
- `For tree-shaking, use: export const ${exportedName} = () => addHTTPPermission('${pattern}', [...])`
528
- )
529
- }
530
-
531
- state.http.routePermissions.set(pattern, {
532
- exportName: exportedName,
533
- sourceFile: node.getSourceFile().fileName,
534
- position: node.getStart(),
535
- services: {
536
- optimized: false,
537
- services: Array.from(allServices),
538
- },
539
- count: permissionNames.length,
540
- instanceIds: permissionNames,
541
- isFactory,
542
- })
543
-
544
- logger.debug(
545
- `• Found HTTP route permission group: ${pattern} -> [${permissionNames.join(', ')}] (${isFactory ? 'factory' : 'direct'})`
546
- )
547
- return
548
- }
549
311
  }
@@ -9,6 +9,7 @@ import {
9
9
  makeContextBasedId,
10
10
  } from '../utils/extract-function-name.js'
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
+ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
12
13
  import { resolveMiddleware } from '../utils/middleware.js'
13
14
  import { extractWireNames } from '../utils/post-process.js'
14
15
  import { resolveAddonName } from '../utils/resolve-addon-package.js'
@@ -82,6 +83,16 @@ export const addQueueWorker: AddWiring = (logger, node, checker, state) => {
82
83
  return
83
84
  }
84
85
 
86
+ // Register metadata for a func inlined into the wiring (see helper).
87
+ ensureInlineWiringFunction(
88
+ state,
89
+ pikkuFuncId,
90
+ name,
91
+ funcInitializer,
92
+ checker,
93
+ extracted.isHelper
94
+ )
95
+
85
96
  // --- resolve middleware ---
86
97
  const middleware = resolveMiddleware(state, obj, tags, checker)
87
98
 
@@ -9,6 +9,7 @@ import {
9
9
  makeContextBasedId,
10
10
  } from '../utils/extract-function-name.js'
11
11
  import { getPropertyAssignmentInitializer } from '../utils/type-utils.js'
12
+ import { ensureInlineWiringFunction } from '../utils/ensure-function-metadata.js'
12
13
  import { resolveMiddleware } from '../utils/middleware.js'
13
14
  import { extractWireNames } from '../utils/post-process.js'
14
15
  import { resolveAddonName } from '../utils/resolve-addon-package.js'
@@ -84,6 +85,16 @@ export const addSchedule: AddWiring = (
84
85
  return
85
86
  }
86
87
 
88
+ // Register metadata for a func inlined into the wiring (see helper).
89
+ ensureInlineWiringFunction(
90
+ state,
91
+ pikkuFuncId,
92
+ nameValue,
93
+ funcInitializer,
94
+ checker,
95
+ extracted.isHelper
96
+ )
97
+
87
98
  // --- resolve middleware ---
88
99
  const middleware = resolveMiddleware(state, obj, tags, checker)
89
100
 
@@ -0,0 +1,346 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
+ ({
12
+ debug: () => {},
13
+ info: () => {},
14
+ warn: () => {},
15
+ error: () => {},
16
+ diagnostic: ({ code, message }) => {
17
+ criticals.push({ code, message })
18
+ },
19
+ critical: (code: ErrorCode, message: string) => {
20
+ criticals.push({ code, message })
21
+ },
22
+ hasCriticalErrors: () => criticals.length > 0,
23
+ }) satisfies InspectorLogger
24
+
25
+ const inspectSource = async (source: string) => {
26
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-scope-'))
27
+ const file = join(rootDir, 'scopes.ts')
28
+ await writeFile(file, source)
29
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
30
+ try {
31
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
32
+ return { state, criticals, file }
33
+ } finally {
34
+ await rm(rootDir, { recursive: true, force: true })
35
+ }
36
+ }
37
+
38
+ describe('addScope inspector', () => {
39
+ test('extracts a flat scope', async () => {
40
+ const { state, criticals } = await inspectSource(
41
+ [
42
+ "import { wireScope } from '@pikku/core/scope'",
43
+ 'wireScope({',
44
+ ' admin: {',
45
+ " displayName: 'Administration',",
46
+ " description: 'Administrative access',",
47
+ ' },',
48
+ '})',
49
+ ].join('\n')
50
+ )
51
+
52
+ assert.equal(criticals.length, 0)
53
+ assert.equal(state.scopes.definitions.length, 1)
54
+ assert.equal(state.scopes.definitions[0]!.name, 'admin')
55
+ assert.equal(state.scopes.definitions[0]!.displayName, 'Administration')
56
+ assert.equal(
57
+ state.scopes.definitions[0]!.description,
58
+ 'Administrative access'
59
+ )
60
+ })
61
+
62
+ test('extracts a displayName at every depth', async () => {
63
+ const { state, criticals } = await inspectSource(
64
+ [
65
+ "import { wireScope } from '@pikku/core/scope'",
66
+ 'wireScope({',
67
+ ' admin: {',
68
+ " displayName: 'Administration',",
69
+ ' scopes: {',
70
+ ' invoices: {',
71
+ " displayName: 'Invoice Management',",
72
+ ' scopes: {',
73
+ " create: { displayName: 'Create Invoices' },",
74
+ ' },',
75
+ ' },',
76
+ ' },',
77
+ ' },',
78
+ '})',
79
+ ].join('\n')
80
+ )
81
+
82
+ assert.equal(criticals.length, 0)
83
+ const admin = state.scopes.definitions[0]!
84
+ assert.equal(admin.displayName, 'Administration')
85
+ const invoices = admin.scopes!.invoices!
86
+ assert.equal(invoices.displayName, 'Invoice Management')
87
+ assert.equal(invoices.scopes!.create!.displayName, 'Create Invoices')
88
+ })
89
+
90
+ test('extracts a nested scope tree', async () => {
91
+ const { state, criticals } = await inspectSource(
92
+ [
93
+ "import { wireScope } from '@pikku/core/scope'",
94
+ 'wireScope({',
95
+ ' admin: {',
96
+ ' scopes: {',
97
+ ' invoices: {',
98
+ " description: 'Invoice management',",
99
+ ' scopes: {',
100
+ " create: { description: 'Create invoices' },",
101
+ ' },',
102
+ ' },',
103
+ ' },',
104
+ ' },',
105
+ '})',
106
+ ].join('\n')
107
+ )
108
+
109
+ assert.equal(criticals.length, 0)
110
+ assert.deepEqual(state.scopes.definitions[0]!.scopes, {
111
+ invoices: {
112
+ description: 'Invoice management',
113
+ scopes: {
114
+ create: { description: 'Create invoices' },
115
+ },
116
+ },
117
+ })
118
+ })
119
+
120
+ test('records the source file', async () => {
121
+ const { state, file } = await inspectSource(
122
+ [
123
+ "import { wireScope } from '@pikku/core/scope'",
124
+ 'wireScope({ admin: {} })',
125
+ ].join('\n')
126
+ )
127
+
128
+ assert.equal(state.scopes.definitions[0]!.sourceFile, file)
129
+ assert.ok(state.scopes.files.has(file))
130
+ })
131
+
132
+ // The point of the keyed form: one call declares as many roots as you like,
133
+ // and a root reads exactly like the nodes beneath it.
134
+ test('extracts several roots from one declaration', async () => {
135
+ const { state, criticals } = await inspectSource(
136
+ [
137
+ "import { wireScope } from '@pikku/core/scope'",
138
+ 'wireScope({ admin: {}, billing: { scopes: { read: {} } } })',
139
+ ].join('\n')
140
+ )
141
+
142
+ assert.equal(criticals.length, 0)
143
+ assert.deepEqual(
144
+ state.scopes.definitions.map((d) => d.name),
145
+ ['admin', 'billing']
146
+ )
147
+ assert.deepEqual(state.scopes.definitions[1]!.scopes, { read: {} })
148
+ })
149
+
150
+ test('extracts several declarations', async () => {
151
+ const { state } = await inspectSource(
152
+ [
153
+ "import { wireScope } from '@pikku/core/scope'",
154
+ 'wireScope({ admin: {} })',
155
+ 'wireScope({ billing: {} })',
156
+ ].join('\n')
157
+ )
158
+
159
+ assert.deepEqual(
160
+ state.scopes.definitions.map((d) => d.name),
161
+ ['admin', 'billing']
162
+ )
163
+ })
164
+
165
+ test('is critical when a root embeds the separator', async () => {
166
+ const { criticals } = await inspectSource(
167
+ [
168
+ "import { wireScope } from '@pikku/core/scope'",
169
+ "wireScope({ 'admin:users': {} })",
170
+ ].join('\n')
171
+ )
172
+
173
+ assert.ok(
174
+ criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
175
+ `expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
176
+ )
177
+ })
178
+
179
+ test('is critical when a root is the wildcard', async () => {
180
+ const { criticals } = await inspectSource(
181
+ [
182
+ "import { wireScope } from '@pikku/core/scope'",
183
+ "wireScope({ '*': {} })",
184
+ ].join('\n')
185
+ )
186
+
187
+ assert.ok(
188
+ criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
189
+ `expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
190
+ )
191
+ })
192
+
193
+ test('is critical when a root key is not a literal', async () => {
194
+ const { criticals } = await inspectSource(
195
+ [
196
+ "import { wireScope } from '@pikku/core/scope'",
197
+ 'const k = String(1)',
198
+ 'wireScope({ [k]: {} } as any)',
199
+ ].join('\n')
200
+ )
201
+
202
+ assert.ok(
203
+ criticals.some((c) => c.code === ErrorCode.NON_LITERAL_WIRE_NAME),
204
+ `expected a NON_LITERAL_WIRE_NAME critical, got ${JSON.stringify(criticals)}`
205
+ )
206
+ })
207
+
208
+ test('is critical when a root is not an object literal', async () => {
209
+ const { criticals } = await inspectSource(
210
+ [
211
+ "import { wireScope } from '@pikku/core/scope'",
212
+ "wireScope({ admin: 'nope' } as any)",
213
+ ].join('\n')
214
+ )
215
+
216
+ assert.ok(
217
+ criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
218
+ `expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
219
+ )
220
+ })
221
+ })
222
+
223
+ describe('validateScopeReferences', () => {
224
+ const funcSource = (scopes: string, decls: string[] = []) =>
225
+ [
226
+ "import { wireScope } from '@pikku/core/scope'",
227
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
228
+ ...decls,
229
+ 'export const f = pikkuSessionlessFunc({',
230
+ ` scopes: ${scopes},`,
231
+ " func: async () => 'ok',",
232
+ '})',
233
+ ].join('\n')
234
+
235
+ test('accepts a declared scope', async () => {
236
+ const { criticals } = await inspectSource(
237
+ funcSource("['admin:invoices:create']", [
238
+ 'wireScope({',
239
+ ' admin: {',
240
+ ' scopes: { invoices: { scopes: { create: {} } } },',
241
+ ' },',
242
+ '})',
243
+ ])
244
+ )
245
+
246
+ assert.deepEqual(criticals, [])
247
+ })
248
+
249
+ test('accepts an intermediate node of a declared tree', async () => {
250
+ const { criticals } = await inspectSource(
251
+ funcSource("['admin:invoices']", [
252
+ 'wireScope({',
253
+ ' admin: {',
254
+ ' scopes: { invoices: { scopes: { create: {} } } },',
255
+ ' },',
256
+ '})',
257
+ ])
258
+ )
259
+
260
+ assert.deepEqual(criticals, [])
261
+ })
262
+
263
+ test('rejects an undeclared scope', async () => {
264
+ const { criticals } = await inspectSource(
265
+ funcSource("['billing:read']", ['wireScope({ admin: {} })'])
266
+ )
267
+
268
+ assert.ok(
269
+ criticals.some(
270
+ (c) =>
271
+ c.code === ErrorCode.INVALID_VALUE &&
272
+ c.message.includes('billing:read')
273
+ ),
274
+ `expected an INVALID_VALUE critical for billing:read, got ${JSON.stringify(criticals)}`
275
+ )
276
+ })
277
+
278
+ test('lists the available scopes when one is undeclared', async () => {
279
+ const { criticals } = await inspectSource(
280
+ funcSource("['nope']", ['wireScope({ admin: {} })'])
281
+ )
282
+
283
+ assert.ok(
284
+ criticals.some((c) => c.message.includes('Available scopes: admin')),
285
+ `expected the message to list available scopes, got ${JSON.stringify(criticals)}`
286
+ )
287
+ })
288
+
289
+ test('rejects a typo in a nested scope', async () => {
290
+ const { criticals } = await inspectSource(
291
+ funcSource("['admin:invoice:create']", [
292
+ 'wireScope({',
293
+ ' admin: {',
294
+ ' scopes: { invoices: { scopes: { create: {} } } },',
295
+ ' },',
296
+ '})',
297
+ ])
298
+ )
299
+
300
+ assert.ok(
301
+ criticals.some((c) => c.message.includes('admin:invoice:create')),
302
+ `expected a critical for the typo, got ${JSON.stringify(criticals)}`
303
+ )
304
+ })
305
+
306
+ test('accepts a wildcard requirement whose node is declared', async () => {
307
+ const { criticals } = await inspectSource(
308
+ funcSource("['admin:*']", ['wireScope({ admin: {} })'])
309
+ )
310
+
311
+ assert.deepEqual(criticals, [])
312
+ })
313
+
314
+ test('rejects a wildcard requirement whose node is undeclared', async () => {
315
+ const { criticals } = await inspectSource(
316
+ funcSource("['billing:*']", ['wireScope({ admin: {} })'])
317
+ )
318
+
319
+ assert.ok(
320
+ criticals.some((c) => c.message.includes('billing:*')),
321
+ `expected a critical for billing:*, got ${JSON.stringify(criticals)}`
322
+ )
323
+ })
324
+
325
+ test('rejects a bare wildcard requirement', async () => {
326
+ const { criticals } = await inspectSource(
327
+ funcSource("['*']", ['wireScope({ admin: {} })'])
328
+ )
329
+
330
+ assert.ok(
331
+ criticals.some((c) => c.message.includes('bare wildcard')),
332
+ `expected a critical for the bare wildcard, got ${JSON.stringify(criticals)}`
333
+ )
334
+ })
335
+
336
+ test('is silent for a function declaring no scopes', async () => {
337
+ const { criticals } = await inspectSource(
338
+ [
339
+ "import { pikkuSessionlessFunc } from '@pikku/core'",
340
+ "export const f = pikkuSessionlessFunc({ func: async () => 'ok' })",
341
+ ].join('\n')
342
+ )
343
+
344
+ assert.deepEqual(criticals, [])
345
+ })
346
+ })