@pikku/inspector 0.12.50 → 0.12.54

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