@pikku/inspector 0.12.58 → 0.12.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,58 @@
1
+ ## 0.12.59
2
+
3
+ ### Patch Changes
4
+
5
+ - 02c4fe5: fix(core,inspector): let a host grant an addon secrets it could not declare
6
+
7
+ Scoping an addon's `SecretService` to its `declaredSecrets` left generic addons
8
+ with nothing readable: `declaredSecrets` is derived from the addon package's own
9
+ source, but the secrets an addon like `@pikku/addon-graph` reads are named by the
10
+ consuming app's workflow nodes at runtime. Every authenticated `graph:httpRequest`
11
+ threw.
12
+
13
+ `wireAddon` now takes `secretGrants: string[]` and `credentialGrants: string[]`,
14
+ completing the grant family alongside `secretOverrides` (grant + rename) and
15
+ `globalSecrets` (grant everything, with a reason). Grants name the secret as the
16
+ addon reads it, since the scope check runs before the override map renames it —
17
+ which is also why an override's key grants and its value does not.
18
+
19
+ A grant naming a secret the project does not declare is an `INVALID_VALUE`
20
+ critical at codegen, resolved through the override map before lookup.
21
+
22
+ - 438b776: Move the scenario and feature surface off `@pikku/core/workflow` and onto
23
+ `@pikku/core/scenario`. Scenarios extend workflows, so the production workflow
24
+ wiring no longer names a scenario module in its import graph. Feature and
25
+ scenario types are declared in their own `scenario.types.ts` rather than in
26
+ `workflow.types.ts`. Import `requireActor`, `requireScenarioEnv`, `pollUntil`,
27
+ `createCookieJar`, `addFeature`, `ScenarioHttpResponse` and the rest from
28
+ `@pikku/core/scenario`; `HttpPersonasConfig` now comes from
29
+ `@pikku/core/persona` rather than `@pikku/core/services`.
30
+ - ad63f47: feat(cli): warn before codegen when a linked dependency splits a package's type identity
31
+
32
+ `pikku all` now runs the split-type-identity check as a preflight, beside the
33
+ existing `@pikku/core` one, and warns with `PKU719` naming each package, both
34
+ versions and both paths.
35
+
36
+ It has to run _before_ the work rather than after it fails. The failure it
37
+ explains is a V8 heap OOM, which aborts the process — `process.on('exit')`,
38
+ `uncaughtException` and `finally` never run, so nothing printed after the fact is
39
+ ever seen. By the time there is a symptom, the only thing that can help is
40
+ already on screen above it. Without this the user sees a codegen step that dies
41
+ of memory pressure with no indication that two copies of one package are the
42
+ reason, and the obvious next move is to raise `--max-old-space-size`, which
43
+ hides it further.
44
+
45
+ Warns rather than throws: a skewed linked dependency is a strong signal, not a
46
+ certainty, and refusing to build on a heuristic would break working setups.
47
+ `PIKKU_SKIP_TYPE_IDENTITY_CHECK=1` silences it, matching
48
+ `PIKKU_ALLOW_DUPLICATE_CORE`. It also swallows its own errors — it runs on every
49
+ codegen, so it must never be the reason a build stops.
50
+
51
+ - Updated dependencies [02c4fe5]
52
+ - Updated dependencies [438b776]
53
+ - Updated dependencies [438b776]
54
+ - @pikku/core@0.12.83
55
+
1
56
  ## 0.12.58
2
57
 
3
58
  ### Patch Changes
