@pikku/inspector 0.12.102 → 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.
- package/CHANGELOG.md +61 -0
- package/dist/add/add-cli.js +21 -11
- package/dist/add/add-define-screens.d.ts +8 -0
- package/dist/add/add-define-screens.js +140 -0
- package/dist/add/add-functions.js +8 -8
- package/dist/add/add-virtual-users.d.ts +13 -0
- package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
- package/dist/add/add-wire-addon.js +39 -2
- package/dist/add/add-workflow.js +14 -10
- package/dist/error-codes.d.ts +9 -3
- package/dist/error-codes.js +9 -3
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +7 -3
- package/dist/types.d.ts +45 -3
- package/dist/utils/extract-function-name.js +1 -1
- package/dist/utils/get-property-value.js +1 -1
- package/dist/utils/load-addon-functions-meta.d.ts +0 -5
- package/dist/utils/load-addon-functions-meta.js +53 -0
- package/dist/utils/post-process.d.ts +36 -34
- package/dist/utils/post-process.js +227 -169
- package/dist/utils/serialize-inspector-state.d.ts +7 -2
- package/dist/utils/serialize-inspector-state.js +10 -6
- package/dist/utils/single-declaration.d.ts +3 -3
- package/dist/utils/single-declaration.js +3 -3
- package/dist/utils/validate-single-step-workflows.d.ts +3 -0
- package/dist/utils/validate-single-step-workflows.js +32 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
- package/dist/utils/workflow/dsl/patterns.d.ts +3 -3
- package/dist/utils/workflow/dsl/patterns.js +4 -4
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
- package/dist/visit.js +4 -2
- package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +22 -0
- package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +20 -0
- package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +27 -0
- package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +20 -0
- package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +25 -0
- package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +20 -0
- package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +27 -0
- package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +27 -0
- package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +22 -0
- package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +29 -0
- package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +21 -0
- package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +21 -0
- package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +25 -0
- package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +19 -0
- package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +23 -0
- package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +24 -0
- package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +18 -0
- package/knowledge/decisions/index.md +38 -0
- package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +20 -0
- package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +19 -0
- package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +23 -0
- package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +23 -0
- package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +22 -0
- package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +28 -0
- package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +14 -0
- package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +30 -0
- package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +26 -0
- package/knowledge/index.md +14 -0
- package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +26 -0
- package/knowledge/questions/index.md +16 -0
- package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +23 -0
- package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +28 -0
- package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +22 -0
- package/package.json +3 -3
- package/dist/add/add-personas.d.ts +0 -13
|
@@ -204,6 +204,9 @@ export interface SerializableInspectorState {
|
|
|
204
204
|
credentialGrants?: string[];
|
|
205
205
|
globalSecrets?: string;
|
|
206
206
|
globalCredentials?: string;
|
|
207
|
+
contentBucket?: string;
|
|
208
|
+
contentGrants?: Record<string, string>;
|
|
209
|
+
globalContent?: string;
|
|
207
210
|
}
|
|
208
211
|
]>;
|
|
209
212
|
wireAddonFiles: string[];
|
|
@@ -266,8 +269,8 @@ export interface SerializableInspectorState {
|
|
|
266
269
|
definitions: InspectorState['featureFlags']['definitions'];
|
|
267
270
|
files: string[];
|
|
268
271
|
};
|
|
269
|
-
|
|
270
|
-
definitions: InspectorState['
|
|
272
|
+
virtualUsers: {
|
|
273
|
+
definitions: InspectorState['virtualUsers']['definitions'];
|
|
271
274
|
files: string[];
|
|
272
275
|
};
|
|
273
276
|
variables: {
|
|
@@ -317,6 +320,8 @@ export interface SerializableInspectorState {
|
|
|
317
320
|
openAPISpec: Record<string, any> | null;
|
|
318
321
|
diagnostics: InspectorDiagnostic[];
|
|
319
322
|
addonFunctions: InspectorState['addonFunctions'];
|
|
323
|
+
screensManifest?: InspectorState['screensManifest'];
|
|
324
|
+
addonScreens?: InspectorState['addonScreens'];
|
|
320
325
|
exportedContracts: InspectorState['exportedContracts'];
|
|
321
326
|
}
|
|
322
327
|
/**
|
|
@@ -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
|
-
|
|
144
|
-
definitions: state.
|
|
145
|
-
files: Array.from(state.
|
|
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,
|
|
@@ -182,6 +182,8 @@ export function serializeInspectorState(state) {
|
|
|
182
182
|
openAPISpec: state.openAPISpec,
|
|
183
183
|
diagnostics: state.diagnostics,
|
|
184
184
|
addonFunctions: state.addonFunctions,
|
|
185
|
+
screensManifest: state.screensManifest,
|
|
186
|
+
addonScreens: state.addonScreens,
|
|
185
187
|
exportedContracts: state.exportedContracts,
|
|
186
188
|
};
|
|
187
189
|
}
|
|
@@ -328,9 +330,9 @@ export function deserializeInspectorState(data) {
|
|
|
328
330
|
definitions: data.featureFlags?.definitions || [],
|
|
329
331
|
files: new Set(data.featureFlags?.files || []),
|
|
330
332
|
},
|
|
331
|
-
|
|
332
|
-
definitions: data.
|
|
333
|
-
files: new Set(data.
|
|
333
|
+
virtualUsers: {
|
|
334
|
+
definitions: data.virtualUsers?.definitions || [],
|
|
335
|
+
files: new Set(data.virtualUsers?.files || []),
|
|
334
336
|
},
|
|
335
337
|
variables: {
|
|
336
338
|
definitions: data.variables?.definitions || [],
|
|
@@ -378,6 +380,8 @@ export function deserializeInspectorState(data) {
|
|
|
378
380
|
openAPISpec: data.openAPISpec || null,
|
|
379
381
|
diagnostics: data.diagnostics || [],
|
|
380
382
|
addonFunctions: data.addonFunctions || {},
|
|
383
|
+
screensManifest: data.screensManifest,
|
|
384
|
+
addonScreens: data.addonScreens,
|
|
381
385
|
exportedContracts: data.exportedContracts || {
|
|
382
386
|
http: {},
|
|
383
387
|
cli: {},
|
|
@@ -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
|
-
* `
|
|
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
|
|
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-
|
|
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
|
-
* `
|
|
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
|
|
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-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
48
|
-
* `{
|
|
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
|
|
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
|
|
92
|
-
* `{
|
|
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
|
|
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 === '
|
|
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.
|
|
63
|
-
node.
|
|
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.
|
|
105
|
-
node.
|
|
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
|
|
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
|
-
|
|
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
|
|
114
|
-
|
|
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
|
|
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
|
-
/**
|
|
166
|
-
|
|
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
|
@@ -15,6 +15,7 @@ import { addChannel } from './add/add-channel.js';
|
|
|
15
15
|
import { addGateway } from './add/add-gateway.js';
|
|
16
16
|
import { addRPCInvocations } from './add/add-rpc-invocations.js';
|
|
17
17
|
import { addWireAddon } from './add/add-wire-addon.js';
|
|
18
|
+
import { addDefineScreens } from './add/add-define-screens.js';
|
|
18
19
|
import { addWireRemoteAddon } from './add/add-wire-remote-addon.js';
|
|
19
20
|
import { addMiddleware } from './add/add-middleware.js';
|
|
20
21
|
import { addPermission } from './add/add-permission.js';
|
|
@@ -25,7 +26,7 @@ import { addCredential } from './add/add-credential.js';
|
|
|
25
26
|
import { addScope } from './add/add-scope.js';
|
|
26
27
|
import { addSystemRole } from './add/add-system-role.js';
|
|
27
28
|
import { addFeatureFlag } from './add/add-feature-flag.js';
|
|
28
|
-
import {
|
|
29
|
+
import { addVirtualUsers } from './add/add-virtual-users.js';
|
|
29
30
|
import { addVariable } from './add/add-variable.js';
|
|
30
31
|
import { addWorkflowGraph } from './add/add-workflow-graph.js';
|
|
31
32
|
import { addFeature } from './add/add-feature.js';
|
|
@@ -47,6 +48,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
47
48
|
addOutgoingWebhook(logger, node, checker, state);
|
|
48
49
|
addRPCInvocations(node, state, logger);
|
|
49
50
|
addWireAddon(node, state, logger);
|
|
51
|
+
addDefineScreens(node, state, logger);
|
|
50
52
|
addWireRemoteAddon(node, state, logger);
|
|
51
53
|
addMiddleware(logger, node, checker, state, options);
|
|
52
54
|
addPermission(logger, node, checker, state, options);
|
|
@@ -79,7 +81,7 @@ export const visitRoutes = (logger, checker, node, state, options) => {
|
|
|
79
81
|
addScope(logger, node, checker, state, nextOptions);
|
|
80
82
|
addSystemRole(logger, node, checker, state, nextOptions);
|
|
81
83
|
addFeatureFlag(logger, node, checker, state, nextOptions);
|
|
82
|
-
|
|
84
|
+
addVirtualUsers(logger, node, checker, state, nextOptions);
|
|
83
85
|
addVariable(logger, node, checker, state, nextOptions);
|
|
84
86
|
addHTTPRoute(logger, node, checker, state, nextOptions);
|
|
85
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.
|
package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md
ADDED
|
@@ -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.
|