@cosmicdrift/kumiko-guards 0.387.0 → 0.389.0
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 +3 -2
- package/package.json +3 -3
- package/src/_lib/agent-docs.ts +12 -7
- package/src/_lib/guards-doc.ts +4 -1
- package/src/_lib/roots.ts +9 -5
- package/src/_lib/run-slots.ts +39 -10
- package/src/_lib/security-audit.ts +15 -8
- package/src/changes.json +50 -0
- package/src/check-complexity.ts +86 -27
- package/src/check-licenses.ts +23 -6
- package/src/check-security.ts +2 -1
- package/src/dependency-license-exceptions.json +0 -24
- package/src/guard-app-feature-structure.ts +9 -7
- package/src/guard-comment-issue-ref-only.ts +4 -2
- package/src/guard-comment-lang.ts +6 -6
- package/src/guard-direct-entity-writes.ts +43 -31
- package/src/guard-file-size.ts +2 -2
- package/src/guard-import-extensions.ts +34 -9
- package/src/guard-no-date-api.ts +2 -2
- package/src/guard-no-direct-fs.ts +7 -1
- package/src/guard-public-exports-changelog.ts +8 -8
- package/src/guard-renderer-boundaries.ts +61 -9
- package/src/guard-restricted-symbols.ts +2 -0
- package/src/guard-test-es-fixtures.ts +83 -0
- package/src/guard-test-timeouts.ts +11 -3
- package/src/guard-thin-wrappers.ts +35 -0
- package/src/pre-push.sh +37 -9
- package/src/repo-manifest/app-ports/static-app-config.ts +15 -11
- package/src/run-guards.ts +11 -4
- package/src/runtime-isolation-classify.ts +2 -1
|
@@ -335,16 +335,51 @@ function extractMarkerReason(node: FnNode): string | null {
|
|
|
335
335
|
return null;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// A typed lookup table (`const T: Record<K, Fn> = {…}` or `{…} satisfies …`) fixes one
|
|
339
|
+
// signature per slot; a one-call entry adapts that signature, so inlining it would mean
|
|
340
|
+
// dropping the table, not the wrapper.
|
|
341
|
+
function isTypedTableLiteral(literal: Node): boolean {
|
|
342
|
+
let parent = literal.getParent();
|
|
343
|
+
while (parent?.isKind(SyntaxKind.AsExpression)) parent = parent.getParent();
|
|
344
|
+
if (parent?.isKind(SyntaxKind.SatisfiesExpression)) return true;
|
|
345
|
+
return parent?.isKind(SyntaxKind.VariableDeclaration) === true && !!parent.getTypeNode();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function collectTypedTableValueNames(sf: SourceFile): Set<string> {
|
|
349
|
+
const names = new Set<string>();
|
|
350
|
+
for (const literal of sf.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
351
|
+
if (!isTypedTableLiteral(literal)) continue;
|
|
352
|
+
for (const property of literal.getProperties()) {
|
|
353
|
+
if (property.isKind(SyntaxKind.ShorthandPropertyAssignment)) names.add(property.getName());
|
|
354
|
+
if (!property.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
355
|
+
const value = property.getInitializer();
|
|
356
|
+
if (value?.isKind(SyntaxKind.Identifier)) names.add(value.getText());
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return names;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isInlineTypedTableValue(node: FnNode): boolean {
|
|
363
|
+
const property = node.getParent();
|
|
364
|
+
const literal = property?.isKind(SyntaxKind.PropertyAssignment)
|
|
365
|
+
? property.getParent()
|
|
366
|
+
: undefined;
|
|
367
|
+
return literal !== undefined && isTypedTableLiteral(literal);
|
|
368
|
+
}
|
|
369
|
+
|
|
338
370
|
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
339
371
|
|
|
340
372
|
export function collectFindings(sf: SourceFile): Finding[] {
|
|
341
373
|
const file = path.relative(ROOT, sf.getFilePath());
|
|
342
374
|
const findings: Finding[] = [];
|
|
375
|
+
const typedTableValueNames = collectTypedTableValueNames(sf);
|
|
343
376
|
|
|
344
377
|
const check = (node: FnNode) => {
|
|
345
378
|
const fnName = getFnName(node);
|
|
346
379
|
// skip: anonymous functions cannot be reported by name
|
|
347
380
|
if (!fnName) return;
|
|
381
|
+
// skip: a typed table slot, see isTypedTableLiteral
|
|
382
|
+
if (typedTableValueNames.has(fnName) || isInlineTypedTableValue(node)) return;
|
|
348
383
|
|
|
349
384
|
const callee = getSingleDirectCallee(node);
|
|
350
385
|
// skip: not a single-callee wrapper
|
package/src/pre-push.sh
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
# sub-repo's own package.json `test` script — refuses the push if that
|
|
19
19
|
# script is missing.
|
|
20
20
|
#
|
|
21
|
-
# Before the checks below, the hook runs a gitleaks scan of origin/main
|
|
22
|
-
# when the binary is installed.
|
|
21
|
+
# Before the checks below, the hook runs a gitleaks scan of origin/main..<pushed sha>
|
|
22
|
+
# for every pushed ref when the binary is installed.
|
|
23
23
|
#
|
|
24
24
|
# Extension point: a tracked, executable scripts/pre-push-extra.sh runs
|
|
25
25
|
# before the main check in every branch above, with KUMIKO_PUSH_REPO_ROOT
|
|
@@ -123,11 +123,27 @@ if ! command -v gitleaks >/dev/null 2>&1; then
|
|
|
123
123
|
elif ! git -C "$REPO_ROOT" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
|
|
124
124
|
echo "[pre-push] origin/main not found — secret scan skipped" >&2
|
|
125
125
|
else
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
# git passes "<local ref> <local sha> <remote ref> <remote sha>" per pushed ref on stdin;
|
|
127
|
+
# scan those tips (not HEAD), skip deletes. Without stdin (direct call) fall back to HEAD.
|
|
128
|
+
SCAN_TIPS=""
|
|
129
|
+
PUSH_LINES=0
|
|
130
|
+
if [ ! -t 0 ]; then
|
|
131
|
+
while read -r _local_ref LOCAL_SHA _remote_ref _remote_sha; do
|
|
132
|
+
[ -n "$LOCAL_SHA" ] || continue
|
|
133
|
+
PUSH_LINES=$((PUSH_LINES + 1))
|
|
134
|
+
case "$LOCAL_SHA" in *[!0]*) SCAN_TIPS="$SCAN_TIPS $LOCAL_SHA" ;; esac
|
|
135
|
+
done
|
|
136
|
+
fi
|
|
137
|
+
if [ "$PUSH_LINES" -eq 0 ]; then
|
|
138
|
+
SCAN_TIPS=" HEAD"
|
|
130
139
|
fi
|
|
140
|
+
for SCAN_TIP in $SCAN_TIPS; do
|
|
141
|
+
echo "[pre-push] gitleaks git origin/main..$SCAN_TIP"
|
|
142
|
+
if ! (cd "$REPO_ROOT" && gitleaks git --no-banner --redact --log-opts="origin/main..$SCAN_TIP" .); then
|
|
143
|
+
echo "[pre-push] gitleaks found secrets — refusing push" >&2
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
done
|
|
131
147
|
fi
|
|
132
148
|
|
|
133
149
|
# infra#559/#569: from a worktree, `--git-common-dir` resolves to the main
|
|
@@ -135,13 +151,25 @@ fi
|
|
|
135
151
|
# canonical sibling path instead of this worktree — checking the wrong
|
|
136
152
|
# code. Run the worktree-local check only when scripts/check-wt.sh is
|
|
137
153
|
# tracked and executable (untracked/symlink stubs must not hijack push).
|
|
138
|
-
#
|
|
139
|
-
|
|
154
|
+
# Re-entry from the wrapping check-wt.sh is detected by process ancestry: the
|
|
155
|
+
# hook records its own PID in KUMIKO_CHECK_WT_OWNER before delegating, so a
|
|
156
|
+
# freely exported variable cannot skip the tracked check-wt.sh.
|
|
157
|
+
is_reentry_from_check_wt() {
|
|
158
|
+
reentry_pid=$$
|
|
159
|
+
case "${KUMIKO_CHECK_WT_OWNER:-}" in '' | *[!0-9]*) return 1 ;; esac
|
|
160
|
+
while [ -n "$reentry_pid" ] && [ "$reentry_pid" -gt 1 ]; do
|
|
161
|
+
[ "$reentry_pid" = "$KUMIKO_CHECK_WT_OWNER" ] && return 0
|
|
162
|
+
reentry_pid=$(ps -o ppid= -p "$reentry_pid" 2>/dev/null | tr -d ' ')
|
|
163
|
+
done
|
|
164
|
+
return 1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ! is_reentry_from_check_wt \
|
|
140
168
|
&& [ "$(git rev-parse --path-format=absolute --git-dir)" != "$GIT_COMMON_DIR" ] \
|
|
141
169
|
&& git -C "$REPO_ROOT" ls-files --error-unmatch scripts/check-wt.sh >/dev/null 2>&1 \
|
|
142
170
|
&& [ -x "$REPO_ROOT/scripts/check-wt.sh" ]; then
|
|
143
171
|
echo "[pre-push] worktree detected — worktree check (see scripts/check-wt.sh)…"
|
|
144
|
-
cd "$REPO_ROOT" && exec "$REPO_ROOT/scripts/check-wt.sh"
|
|
172
|
+
cd "$REPO_ROOT" && KUMIKO_CHECK_WT_OWNER=$$ exec "$REPO_ROOT/scripts/check-wt.sh"
|
|
145
173
|
fi
|
|
146
174
|
|
|
147
175
|
# Runs a guard bin from the worktree's or the parent's node_modules/.bin
|
|
@@ -39,20 +39,24 @@ function propertyName(node: Node): string | undefined {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function objectProperty(object: ObjectLiteralExpression, name: string): Node | undefined {
|
|
42
|
+
let found: Node | undefined;
|
|
43
|
+
let opaqueAfterFound = false;
|
|
42
44
|
for (const property of object.getProperties()) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
if (propertyName(property) === name && property.isKind(SyntaxKind.PropertyAssignment)) {
|
|
52
|
-
return property.getInitializerOrThrow();
|
|
45
|
+
if (property.isKind(SyntaxKind.SpreadAssignment)) {
|
|
46
|
+
opaqueAfterFound = true;
|
|
47
|
+
} else if (property.isKind(SyntaxKind.ShorthandPropertyAssignment)) {
|
|
48
|
+
if (property.getName() === name) opaqueAfterFound = true;
|
|
49
|
+
} else if (propertyName(property) === name && property.isKind(SyntaxKind.PropertyAssignment)) {
|
|
50
|
+
found = property.getInitializerOrThrow();
|
|
51
|
+
opaqueAfterFound = false;
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
if (opaqueAfterFound) {
|
|
55
|
+
throw new StaticConfigError(
|
|
56
|
+
`${name}: spread and shorthand properties cannot be read statically`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function numericLiteral(node: Node | undefined, path: string): number {
|
package/src/run-guards.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { guard as restrictedSymbols } from "./guard-restricted-symbols";
|
|
|
47
47
|
import { guard as screenConventions } from "./guard-screen-conventions";
|
|
48
48
|
import { guard as silentSkip } from "./guard-silent-skip";
|
|
49
49
|
import { guard as tenantEscalation } from "./guard-tenant-escalation";
|
|
50
|
+
import { guard as testEsFixtures } from "./guard-test-es-fixtures";
|
|
50
51
|
import { guard as testTemplateDrift } from "./guard-test-template-drift";
|
|
51
52
|
import { guard as testTimeouts } from "./guard-test-timeouts";
|
|
52
53
|
import { guard as unsafeJsonParse } from "./guard-unsafe-json-parse";
|
|
@@ -81,6 +82,7 @@ export const GUARDS = [
|
|
|
81
82
|
appFeatureStructure,
|
|
82
83
|
libTestCoverage,
|
|
83
84
|
testTimeouts,
|
|
85
|
+
testEsFixtures,
|
|
84
86
|
testTemplateDrift,
|
|
85
87
|
fileSize,
|
|
86
88
|
commentIssueRefOnly,
|
|
@@ -97,6 +99,8 @@ const GUARD_NAME_PREFIX = "--guard=";
|
|
|
97
99
|
|
|
98
100
|
export type RunGuardsCliDeps = {
|
|
99
101
|
readonly writeBaselineRoots?: readonly RepoRoot[];
|
|
102
|
+
readonly acquireSlot?: () => () => void;
|
|
103
|
+
readonly buildProject?: typeof buildSharedProject;
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
// Shared by the direct `bun run-guards.ts` invocation below and by the
|
|
@@ -115,7 +119,7 @@ export function runGuardsCli(argv: readonly string[], deps: RunGuardsCliDeps = {
|
|
|
115
119
|
return 1;
|
|
116
120
|
}
|
|
117
121
|
if (flags.includes("--explain")) {
|
|
118
|
-
for (const line of explainGuards(GUARDS, buildSharedProject(GUARDS))) {
|
|
122
|
+
for (const line of explainGuards(GUARDS, (deps.buildProject ?? buildSharedProject)(GUARDS))) {
|
|
119
123
|
console.log(line);
|
|
120
124
|
}
|
|
121
125
|
return 0;
|
|
@@ -155,12 +159,15 @@ export function runGuardsCli(argv: readonly string[], deps: RunGuardsCliDeps = {
|
|
|
155
159
|
);
|
|
156
160
|
return 0;
|
|
157
161
|
}
|
|
158
|
-
|
|
162
|
+
return runAllGuards(flags.includes("--strict-security-baseline"), deps);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function runAllGuards(strictSecurityBaseline: boolean, deps: RunGuardsCliDeps): number {
|
|
159
166
|
const guards = strictSecurityBaseline ? GUARDS.filter(isSecurityGuard) : GUARDS;
|
|
160
167
|
// Taken before the ts-morph project is built: that is where the memory goes.
|
|
161
|
-
const releaseSlot = acquireGuardRunSlot();
|
|
168
|
+
const releaseSlot = (deps.acquireSlot ?? acquireGuardRunSlot)();
|
|
162
169
|
try {
|
|
163
|
-
const project = buildSharedProject(guards);
|
|
170
|
+
const project = (deps.buildProject ?? buildSharedProject)(guards);
|
|
164
171
|
printGuardKitBanner(guards.length, project);
|
|
165
172
|
return reportResults(runGuards(guards, project, { strictSecurityBaseline }));
|
|
166
173
|
} finally {
|
|
@@ -49,7 +49,6 @@ const PATH_RULES: readonly (readonly [RegExp, Runtime])[] = [
|
|
|
49
49
|
// sample and import kumiko-testing/e2e; without this rule they read as
|
|
50
50
|
// runtime code importing the test tier. Deliberately not any nested e2e/.
|
|
51
51
|
[/^(?:e2e|packages\/[^/]+\/e2e|samples\/(?:.+\/)?e2e)\//, "test"],
|
|
52
|
-
[/(?:^|\/)dev\//, "dev"],
|
|
53
52
|
[/(?:^|\/)scripts\//, "tooling"],
|
|
54
53
|
[/(?:^|\/)bin\//, "tooling"],
|
|
55
54
|
[/\/drizzle\/[^/]+\.ts$/, "tooling"],
|
|
@@ -75,6 +74,8 @@ const PATH_RULES: readonly (readonly [RegExp, Runtime])[] = [
|
|
|
75
74
|
// some other package.
|
|
76
75
|
[/(?:^|\/)web\//, "client"],
|
|
77
76
|
[/\/web\.tsx?$/, "client"],
|
|
77
|
+
// After the web/ carve-out: client code under web/dev/ must stay client.
|
|
78
|
+
[/(?:^|\/)dev\//, "dev"],
|
|
78
79
|
[/^packages\/framework\/src\/(?:time|utils|engine\/types|errors)\//, "client"],
|
|
79
80
|
|
|
80
81
|
// More single-file carve-outs, same "package marker is coarser than the
|