@pikku/inspector 0.12.103 → 0.12.106

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 (62) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/add/add-cli.js +21 -11
  3. package/dist/add/add-functions.js +8 -8
  4. package/dist/add/add-virtual-users.d.ts +13 -0
  5. package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
  6. package/dist/add/add-workflow.js +14 -10
  7. package/dist/error-codes.d.ts +4 -3
  8. package/dist/error-codes.js +4 -3
  9. package/dist/inspector.js +5 -3
  10. package/dist/types.d.ts +3 -3
  11. package/dist/utils/extract-function-name.js +1 -1
  12. package/dist/utils/get-property-value.js +1 -1
  13. package/dist/utils/load-addon-functions-meta.js +1 -0
  14. package/dist/utils/post-process.d.ts +36 -36
  15. package/dist/utils/post-process.js +198 -173
  16. package/dist/utils/serialize-inspector-state.d.ts +2 -2
  17. package/dist/utils/serialize-inspector-state.js +6 -6
  18. package/dist/utils/single-declaration.d.ts +3 -3
  19. package/dist/utils/single-declaration.js +3 -3
  20. package/dist/utils/validate-single-step-workflows.d.ts +3 -0
  21. package/dist/utils/validate-single-step-workflows.js +32 -0
  22. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
  23. package/dist/utils/workflow/dsl/patterns.d.ts +3 -3
  24. package/dist/utils/workflow/dsl/patterns.js +4 -4
  25. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
  26. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
  27. package/dist/visit.js +2 -2
  28. package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +22 -0
  29. package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +20 -0
  30. package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +27 -0
  31. package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +20 -0
  32. package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +25 -0
  33. package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +20 -0
  34. package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +27 -0
  35. package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +27 -0
  36. package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +22 -0
  37. package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +29 -0
  38. package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +21 -0
  39. package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +21 -0
  40. package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +25 -0
  41. package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +19 -0
  42. package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +23 -0
  43. package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +24 -0
  44. package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +18 -0
  45. package/knowledge/decisions/index.md +38 -0
  46. package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +20 -0
  47. package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +19 -0
  48. package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +23 -0
  49. package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +23 -0
  50. package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +22 -0
  51. package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +28 -0
  52. package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +14 -0
  53. package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +30 -0
  54. package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +26 -0
  55. package/knowledge/index.md +14 -0
  56. package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +26 -0
  57. package/knowledge/questions/index.md +16 -0
  58. package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +23 -0
  59. package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +28 -0
  60. package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +22 -0
  61. package/package.json +3 -3
  62. package/dist/add/add-personas.d.ts +0 -13
@@ -269,8 +269,8 @@ export interface SerializableInspectorState {
269
269
  definitions: InspectorState['featureFlags']['definitions'];
270
270
  files: string[];
271
271
  };
