candor-ts 0.40.0 → 0.40.2
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/README.md +14 -0
- package/package.json +6 -3
- package/policy.mjs +8 -0
- package/scan.mjs +1356 -108
package/README.md
CHANGED
|
@@ -185,6 +185,20 @@ functions analyzed): its DI-style fs injection means many functions read `Unknow
|
|
|
185
185
|
that's the contract working, not noise. The report says "can reach", never "does"; an absent
|
|
186
186
|
literal is never a claim of absence.
|
|
187
187
|
|
|
188
|
+
**Framework dependency injection (spec §2's bounded-CHA clause).** rimraf's injection is a function-valued
|
|
189
|
+
parameter, which reads `Unknown`. A DI *container* is measured separately, on NestJS 10 with an executed fixture:
|
|
190
|
+
|
|
191
|
+
- a provider entry `{ provide: Store, useClass | useValue | useFactory | useExisting: … }` whose token is a project
|
|
192
|
+
CLASS is read as a conversion: the substitute joins `Store`'s candidates, so `this.store.save()` is charged the
|
|
193
|
+
union of `Store.save` and the substitute's `save` (SOUNDNESS R1061 — silent before, all three forms executed);
|
|
194
|
+
- a parameter typed by an INTERFACE (`@Inject("SINK") sink: Sink`) dispatches over the project's visible
|
|
195
|
+
implementors of `Sink`, so a `useClass` that `implements Sink` is charged; a `useValue` object literal that
|
|
196
|
+
implements nothing reads `Unknown` (`dispatch:`).
|
|
197
|
+
|
|
198
|
+
**Named miss:** a provider list the scan cannot see (built at runtime, or imported from an unscanned package), a
|
|
199
|
+
token that is a variable rather than the class itself, and a substitute supplied by a dependency are not
|
|
200
|
+
substitutions to this engine. Gate a DI-heavy service with `deny <E> Unknown` as well as `deny <E>`.
|
|
201
|
+
|
|
188
202
|
## Cross-engine consistency — machine-checked
|
|
189
203
|
|
|
190
204
|
candor-ts is one of the **four code engines** (with the reference engine candor-java, the Rust
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.2",
|
|
4
4
|
"mcpName": "io.github.tombaldwin/candor",
|
|
5
5
|
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.40)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@types/node": "
|
|
9
|
-
"typescript": "
|
|
8
|
+
"@types/node": "25.9.2",
|
|
9
|
+
"typescript": "6.0.3"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"candor-ts": "./scan.mjs",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"lint": "eslint *.mjs",
|
|
24
|
+
"prepack": "node -e \"require('fs').copyFileSync('package-lock.json','npm-shrinkwrap.json')\"",
|
|
25
|
+
"postpack": "node -e \"require('fs').rmSync('npm-shrinkwrap.json',{force:true})\"",
|
|
24
26
|
"test": "npm run lint && node --test test-unit.mjs && node test.mjs --parallel && node test-mcp.mjs && node test-lsp.mjs && node test-watch.mjs && npm run test:probe && npm run test:fuzz",
|
|
25
27
|
"test:unit": "node --test test-unit.mjs",
|
|
26
28
|
"test:probe": "node fabrication_probe.mjs",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
"node": ">=20"
|
|
46
48
|
},
|
|
47
49
|
"files": [
|
|
50
|
+
"npm-shrinkwrap.json",
|
|
48
51
|
"Cases.ts",
|
|
49
52
|
"scan.mjs",
|
|
50
53
|
"query.mjs",
|
package/policy.mjs
CHANGED
|
@@ -1296,11 +1296,19 @@ export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map()
|
|
|
1296
1296
|
// a rule whose destinations all resolve while its `from` names nothing has bound nothing, and that is
|
|
1297
1297
|
// exactly the typo that leaves an operator believing a leaf is protected.
|
|
1298
1298
|
for (const r of pol.only ?? []) zeroCount.set(r.raw, 0);
|
|
1299
|
+
// ⟨SOUNDNESS R952⟩ …and a SCOPED `allow`. It was never enrolled, so `allow Net in nosuch.fn ok.example` — a
|
|
1300
|
+
// typo'd scope — bound nothing, said nothing, and exited 0: a gate that cannot fail, exactly the `deny` typo
|
|
1301
|
+
// this pass exists for, one rule kind over. A scoped allow makes a promise about the functions its scope names;
|
|
1302
|
+
// naming none is the same unanswerable rule. A SCOPELESS allow applies to every function and stays exempt, as a
|
|
1303
|
+
// scopeless `deny` does. Disclosure only: the line and `zeroMatch` entry `deny` already gets, exit unchanged
|
|
1304
|
+
// (candor-rust's half, same shape).
|
|
1305
|
+
for (const r of pol.allow ?? []) if (r.scope) zeroCount.set(r.raw, 0);
|
|
1299
1306
|
if (zeroCount.size) {
|
|
1300
1307
|
const names = new Set(functions.map((f) => f.fn));
|
|
1301
1308
|
for (const k of Object.keys(callgraph ?? {})) names.add(k);
|
|
1302
1309
|
for (const n of names) {
|
|
1303
1310
|
for (const r of pol.deny) if (r.scope && scopeMatches(n, r.scope)) zeroCount.set(r.raw, zeroCount.get(r.raw) + 1);
|
|
1311
|
+
for (const r of pol.allow ?? []) if (r.scope && scopeMatches(n, r.scope)) zeroCount.set(r.raw, zeroCount.get(r.raw) + 1);
|
|
1304
1312
|
for (const r of pol.forbid) {
|
|
1305
1313
|
if (scopeMatches(n, r.from) || scopeMatches(n, r.to)) zeroCount.set(r.raw, zeroCount.get(r.raw) + 1);
|
|
1306
1314
|
}
|