@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/OPTIMIZATION-PLAN.md +9 -3
  3. package/dist/add/add-auth.d.ts +1 -1
  4. package/dist/add/add-auth.js +1 -1
  5. package/dist/add/add-cli.js +7 -0
  6. package/dist/add/add-credential.js +1 -1
  7. package/dist/add/add-functions.js +156 -7
  8. package/dist/add/add-http-route.js +16 -6
  9. package/dist/add/add-middleware.js +46 -3
  10. package/dist/add/add-personas.d.ts +13 -0
  11. package/dist/add/add-personas.js +210 -0
  12. package/dist/add/add-scope.js +1 -1
  13. package/dist/add/add-secret.js +1 -1
  14. package/dist/add/add-system-role.d.ts +2 -0
  15. package/dist/add/add-system-role.js +102 -0
  16. package/dist/add/add-variable.js +1 -1
  17. package/dist/add/add-wire-addon.js +31 -0
  18. package/dist/error-codes.d.ts +10 -1
  19. package/dist/error-codes.js +11 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/inspector.js +33 -1
  22. package/dist/types.d.ts +46 -2
  23. package/dist/utils/annotate-http-route-auth.d.ts +22 -0
  24. package/dist/utils/annotate-http-route-auth.js +48 -0
  25. package/dist/utils/extract-function-name.js +13 -4
  26. package/dist/utils/extract-function-node.js +18 -1
  27. package/dist/utils/extract-secret-usage.d.ts +9 -0
  28. package/dist/utils/extract-secret-usage.js +61 -0
  29. package/dist/utils/literal-properties.d.ts +22 -0
  30. package/dist/utils/literal-properties.js +69 -0
  31. package/dist/utils/load-addon-functions-meta.js +26 -1
  32. package/dist/utils/permissions.js +8 -0
  33. package/dist/utils/post-process.d.ts +68 -3
  34. package/dist/utils/post-process.js +243 -13
  35. package/dist/utils/secret-alias-services.d.ts +8 -0
  36. package/dist/utils/secret-alias-services.js +45 -0
  37. package/dist/utils/secret-brokers.d.ts +2 -0
  38. package/dist/utils/secret-brokers.js +9 -0
  39. package/dist/utils/serialize-inspector-state.d.ts +10 -0
  40. package/dist/utils/serialize-inspector-state.js +18 -0
  41. package/dist/utils/serialize-middleware-groups-meta.js +3 -0
  42. package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
  43. package/dist/utils/validate-exposed-functions-gated.js +85 -0
  44. package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
  45. package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
  46. package/dist/visit.js +4 -0
  47. package/package.json +3 -3
  48. package/src/add/add-auth.test.ts +5 -3
  49. package/src/add/add-auth.ts +1 -1
  50. package/src/add/add-cli.ts +8 -0
  51. package/src/add/add-credential.ts +1 -1
  52. package/src/add/add-functions.test.ts +263 -1
  53. package/src/add/add-functions.ts +219 -9
  54. package/src/add/add-http-route.ts +20 -7
  55. package/src/add/add-middleware.test.ts +196 -0
  56. package/src/add/add-middleware.ts +64 -4
  57. package/src/add/add-personas.test.ts +268 -0
  58. package/src/add/add-personas.ts +276 -0
  59. package/src/add/add-scenario-step.test.ts +49 -3
  60. package/src/add/add-scope.test.ts +30 -30
  61. package/src/add/add-scope.ts +1 -1
  62. package/src/add/add-secret.ts +1 -1
  63. package/src/add/add-subject-scenario-steps.test.ts +161 -0
  64. package/src/add/add-system-role.test.ts +214 -0
  65. package/src/add/add-system-role.ts +164 -0
  66. package/src/add/add-variable.ts +1 -1
  67. package/src/add/add-wire-addon.test.ts +111 -0
  68. package/src/add/add-wire-addon.ts +29 -0
  69. package/src/add/addon-bans.test.ts +4 -4
  70. package/src/add/config-value-metadata.test.ts +11 -11
  71. package/src/add/wire-name-literal.test.ts +2 -2
  72. package/src/error-codes.ts +13 -0
  73. package/src/index.ts +4 -1
  74. package/src/inspector.ts +39 -0
  75. package/src/types.ts +47 -2
  76. package/src/utils/annotate-http-route-auth.test.ts +163 -0
  77. package/src/utils/annotate-http-route-auth.ts +52 -0
  78. package/src/utils/extract-function-name.ts +14 -4
  79. package/src/utils/extract-function-node.ts +18 -6
  80. package/src/utils/extract-secret-usage.test.ts +81 -0
  81. package/src/utils/extract-secret-usage.ts +73 -0
  82. package/src/utils/literal-properties.ts +97 -0
  83. package/src/utils/load-addon-functions-meta.ts +32 -2
  84. package/src/utils/permissions-shorthand.test.ts +79 -0
  85. package/src/utils/permissions.ts +7 -0
  86. package/src/utils/post-process.test.ts +133 -0
  87. package/src/utils/post-process.ts +310 -12
  88. package/src/utils/secret-alias-services.test.ts +146 -0
  89. package/src/utils/secret-alias-services.ts +58 -0
  90. package/src/utils/secret-brokers.test.ts +53 -0
  91. package/src/utils/secret-brokers.ts +11 -0
  92. package/src/utils/serialize-inspector-state.ts +28 -0
  93. package/src/utils/serialize-middleware-groups-meta.ts +3 -0
  94. package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
  95. package/src/utils/validate-exposed-functions-gated.ts +94 -0
  96. package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
  97. package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
  98. package/src/visit.ts +4 -0
  99. 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 { wireScope } from '@pikku/core/scope'",
