@pikku/inspector 0.12.50 → 0.12.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +743 -0
- package/OPTIMIZATION-PLAN.md +9 -3
- package/dist/add/add-auth.d.ts +1 -1
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +7 -0
- package/dist/add/add-credential.js +1 -1
- package/dist/add/add-functions.js +199 -8
- package/dist/add/add-http-route.js +16 -6
- package/dist/add/add-middleware.js +46 -3
- package/dist/add/add-personas.d.ts +13 -0
- package/dist/add/add-personas.js +216 -0
- package/dist/add/add-scope.js +1 -1
- package/dist/add/add-secret.js +1 -1
- package/dist/add/add-system-role.d.ts +2 -0
- package/dist/add/add-system-role.js +102 -0
- package/dist/add/add-variable.js +1 -1
- package/dist/add/add-wire-addon.js +31 -0
- package/dist/add/add-workflow.js +12 -1
- package/dist/error-codes.d.ts +17 -1
- package/dist/error-codes.js +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +33 -1
- package/dist/types.d.ts +55 -2
- package/dist/utils/annotate-http-route-auth.d.ts +22 -0
- package/dist/utils/annotate-http-route-auth.js +48 -0
- package/dist/utils/check-secret-sinks.d.ts +31 -0
- package/dist/utils/check-secret-sinks.js +117 -0
- package/dist/utils/extract-function-name.js +13 -4
- package/dist/utils/extract-function-node.js +18 -1
- package/dist/utils/extract-secret-usage.d.ts +9 -0
- package/dist/utils/extract-secret-usage.js +61 -0
- package/dist/utils/literal-properties.d.ts +22 -0
- package/dist/utils/literal-properties.js +69 -0
- package/dist/utils/load-addon-functions-meta.js +26 -1
- package/dist/utils/permissions.js +8 -0
- package/dist/utils/post-process.d.ts +68 -3
- package/dist/utils/post-process.js +246 -13
- package/dist/utils/secret-alias-services.d.ts +8 -0
- package/dist/utils/secret-alias-services.js +45 -0
- package/dist/utils/secret-brokers.d.ts +2 -0
- package/dist/utils/secret-brokers.js +9 -0
- package/dist/utils/serialize-inspector-state.d.ts +10 -0
- package/dist/utils/serialize-inspector-state.js +18 -0
- package/dist/utils/serialize-middleware-groups-meta.js +3 -0
- package/dist/utils/single-declaration.d.ts +32 -0
- package/dist/utils/single-declaration.js +43 -0
- package/dist/utils/validate-exposed-functions-gated.d.ts +14 -0
- package/dist/utils/validate-exposed-functions-gated.js +93 -0
- package/dist/utils/validate-tags-resolve-to-middleware.d.ts +20 -0
- package/dist/utils/validate-tags-resolve-to-middleware.js +41 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +6 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +48 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +9 -4
- package/dist/utils/workflow/dsl/patterns.js +26 -4
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/src/add/add-auth.test.ts +5 -3
- package/src/add/add-auth.ts +1 -1
- package/src/add/add-cli.ts +8 -0
- package/src/add/add-credential.ts +1 -1
- package/src/add/add-functions.test.ts +263 -1
- package/src/add/add-functions.ts +273 -11
- package/src/add/add-http-route.ts +20 -7
- package/src/add/add-middleware.test.ts +196 -0
- package/src/add/add-middleware.ts +64 -4
- package/src/add/add-personas.test.ts +382 -0
- package/src/add/add-personas.ts +285 -0
- package/src/add/add-scenario-step.test.ts +87 -5
- package/src/add/add-scope.test.ts +103 -30
- package/src/add/add-scope.ts +1 -1
- package/src/add/add-secret.ts +1 -1
- package/src/add/add-subject-scenario-steps.test.ts +161 -0
- package/src/add/add-system-role.test.ts +297 -0
- package/src/add/add-system-role.ts +164 -0
- package/src/add/add-variable.ts +1 -1
- package/src/add/add-wire-addon.test.ts +111 -0
- package/src/add/add-wire-addon.ts +29 -0
- package/src/add/add-workflow.ts +21 -1
- package/src/add/addon-bans.test.ts +4 -4
- package/src/add/allow-escape-hatches.test.ts +144 -0
- package/src/add/audit-meta.test.ts +75 -0
- package/src/add/config-value-metadata.test.ts +11 -11
- package/src/add/secret-sink-check.test.ts +211 -0
- package/src/add/wire-name-literal.test.ts +2 -2
- package/src/error-codes.ts +26 -0
- package/src/index.ts +4 -1
- package/src/inspector.ts +39 -0
- package/src/types.ts +56 -2
- package/src/utils/annotate-http-route-auth.test.ts +163 -0
- package/src/utils/annotate-http-route-auth.ts +52 -0
- package/src/utils/check-secret-sinks.ts +145 -0
- package/src/utils/extract-function-name.ts +14 -4
- package/src/utils/extract-function-node.ts +18 -6
- package/src/utils/extract-secret-usage.test.ts +81 -0
- package/src/utils/extract-secret-usage.ts +73 -0
- package/src/utils/literal-properties.ts +97 -0
- package/src/utils/load-addon-functions-meta.ts +32 -2
- package/src/utils/permissions-shorthand.test.ts +79 -0
- package/src/utils/permissions.ts +7 -0
- package/src/utils/post-process.test.ts +133 -0
- package/src/utils/post-process.ts +313 -12
- package/src/utils/secret-alias-services.test.ts +146 -0
- package/src/utils/secret-alias-services.ts +58 -0
- package/src/utils/secret-brokers.test.ts +53 -0
- package/src/utils/secret-brokers.ts +11 -0
- package/src/utils/serialize-inspector-state.ts +28 -0
- package/src/utils/serialize-middleware-groups-meta.ts +3 -0
- package/src/utils/single-declaration.ts +57 -0
- package/src/utils/validate-exposed-functions-gated.test.ts +230 -0
- package/src/utils/validate-exposed-functions-gated.ts +103 -0
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +105 -0
- package/src/utils/validate-tags-resolve-to-middleware.ts +48 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +57 -0
- package/src/utils/workflow/dsl/patterns.ts +33 -4
- package/src/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A function reachable through the generated `POST /rpc/:rpcName` dispatcher
|
|
4
|
+
* that nothing will stop.
|
|
5
|
+
*
|
|
6
|
+
* `rpc.exposed` refuses anything without `expose: true`, so the population is
|
|
7
|
+
* small and deliberate — someone typed it. What makes one dangerous is the
|
|
8
|
+
* combination: exposed, sessionless, and carrying no gate of its own or from
|
|
9
|
+
* the addon it belongs to. The dispatcher cannot help, because it does not know
|
|
10
|
+
* what it is dispatching to.
|
|
11
|
+
*
|
|
12
|
+
* knowledge: decisions/security/console-addon-privileged-functions-gate-themselves.md
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateExposedFunctionsGated(logger: InspectorLogger, state: InspectorState): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ErrorCode } from '../error-codes.js';
|
|
2
|
+
/**
|
|
3
|
+
* A function reachable through the generated `POST /rpc/:rpcName` dispatcher
|
|
4
|
+
* that nothing will stop.
|
|
5
|
+
*
|
|
6
|
+
* `rpc.exposed` refuses anything without `expose: true`, so the population is
|
|
7
|
+
* small and deliberate — someone typed it. What makes one dangerous is the
|
|
8
|
+
* combination: exposed, sessionless, and carrying no gate of its own or from
|
|
9
|
+
* the addon it belongs to. The dispatcher cannot help, because it does not know
|
|
10
|
+
* what it is dispatching to.
|
|
11
|
+
*
|
|
12
|
+
* knowledge: decisions/security/console-addon-privileged-functions-gate-themselves.md
|
|
13
|
+
*/
|
|
14
|
+
export function validateExposedFunctionsGated(logger, state) {
|
|
15
|
+
const ungated = [];
|
|
16
|
+
for (const [funcName, meta] of Object.entries(state.functions.meta)) {
|
|
17
|
+
if (!meta.expose)
|
|
18
|
+
continue;
|
|
19
|
+
// A pikkuFunc always requires a session, on every path.
|
|
20
|
+
if (meta.sessionless === false)
|
|
21
|
+
continue;
|
|
22
|
+
// Scenario steps are refused by rpcExposed regardless of `expose`.
|
|
23
|
+
if (meta.scenarioStep)
|
|
24
|
+
continue;
|
|
25
|
+
// The author declared that the body authorizes its own callers — a gate
|
|
26
|
+
// this check has no way to see. Taking the claim at face value is the
|
|
27
|
+
// point: without it the warning fires forever on functions that are fine,
|
|
28
|
+
// and a warning that is usually wrong stops being read.
|
|
29
|
+
if (meta.permissionsInBody === true)
|
|
30
|
+
continue;
|
|
31
|
+
// The function gates itself.
|
|
32
|
+
if (meta.auth === true)
|
|
33
|
+
continue;
|
|
34
|
+
if (meta.scopes && meta.scopes.length > 0)
|
|
35
|
+
continue;
|
|
36
|
+
if (meta.permissions && meta.permissions.length > 0)
|
|
37
|
+
continue;
|
|
38
|
+
if (isGatedByItsAddon(state, meta.packageName))
|
|
39
|
+
continue;
|
|
40
|
+
ungated.push(funcName);
|
|
41
|
+
}
|
|
42
|
+
if (ungated.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
const shown = ungated.slice(0, 10);
|
|
45
|
+
const rest = ungated.length - shown.length;
|
|
46
|
+
const one = ungated.length === 1;
|
|
47
|
+
// Every function here is sessionless — that is the population, not the
|
|
48
|
+
// finding, so the message names it as given rather than reporting it as a
|
|
49
|
+
// defect. The remedies are the ones that can actually apply to an anonymous
|
|
50
|
+
// caller: `scopes` cannot, because `verifyScopes` fails closed on a session
|
|
51
|
+
// that does not exist, and telling someone to add them sends them to a
|
|
52
|
+
// MissingScopeError on every public request.
|
|
53
|
+
logger.diagnostic({
|
|
54
|
+
severity: 'warn',
|
|
55
|
+
code: ErrorCode.EXPOSED_FUNCTION_HAS_NO_GATE,
|
|
56
|
+
message: `${ungated.length} exposed sessionless function${one ? '' : 's'} ` +
|
|
57
|
+
`carr${one ? 'ies' : 'y'} no permission and no addon gate, so ` +
|
|
58
|
+
`${one ? 'it is' : 'they are'} reachable by anyone through ` +
|
|
59
|
+
`POST /rpc/:rpcName: ${shown.join(', ')}` +
|
|
60
|
+
(rest > 0 ? `, and ${rest} more` : '') +
|
|
61
|
+
`. Add permissions to the function, set \`auth: true\` if a session is ` +
|
|
62
|
+
`required after all, gate the whole addon with ` +
|
|
63
|
+
`wireAddon({ auth: true }), or drop \`expose: true\` if ` +
|
|
64
|
+
`${one ? 'it was' : 'they were'} not meant to be callable from outside.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether every function in this package is already gated by its `wireAddon`.
|
|
69
|
+
* Those gates are applied by `runPikkuFunc` on every wiring path, so a function
|
|
70
|
+
* with no gate of its own is still covered by them.
|
|
71
|
+
*
|
|
72
|
+
* An addon whose `scopes`/`auth` were not statically knowable reads as
|
|
73
|
+
* unknown, and unknown is treated as gated — a false negative here costs a
|
|
74
|
+
* missing warning, a false positive costs trust in every warning.
|
|
75
|
+
*
|
|
76
|
+
* knowledge: decisions/security/addon-scopes-are-resolved-where-the-function-runs.md
|
|
77
|
+
*/
|
|
78
|
+
function isGatedByItsAddon(state, packageName) {
|
|
79
|
+
if (!packageName)
|
|
80
|
+
return false;
|
|
81
|
+
const declarations = state.rpc?.wireAddonDeclarations;
|
|
82
|
+
if (!declarations)
|
|
83
|
+
return false;
|
|
84
|
+
for (const declaration of declarations.values()) {
|
|
85
|
+
if (declaration.package !== packageName)
|
|
86
|
+
continue;
|
|
87
|
+
if (declaration.auth === true)
|
|
88
|
+
return true;
|
|
89
|
+
if (declaration.scopes && declaration.scopes.length > 0)
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* An addon tag naming middleware nobody registered.
|
|
4
|
+
*
|
|
5
|
+
* `wireAddon({ tags: ['admin'] })` reads like a gate and is applied like one —
|
|
6
|
+
* right up until no `addTagMiddleware('admin', …)` exists, at which point it
|
|
7
|
+
* resolves to an empty list and every request sails through. Nothing at runtime
|
|
8
|
+
* distinguishes "this tag runs three middlewares" from "this tag does nothing".
|
|
9
|
+
*
|
|
10
|
+
* Only **addon** tags are reported. A tag on a function is just as likely to be
|
|
11
|
+
* organizational — grouping, documentation, OpenAPI — and warning about every
|
|
12
|
+
* one of those would bury the case that matters. A tag on `wireAddon` is
|
|
13
|
+
* different: it is the documented way to apply a gate across a whole addon, so
|
|
14
|
+
* an inert one is never decorative. An exposed function relying on a tag it
|
|
15
|
+
* has no other gate behind is already reported by
|
|
16
|
+
* `validateExposedFunctionsGated`, which does not count tags as a gate.
|
|
17
|
+
*
|
|
18
|
+
* knowledge: decisions/security/addon-auth-and-tags-only-tighten.md
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateTagsResolveToMiddleware(logger: InspectorLogger, state: InspectorState): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ErrorCode } from '../error-codes.js';
|
|
2
|
+
/**
|
|
3
|
+
* An addon tag naming middleware nobody registered.
|
|
4
|
+
*
|
|
5
|
+
* `wireAddon({ tags: ['admin'] })` reads like a gate and is applied like one —
|
|
6
|
+
* right up until no `addTagMiddleware('admin', …)` exists, at which point it
|
|
7
|
+
* resolves to an empty list and every request sails through. Nothing at runtime
|
|
8
|
+
* distinguishes "this tag runs three middlewares" from "this tag does nothing".
|
|
9
|
+
*
|
|
10
|
+
* Only **addon** tags are reported. A tag on a function is just as likely to be
|
|
11
|
+
* organizational — grouping, documentation, OpenAPI — and warning about every
|
|
12
|
+
* one of those would bury the case that matters. A tag on `wireAddon` is
|
|
13
|
+
* different: it is the documented way to apply a gate across a whole addon, so
|
|
14
|
+
* an inert one is never decorative. An exposed function relying on a tag it
|
|
15
|
+
* has no other gate behind is already reported by
|
|
16
|
+
* `validateExposedFunctionsGated`, which does not count tags as a gate.
|
|
17
|
+
*
|
|
18
|
+
* knowledge: decisions/security/addon-auth-and-tags-only-tighten.md
|
|
19
|
+
*/
|
|
20
|
+
export function validateTagsResolveToMiddleware(logger, state) {
|
|
21
|
+
const registered = new Set([
|
|
22
|
+
...state.middleware.tagMiddleware.keys(),
|
|
23
|
+
...state.channelMiddleware.tagMiddleware.keys(),
|
|
24
|
+
]);
|
|
25
|
+
for (const [namespace, declaration] of state.rpc?.wireAddonDeclarations ??
|
|
26
|
+
[]) {
|
|
27
|
+
for (const tag of declaration.tags ?? []) {
|
|
28
|
+
if (registered.has(tag))
|
|
29
|
+
continue;
|
|
30
|
+
logger.diagnostic({
|
|
31
|
+
severity: 'warn',
|
|
32
|
+
code: ErrorCode.TAG_RESOLVES_TO_NO_MIDDLEWARE,
|
|
33
|
+
message: `wireAddon('${namespace}') declares tag '${tag}', but no ` +
|
|
34
|
+
`addTagMiddleware('${tag}', …) registers anything for it. The tag ` +
|
|
35
|
+
`resolves to an empty middleware list, so it applies no gate to the ` +
|
|
36
|
+
`addon at all. Register middleware for it, or drop the tag if it is ` +
|
|
37
|
+
`only meant to group and document.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -8,6 +8,12 @@ export interface ExtractionResult {
|
|
|
8
8
|
steps?: WorkflowStepMeta[];
|
|
9
9
|
/** Workflow context (top-level variables) */
|
|
10
10
|
context?: WorkflowContext;
|
|
11
|
+
/**
|
|
12
|
+
* The flow asserts something through an expectation helper rather than a
|
|
13
|
+
* `then` step. Recorded separately because those helpers are inline steps and
|
|
14
|
+
* leave no phase behind, so PKU680 has no other way to see them.
|
|
15
|
+
*/
|
|
16
|
+
asserts?: boolean;
|
|
11
17
|
reason?: string;
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, 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, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, 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
|
+
/**
|
|
6
|
+
* A function the walk must not step into, because nothing here says it runs.
|
|
7
|
+
*
|
|
8
|
+
* A callback handed straight to a call — a fanout's `map`, an inline
|
|
9
|
+
* `workflow.do` step — executes as part of the body and counts. A function that
|
|
10
|
+
* is merely *declared* or assigned to a name does not: an assertion parked in an
|
|
11
|
+
* unused helper asserts nothing, and counting it would let a scenario silence
|
|
12
|
+
* PKU680 with dead code.
|
|
13
|
+
*/
|
|
14
|
+
function isUncalledFunction(node) {
|
|
15
|
+
if (!ts.isFunctionDeclaration(node) &&
|
|
16
|
+
!ts.isFunctionExpression(node) &&
|
|
17
|
+
!ts.isArrowFunction(node)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (ts.isFunctionDeclaration(node))
|
|
21
|
+
return true;
|
|
22
|
+
const parent = node.parent;
|
|
23
|
+
return !(parent &&
|
|
24
|
+
ts.isCallExpression(parent) &&
|
|
25
|
+
parent.arguments.some((argument) => argument === node));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether the body calls one of the expectation helpers anywhere it runs.
|
|
29
|
+
*
|
|
30
|
+
* A whole-body walk rather than a check inside step extraction: the helpers
|
|
31
|
+
* produce no step of their own, so there is no extraction result to hang the
|
|
32
|
+
* answer off, and one nested inside a branch asserts just as much as one at the
|
|
33
|
+
* top level.
|
|
34
|
+
*/
|
|
35
|
+
function hasScenarioExpectation(body) {
|
|
36
|
+
let found = false;
|
|
37
|
+
const visit = (node) => {
|
|
38
|
+
if (found)
|
|
39
|
+
return;
|
|
40
|
+
if (ts.isCallExpression(node) && isScenarioExpectationCall(node)) {
|
|
41
|
+
found = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (node !== body && isUncalledFunction(node))
|
|
45
|
+
return;
|
|
46
|
+
ts.forEachChild(node, visit);
|
|
47
|
+
};
|
|
48
|
+
visit(body);
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
5
51
|
/**
|
|
6
52
|
* Extract DSL workflow metadata from a function declaration
|
|
7
53
|
*/
|
|
@@ -74,6 +120,7 @@ export function extractDSLWorkflow(funcNode, checker, options) {
|
|
|
74
120
|
status: 'ok',
|
|
75
121
|
steps,
|
|
76
122
|
context: Object.keys(workflowContext).length > 0 ? workflowContext : undefined,
|
|
123
|
+
asserts: hasScenarioExpectation(arrowFunc.body) || undefined,
|
|
77
124
|
};
|
|
78
125
|
}
|
|
79
126
|
catch (error) {
|
|
@@ -20,12 +20,12 @@ export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.Ty
|
|
|
20
20
|
* hit repeatedly on workflow graph round-trips.
|
|
21
21
|
*/
|
|
22
22
|
export declare const DYNAMIC_SCENARIO_STEP_TARGET = "<dynamic>";
|
|
23
|
-
declare const SCENARIO_STEP_PHASES: readonly ["
|
|
23
|
+
declare const SCENARIO_STEP_PHASES: readonly ["given", "when", "then"];
|
|
24
24
|
export type ScenarioStepPhaseName = (typeof SCENARIO_STEP_PHASES)[number];
|
|
25
25
|
/**
|
|
26
|
-
* Check if a call expression is scenario.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Check if a call expression is scenario.given()/when()/then() — a declared
|
|
27
|
+
* `pikkuScenarioStep` invoked by name. `given`/`when` are pure sugar for each
|
|
28
|
+
* other; `then` additionally makes the step's bindings witnesses.
|
|
29
29
|
*/
|
|
30
30
|
export declare function isScenarioStepCall(node: ts.CallExpression): boolean;
|
|
31
31
|
/**
|
|
@@ -33,6 +33,11 @@ export declare function isScenarioStepCall(node: ts.CallExpression): boolean;
|
|
|
33
33
|
* isn't one.
|
|
34
34
|
*/
|
|
35
35
|
export declare function getScenarioStepPhase(node: ts.CallExpression): ScenarioStepPhaseName | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Check if a call expression is one of the scenario expectation helpers —
|
|
38
|
+
* `scenario.expectService()`, `.expectError()` or `.expectEventually()`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isScenarioExpectationCall(node: ts.CallExpression): boolean;
|
|
36
41
|
/**
|
|
37
42
|
* Check if a call expression is workflow.expectEventually()
|
|
38
43
|
*/
|
|
@@ -31,11 +31,11 @@ export function isWorkflowDoCall(node, checker) {
|
|
|
31
31
|
* hit repeatedly on workflow graph round-trips.
|
|
32
32
|
*/
|
|
33
33
|
export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>';
|
|
34
|
-
const SCENARIO_STEP_PHASES = ['
|
|
34
|
+
const SCENARIO_STEP_PHASES = ['given', 'when', 'then'];
|
|
35
35
|
/**
|
|
36
|
-
* Check if a call expression is scenario.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Check if a call expression is scenario.given()/when()/then() — a declared
|
|
37
|
+
* `pikkuScenarioStep` invoked by name. `given`/`when` are pure sugar for each
|
|
38
|
+
* other; `then` additionally makes the step's bindings witnesses.
|
|
39
39
|
*/
|
|
40
40
|
export function isScenarioStepCall(node) {
|
|
41
41
|
return getScenarioStepPhase(node) !== undefined;
|
|
@@ -54,6 +54,28 @@ export function getScenarioStepPhase(node) {
|
|
|
54
54
|
}
|
|
55
55
|
return SCENARIO_STEP_PHASES.find((phase) => phase === propAccess.name.text);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The expectation helpers that assert without being a `then`.
|
|
59
|
+
*
|
|
60
|
+
* `expectService` and `expectError` are inline steps rather than scenario
|
|
61
|
+
* steps, so they carry no phase and never reach the step meta — but each one is
|
|
62
|
+
* a witness: a recorded service call, or a refusal. PKU680 has to see them, or
|
|
63
|
+
* a scenario whose only assertion is one of these reads as asserting nothing.
|
|
64
|
+
*/
|
|
65
|
+
const SCENARIO_EXPECTATION_CALLS = [
|
|
66
|
+
'expectService',
|
|
67
|
+
'expectError',
|
|
68
|
+
'expectEventually',
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Check if a call expression is one of the scenario expectation helpers —
|
|
72
|
+
* `scenario.expectService()`, `.expectError()` or `.expectEventually()`.
|
|
73
|
+
*/
|
|
74
|
+
export function isScenarioExpectationCall(node) {
|
|
75
|
+
return (ts.isPropertyAccessExpression(node.expression) &&
|
|
76
|
+
SCENARIO_EXPECTATION_CALLS.some((name) => name === node.expression.name.text) &&
|
|
77
|
+
isWorkflowWireIdentifier(node.expression.expression));
|
|
78
|
+
}
|
|
57
79
|
/**
|
|
58
80
|
* Check if a call expression is workflow.expectEventually()
|
|
59
81
|
*/
|
package/dist/visit.js
CHANGED
|
@@ -23,6 +23,8 @@ import { addAuth } from './add/add-auth.js';
|
|
|
23
23
|
import { addSecret } from './add/add-secret.js';
|
|
24
24
|
import { addCredential } from './add/add-credential.js';
|
|
25
25
|
import { addScope } from './add/add-scope.js';
|
|
26
|
+
import { addSystemRole } from './add/add-system-role.js';
|
|
27
|
+
import { addPersonas } from './add/add-personas.js';
|
|
26
28
|
import { addVariable } from './add/add-variable.js';
|
|
27
29
|
import { addWorkflowGraph } from './add/add-workflow-graph.js';
|
|
28
30
|
import { addFeature } from './add/add-feature.js';
|
|
@@ -69,6 +71,8 @@ export const visitRoutes = (logger, checker, node, state, options) => {
|
|
|
69
71
|
addSecret(logger, node, checker, state, nextOptions);
|
|
70
72
|
addCredential(logger, node, checker, state, nextOptions);
|
|
71
73
|
addScope(logger, node, checker, state, nextOptions);
|
|
74
|
+
addSystemRole(logger, node, checker, state, nextOptions);
|
|
75
|
+
addPersonas(logger, node, checker, state, nextOptions);
|
|
72
76
|
addVariable(logger, node, checker, state, nextOptions);
|
|
73
77
|
addHTTPRoute(logger, node, checker, state, nextOptions);
|
|
74
78
|
addHTTPRoutes(logger, node, checker, state, nextOptions);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.54",
|
|
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.
|
|
48
|
+
"@pikku/core": "^0.12.76"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@pikku/core": "^0.12.
|
|
51
|
+
"@pikku/core": "^0.12.76",
|
|
52
52
|
"@types/node": "^24.11.0"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
package/src/add/add-auth.test.ts
CHANGED
|
@@ -13,8 +13,10 @@ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
|
13
13
|
info: () => {},
|
|
14
14
|
warn: () => {},
|
|
15
15
|
error: () => {},
|
|
16
|
-
diagnostic: ({ code, message }) => {
|
|
17
|
-
|
|
16
|
+
diagnostic: ({ severity, code, message }) => {
|
|
17
|
+
if (severity !== 'warn') {
|
|
18
|
+
criticals.push({ code, message })
|
|
19
|
+
}
|
|
18
20
|
},
|
|
19
21
|
critical: (code: ErrorCode, message: string) => {
|
|
20
22
|
criticals.push({ code, message })
|
|
@@ -111,7 +113,7 @@ describe('addAuth inspector', () => {
|
|
|
111
113
|
'export const auth = pikkuBetterAuth(() =>',
|
|
112
114
|
' betterAuth({',
|
|
113
115
|
' session: { cookieCache: { enabled: true } },',
|
|
114
|
-
|
|
116
|
+
' emailAndPassword: { enabled: true },',
|
|
115
117
|
' })',
|
|
116
118
|
')',
|
|
117
119
|
].join('\n')
|
package/src/add/add-auth.ts
CHANGED
|
@@ -139,7 +139,7 @@ const isInsideGlobalMiddlewareRegistration = (node: ts.Node): boolean => {
|
|
|
139
139
|
*
|
|
140
140
|
* The pikku CLI discovers that single export and generates a catch-all
|
|
141
141
|
* `auth.gen.ts` that wires `${basePath}/**` to one shared handler, registers the
|
|
142
|
-
* better-auth session middleware, and emits a `
|
|
142
|
+
* better-auth session middleware, and emits a `defineSecret` for every configured
|
|
143
143
|
* social provider — so the auth routes and secret requirements flow through
|
|
144
144
|
* normal inspection into the deploy manifest.
|
|
145
145
|
*
|
package/src/add/add-cli.ts
CHANGED
|
@@ -153,6 +153,14 @@ function processCLIConfig(
|
|
|
153
153
|
// Already handled in first pass
|
|
154
154
|
break
|
|
155
155
|
|
|
156
|
+
case 'auth': {
|
|
157
|
+
const value = getPropertyValue(node, 'auth')
|
|
158
|
+
if (typeof value === 'boolean') {
|
|
159
|
+
programMeta.auth = value
|
|
160
|
+
}
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
case 'commands':
|
|
157
165
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
158
166
|
programMeta.commands = processCommands(
|
|
@@ -24,7 +24,7 @@ export const addCredential: AddWiring = (
|
|
|
24
24
|
const firstArg = args[0]
|
|
25
25
|
const expression = node.expression
|
|
26
26
|
|
|
27
|
-
if (!ts.isIdentifier(expression) || expression.text !== '
|
|
27
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineCredential') {
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
30
|
|