@@ -53,6 +53,8 @@ export function addWireAddon(node, state, logger) {
53
53
  let secretOverrides;
54
54
  let variableOverrides;
55
55
  let credentialOverrides;
56
+ let secretGrants;
57
+ let credentialGrants;
56
58
  let globalSecrets;
57
59
  let globalCredentials;
58
60
  for (const prop of firstArg.properties) {
@@ -96,6 +98,12 @@ export function addWireAddon(node, state, logger) {
96
98
  ts.isObjectLiteralExpression(prop.initializer)) {
97
99
  credentialOverrides = parseStringRecord(prop.initializer);
98
100
  }
101
+ else if (key === 'secretGrants') {
102
+ secretGrants = parseStringArray(prop.initializer);
103
+ }
104
+ else if (key === 'credentialGrants') {
105
+ credentialGrants = parseStringArray(prop.initializer);
106
+ }
99
107
  else if (key === 'globalSecrets') {
100
108
  // The grant is real at runtime whatever the reason evaluates to, so a
101
109
  // non-literal is recorded as its source text rather than dropped.
@@ -122,6 +130,8 @@ export function addWireAddon(node, state, logger) {
122
130
  secretOverrides,
123
131
  variableOverrides,
124
132
  credentialOverrides,
133
+ secretGrants,
134
+ credentialGrants,
125
135
  globalSecrets,
126
136
  globalCredentials,
127
137
  });
@@ -69,6 +69,7 @@ export declare enum ErrorCode {
69
69
  FUNCTION_DYNAMIC_IMPORT = "PKU498",
70
70
  DUPLICATE_CORE_VERSION = "PKU717",
71
71
  CORE_VERSION_SKEW = "PKU718",
72
+ SPLIT_TYPE_IDENTITY = "PKU719",
72
73
  PII_IN_OUTPUT = "PKU910",
73
74
  ADDON_WIRING_NOT_ALLOWED = "PKU920",
74
75
  ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
@@ -90,6 +90,7 @@ export var ErrorCode;
90
90
  // Dependency integrity errors
91
91
  ErrorCode["DUPLICATE_CORE_VERSION"] = "PKU717";
92
92
  ErrorCode["CORE_VERSION_SKEW"] = "PKU718";
93
+ ErrorCode["SPLIT_TYPE_IDENTITY"] = "PKU719";
93
94
  // Data classification errors
94
95
  ErrorCode["PII_IN_OUTPUT"] = "PKU910";
95
96
  // Addon authoring errors
package/dist/types.d.ts CHANGED
@@ -521,6 +521,10 @@ export interface InspectorState {
521
521
  secretOverrides?: Record<string, string>;
522
522
  variableOverrides?: Record<string, string>;
523
523
  credentialOverrides?: Record<string, string>;
524
+ /** Secrets the app lends this instance, named as the addon reads them. */
525
+ secretGrants?: string[];
526
+ /** Credentials the app lends this instance, named as the addon reads them. */
527
+ credentialGrants?: string[];
524
528
  /**
525
529
  * The app's stated reason for handing this instance the whole
526
530
  * `SecretService` instead of one scoped to its declared secrets.
@@ -317,14 +317,21 @@ export function validateSecretOverrides(logger, state) {
317
317
  // older meta without a secretId field.
318
318
  const secretIds = new Set(state.secrets.definitions.map((d) => d.secretId ?? d.name));
319
319
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
320
- if (!addonDecl.secretOverrides)
321
- continue;
322
- for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides)) {
320
+ for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
323
321
  if (!secretIds.has(resolvedName)) {
324
322
  const availableSecrets = Array.from(secretIds);
325
323
  logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
326
324
  }
327
325
  }
326
+ // A grant names the secret as the addon reads it, so it resolves through
327
+ // the override map before it can be looked up in the project.
328
+ for (const logicalName of addonDecl.secretGrants ?? []) {
329
+ const resolvedName = addonDecl.secretOverrides?.[logicalName] ?? logicalName;
330
+ if (!secretIds.has(resolvedName)) {
331
+ const availableSecrets = Array.from(secretIds);
332
+ logger.critical(ErrorCode.INVALID_VALUE, `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
333
+ }
334
+ }
328
335
  }
329
336
  }
330
337
  export function validateCredentialOverrides(logger, state) {
@@ -333,14 +340,19 @@ export function validateCredentialOverrides(logger, state) {
333
340
  return;
334
341
  const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
335
342
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
336
- if (!addonDecl.credentialOverrides)
337
- continue;
338
- for (const [logicalName, resolvedName] of Object.entries(addonDecl.credentialOverrides)) {
343
+ for (const [logicalName, resolvedName] of Object.entries(addonDecl.credentialOverrides ?? {})) {
339
344
  if (!credentialNames.has(resolvedName)) {
340
345
  const availableCredentials = Array.from(credentialNames);
341
346
  logger.critical(ErrorCode.INVALID_VALUE, `Credential override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
342
347
  }
343
348
  }
349
+ for (const logicalName of addonDecl.credentialGrants ?? []) {
350
+ const resolvedName = addonDecl.credentialOverrides?.[logicalName] ?? logicalName;
351
+ if (!credentialNames.has(resolvedName)) {
352
+ const availableCredentials = Array.from(credentialNames);
353
+ logger.critical(ErrorCode.INVALID_VALUE, `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
354
+ }
355
+ }
344
356
  }
345
357
  }
346
358
  export function validateVariableOverrides(logger, state) {
@@ -195,6 +195,8 @@ export interface SerializableInspectorState {
195
195
  secretOverrides?: Record<string, string>;
196
196
  variableOverrides?: Record<string, string>;
197
197
  credentialOverrides?: Record<string, string>;
198
+ secretGrants?: string[];
199
+ credentialGrants?: string[];
198
200
  globalSecrets?: string;
199
201
  globalCredentials?: string;
200
202
  }
@@ -67,7 +67,8 @@ export interface NodeOptions {
67
67
  * Flow node types for control flow (no RPC call)
68
68
  */
69
69
  export type FlowType = 'sleep' | 'suspend' | 'approval' | 'branch' | 'parallel' | 'fanout' | 'inline' | 'switch' | 'filter' | 'arrayPredicate' | 'return' | 'cancel' | 'set';
70
- import type { ContextVariable, ScenarioStepPhase, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
70
+ import type { ContextVariable, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
71
+ import type { ScenarioStepPhase } from '@pikku/core/scenario';
71
72
  export type { ContextVariable, WorkflowContext };
72
73
  /**
73
74
  * Base node properties shared by all node types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.58",
3
+ "version": "0.12.59",
4
4
  "description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "BUSL-1.1",
@@ -45,10 +45,10 @@
45
45
  "zod-to-ts": "^2.1.0"
46
46
  },
47
47
  "peerDependencies": {
48
- "@pikku/core": "^0.12.82"
48
+ "@pikku/core": "^0.12.83"
49
49
  },
50
50
  "devDependencies": {
51
- "@pikku/core": "^0.12.82",
51
+ "@pikku/core": "^0.12.83",
52
52
  "@types/node": "^24.11.0"
53
53
  },
54
54
  "engines": {
@@ -9,7 +9,7 @@ import {
9
9
  import { extractFunctionNode } from '../utils/extract-function-node.js'
10
10
  import { extractUsedWires } from '../utils/extract-services.js'
11
11
  import type { AuditDurability, FunctionServicesMeta } from '@pikku/core'
12
- import type { ScenarioStepKind, ScenarioSurface } from '@pikku/core/workflow'
12
+ import type { ScenarioStepKind, ScenarioSurface } from '@pikku/core/scenario'
13
13
  import { formatVersionedId, parseVersionedId } from '@pikku/core'
14
14
 
15
15
  /** Binding keys a `pikkuScenarioStep` may declare, in report order. */
@@ -5,7 +5,8 @@ import { tmpdir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import { inspect } from '../inspector.js'
7
7
  import type { InspectorLogger } from '../types.js'
8
- import type { ScenarioStepMeta, WorkflowStepMeta } from '@pikku/core/workflow'
8
+ import type { WorkflowStepMeta } from '@pikku/core/workflow'
9
+ import type { ScenarioStepMeta } from '@pikku/core/scenario'
9
10
 
10
11
  function makeLogger(
11
12
  criticals: Array<{ code: string; message: string }>
@@ -61,11 +61,42 @@ describe('addWireAddon', () => {
61
61
  secretOverrides: undefined,
62
62
  variableOverrides: undefined,
63
63
  credentialOverrides: undefined,
64
+ secretGrants: undefined,
65
+ credentialGrants: undefined,
64
66
  globalSecrets: undefined,
65
67
  globalCredentials: undefined,
66
68
  })
67
69
  })
68
70
 
71
+ test('captures the secrets and credentials the host lends an addon', () => {
72
+ const declarations = inspect(`
73
+ wireAddon({
74
+ name: 'graph',
75
+ package: '@pikku/addon-graph',
76
+ secretGrants: ['STRIPE_KEY', 'GITHUB_TOKEN'],
77
+ credentialGrants: ['slack'],
78
+ })
79
+ `)
80
+
81
+ assert.deepEqual(declarations.get('graph').secretGrants, [
82
+ 'STRIPE_KEY',
83
+ 'GITHUB_TOKEN',
84
+ ])
85
+ assert.deepEqual(declarations.get('graph').credentialGrants, ['slack'])
86
+ })
87
+
88
+ test('drops a grant list that is not statically knowable', () => {
89
+ const declarations = inspect(`
90
+ wireAddon({
91
+ name: 'graph',
92
+ package: '@pikku/addon-graph',
93
+ secretGrants: someRuntimeList,
94
+ })
95
+ `)
96
+
97
+ assert.equal(declarations.get('graph').secretGrants, undefined)
98
+ })
99
+
69
100
  test('captures the reason an addon was exempted from credential scoping', () => {
70
101
  const declarations = inspect(`
71
102
  wireAddon({
@@ -60,6 +60,8 @@ export function addWireAddon(
60
60
  let secretOverrides: Record<string, string> | undefined
61
61
  let variableOverrides: Record<string, string> | undefined
62
62
  let credentialOverrides: Record<string, string> | undefined
63
+ let secretGrants: string[] | undefined
64
+ let credentialGrants: string[] | undefined
63
65
  let globalSecrets: string | undefined
64
66
  let globalCredentials: string | undefined
65
67
 
@@ -104,6 +106,10 @@ export function addWireAddon(
104
106
  ts.isObjectLiteralExpression(prop.initializer)
105
107
  ) {
106
108
  credentialOverrides = parseStringRecord(prop.initializer)
109
+ } else if (key === 'secretGrants') {
110
+ secretGrants = parseStringArray(prop.initializer)
111
+ } else if (key === 'credentialGrants') {
112
+ credentialGrants = parseStringArray(prop.initializer)
107
113
  } else if (key === 'globalSecrets') {
108
114
  // The grant is real at runtime whatever the reason evaluates to, so a
109
115
  // non-literal is recorded as its source text rather than dropped.
@@ -130,6 +136,8 @@ export function addWireAddon(
130
136
  secretOverrides,
131
137
  variableOverrides,
132
138
  credentialOverrides,
139
+ secretGrants,
140
+ credentialGrants,
133
141
  globalSecrets,
134
142
  globalCredentials,
135
143
  })
@@ -105,6 +105,7 @@ export enum ErrorCode {
105
105
  // Dependency integrity errors
106
106
  DUPLICATE_CORE_VERSION = 'PKU717',
107
107
  CORE_VERSION_SKEW = 'PKU718',
108
+ SPLIT_TYPE_IDENTITY = 'PKU719',
108
109
 
109
110
  // Data classification errors
110
111
  PII_IN_OUTPUT = 'PKU910',
package/src/types.ts CHANGED
@@ -588,6 +588,10 @@ export interface InspectorState {
588
588
  secretOverrides?: Record<string, string>
589
589
  variableOverrides?: Record<string, string>
590
590
  credentialOverrides?: Record<string, string>
591
+ /** Secrets the app lends this instance, named as the addon reads them. */
592
+ secretGrants?: string[]
593
+ /** Credentials the app lends this instance, named as the addon reads them. */
594
+ credentialGrants?: string[]
591
595
  /**
592
596
  * The app's stated reason for handing this instance the whole
593
597
  * `SecretService` instead of one scoped to its declared secrets.
@@ -59,6 +59,33 @@ const makeOverrideState = (
59
59
  },
60
60
  }) as unknown as Omit<InspectorState, 'typesLookup'>
61
61
 
62
+ const makeGrantState = (
63
+ kind: 'secrets' | 'credentials',
64
+ declaredNames: string[],
65
+ decl: {
66
+ secretGrants?: string[]
67
+ credentialGrants?: string[]
68
+ secretOverrides?: Record<string, string>
69
+ credentialOverrides?: Record<string, string>
70
+ }
71
+ ): Omit<InspectorState, 'typesLookup'> =>
72
+ ({
73
+ rpc: {
74
+ wireAddonDeclarations: new Map([
75
+ ['slack-marketing', { package: '@addon/slack', ...decl }],
76
+ ]),
77
+ },
78
+ secrets: {
79
+ definitions:
80
+ kind === 'secrets' ? declaredNames.map((name) => ({ name })) : [],
81
+ },
82
+ credentials: {
83
+ definitions:
84
+ kind === 'credentials' ? declaredNames.map((name) => ({ name })) : [],
85
+ },
86
+ variables: { definitions: [] },
87
+ }) as unknown as Omit<InspectorState, 'typesLookup'>
88
+
62
89
  function makeState(
63
90
  overrides: {
64
91
  usedFunctions?: string[]
@@ -414,6 +441,46 @@ describe('override validation resolves the override target (value), not the logi
414
441
  validateVariableOverrides(logger, state)
415
442
  assert.deepEqual(criticals, [])
416
443
  })
444
+
445
+ test('validateSecretOverrides accepts a grant naming a declared secret', () => {
446
+ const { logger, criticals } = makeCriticalLogger()
447
+ const state = makeGrantState('secrets', ['STRIPE_KEY'], {
448
+ secretGrants: ['STRIPE_KEY'],
449
+ })
450
+ validateSecretOverrides(logger, state)
451
+ assert.deepEqual(criticals, [])
452
+ })
453
+
454
+ test('validateSecretOverrides flags a grant naming no declared secret', () => {
455
+ const { logger, criticals } = makeCriticalLogger()
456
+ const state = makeGrantState('secrets', ['STRIPE_KEY'], {
457
+ secretGrants: ['STIRPE_KEY'],
458
+ })
459
+ validateSecretOverrides(logger, state)
460
+ assert.equal(criticals.length, 1)
461
+ assert.match(criticals[0]!.message, /STIRPE_KEY/)
462
+ assert.match(criticals[0]!.message, /STRIPE_KEY/)
463
+ })
464
+
465
+ test('validateSecretOverrides resolves a grant through its override before looking it up', () => {
466
+ const { logger, criticals } = makeCriticalLogger()
467
+ const state = makeGrantState('secrets', ['PROD_EMAIL_KEY'], {
468
+ secretGrants: ['MAILGUN_KEY'],
469
+ secretOverrides: { MAILGUN_KEY: 'PROD_EMAIL_KEY' },
470
+ })
471
+ validateSecretOverrides(logger, state)
472
+ assert.deepEqual(criticals, [])
473
+ })
474
+
475
+ test('validateCredentialOverrides flags a grant naming no declared credential', () => {
476
+ const { logger, criticals } = makeCriticalLogger()
477
+ const state = makeGrantState('credentials', ['marketing_cred'], {
478
+ credentialGrants: ['ghost_cred'],
479
+ })
480
+ validateCredentialOverrides(logger, state)
481
+ assert.equal(criticals.length, 1)
482
+ assert.match(criticals[0]!.message, /ghost_cred/)
483
+ })
417
484
  })
418
485
 
419
486
  const makeRemoteState = (
@@ -383,10 +383,8 @@ export function validateSecretOverrides(
383
383
  )
384
384
 
385
385
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
386
- if (!addonDecl.secretOverrides) continue
387
-
388
386
  for (const [logicalName, resolvedName] of Object.entries(
389
- addonDecl.secretOverrides
387
+ addonDecl.secretOverrides ?? {}
390
388
  )) {
391
389
  if (!secretIds.has(resolvedName)) {
392
390
  const availableSecrets = Array.from(secretIds)
@@ -396,6 +394,20 @@ export function validateSecretOverrides(
396
394
  )
397
395
  }
398
396
  }
397
+
398
+ // A grant names the secret as the addon reads it, so it resolves through
399
+ // the override map before it can be looked up in the project.
400
+ for (const logicalName of addonDecl.secretGrants ?? []) {
401
+ const resolvedName =
402
+ addonDecl.secretOverrides?.[logicalName] ?? logicalName
403
+ if (!secretIds.has(resolvedName)) {
404
+ const availableSecrets = Array.from(secretIds)
405
+ logger.critical(
406
+ ErrorCode.INVALID_VALUE,
407
+ `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`
408
+ )
409
+ }
410
+ }
399
411
  }
400
412
  }
401
413
 
@@ -411,10 +423,8 @@ export function validateCredentialOverrides(
411
423
  )
412
424
 
413
425
  for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
414
- if (!addonDecl.credentialOverrides) continue
415
-
416
426
  for (const [logicalName, resolvedName] of Object.entries(
417
- addonDecl.credentialOverrides
427
+ addonDecl.credentialOverrides ?? {}
418
428
  )) {
419
429
  if (!credentialNames.has(resolvedName)) {
420
430
  const availableCredentials = Array.from(credentialNames)
@@ -424,6 +434,18 @@ export function validateCredentialOverrides(
424
434
  )
425
435
  }
426
436
  }
437
+
438
+ for (const logicalName of addonDecl.credentialGrants ?? []) {
439
+ const resolvedName =
440
+ addonDecl.credentialOverrides?.[logicalName] ?? logicalName
441
+ if (!credentialNames.has(resolvedName)) {
442
+ const availableCredentials = Array.from(credentialNames)
443
+ logger.critical(
444
+ ErrorCode.INVALID_VALUE,
445
+ `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`
446
+ )
447
+ }
448
+ }
427
449
  }
428
450
  }
429
451
 
@@ -170,6 +170,8 @@ export interface SerializableInspectorState {
170
170
  secretOverrides?: Record<string, string>
171
171
  variableOverrides?: Record<string, string>
172
172
  credentialOverrides?: Record<string, string>
173
+ secretGrants?: string[]
174
+ credentialGrants?: string[]
173
175
  globalSecrets?: string
174
176
  globalCredentials?: string
175
177
  },
@@ -15,13 +15,13 @@ import type {
15
15
  SwitchCaseMeta,
16
16
  FilterStepMeta,
17
17
  ArrayPredicateStepMeta,
18
- ScenarioStepMeta,
19
18
  InputSource,
20
19
  OutputBinding,
21
20
  Condition,
22
21
  WorkflowContext,
23
22
  ContextVariable,
24
23
  } from '@pikku/core/workflow'
24
+ import type { ScenarioStepMeta } from '@pikku/core/scenario'
25
25
  import {
26
26
  isWorkflowDoCall,
27
27
  isWorkflowExpectEventuallyCall,
@@ -113,10 +113,10 @@ export type FlowType =
113
113
  // Import and re-export context types from core
114
114
  import type {
115
115
  ContextVariable,
116
- ScenarioStepPhase,
117
116
  WorkflowContext,
118
117
  WorkflowPlannedStep,
119
118
  } from '@pikku/core/workflow'
119
+ import type { ScenarioStepPhase } from '@pikku/core/scenario'
120
120
 
121
121
  export type { ContextVariable, WorkflowContext }
122
122