proof-harness 0.1.0-next.8 → 0.1.0-next.9
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/COMPATIBILITY.md +111 -4
- package/PROOF_SDK_CONTRACT.md +20 -0
- package/README.md +10 -4
- package/cli/config.mjs +2 -0
- package/cli/engines/proof_control_sensitivity.mjs +95 -0
- package/cli/engines/proof_input_scope.mjs +177 -0
- package/cli/engines/proof_mutation_check.mjs +132 -57
- package/cli/engines/scan_proof_capabilities.mjs +7 -3
- package/cli/proof-harness.mjs +3 -1
- package/dist/{action-types-C3OmHRyH.d.ts → action-types-Cw-dmSKj.d.ts} +11 -1
- package/dist/playwright.d.ts +13 -2
- package/dist/playwright.js +113 -5
- package/dist/playwright.js.map +1 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.js +21 -0
- package/dist/server.js.map +1 -1
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +8 -0
- package/dist/shared.js.map +1 -1
- package/package.json +1 -1
- package/proof-consumer-fixtures/action-invocation/auth-refusal.json +8 -0
- package/proof-consumer-fixtures/control-sensitivity/catalog.json +14 -0
package/COMPATIBILITY.md
CHANGED
|
@@ -11,7 +11,7 @@ Consumers must pin an exact prerelease version, for example:
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"proof-harness": "0.1.0-next.
|
|
14
|
+
"proof-harness": "0.1.0-next.8"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
```
|
|
@@ -27,11 +27,118 @@ versions rather than guessing how to interpret them.
|
|
|
27
27
|
|
|
28
28
|
## Prerelease migration notes
|
|
29
29
|
|
|
30
|
+
### 0.1.0-next.9 — anonymous actions and control sensitivity (release candidate)
|
|
31
|
+
|
|
32
|
+
Prepared for release; not yet published. After publication, pin exactly
|
|
33
|
+
`proof-harness@0.1.0-next.9` and regenerate capabilities and baseline traces.
|
|
34
|
+
Trace/mission/health versions remain unchanged. Authentication refusal and
|
|
35
|
+
control-sensitivity output contracts use protocol/schema version 1.
|
|
36
|
+
|
|
37
|
+
#### Anonymous self-service actions (#23)
|
|
38
|
+
|
|
39
|
+
`assert.authorization` accepts `actor: "anonymous"` for **action probes only**.
|
|
40
|
+
It sends the protected POST invoker request in a fresh API context, without
|
|
41
|
+
login cookies or Authorization, and disables redirect following. It does not
|
|
42
|
+
clear or reuse the authenticated page session. Use the Playwright project's
|
|
43
|
+
`use.baseURL`, an explicit `action.baseURL`, or an already navigated page URL.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await assert.authorization({
|
|
47
|
+
actor: "anonymous", page,
|
|
48
|
+
action: {
|
|
49
|
+
module: "user-management", name: "selfDeleteAccount", inputParams: {},
|
|
50
|
+
expectedAuthRedirect: "/sign-in",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The consumer must update its **guarded invoker**, after request validation and
|
|
56
|
+
registered-handler lookup. In the catch around actual handler execution:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { authenticationRedirectResponse } from "proof-harness/server";
|
|
60
|
+
// Existing proofGuard, body validation, registry lookup, and handler call stay.
|
|
61
|
+
const refusal = authenticationRedirectResponse(error, request, {
|
|
62
|
+
module: body.module, action: body.action, allowedRedirects: ["/sign-in"],
|
|
63
|
+
});
|
|
64
|
+
if (refusal) return refusal;
|
|
65
|
+
// Keep the existing failure response for all other exceptions.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Choose the exact authentication destination from the consumer's own auth
|
|
69
|
+
middleware. The adapter recognizes Next.js redirect errors only, requires no
|
|
70
|
+
Cookie/Authorization request headers, and matches an exact allowlisted relative
|
|
71
|
+
URL. It returns HTTP 401 with `{ protocolVersion: 1,
|
|
72
|
+
type: "proof_action_auth_refusal", module, action,
|
|
73
|
+
reason: "authentication_required", redirect }`. The client binds that envelope
|
|
74
|
+
to the invoked action and the assertion checks `expectedAuthRedirect`.
|
|
75
|
+
A generic 401/500, `action_threw`, invalid-form rejection, unexpected redirect,
|
|
76
|
+
malformed result, or missing registry entry is incomplete evidence. Anonymous
|
|
77
|
+
success is a failed authorization assertion. No seeded denied role is needed.
|
|
78
|
+
|
|
79
|
+
Use `assert.actionSucceeds` with a disposable authenticated actor for the
|
|
80
|
+
allowed-path control; account-deletion fixtures remain consumer-owned. The
|
|
81
|
+
adapter's destination list and placement are protected trust inputs: it must
|
|
82
|
+
only normalize authentication redirects from an executed registered action,
|
|
83
|
+
not invoker setup errors or unrelated redirects. This is not automatic proof
|
|
84
|
+
of an arbitrary application's authentication semantics.
|
|
85
|
+
|
|
86
|
+
Workspace scope discovery now follows local caller-input bindings and accesses,
|
|
87
|
+
including destructuring aliases and direct parse/safeParse results. It no
|
|
88
|
+
longer counts comments, SQL/output keys, or generated/result workspace IDs.
|
|
89
|
+
Unknown user-input dataflow used as workspace scope and dynamic input keys
|
|
90
|
+
remain unassessed. Internal `_BOT.ts` actions retain their existing exemption
|
|
91
|
+
from caller-boundary coverage. This is bounded local analysis, not intermodule
|
|
92
|
+
schema inference. In particular `createTeamWorkspace` no longer inherits a
|
|
93
|
+
cross-tenant requirement merely because it returns a new workspace ID. Declare
|
|
94
|
+
any additional product invariants and add their proofs in the consumer.
|
|
95
|
+
|
|
96
|
+
#### Separate positive-control sensitivity (#24)
|
|
97
|
+
|
|
98
|
+
Move the old `TENANT-deny-all` entry out of the primary mutation catalog into
|
|
99
|
+
a separate protected module named by `controlSensitivityCatalog` in
|
|
100
|
+
`proof.config.mjs`. Keep its existing subject, apply SQL and spec; add:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
claims: [],
|
|
104
|
+
controls: [{
|
|
105
|
+
kind: "tenant_isolation", target: "workspace_members", operation: "select",
|
|
106
|
+
emittedBy: "assert.tenantIsolation",
|
|
107
|
+
}],
|
|
108
|
+
expectedFailureCode: "tenant_isolation_control",
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Run `proof-harness controls --inventory`, then `proof-harness controls` (or
|
|
112
|
+
`--only TENANT-deny-all`). Both require fresh passing baseline evidence for
|
|
113
|
+
every mapped package-origin **control** in the selected spec. The initial
|
|
114
|
+
contract supports tenant-isolation SELECT controls emitted by
|
|
115
|
+
`assert.tenantIsolation` or `assert.authorization`, with the exact failure code
|
|
116
|
+
`tenant_isolation_control`. Other sensitivity categories remain unsupported.
|
|
117
|
+
|
|
118
|
+
The runner reuses mutation planting/readback, isolated execution, restoration,
|
|
119
|
+
and recovery checks. A successful test needs a non-timeout failed proof, a
|
|
120
|
+
failed mapped control assertion of the same helper origin, and a step error
|
|
121
|
+
beginning with that exact failure code. Setup errors, incomplete/skipped
|
|
122
|
+
assertions, another spec, primary-only failures, or a missing failed control do
|
|
123
|
+
not count. Unexpected owner-control database errors are setup failures rather
|
|
124
|
+
than control rejection; explicit permission denial or zero visible owner rows
|
|
125
|
+
can establish the intended anti-vacuity failure.
|
|
126
|
+
|
|
127
|
+
Results live separately in `artifacts.controlSensitivity` (default
|
|
128
|
+
`.proof/control-sensitivity`), with `mode: "control-sensitivity"`,
|
|
129
|
+
`controlRejected`/`controlsTurnedRed`, and `detected: false`. They remain planted
|
|
130
|
+
traces and cannot satisfy baseline claims or primary mutation coverage. Use
|
|
131
|
+
`proof-harness controls --recover` for an interrupted run against the same
|
|
132
|
+
local stack; the shared recovery journal blocks either runner until restored.
|
|
133
|
+
Primary `mutate` still rejects `claims: []` with no resolved primary baseline.
|
|
134
|
+
A failed control can no longer count as a failed primary claim of the same
|
|
135
|
+
kind/target/operation. Wire both commands into consumer CI if both properties
|
|
136
|
+
are required.
|
|
137
|
+
|
|
30
138
|
### 0.1.0-next.8
|
|
31
139
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`proof-harness@0.1.0-next.8` after the publish workflow succeeds.
|
|
140
|
+
Published release `0.1.0-next.8`. Consumers pin exactly
|
|
141
|
+
`proof-harness@0.1.0-next.8` when adopting these features.
|
|
35
142
|
|
|
36
143
|
#### Direct RLS clients and Auth administration (#18)
|
|
37
144
|
|
package/PROOF_SDK_CONTRACT.md
CHANGED
|
@@ -1246,3 +1246,23 @@ Such user-facing actions require denial and allowed-control evidence in
|
|
|
1246
1246
|
coverage even without a workspace input. Changes use the existing
|
|
1247
1247
|
`service_role_mutations_changed` drift facet. See `COMPATIBILITY.md` for
|
|
1248
1248
|
supported factory/binding shapes and required consumer regeneration.
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
## Anonymous invocations and control sensitivity (0.1.0-next.9 candidate)
|
|
1252
|
+
|
|
1253
|
+
The versioned anonymous refusal envelope is `ActionAuthenticationRefusal`
|
|
1254
|
+
(protocol 1), validated by `isActionAuthenticationRefusal` from the shared
|
|
1255
|
+
entry point. The server exports `authenticationRedirectResponse`; Playwright
|
|
1256
|
+
exports `actAsUser.invokeAnonymousAction` and accepts `actor: "anonymous"` in
|
|
1257
|
+
`assert.authorization` action probes. Only an action-bound, expected
|
|
1258
|
+
authentication redirect can establish a passing anonymous denial; generic
|
|
1259
|
+
exceptions are not evidence. See `COMPATIBILITY.md` for the required protected
|
|
1260
|
+
invoker integration and allowed authenticated control.
|
|
1261
|
+
|
|
1262
|
+
`proof-harness controls` consumes `controlSensitivityCatalog` independently
|
|
1263
|
+
from primary mutation inventory and writes `mode: "control-sensitivity"`
|
|
1264
|
+
results under `artifacts.controlSensitivity`. Its mapped failed controls never
|
|
1265
|
+
count as primary mutation detection. The initial contract is limited to
|
|
1266
|
+
tenant-isolation SELECT anti-vacuity checks with `tenant_isolation_control`;
|
|
1267
|
+
see `COMPATIBILITY.md` for exact selectors, fresh baseline requirements and
|
|
1268
|
+
recovery. Consumer claims and product fixtures remain consumer-owned.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Public prereleases are published under the npm `next` tag. Pin an exact
|
|
|
17
17
|
prerelease version while the API is stabilizing:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
pnpm add proof-harness@0.1.0-next.
|
|
20
|
+
pnpm add proof-harness@0.1.0-next.8
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
The package exposes environment-specific entry points:
|
|
@@ -61,7 +61,7 @@ code via `allowProofServiceHosts`; there is deliberately no environment
|
|
|
61
61
|
override.
|
|
62
62
|
|
|
63
63
|
The package also provides one `proof-harness` executable with `scan`, `parse`,
|
|
64
|
-
`registry`, `build`, `verify`, `coverage`, `inventory`, `drift`, and `
|
|
64
|
+
`registry`, `build`, `verify`, `coverage`, `inventory`, `drift`, `mutate`, and `controls`
|
|
65
65
|
subcommands. Commands read repository paths from
|
|
66
66
|
`proof.config.mjs`; without one they use a conventional application layout.
|
|
67
67
|
Product migration aggregation and explicit mutation definitions remain
|
|
@@ -101,9 +101,9 @@ See [required migration changes](COMPATIBILITY.md#010-next7--audit-hardening)
|
|
|
101
101
|
before upgrading a consumer. In particular, existing traces must be rerun and
|
|
102
102
|
unsupported SQL now blocks assessment.
|
|
103
103
|
|
|
104
|
-
Release
|
|
104
|
+
Release `0.1.0-next.8` adds direct RLS client and Auth-admin discovery,
|
|
105
105
|
a consumer-owned catalog provider for full SQL histories, and transactional
|
|
106
|
-
INSERT controls for append-only tables.
|
|
106
|
+
INSERT controls for append-only tables. Pin exactly
|
|
107
107
|
`proof-harness@0.1.0-next.8`. See [compatibility and setup](COMPATIBILITY.md#010-next8)
|
|
108
108
|
for the required consumer changes.
|
|
109
109
|
|
|
@@ -111,3 +111,9 @@ The harness establishes specific tested claims. Acceptance still requires
|
|
|
111
111
|
consumer-owned requirements and a trusted CI job that controls the harness,
|
|
112
112
|
policies, and evidence. A passing trace is not a general proof of code quality
|
|
113
113
|
or a defense against an executor that can rewrite the acceptance gate.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
Release candidate `0.1.0-next.9` adds isolated anonymous action probes and a
|
|
117
|
+
separate positive-control sensitivity runner. It also distinguishes caller
|
|
118
|
+
workspace inputs from generated result IDs. See [next.9 setup and migration](COMPATIBILITY.md)
|
|
119
|
+
before upgrading; consumer invoker and catalog changes are required.
|
package/cli/config.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
9
9
|
traces: ".proof/traces",
|
|
10
10
|
drift: ".proof/drift.json",
|
|
11
11
|
mutations: ".proof/mutations",
|
|
12
|
+
controlSensitivity: ".proof/control-sensitivity",
|
|
12
13
|
},
|
|
13
14
|
// The module-descriptor system (module.meta.ts → modules.json and its
|
|
14
15
|
// checker) moved to the consumer template in 0.1.0-next.6: it serves the
|
|
@@ -50,6 +51,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
50
51
|
insertControl: null,
|
|
51
52
|
driftSources: ["app", "package.json"],
|
|
52
53
|
mutationCatalog: null,
|
|
54
|
+
controlSensitivityCatalog: null,
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
function mergeConfig(config = {}) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const assertionHelpers = new Set([
|
|
2
|
+
"assert.tenantIsolation",
|
|
3
|
+
"assert.authorization",
|
|
4
|
+
"assert.actionSucceeds",
|
|
5
|
+
"assert.httpResponse",
|
|
6
|
+
]);
|
|
7
|
+
const helpers = new Set(["assert.tenantIsolation", "assert.authorization"]);
|
|
8
|
+
const primary = (a) =>
|
|
9
|
+
a.role === undefined ||
|
|
10
|
+
a.role === null ||
|
|
11
|
+
["primary", "negative"].includes(a.role);
|
|
12
|
+
const control = (a) => ["control", "positive_control"].includes(a.role);
|
|
13
|
+
const decisive = (a) => a.status !== "incomplete" && a.status !== "skipped";
|
|
14
|
+
const matches = (a, s) =>
|
|
15
|
+
a.kind === s.kind &&
|
|
16
|
+
a.target === s.target &&
|
|
17
|
+
a.operation === s.operation &&
|
|
18
|
+
(!s.emittedBy || a.emittedBy === s.emittedBy);
|
|
19
|
+
|
|
20
|
+
export function validateControlBaseline(mutation, traces) {
|
|
21
|
+
if (
|
|
22
|
+
!Array.isArray(mutation.claims) ||
|
|
23
|
+
mutation.claims.length ||
|
|
24
|
+
!Array.isArray(mutation.controls) ||
|
|
25
|
+
!mutation.controls.length ||
|
|
26
|
+
mutation.expectedFailureCode !== "tenant_isolation_control"
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[PROOF_FAIL] control_contract: ${mutation.id} requires claims: [], nonempty controls, and expectedFailureCode: tenant_isolation_control`,
|
|
30
|
+
);
|
|
31
|
+
for (const selector of mutation.controls) {
|
|
32
|
+
if (
|
|
33
|
+
selector.kind !== "tenant_isolation" ||
|
|
34
|
+
selector.operation !== "select" ||
|
|
35
|
+
typeof selector.target !== "string" ||
|
|
36
|
+
!selector.target ||
|
|
37
|
+
!helpers.has(selector.emittedBy)
|
|
38
|
+
)
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[PROOF_FAIL] control_contract: ${mutation.id} has an unsupported control selector`,
|
|
41
|
+
);
|
|
42
|
+
const exists = traces.some(
|
|
43
|
+
(t) =>
|
|
44
|
+
t.specFile === mutation.spec &&
|
|
45
|
+
t.passed === true &&
|
|
46
|
+
!t.mutation &&
|
|
47
|
+
t.steps.some(
|
|
48
|
+
(step) =>
|
|
49
|
+
step.passed === true &&
|
|
50
|
+
(step.assertions ?? []).some(
|
|
51
|
+
(a) =>
|
|
52
|
+
a.passed === true &&
|
|
53
|
+
decisive(a) &&
|
|
54
|
+
control(a) &&
|
|
55
|
+
matches(a, selector),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
if (!exists)
|
|
60
|
+
throw new Error(
|
|
61
|
+
`[PROOF_FAIL] control_baseline_missing: ${mutation.id} has no fresh passing package-origin control for ${selector.target}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return mutation.controls;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Control failures cannot satisfy primary claim detection, even for the same key. */
|
|
68
|
+
export function assertionSelectorsTurnedRed(
|
|
69
|
+
traces,
|
|
70
|
+
selectors,
|
|
71
|
+
role,
|
|
72
|
+
expectedFailureCode,
|
|
73
|
+
) {
|
|
74
|
+
if (!selectors.length) return false;
|
|
75
|
+
return selectors.every((selector) =>
|
|
76
|
+
traces.some(
|
|
77
|
+
(t) =>
|
|
78
|
+
t.passed === false &&
|
|
79
|
+
t.steps.some(
|
|
80
|
+
(step) =>
|
|
81
|
+
step.passed === false &&
|
|
82
|
+
(!expectedFailureCode ||
|
|
83
|
+
step.error?.startsWith(`[PROOF_FAIL] ${expectedFailureCode}:`)) &&
|
|
84
|
+
(step.assertions ?? []).some(
|
|
85
|
+
(a) =>
|
|
86
|
+
a.passed === false &&
|
|
87
|
+
decisive(a) &&
|
|
88
|
+
(role === "control" ? control(a) : primary(a)) &&
|
|
89
|
+
assertionHelpers.has(a.emittedBy) &&
|
|
90
|
+
matches(a, selector),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
const workspace = (name) => /^(workspaceId|workspace_id)$/.test(name);
|
|
3
|
+
function visit(node, fn) {
|
|
4
|
+
fn(node);
|
|
5
|
+
ts.forEachChild(node, (child) => visit(child, fn));
|
|
6
|
+
}
|
|
7
|
+
function unwrap(node) {
|
|
8
|
+
while (
|
|
9
|
+
node &&
|
|
10
|
+
(ts.isParenthesizedExpression(node) ||
|
|
11
|
+
ts.isAwaitExpression(node) ||
|
|
12
|
+
ts.isAsExpression(node))
|
|
13
|
+
)
|
|
14
|
+
node = node.expression;
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Trace caller input bindings, never result fields, comments or SQL keys. */
|
|
19
|
+
export function actionAcceptsWorkspaceInput(
|
|
20
|
+
source,
|
|
21
|
+
checker,
|
|
22
|
+
exportName,
|
|
23
|
+
problems,
|
|
24
|
+
) {
|
|
25
|
+
let owner;
|
|
26
|
+
visit(source, (node) => {
|
|
27
|
+
if (
|
|
28
|
+
(ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) &&
|
|
29
|
+
node.name?.getText(source) === exportName
|
|
30
|
+
)
|
|
31
|
+
owner = node;
|
|
32
|
+
});
|
|
33
|
+
if (owner && ts.isVariableDeclaration(owner))
|
|
34
|
+
owner = unwrap(owner.initializer);
|
|
35
|
+
if (
|
|
36
|
+
!owner ||
|
|
37
|
+
!(
|
|
38
|
+
ts.isFunctionDeclaration(owner) ||
|
|
39
|
+
ts.isArrowFunction(owner) ||
|
|
40
|
+
ts.isFunctionExpression(owner)
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return false;
|
|
44
|
+
const inputs = new Map();
|
|
45
|
+
const containers = new Set();
|
|
46
|
+
let found = false;
|
|
47
|
+
const symbol = (node) => checker.getSymbolAtLocation(node);
|
|
48
|
+
const typeHasWorkspace = (type, seen = new Set()) => {
|
|
49
|
+
if (!type || seen.has(type)) return false;
|
|
50
|
+
seen.add(type);
|
|
51
|
+
return checker
|
|
52
|
+
.getPropertiesOfType(type)
|
|
53
|
+
.some(
|
|
54
|
+
(p) =>
|
|
55
|
+
workspace(p.name) ||
|
|
56
|
+
(p.name !== "contextParams" &&
|
|
57
|
+
p.valueDeclaration &&
|
|
58
|
+
typeHasWorkspace(
|
|
59
|
+
checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration),
|
|
60
|
+
seen,
|
|
61
|
+
)),
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
function bind(name, state) {
|
|
65
|
+
if (ts.isIdentifier(name)) {
|
|
66
|
+
inputs.set(symbol(name), state);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (ts.isObjectBindingPattern(name))
|
|
70
|
+
for (const element of name.elements) {
|
|
71
|
+
const key = (element.propertyName ?? element.name)
|
|
72
|
+
.getText(source)
|
|
73
|
+
.replace(/["']/g, "");
|
|
74
|
+
if (workspace(key)) {
|
|
75
|
+
if (state === "known") found = true;
|
|
76
|
+
else
|
|
77
|
+
problems.push("workspace input passes through unresolved dataflow");
|
|
78
|
+
}
|
|
79
|
+
bind(element.name, state);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const parameter of owner.parameters) {
|
|
83
|
+
if (ts.isIdentifier(parameter.name)) {
|
|
84
|
+
if (parameter.name.text === "contextParams") continue;
|
|
85
|
+
if (workspace(parameter.name.text)) found = true;
|
|
86
|
+
const type = checker.getTypeAtLocation(parameter);
|
|
87
|
+
const nested = type.getProperty("inputParams");
|
|
88
|
+
if (nested) {
|
|
89
|
+
containers.add(symbol(parameter.name));
|
|
90
|
+
if (
|
|
91
|
+
typeHasWorkspace(checker.getTypeOfSymbolAtLocation(nested, parameter))
|
|
92
|
+
)
|
|
93
|
+
found = true;
|
|
94
|
+
} else {
|
|
95
|
+
bind(parameter.name, "known");
|
|
96
|
+
if (typeHasWorkspace(type)) found = true;
|
|
97
|
+
}
|
|
98
|
+
} else if (ts.isObjectBindingPattern(parameter.name)) {
|
|
99
|
+
for (const element of parameter.name.elements) {
|
|
100
|
+
const key = (element.propertyName ?? element.name).getText(source);
|
|
101
|
+
if (key === "contextParams") continue;
|
|
102
|
+
bind(element.name, "known");
|
|
103
|
+
if (
|
|
104
|
+
workspace(key) ||
|
|
105
|
+
typeHasWorkspace(checker.getTypeAtLocation(element))
|
|
106
|
+
)
|
|
107
|
+
found = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function state(node) {
|
|
112
|
+
node = unwrap(node);
|
|
113
|
+
if (!node) return undefined;
|
|
114
|
+
if (ts.isIdentifier(node)) return inputs.get(symbol(node));
|
|
115
|
+
if (
|
|
116
|
+
ts.isPropertyAccessExpression(node) ||
|
|
117
|
+
ts.isElementAccessExpression(node)
|
|
118
|
+
) {
|
|
119
|
+
const key = ts.isPropertyAccessExpression(node)
|
|
120
|
+
? node.name.text
|
|
121
|
+
: node.argumentExpression?.text;
|
|
122
|
+
if (
|
|
123
|
+
ts.isIdentifier(node.expression) &&
|
|
124
|
+
containers.has(symbol(node.expression))
|
|
125
|
+
)
|
|
126
|
+
return key === "inputParams" ? "known" : undefined;
|
|
127
|
+
return state(node.expression);
|
|
128
|
+
}
|
|
129
|
+
if (ts.isCallExpression(node)) {
|
|
130
|
+
const fromInput = node.arguments.map(state).find(Boolean);
|
|
131
|
+
if (fromInput)
|
|
132
|
+
return ts.isPropertyAccessExpression(node.expression) &&
|
|
133
|
+
["parse", "safeParse"].includes(node.expression.name.text)
|
|
134
|
+
? fromInput
|
|
135
|
+
: "unknown";
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const declarations = [];
|
|
140
|
+
visit(owner, (node) => {
|
|
141
|
+
if (ts.isVariableDeclaration(node)) declarations.push(node);
|
|
142
|
+
});
|
|
143
|
+
for (let pass = 0; pass <= declarations.length; pass++)
|
|
144
|
+
for (const node of declarations) {
|
|
145
|
+
const input = state(node.initializer);
|
|
146
|
+
if (input) {
|
|
147
|
+
bind(
|
|
148
|
+
node.name,
|
|
149
|
+
node.parent.flags & ts.NodeFlags.Const ? input : "unknown",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
visit(owner, (node) => {
|
|
154
|
+
if (
|
|
155
|
+
!(
|
|
156
|
+
ts.isPropertyAccessExpression(node) ||
|
|
157
|
+
ts.isElementAccessExpression(node)
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return;
|
|
161
|
+
const key = ts.isPropertyAccessExpression(node)
|
|
162
|
+
? node.name.text
|
|
163
|
+
: node.argumentExpression?.text;
|
|
164
|
+
const input = state(node.expression);
|
|
165
|
+
if (workspace(key) && input) {
|
|
166
|
+
if (input === "known") found = true;
|
|
167
|
+
else problems.push("workspace input passes through unresolved dataflow");
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
ts.isElementAccessExpression(node) &&
|
|
171
|
+
input &&
|
|
172
|
+
!ts.isStringLiteral(node.argumentExpression)
|
|
173
|
+
)
|
|
174
|
+
problems.push("dynamic caller input key cannot be assessed");
|
|
175
|
+
});
|
|
176
|
+
return found;
|
|
177
|
+
}
|