@cosmicdrift/kumiko-guards 0.315.0 → 0.317.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.317.0",
|
|
4
4
|
"description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"kumiko-pre-push": "./src/pre-push.sh"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@cosmicdrift/kumiko-repo-manifest": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-repo-manifest": "0.317.0",
|
|
32
32
|
"ts-morph": "^28.0.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.317.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "no-framed-extension-sections now resolves the actual mount kind before flagging",
|
|
6
|
+
"detail": "extensionSectionComponents backs three different mount shapes: a\nkind: \"extension\" section (framed by the host's own Card/Section), a\ndashboard kind: \"custom\" panel, and an entityEdit slots.header slot. Only\nthe section mount is actually framed, but the guard previously flagged\nevery registered component that rendered its own Card/SectionCard/\nCollapsibleSection regardless of which of the three ways it was mounted,\nproducing false positives for components that only ever render as a bare\ndashboard panel or header slot.\nThe guard now scans every __component: SOME_CONST usage site across the\nscanned files (extending its scan to plain .ts files, not just .tsx, so\nscreens.ts/feature.ts usage sites are actually seen), classifies each by\nits enclosing mount kind, and correlates it back to the registration via\nthe shared registry key both sides reference by name. A component is only\nexempted when every usage site resolves to a custom panel and/or a header\nslot; a component used as a section anywhere, or whose usage sites cannot\nbe classified at all, keeps today's conservative \"stays flagged\"\nbehavior. List-header-slot mounts and footer/titleAction slots also keep\nthe conservative behavior since this fix only covers the two shapes that\nproduced real false positives.\nThe scan now also covers plain .ts files on both the registration and the\nusage side, not just .tsx, so a registration declared inside a\nscreens.ts/feature.ts file is checked for the first time. Checked against\npublicstatus and solon, the two consumers currently registering\nextensionSectionComponents from a .ts file: the fix does not add findings\nthere, since their one Card-rendering registration is mounted exclusively\nas a kind: \"custom\" panel, which this guard now correctly exempts."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.314.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -24,11 +24,17 @@
|
|
|
24
24
|
// A module specifier that does not resolve to a project source file (a real
|
|
25
25
|
// external package import, e.g. `@cosmicdrift/kumiko-bundled-features/...`)
|
|
26
26
|
// is treated as "package component, already checked inside the package
|
|
27
|
-
// itself" and is not flagged here
|
|
28
|
-
// via `frameworkWithin` when this guard runs against the framework repo.
|
|
27
|
+
// itself" and is not flagged here, since the package's own source is in
|
|
28
|
+
// scope via `frameworkWithin` when this guard runs against the framework repo.
|
|
29
|
+
//
|
|
30
|
+
// Mount-kind scoping: the same registry also backs `kind: "custom"`
|
|
31
|
+
// dashboard panels and `slots: { header: {...} }` entityEdit slots, both of
|
|
32
|
+
// which render bare. The guard classifies every `__component` usage site by
|
|
33
|
+
// mount kind and only exempts a component used exclusively as one of those
|
|
34
|
+
// two; no usage site found at all, or any other mount kind, stays flagged.
|
|
29
35
|
|
|
30
36
|
import * as path from "node:path";
|
|
31
|
-
import { Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
37
|
+
import { Node, type ObjectLiteralExpression, type SourceFile, SyntaxKind } from "ts-morph";
|
|
32
38
|
import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
|
|
33
39
|
import { hasIgnoreTag } from "./_lib/ignore-tag";
|
|
34
40
|
|
|
@@ -36,7 +42,7 @@ const ROOT = process.cwd();
|
|
|
36
42
|
|
|
37
43
|
const SCAN: ScanSpec = {
|
|
38
44
|
scope: "source",
|
|
39
|
-
extensions: ["tsx"],
|
|
45
|
+
extensions: ["ts", "tsx"],
|
|
40
46
|
frameworkWithin: ["packages/bundled-features/src/**", "samples/**"],
|
|
41
47
|
};
|
|
42
48
|
const EXCLUDE = /(__tests__|\.test\.tsx?$|\.integration\.tsx?$|\.d\.ts$)/;
|
|
@@ -53,8 +59,9 @@ function isExternalSourceFile(sf: SourceFile): boolean {
|
|
|
53
59
|
return sf.getFilePath().includes("/node_modules/");
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
/** Maps each registered component's identifier to the registry key usage sites reference via `__component: SOME_CONST`. */
|
|
63
|
+
function findExtensionComponentEntries(sf: SourceFile): Map<string, string> {
|
|
64
|
+
const entries = new Map<string, string>();
|
|
58
65
|
for (const prop of sf.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
59
66
|
if (prop.getName() !== "extensionSectionComponents") continue;
|
|
60
67
|
const init = prop.getInitializer();
|
|
@@ -63,16 +70,23 @@ function findExtensionComponentNames(sf: SourceFile): Set<string> {
|
|
|
63
70
|
// { NOTES_SECTION_EXTENSION_NAME: NotesSection } (PropertyAssignment,
|
|
64
71
|
// possibly computed-key) or { NotesSection } (ShorthandPropertyAssignment).
|
|
65
72
|
if (member.getKind() === SyntaxKind.PropertyAssignment) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
const pa = member.asKindOrThrow(SyntaxKind.PropertyAssignment);
|
|
74
|
+
const valueInit = pa.getInitializer();
|
|
75
|
+
if (valueInit === undefined || valueInit.getKind() !== SyntaxKind.Identifier) continue;
|
|
76
|
+
const nameNode = pa.getNameNode();
|
|
77
|
+
const key = Node.isComputedPropertyName(nameNode)
|
|
78
|
+
? nameNode.getExpression().getText()
|
|
79
|
+
: Node.isStringLiteral(nameNode)
|
|
80
|
+
? nameNode.getLiteralValue()
|
|
81
|
+
: pa.getName();
|
|
82
|
+
entries.set(valueInit.getText(), key);
|
|
70
83
|
} else if (member.getKind() === SyntaxKind.ShorthandPropertyAssignment) {
|
|
71
|
-
|
|
84
|
+
const name = member.asKindOrThrow(SyntaxKind.ShorthandPropertyAssignment).getName();
|
|
85
|
+
entries.set(name, name);
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
return
|
|
89
|
+
return entries;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
function findLocalDeclaration(sf: SourceFile, name: string): Node[] {
|
|
@@ -203,18 +217,87 @@ function collectFramedViolations(
|
|
|
203
217
|
return violations;
|
|
204
218
|
}
|
|
205
219
|
|
|
220
|
+
type MountKind = "extension" | "custom" | "header" | "other";
|
|
221
|
+
|
|
222
|
+
function findAncestorObjectLiterals(node: Node): ObjectLiteralExpression[] {
|
|
223
|
+
return node.getAncestors().filter(Node.isObjectLiteralExpression);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function stringPropertyValue(obj: ObjectLiteralExpression, propName: string): string | undefined {
|
|
227
|
+
const prop = obj.getProperty(propName);
|
|
228
|
+
if (prop === undefined || !Node.isPropertyAssignment(prop)) return undefined;
|
|
229
|
+
const init = prop.getInitializer();
|
|
230
|
+
return init !== undefined && Node.isStringLiteral(init) ? init.getLiteralValue() : undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDirectlyUnderSlotsHeader(obj: ObjectLiteralExpression): boolean {
|
|
234
|
+
const headerAssignment = obj.getParentIfKind(SyntaxKind.PropertyAssignment);
|
|
235
|
+
if (headerAssignment === undefined || headerAssignment.getName() !== "header") return false;
|
|
236
|
+
const slotsObject = headerAssignment.getFirstAncestorByKind(SyntaxKind.ObjectLiteralExpression);
|
|
237
|
+
if (slotsObject === undefined) return false;
|
|
238
|
+
const slotsAssignment = slotsObject.getParentIfKind(SyntaxKind.PropertyAssignment);
|
|
239
|
+
return slotsAssignment !== undefined && slotsAssignment.getName() === "slots";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mountKindFor(componentPropertyAssignment: Node): MountKind {
|
|
243
|
+
for (const obj of findAncestorObjectLiterals(componentPropertyAssignment)) {
|
|
244
|
+
const kind = stringPropertyValue(obj, "kind");
|
|
245
|
+
if (kind === "extension") return "extension";
|
|
246
|
+
if (kind === "custom") return "custom";
|
|
247
|
+
if (isDirectlyUnderSlotsHeader(obj)) return "header";
|
|
248
|
+
}
|
|
249
|
+
return "other";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function findComponentNameUsages(sf: SourceFile): { name: string; kind: MountKind }[] {
|
|
253
|
+
const usages: { name: string; kind: MountKind }[] = [];
|
|
254
|
+
for (const prop of sf.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
255
|
+
if (prop.getName() !== "__component") continue;
|
|
256
|
+
const init = prop.getInitializer();
|
|
257
|
+
if (init === undefined) continue;
|
|
258
|
+
const name = Node.isStringLiteral(init)
|
|
259
|
+
? init.getLiteralValue()
|
|
260
|
+
: Node.isIdentifier(init)
|
|
261
|
+
? init.getText()
|
|
262
|
+
: undefined;
|
|
263
|
+
if (name === undefined) continue;
|
|
264
|
+
usages.push({ name, kind: mountKindFor(prop) });
|
|
265
|
+
}
|
|
266
|
+
return usages;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildUsageKindsByName(files: readonly SourceFile[]): Map<string, Set<MountKind>> {
|
|
270
|
+
const byName = new Map<string, Set<MountKind>>();
|
|
271
|
+
for (const sf of files) {
|
|
272
|
+
for (const { name, kind } of findComponentNameUsages(sf)) {
|
|
273
|
+
const kinds = byName.get(name) ?? new Set<MountKind>();
|
|
274
|
+
kinds.add(kind);
|
|
275
|
+
byName.set(name, kinds);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return byName;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Exempt only when every known usage is a custom panel and/or a header slot. */
|
|
282
|
+
function stillNeedsFraming(kinds: Set<MountKind> | undefined): boolean {
|
|
283
|
+
if (kinds === undefined) return true;
|
|
284
|
+
return [...kinds].some((kind) => kind !== "custom" && kind !== "header");
|
|
285
|
+
}
|
|
286
|
+
|
|
206
287
|
function analyse(files: readonly SourceFile[]): {
|
|
207
288
|
violations: GuardViolation[];
|
|
208
289
|
} {
|
|
209
290
|
const violations: GuardViolation[] = [];
|
|
291
|
+
const usageKindsByName = buildUsageKindsByName(files);
|
|
210
292
|
for (const sf of files) {
|
|
211
293
|
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
for (const name of
|
|
294
|
+
const registeredEntries = findExtensionComponentEntries(sf);
|
|
295
|
+
if (registeredEntries.size === 0) continue;
|
|
296
|
+
for (const [name, registryKey] of registeredEntries.entries()) {
|
|
215
297
|
const visited = new Set<string>();
|
|
216
298
|
const resolved = resolveComponent(sf, name, visited, 0);
|
|
217
299
|
if (resolved === undefined || resolved === "external") continue;
|
|
300
|
+
if (!stillNeedsFraming(usageKindsByName.get(registryKey))) continue;
|
|
218
301
|
violations.push(...collectFramedViolations(name, resolved, [name], visited, 0));
|
|
219
302
|
}
|
|
220
303
|
}
|