43
- 'wireScope({',
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 { wireScope } from '@pikku/core/scope'",
66
- 'wireScope({',
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 { wireScope } from '@pikku/core/scope'",
94
- 'wireScope({',
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 { wireScope } from '@pikku/core/scope'",
124
- 'wireScope({ admin: {} })',
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 { wireScope } from '@pikku/core/scope'",
138
- 'wireScope({ admin: {}, billing: { scopes: { read: {} } } })',
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 { wireScope } from '@pikku/core/scope'",
154
- 'wireScope({ admin: {} })',
155
- 'wireScope({ billing: {} })',
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 { wireScope } from '@pikku/core/scope'",
169
- "wireScope({ 'admin:users': {} })",
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 { wireScope } from '@pikku/core/scope'",
183
- "wireScope({ '*': {} })",
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 { wireScope } from '@pikku/core/scope'",
196
+ "import { defineScope } from '@pikku/core/scope'",
197
197
  'const k = String(1)',
198
- 'wireScope({ [k]: {} } as any)',
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 { wireScope } from '@pikku/core/scope'",
212
- "wireScope({ admin: 'nope' } as any)",
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 { wireScope } from '@pikku/core/scope'",
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
- 'wireScope({',
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
- 'wireScope({',
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']", ['wireScope({ admin: {} })'])
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']", ['wireScope({ admin: {} })'])
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
- 'wireScope({',
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:*']", ['wireScope({ 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:*']", ['wireScope({ admin: {} })'])
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("['*']", ['wireScope({ admin: {} })'])
327
+ funcSource("['*']", ['defineScope({ admin: {} })'])
328
328
  )
329
329
 
330
330
  assert.ok(
@@ -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 !== 'wireScope') {
145
+ if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
146
146
  return
147
147
  }
148
148
 
@@ -1,7 +1,7 @@
1
1
  import { createAddKeyedWiring } from './add-keyed-wiring.js'
2
2
 
3
3
  export const addSecret = createAddKeyedWiring({
4
- functionName: 'wireSecret',
4
+ functionName: 'defineSecret',
5
5
  idField: 'secretId',
6
6
  label: 'Secret',
7
7
  schemaPrefix: 'SecretSchema',
@@ -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
+ })