272
- personas: {
273
- definitions: InspectorState['personas']['definitions'];
272
+ virtualUsers: {
273
+ definitions: InspectorState['virtualUsers']['definitions'];
274
274
  files: string[];
275
275
  };
276
276
  variables: {
@@ -140,9 +140,9 @@ export function serializeInspectorState(state) {
140
140
  definitions: state.featureFlags.definitions,
141
141
  files: Array.from(state.featureFlags.files),
142
142
  },
143
- personas: {
144
- definitions: state.personas.definitions,
145
- files: Array.from(state.personas.files),
143
+ virtualUsers: {
144
+ definitions: state.virtualUsers.definitions,
145
+ files: Array.from(state.virtualUsers.files),
146
146
  },
147
147
  variables: {
148
148
  definitions: state.variables.definitions,
@@ -330,9 +330,9 @@ export function deserializeInspectorState(data) {
330
330
  definitions: data.featureFlags?.definitions || [],
331
331
  files: new Set(data.featureFlags?.files || []),
332
332
  },
333
- personas: {
334
- definitions: data.personas?.definitions || [],
335
- files: new Set(data.personas?.files || []),
333
+ virtualUsers: {
334
+ definitions: data.virtualUsers?.definitions || [],
335
+ files: new Set(data.virtualUsers?.files || []),
336
336
  },
337
337
  variables: {
338
338
  definitions: data.variables?.definitions || [],
@@ -3,18 +3,18 @@ import type { InspectorLogger } from '../types.js';
3
3
  /**
4
4
  * Claims the one call site a single-declaration construct is allowed.
5
5
  *
6
- * `definePersonas` takes a keyed object, so one call already declares as many
6
+ * `defineVirtualUsers` takes a keyed object, so one call already declares as many
7
7
  * entries as you like. Letting the call itself repeat buys nothing and costs
8
8
  * the thing that matters: a single place to read the declared set from, and a
9
9
  * single place to add to it. The same rule `pikkuBetterAuth` has had all along.
10
10
  *
11
11
  * A second call in the SAME file is refused too — "the file" is not an answer
12
- * to "where do I add a persona?" when the file holds two calls, so allowing it
12
+ * to "where do I add a virtual user?" when the file holds two calls, so allowing it
13
13
  * would keep the ambiguity the rule exists to remove.
14
14
  *
15
15
  * A generated file is exempt, and never claims the slot either. The rule exists
16
16
  * to name the one place a person adds to, and nobody adds to a file the CLI
17
- * rewrites on the next run — `pikku-personas.gen.ts` would otherwise make every
17
+ * rewrites on the next run — `pikku-virtual-users.gen.ts` would otherwise make every
18
18
  * project that scaffolds one unbuildable, and take the app's own declaration
19
19
  * down with it.
20
20
  *
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Claims the one call site a single-declaration construct is allowed.
3
3
  *
4
- * `definePersonas` takes a keyed object, so one call already declares as many
4
+ * `defineVirtualUsers` takes a keyed object, so one call already declares as many
5
5
  * entries as you like. Letting the call itself repeat buys nothing and costs
6
6
  * the thing that matters: a single place to read the declared set from, and a
7
7
  * single place to add to it. The same rule `pikkuBetterAuth` has had all along.
8
8
  *
9
9
  * A second call in the SAME file is refused too — "the file" is not an answer
10
- * to "where do I add a persona?" when the file holds two calls, so allowing it
10
+ * to "where do I add a virtual user?" when the file holds two calls, so allowing it
11
11
  * would keep the ambiguity the rule exists to remove.
12
12
  *
13
13
  * A generated file is exempt, and never claims the slot either. The rule exists
14
14
  * to name the one place a person adds to, and nobody adds to a file the CLI
15
- * rewrites on the next run — `pikku-personas.gen.ts` would otherwise make every
15
+ * rewrites on the next run — `pikku-virtual-users.gen.ts` would otherwise make every
16
16
  * project that scaffolds one unbuildable, and take the app's own declaration
17
17
  * down with it.
18
18
  *
@@ -0,0 +1,3 @@
1
+ import type { InspectorLogger, InspectorState } from '../types.js';
2
+ /** Warns about a workflow with at most one RPC step and nothing to wait on or fan out: it is a function or a queue job wearing a workflow's cost. */
3
+ export declare function validateSingleStepWorkflows(logger: InspectorLogger, state: InspectorState): void;
@@ -0,0 +1,32 @@
1
+ import { ErrorCode } from '../error-codes.js';
2
+ const DURABLE_FLOWS = new Set([
3
+ 'sleep',
4
+ 'suspend',
5
+ 'approval',
6
+ 'parallel',
7
+ 'fanout',
8
+ ]);
9
+ /** Warns about a workflow with at most one RPC step and nothing to wait on or fan out: it is a function or a queue job wearing a workflow's cost. */
10
+ export function validateSingleStepWorkflows(logger, state) {
11
+ for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
12
+ if (graph.source === 'scenario')
13
+ continue;
14
+ const file = state.workflows.files.get(graph.pikkuFuncId)?.path ??
15
+ state.workflows.graphFiles.get(name)?.path;
16
+ if (!file)
17
+ continue;
18
+ const nodes = Object.values(graph.nodes);
19
+ const rpcSteps = nodes.filter((n) => 'rpcName' in n && n.rpcName);
20
+ if (rpcSteps.length > 1)
21
+ continue;
22
+ if (nodes.some((n) => 'flow' in n && DURABLE_FLOWS.has(n.flow)))
23
+ continue;
24
+ logger.diagnostic({
25
+ severity: 'warn',
26
+ code: ErrorCode.SINGLE_STEP_WORKFLOW,
27
+ message: `Workflow '${name}' (${file}) runs ${rpcSteps.length === 0 ? 'no RPC step' : 'a single RPC step'} ` +
28
+ `and never sleeps, suspends or fans out, so it buys nothing a workflow is for. ` +
29
+ `Call the function directly, or enqueue it on a queue worker if it must survive a restart.`,
30
+ });
31
+ }
32
+ }
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, isScenarioExpectationCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowMilestoneCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
2
+ import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, isScenarioExpectationCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractVirtualUserFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowMilestoneCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
3
3
  import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
4
4
  import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
5
5
  /**
@@ -571,7 +571,7 @@ function extractRpcStep(call, context, outputVar) {
571
571
  outputVar,
572
572
  inputs,
573
573
  options,
574
- actor: extractActorFromOptions(optionsArg),
574
+ virtualUser: extractVirtualUserFromOptions(optionsArg),
575
575
  expectEventually: expectEventually || undefined,
576
576
  };
577
577
  }
@@ -623,7 +623,7 @@ function extractScenarioStep(call, context, outputVar) {
623
623
  outputVar,
624
624
  inputs,
625
625
  options,
626
- actor: extractActorFromOptions(optionsArg),
626
+ virtualUser: extractVirtualUserFromOptions(optionsArg),
627
627
  };
628
628
  }
629
629
  catch (error) {
@@ -44,11 +44,11 @@ export declare function isScenarioExpectationCall(node: ts.CallExpression): bool
44
44
  */
45
45
  export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
46
46
  /**
47
- * Extract the actor NAME from a step options object literal:
48
- * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
47
+ * Extract the virtual user NAME from a step options object literal:
48
+ * `{ virtualUser: virtualUsers.x }` → 'x' (also `virtualUsers['x']`). Undefined when absent
49
49
  * or not statically resolvable.
50
50
  */
51
- export declare function extractActorFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
51
+ export declare function extractVirtualUserFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
52
52
  /**
53
53
  * Check if a call expression is workflow.sleep()
54
54
  */
@@ -88,17 +88,17 @@ export function isWorkflowExpectEventuallyCall(node) {
88
88
  isWorkflowWireIdentifier(node.expression.expression));
89
89
  }
90
90
  /**
91
- * Extract the actor NAME from a step options object literal:
92
- * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
91
+ * Extract the virtual user NAME from a step options object literal:
92
+ * `{ virtualUser: virtualUsers.x }` → 'x' (also `virtualUsers['x']`). Undefined when absent
93
93
  * or not statically resolvable.
94
94
  */
95
- export function extractActorFromOptions(optionsArg) {
95
+ export function extractVirtualUserFromOptions(optionsArg) {
96
96
  if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
97
97
  return;
98
98
  for (const prop of optionsArg.properties) {
99
99
  if (ts.isPropertyAssignment(prop) &&
100
100
  ts.isIdentifier(prop.name) &&
101
- prop.name.text === 'actor') {
101
+ prop.name.text === 'virtualUser') {
102
102
  const init = prop.initializer;
103
103
  if (ts.isPropertyAccessExpression(init)) {
104
104
  return init.name.text;
@@ -59,8 +59,8 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
59
59
  stepName: step.stepName,
60
60
  next: nextNodeId,
61
61
  };
62
- if (step.actor) {
63
- node.actor = step.actor;
62
+ if (step.virtualUser) {
63
+ node.virtualUser = step.virtualUser;
64
64
  }
65
65
  if (step.expectEventually) {
66
66
  node.expectEventually = true;
@@ -101,8 +101,8 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
101
101
  scenarioStepPhase: step.phase,
102
102
  next: nextNodeId,
103
103
  };
104
- if (step.actor) {
105
- node.actor = step.actor;
104
+ if (step.virtualUser) {
105
+ node.virtualUser = step.virtualUser;
106
106
  }
107
107
  if (step.inputs) {
108
108
  if (step.inputs === 'passthrough') {
@@ -401,7 +401,7 @@ export function convertDslToGraph(workflowName, meta) {
401
401
  }
402
402
  const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : [];
403
403
  // Determine source type:
404
- // - scenario: complex workflow whose steps run as actors (scenario)
404
+ // - scenario: complex workflow whose steps run as virtual users (scenario)
405
405
  // - dsl === true: pure DSL workflow, can be serialized
406
406
  // - dsl === false: complex workflow with inline steps, not serializable
407
407
  const source = meta.scenario
@@ -417,7 +417,7 @@ export function convertDslToGraph(workflowName, meta) {
417
417
  description: meta.description,
418
418
  tags: meta.tags,
419
419
  skip: meta.skip,
420
- actors: meta.actors,
420
+ virtualUsers: meta.virtualUsers,
421
421
  context: meta.context,
422
422
  nodes: nodesRecord,
423
423
  entryNodeIds,
@@ -110,8 +110,8 @@ export interface FunctionNode extends BaseNode {
110
110
  outputVar?: string;
111
111
  /** Hash of nodeId + RPC input/output schemas for version detection */
112
112
  stepHash?: string;
113
- /** Scenario actor name this step runs as (workflow.do {actor: actors.x}) */
114
- actor?: string;
113
+ /** Scenario virtual user name this step runs as (workflow.do {virtualUser: virtualUsers.x}) */
114
+ virtualUser?: string;
115
115
  /** True for workflow.expectEventually polling steps (scenarios) */
116
116
  expectEventually?: boolean;
117
117
  /** Gherkin-style phase for a declared scenario step (scenario.given/when/then) */
@@ -143,7 +143,7 @@ export declare const isFlowNode: (node: SerializedGraphNode) => node is FlowNode
143
143
  * - 'dsl': Pure DSL workflow (pikkuWorkflowFunc) - can be round-tripped to code
144
144
  * - 'complex': Complex workflow (pikkuWorkflowComplexFunc) - contains inline steps, not serializable
145
145
  * - 'graph': Graph-based workflow (pikkuWorkflowGraph)
146
- * - 'scenario': Scenario (pikkuScenario) - complex workflow whose steps run as actors
146
+ * - 'scenario': Scenario (pikkuScenario) - complex workflow whose steps run as virtual users
147
147
  */
148
148
  export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | 'scenario';
149
149
  /**
@@ -162,8 +162,8 @@ export interface SerializedWorkflowGraph {
162
162
  description?: string;
163
163
  /** Tags for organization */
164
164
  tags?: string[];
165
- /** Actor names a scenario's steps run as */
166
- actors?: string[];
165
+ /** Virtual user names a scenario's steps run as */
166
+ virtualUsers?: string[];
167
167
  /** Why a scenario is held out of a default run, stated where it is declared */
168
168
  skip?: string;
169
169
  /** If true, workflow always executes inline without queues */
package/dist/visit.js CHANGED
@@ -26,7 +26,7 @@ import { addCredential } from './add/add-credential.js';
26
26
  import { addScope } from './add/add-scope.js';
27
27
  import { addSystemRole } from './add/add-system-role.js';
28
28
  import { addFeatureFlag } from './add/add-feature-flag.js';
29
- import { addPersonas } from './add/add-personas.js';
29
+ import { addVirtualUsers } from './add/add-virtual-users.js';
30
30
  import { addVariable } from './add/add-variable.js';
31
31
  import { addWorkflowGraph } from './add/add-workflow-graph.js';
32
32
  import { addFeature } from './add/add-feature.js';
@@ -81,7 +81,7 @@ export const visitRoutes = (logger, checker, node, state, options) => {
81
81
  addScope(logger, node, checker, state, nextOptions);
82
82
  addSystemRole(logger, node, checker, state, nextOptions);
83
83
  addFeatureFlag(logger, node, checker, state, nextOptions);
84
- addPersonas(logger, node, checker, state, nextOptions);
84
+ addVirtualUsers(logger, node, checker, state, nextOptions);
85
85
  addVariable(logger, node, checker, state, nextOptions);
86
86
  addHTTPRoute(logger, node, checker, state, nextOptions);
87
87
  addHTTPRoutes(logger, node, checker, state, nextOptions);
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: A channel with auth false marks its connect and disconnect handlers sessionless
4
+ description: The inspector synthesises metadata for a channel's connect and disconnect functions and sets sessionless from the channel's auth
5
+ tags: [inspector, channels, auth]
6
+ ---
7
+
8
+ # A channel with auth false marks its connect and disconnect handlers sessionless
9
+
10
+ In `packages/inspector/src/add/add-channel.ts`, `channelAuth` is read from the channel's
11
+ `auth` property (line 705). `sessionless` is `true` only when that value is `false`
12
+ (line 708). Otherwise it is `undefined`.
13
+
14
+ For each of the connect and disconnect function ids (line 709), the inspector creates a
15
+ metadata entry with that `sessionless` value when none exists yet (lines 710-717). An entry
16
+ that already exists is left alone (line 710).
17
+
18
+ The comment at lines 701-704 says these handlers are defined with the channel-specific
19
+ helpers and not `pikkuFunc` or `pikkuSessionlessFunc`, so the runtime needs the flag here.
20
+
21
+ **What this rules out:** a channel with `auth: false` whose connect or disconnect handler is
22
+ still treated as needing a session. The check covers only the channel's own `auth` value.
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: decision
3
+ title: A declared scope id never shadows a definition
4
+ description: A bare declareScopes id is added only when no definition already holds it, so an addon's richer scope tree is kept
5
+ tags: [inspector, scopes]
6
+ ---
7
+
8
+ # A declared scope id never shadows a definition
9
+
10
+ `addDeclareScopes` in `packages/inspector/src/add/add-scope.ts` records each valid id
11
+ in `state.scopes.declared` and keeps the first source file for it (lines 164-167).
12
+
13
+ `applyDeclaredScopes` folds those ids into `state.scopes.definitions` later (lines
14
+ 178-192). It reuses a root that already exists (lines 181-185). For each further
15
+ segment it uses `??=`, so a node that is already there is kept (lines 188-189). The
16
+ comments at lines 137-141 and 173-177 say an addon's richer declaration "is never
17
+ shadowed by a bare id" and that an id already held "is left alone".
18
+
19
+ **What this rules out:** a bare `declareScopes` entry overwriting the richer tree an
20
+ addon or `defineScope` provided.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: decision
3
+ title: A workflow DSL step's virtual user is the literal name written in its options object
4
+ description: The DSL extractor reads a step's virtual user from a property access or string-keyed element access in the options literal, and leaves it undefined when the value is not static
5
+ tags: inspector, workflow, dsl, virtual-users
6
+ ---
7
+
8
+ # A workflow DSL step's virtual user is the literal name written in its options object
9
+
10
+ `extractVirtualUserFromOptions` in `packages/inspector/src/utils/workflow/dsl/patterns.ts`
11
+ returns the virtual user name for a step. Its doc comment says
12
+ `{ virtualUser: virtualUsers.x }` gives `'x'`, also `virtualUsers['x']`, and is
13
+ `undefined` when the virtual user is absent or "not statically resolvable"
14
+ (`patterns.ts:122-126`).
15
+
16
+ The function reads the `virtualUser` property of the options object literal
17
+ (`patterns.ts:127-150`). A property access returns its name, and a string-keyed
18
+ element access returns the string (`patterns.ts:138-146`). Any other form returns
19
+ `undefined` (`patterns.ts:149`).
20
+
21
+ The DSL extractor calls it for RPC steps (`packages/inspector/src/utils/workflow/dsl/extract-dsl-workflow.ts:790`)
22
+ and for scenario steps (`extract-dsl-workflow.ts:853`). The step is still recorded
23
+ with `virtualUser` set to `undefined` when the name is not literal.
24
+
25
+ **What this rules out:** resolving a step's virtual user from a variable, a computed
26
+ key or a runtime value at inspection time. A virtual user the inspector cannot read
27
+ statically is left blank, not guessed.
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: decision
3
+ title: A feature flag's anyOf is read from the AST, and an empty anyOf is refused
4
+ description: add-feature-flag reads the scope names in anyOf statically, refuses a non-array or empty anyOf, and refuses a flag name containing the scope separator
5
+ tags: [inspector, feature-flags, scopes]
6
+ ---
7
+
8
+ # A feature flag's anyOf is read from the AST, and an empty anyOf is refused
9
+
10
+ `extractAnyOf` in `packages/inspector/src/add/add-feature-flag.ts` (lines 96-137) reads the
11
+ `anyOf` property. If the property is absent it returns `undefined` (lines 108-110). If it is
12
+ present but not an array literal, it reports a critical error (lines 112-121). Each element must be
13
+ a string literal (lines 124-133). A non-literal element is reported and skipped.
14
+
15
+ In `addFeatureFlag`, a flag name containing the scope separator is refused (lines 193-199). A body
16
+ that is not an object literal, or a variable holding one, is refused (lines 204-211). An `anyOf` that
17
+ is an empty array is refused (lines 215-221), and the message says to omit `anyOf` for a pure switch.
18
+
19
+ **What this rules out:** a flag whose capability list cannot be read from the source, a flag named
20
+ like a scope, and `anyOf: []`. An omitted `anyOf` is not refused; it is recorded as `undefined` (line 230).
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: decision
3
+ title: A string array with one computed entry reads as absent, not as its literal entries
4
+ description: stringArrayProperty returns undefined unless every element of the array is a string literal
5
+ tags: [inspector, literals, virtual-users]
6
+ ---
7
+
8
+ # A string array with one computed entry reads as absent, not as its literal entries
9
+
10
+ `stringArrayProperty` in `packages/inspector/src/utils/literal-properties.ts` returns
11
+ `undefined` when the property is missing or is not an array literal (lines 84-86). It keeps
12
+ the string-literal entries (lines 87-89) and returns them only when every element was kept
13
+ (line 90). One computed entry therefore makes the whole result `undefined`.
14
+
15
+ The comment at lines 74-77 says a half-read array "typechecks, runs, and grants less than
16
+ the source says", which is why the helper is all-or-nothing.
17
+
18
+ Callers in this package include `add-virtual-users.ts` for `roles`, `goals` and `tags`, which
19
+ fall back to `[]` when the helper returns `undefined` (lines 248-250), and for `fixtures`
20
+ and `environments` (lines 274 and 279). `add-feature.ts` keeps a local copy of the helper with the same all-or-nothing rule
21
+ and reads `tags` through it (line 256).
22
+ A search of the package found no caller that reads `scopes` through this helper.
23
+
24
+ **What this rules out:** a virtual user or feature keeping the literal entries of a roles or tags
25
+ array that also holds a computed value. Such a virtual user gets an empty list instead.
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: decision
3
+ title: A permission whose data parameter starts with an underscore takes no data
4
+ description: An underscore-prefixed data parameter marks a permission as auth-only, and pikkuAuth always has no data
5
+ tags: [inspector, permissions]
6
+ ---
7
+
8
+ # A permission whose data parameter starts with an underscore takes no data
9
+
10
+ In `packages/inspector/src/add/add-permission.ts`, `requiresData` is false when the
11
+ second parameter's name starts with `_` (lines 107-110). The permission entry then
12
+ gets `requiresData: false` (line 120). The debug log labels such an entry
13
+ "auth-only" (line 124).
14
+
15
+ `pikkuAuth` always sets `requiresData: false` (line 204) and an empty, optimized wires
16
+ list (line 171). Its handler's second parameter is the resolved session, not a wires
17
+ bag (comment at lines 167-170).
18
+
19
+ **What this rules out:** a permission whose data parameter is underscore-prefixed
20
+ being treated as needing request data.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: decision
3
+ title: A provider-login virtual user is never runnable, even when declared runnable
4
+ description: Runnable is derived from the account, so a virtual user with a provider login cannot be driven, and an explicit runnable true does not change that
5
+ tags: [inspector, virtual-users]
6
+ ---
7
+
8
+ # A provider-login virtual user is never runnable, even when declared runnable
9
+
10
+ `addVirtualUsers` in `packages/inspector/src/add/add-virtual-users.ts` sets `runnable`
11
+ through `isRunnableVirtualUser`, passing the declared `runnable` value and the account
12
+ (lines 242-256). It imports the function from `@pikku/core/virtual-user` (line 3).
13
+
14
+ The function is in `packages/core/src/wirings/virtual-user/validate-virtual-users.ts`.
15
+ An explicit `runnable: false` gives false (lines 23-25). Otherwise the result is
16
+ `account?.provider === undefined` (line 26). A declared `runnable: true` does not
17
+ override the provider check, so a provider account is never runnable.
18
+ `packages/core/src/wirings/virtual-user/virtual-user-declaration.test.ts:89-90` covers
19
+ the provider case.
20
+
21
+ The doc comment gives the reason (`validate-virtual-users.ts:7-18`). Driving a
22
+ provider's consent screen needs a human, so runnability is derived from the account
23
+ rather than asked for: "a flag someone must remember to set is a flag that ships
24
+ wrong". An explicit `false` marks a person who is only ever acted upon.
25
+
26
+ **What this rules out:** a provider-login virtual user being driven by a run,
27
+ whatever the declaration says.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: decision
3
+ title: A remote addon's bound credential or secret id must name a wired slot, or validation reports a critical error
4
+ description: validateRemoteAddonAuth checks that a literal auth.credentialId or auth.secretId on a wireRemoteAddon names a declared credential or secret, and runs in every inspector pass
5
+ tags: inspector, addon, auth, validation
6
+ ---
7
+
8
+ # A remote addon's bound credential or secret id must name a wired slot, or validation reports a critical error
9
+
10
+ `validateRemoteAddonAuth` in `packages/inspector/src/utils/post-process.ts:760` is one of the validators the
11
+ inspector runs in every pass that is not setup-only (`packages/inspector/src/inspector.ts:523`). It is called
12
+ at `packages/inspector/src/inspector.ts:578`, after the remote addon dependency check and before the scope checks.
13
+
14
+ Its doc comment limits what it checks. A `credentialId` must name a wired credential and a `secretId` a
15
+ wired secret. A custom `resolve()` and an omitted surface are not statically checkable, so they are left to
16
+ runtime (`post-process.ts:754-759`).
17
+
18
+ The function returns when no `wireAddon` declarations exist (`post-process.ts:764-765`). It then loops over
19
+ the remote declarations (`post-process.ts:771`). A `credentialId` that matches no credential definition
20
+ logs a critical `REMOTE_ADDON_AUTH_UNRESOLVED` error that lists the available credentials (`post-process.ts:773-776`).
21
+ A `secretId` that matches no secret definition gets the same error with the available secrets (`post-process.ts:779-782`).
22
+
23
+ Whether a non-literal `auth` value is checked at all is a separate gap, in
24
+ [does a non-literal remote addon auth get checked](../questions/does-a-non-literal-remote-addon-auth-get-checked.md).
25
+
26
+ **What this rules out:** a remote addon binding that names a credential or secret the consumer never wires,
27
+ passing inspection silently and failing only at runtime.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: decision
3
+ title: A route's own auth overrides its group's, and auth false on a route makes an inline function sessionless
4
+ description: The route's auth is used when it is true or false, the group's inherited value otherwise; a resolved false sets sessionless on an inline function
5
+ tags: [inspector, http, auth]
6
+ ---
7
+
8
+ # A route's own auth overrides its group's, and auth false on a route makes an inline function sessionless
9
+
10
+ In `packages/inspector/src/add/add-http-route.ts`, the route's own `auth` is read first
11
+ (line 281). `resolvedAuth` takes that value when it is `true` or `false` (lines 282-284),
12
+ and otherwise takes `inheritedAuth` (line 285), which the caller passes in from the group.
13
+
14
+ The comment at lines 277-280 says the value is recorded on the meta so an audit can tell
15
+ whether a route is open without re-reading the source.
16
+
17
+ Lines 287-289 say that for inline functions, `sessionless` is set to `true` when auth is
18
+ disabled at the route or group level. The code sets it only when the inline function's
19
+ `sessionless` is still `undefined` and `resolvedAuth` is `false` (lines 290-298).
20
+
21
+ **What this rules out:** a route with `auth: true` inheriting `false` from its group. It also
22
+ rules out letting an inline function be marked sessionless while the route asks for auth.
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: decision
3
+ title: A route requires a session when any auth signal says so
4
+ description: Four places can demand a session for an HTTP route and they combine with OR; a scope gate counts as a session demand
5
+ tags: [inspector, auth, http]
6
+ ---
7
+
8
+ # A route requires a session when any auth signal says so
9
+
10
+ `annotateHttpRouteAuth` in `packages/inspector/src/utils/annotate-http-route-auth.ts`
11
+ sets `route.requiresSession` to true when any one of these holds (lines 31-38):
12
+
13
+ - the function's `sessionless` is `false`, which is a `pikkuFunc`;
14
+ - the function's own `auth` is `true`;
15
+ - the function declares one or more `scopes`;
16
+ - the route's own `auth` is `true`;
17
+ - the route's package is in the set of packages gated by their addon.
18
+
19
+ The last signal comes from `packagesGatedByTheirAddon` (lines 44-53). It gates a
20
+ package when one of its `wireAddon` declarations has `auth === true` or a
21
+ non-empty `scopes` list.
22
+
23
+ The scope rule is argued in the file's own doc comment. Scopes are checked
24
+ against the session and fail closed, so an anonymous caller is rejected either
25
+ way. Not counting them "would mark a route open that nobody can reach anonymously"
26
+ (`annotateHttpRouteAuth`, lines 14-16).
27
+
28
+ **What this rules out:** reading a scope-gated route as open. It also rules out
29
+ letting one signal cancel another; the four places only add demands.
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: decision
3
+ title: A webhook source with a verify step declares a singleton credential for its secret
4
+ description: When a webhook source has a verify step, the inspector registers a string credential named by the source unless the author sets credential
5
+ tags: [inspector, triggers, webhooks, credentials]
6
+ ---
7
+
8
+ # A webhook source with a verify step declares a singleton credential for its secret
9
+
10
+ In `packages/inspector/src/add/add-trigger.ts`, the code returns early unless the source has a
11
+ `verify` initializer (line 283). When it has one, the credential name is the `credential`
12
+ property if set, otherwise `webhookSecretCredentialName(name)` (lines 285-287). That helper is
13
+ imported from `@pikku/core/trigger` (line 21).
14
+
15
+ The inspector then records a credential definition with `type: 'singleton'` and a string schema,
16
+ named by that value (lines 288-297). Its display name is `<name> webhook secret` (line 291), and
17
+ its description is the `credentialDescription` property when set (lines 292-294).
18
+
19
+ **What this rules out:** a verified webhook source whose secret is left out of the credential
20
+ declarations. The author can choose a different credential name, but a verify step always
21
+ registers one.
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: decision
3
+ title: A wire with auth disabled cannot point at a pikkuFunc
4
+ description: A route or wiring with auth false that names a session-requiring pikkuFunc is reported as critical, and the function is not accepted
5
+ tags: [inspector, auth, validation]
6
+ ---
7
+
8
+ # A wire with auth disabled cannot point at a pikkuFunc
9
+
10
+ `validateAuthSessionless` in `packages/inspector/src/utils/validate-auth-sessionless.ts`
11
+ resolves the wire's auth. The wire's own `auth` wins when it is a boolean; otherwise
12
+ the inherited value is used (lines 18-20).
13
+
14
+ When that resolves to `false` and the function's meta has `sessionless === false`,
15
+ the function logs `AUTH_DISABLED_REQUIRES_SESSIONLESS` as critical and returns
16
+ `false` (lines 21-27). The message tells the author to use `pikkuSessionlessFunc`
17
+ instead (line 24).
18
+
19
+ **What this rules out:** a wire that declares no session needed while its function
20
+ does need one. The check runs on the wire's auth, not on the function's own `auth`
21
+ field.
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: decision
3
+ title: Addons may not call wiring helpers or declare middleware or permissions
4
+ description: Addon packages are banned from transport wirings, and their define* contracts may not carry middleware or permissions
5
+ tags: [inspector, addon]
6
+ ---
7
+
8
+ # Addons may not call wiring helpers or declare middleware or permissions
9
+
10
+ `checkAddonBans` in `packages/inspector/src/add/add-addon-bans.ts` runs only when the
11
+ package is inspected as an addon (line 63). It enforces two rules.
12
+
13
+ - A call to any name in `BANNED_WIRINGS` is critical `ADDON_WIRING_NOT_ALLOWED`
14
+ (lines 10-24, 68-74). The list includes `wireAddon`, `wireRemoteAddon`, `wireHTTP`
15
+ and `wireChannel`.
16
+ - A `defineHTTPRoutes`, `defineChannelRoutes` or `defineCLICommands` call whose
17
+ first argument has a `middleware` or `permissions` property is critical
18
+ `ADDON_CONTRACT_HANDLERS_NOT_ALLOWED` (lines 26-30, 32-49, 76-83).
19
+
20
+ The file comment (lines 6-8) says addons "declare contracts with the define*
21
+ helpers and export functions", and the consuming app wires them. The comment at
22
+ lines 52-54 says middleware and permissions "are the consuming app's concern".
23
+
24
+ **What this rules out:** an addon fixing its own transports, or attaching its own
25
+ access rules to the routes it contributes.
@@ -0,0 +1,19 @@
1
+ ---
2
+ type: decision
3
+ title: An addon declaration names the secrets and credentials the app lends it
4
+ description: secretGrants and credentialGrants are declared on the addon instance as lists of names the addon reads
5
+ tags: [inspector, addons, grants]
6
+ ---
7
+
8
+ # An addon declaration names the secrets and credentials the app lends it
9
+
10
+ The addon declaration type in `packages/inspector/src/types.ts` has `secretGrants?: string[]` and
11
+ `credentialGrants?: string[]`, each commented as the secrets or credentials "the app lends this
12
+ instance, named as the addon reads them" (lines 757-760).
13
+
14
+ In `packages/inspector/src/add/add-wire-addon.ts`, those two properties are read with
15
+ `parseStringArray` (lines 212-215). The helper itself is not read here, so this note does not say how
16
+ it treats a non-literal entry.
17
+
18
+ **What this rules out:** an addon instance receiving secrets or credentials that the app did not name in
19
+ its `wireAddon` call. The names are the grant.