@cosmicdrift/kumiko-guards 0.336.0 → 0.337.1
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 +2 -2
- package/src/changes.json +6 -0
- package/src/check-runtime-isolation.ts +8 -2
- package/src/guard-i18n-keys.ts +6 -7
- package/src/guard-loadall-events.ts +2 -2
- package/src/guard-no-framed-extension-sections.ts +1 -0
- package/src/guard-renderer-boundaries.ts +5 -5
- package/src/guard-upgrade-state.ts +12 -1
- package/src/runtime-isolation-classify.ts +15 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.337.1",
|
|
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.337.1",
|
|
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.337.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "i18n-keys guard decodes escaped literals, framed-sections guard skips test usages, renderer-boundaries guard blocks bare fetch and reports all JSX tags per line, pre-push shim no longer execs binaries from shared ancestor dirs",
|
|
6
|
+
"detail": "The i18n-keys guard reads string literals with `getLiteralText()` so escaped keys match their definitions. The no-framed-extension-sections guard ignores test files when collecting component usages. The renderer-boundaries guard now also rejects a bare `fetch(` call in `packages/renderer/src`, and reports every JSX tag on a line. The `.husky/pre-push` shim only resolves `kumiko-pre-push` from the repo's own `node_modules` or the parent-workspace root, never from arbitrary ancestor directories."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.336.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -187,8 +187,14 @@ export const check: RepoCheck = {
|
|
|
187
187
|
|
|
188
188
|
const counts = countByKey(rootAbsPath, allViolations);
|
|
189
189
|
const resolveLine = (key: string): number => {
|
|
190
|
-
const
|
|
191
|
-
|
|
190
|
+
const separator = key.indexOf("::");
|
|
191
|
+
const relFile = key.slice(0, separator);
|
|
192
|
+
const importedSpec = key.slice(separator + 2);
|
|
193
|
+
return (
|
|
194
|
+
allViolations.find(
|
|
195
|
+
(v) => path.relative(rootAbsPath, v.file) === relFile && v.importedSpec === importedSpec,
|
|
196
|
+
)?.line ?? 1
|
|
197
|
+
);
|
|
192
198
|
};
|
|
193
199
|
const violations: GuardViolation[] = ratchetFor(rootAbsPath).check(
|
|
194
200
|
counts,
|
package/src/guard-i18n-keys.ts
CHANGED
|
@@ -71,7 +71,7 @@ function collectUsedKeys(sf: SourceFile): UsedKey[] {
|
|
|
71
71
|
!first?.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
72
72
|
)
|
|
73
73
|
continue;
|
|
74
|
-
const literal = first.
|
|
74
|
+
const literal = first.getLiteralText();
|
|
75
75
|
if (!literal.includes(":")) continue;
|
|
76
76
|
keys.push({
|
|
77
77
|
key: literal,
|
|
@@ -90,7 +90,7 @@ function findEnclosingFeatureName(node: Node): string | null {
|
|
|
90
90
|
if (call.getExpression().getText() === "defineFeature") {
|
|
91
91
|
const first = call.getArguments()[0];
|
|
92
92
|
if (first?.isKind(SyntaxKind.StringLiteral)) {
|
|
93
|
-
return first.
|
|
93
|
+
return first.getLiteralText();
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -108,7 +108,7 @@ function extractKeysFromTranslationsObject(
|
|
|
108
108
|
const nameNode = prop.getNameNode();
|
|
109
109
|
let keyName: string;
|
|
110
110
|
if (nameNode.isKind(SyntaxKind.StringLiteral)) {
|
|
111
|
-
keyName = nameNode.
|
|
111
|
+
keyName = nameNode.getLiteralText();
|
|
112
112
|
} else if (nameNode.isKind(SyntaxKind.Identifier)) {
|
|
113
113
|
keyName = nameNode.getText();
|
|
114
114
|
} else continue;
|
|
@@ -119,8 +119,7 @@ function extractKeysFromTranslationsObject(
|
|
|
119
119
|
for (const localeProp of initializer.getProperties()) {
|
|
120
120
|
if (!localeProp.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
121
121
|
const localeName = localeProp.getNameNode();
|
|
122
|
-
if (localeName.isKind(SyntaxKind.StringLiteral))
|
|
123
|
-
locales.add(localeName.getText().slice(1, -1));
|
|
122
|
+
if (localeName.isKind(SyntaxKind.StringLiteral)) locales.add(localeName.getLiteralText());
|
|
124
123
|
else if (localeName.isKind(SyntaxKind.Identifier)) locales.add(localeName.getText());
|
|
125
124
|
}
|
|
126
125
|
out.push({ key: keyName, locales, line: prop.getStartLineNumber() });
|
|
@@ -148,7 +147,7 @@ function extractLocaleFirstKeys(
|
|
|
148
147
|
if (!prop.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
149
148
|
const nameNode = prop.getNameNode();
|
|
150
149
|
if (nameNode.isKind(SyntaxKind.StringLiteral)) {
|
|
151
|
-
keySet.add(nameNode.
|
|
150
|
+
keySet.add(nameNode.getLiteralText());
|
|
152
151
|
}
|
|
153
152
|
}
|
|
154
153
|
}
|
|
@@ -160,7 +159,7 @@ function extractLocaleFirstKeys(
|
|
|
160
159
|
const has = map.getProperties().some((prop) => {
|
|
161
160
|
if (!prop.isKind(SyntaxKind.PropertyAssignment)) return false;
|
|
162
161
|
const nameNode = prop.getNameNode();
|
|
163
|
-
return nameNode.isKind(SyntaxKind.StringLiteral) && nameNode.
|
|
162
|
+
return nameNode.isKind(SyntaxKind.StringLiteral) && nameNode.getLiteralText() === key;
|
|
164
163
|
});
|
|
165
164
|
if (has) locales.add(locale);
|
|
166
165
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import * as path from "node:path";
|
|
22
|
-
import { type CallExpression, type
|
|
22
|
+
import { type CallExpression, type SourceFile, SyntaxKind } from "ts-morph";
|
|
23
23
|
import {
|
|
24
24
|
type AstGuard,
|
|
25
25
|
type GuardViolation,
|
|
@@ -88,7 +88,7 @@ export function collectViolations(sourceFile: SourceFile): Violation[] {
|
|
|
88
88
|
function getCalleeName(call: CallExpression): string | null {
|
|
89
89
|
const expr = call.getExpression();
|
|
90
90
|
if (expr.getKind() === SyntaxKind.Identifier) {
|
|
91
|
-
return
|
|
91
|
+
return expr.getText();
|
|
92
92
|
}
|
|
93
93
|
if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
94
94
|
return expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName();
|
|
@@ -337,6 +337,7 @@ function findComponentNameUsages(sf: SourceFile): { name: string; kind: MountKin
|
|
|
337
337
|
function buildUsageKindsByName(files: readonly SourceFile[]): Map<string, Set<MountKind>> {
|
|
338
338
|
const byName = new Map<string, Set<MountKind>>();
|
|
339
339
|
for (const sf of files) {
|
|
340
|
+
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
340
341
|
for (const { name, kind } of findComponentNameUsages(sf)) {
|
|
341
342
|
const kinds = byName.get(name) ?? new Set<MountKind>();
|
|
342
343
|
kinds.add(kind);
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* Checked:
|
|
13
13
|
* - Imports: react-dom/*, jsdom, @cosmicdrift/kumiko-renderer-web, @cosmicdrift/kumiko-renderer-native
|
|
14
14
|
* - Symbols: window., document., location., history., localStorage,
|
|
15
|
-
* sessionStorage, navigator., EventSource, fetch
|
|
16
|
-
* (bare — unqualified)
|
|
15
|
+
* sessionStorage, navigator., EventSource, fetch(
|
|
16
|
+
* (bare — unqualified), HTMLElement, createRoot
|
|
17
17
|
*
|
|
18
18
|
* __tests__ folders are excluded — tests mount in jsdom, that's expected.
|
|
19
19
|
*/
|
|
@@ -43,6 +43,7 @@ const FORBIDDEN_SYMBOLS = [
|
|
|
43
43
|
/\bsessionStorage\b/,
|
|
44
44
|
/\bnavigator\s*\./,
|
|
45
45
|
/\bEventSource\b/,
|
|
46
|
+
/(?<![.\w])fetch\s*\(/,
|
|
46
47
|
/\bHTMLElement\b/,
|
|
47
48
|
/\bcreateRoot\b/,
|
|
48
49
|
];
|
|
@@ -56,7 +57,7 @@ const FORBIDDEN_SYMBOLS = [
|
|
|
56
57
|
// Ignored: fragment `<>`, components `<Capitalized`, attribute values like
|
|
57
58
|
// `<string>` in a type-annotation context (the regex only matches right
|
|
58
59
|
// after whitespace/newline/>, not after an identifier character).
|
|
59
|
-
const FORBIDDEN_JSX_TAG = /(^|\s|>|\()<([a-z][a-zA-Z0-9-]*)[\s/>]
|
|
60
|
+
const FORBIDDEN_JSX_TAG = /(^|\s|>|\()<([a-z][a-zA-Z0-9-]*)(?=[\s/>])/g;
|
|
60
61
|
|
|
61
62
|
type Violation = {
|
|
62
63
|
readonly file: string;
|
|
@@ -113,8 +114,7 @@ export function findViolations(file: string, root: string): Violation[] {
|
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
const jsxMatch
|
|
117
|
-
if (jsxMatch !== null) {
|
|
117
|
+
for (const jsxMatch of line.matchAll(FORBIDDEN_JSX_TAG)) {
|
|
118
118
|
violations.push({
|
|
119
119
|
file: path.relative(root, file),
|
|
120
120
|
line: i + 1,
|
|
@@ -126,9 +126,12 @@ export function resolveKumikoUpgradeBin(
|
|
|
126
126
|
return Bun.which("kumiko-upgrade", { PATH: pathEnv }) ?? undefined;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
const KUMIKO_UPGRADE_TIMEOUT_MS = 120_000;
|
|
130
|
+
|
|
131
|
+
export async function runKumikoUpgrade(
|
|
130
132
|
version: string,
|
|
131
133
|
cwd: string,
|
|
134
|
+
timeoutMs: number = KUMIKO_UPGRADE_TIMEOUT_MS,
|
|
132
135
|
): Promise<{ ok: true; json: UpgradeJson } | { ok: false; error: string }> {
|
|
133
136
|
const binPath = resolveKumikoUpgradeBin(cwd);
|
|
134
137
|
if (!binPath) {
|
|
@@ -142,14 +145,22 @@ async function runKumikoUpgrade(
|
|
|
142
145
|
cmd: [binPath, "--from", version, "--json"],
|
|
143
146
|
cwd,
|
|
144
147
|
env: { ...process.env, INIT_CWD: cwd },
|
|
148
|
+
stdin: "ignore",
|
|
145
149
|
stdout: "pipe",
|
|
146
150
|
stderr: "pipe",
|
|
151
|
+
timeout: timeoutMs,
|
|
147
152
|
});
|
|
148
153
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
149
154
|
new Response(proc.stdout).text(),
|
|
150
155
|
new Response(proc.stderr).text(),
|
|
151
156
|
proc.exited,
|
|
152
157
|
]);
|
|
158
|
+
if (proc.signalCode !== null) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
error: `\`kumiko-upgrade --from ${version} --json\` timed out after ${timeoutMs} ms (killed by ${proc.signalCode})`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
153
164
|
if (exitCode !== 0) {
|
|
154
165
|
return {
|
|
155
166
|
ok: false,
|
|
@@ -15,16 +15,15 @@ import { closeSync, existsSync, openSync, readFileSync, readSync } from "node:fs
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import type { ImportDeclaration, SourceFile } from "ts-morph";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const RUNTIME_KINDS = ["runtime", "client", "prod", "dev", "tooling", "test"] as const;
|
|
19
19
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
]);
|
|
20
|
+
export type Runtime = (typeof RUNTIME_KINDS)[number];
|
|
21
|
+
|
|
22
|
+
export const ALL_RUNTIMES: ReadonlySet<string> = new Set(RUNTIME_KINDS);
|
|
23
|
+
|
|
24
|
+
function isRuntime(value: unknown): value is Runtime {
|
|
25
|
+
return typeof value === "string" && ALL_RUNTIMES.has(value);
|
|
26
|
+
}
|
|
28
27
|
|
|
29
28
|
export const COMPAT: Record<Runtime, ReadonlySet<Runtime>> = {
|
|
30
29
|
runtime: new Set(["runtime", "client", "prod"]),
|
|
@@ -284,7 +283,8 @@ export function classifyByDirective(
|
|
|
284
283
|
if (head === null) return null;
|
|
285
284
|
for (const line of head.split("\n").slice(0, DIRECTIVE_HEAD_LINES)) {
|
|
286
285
|
const m = line.match(/\/\/\s*@runtime\s+(\w+)/);
|
|
287
|
-
|
|
286
|
+
const declared = m?.[1];
|
|
287
|
+
if (isRuntime(declared)) return declared;
|
|
288
288
|
}
|
|
289
289
|
return null;
|
|
290
290
|
}
|
|
@@ -346,9 +346,11 @@ function readWorkspaceRuntime(dir: string, cache: Map<string, Runtime | null>):
|
|
|
346
346
|
const pkgPath = path.join(dir, "package.json");
|
|
347
347
|
let result: Runtime | null = null;
|
|
348
348
|
try {
|
|
349
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
350
|
-
const
|
|
351
|
-
|
|
349
|
+
const pkg: unknown = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
350
|
+
const kumiko = typeof pkg === "object" && pkg !== null && "kumiko" in pkg ? pkg.kumiko : null;
|
|
351
|
+
const declared =
|
|
352
|
+
typeof kumiko === "object" && kumiko !== null && "runtime" in kumiko ? kumiko.runtime : null;
|
|
353
|
+
if (isRuntime(declared)) result = declared;
|
|
352
354
|
} catch {
|
|
353
355
|
// package.json missing or unreadable — unmarked
|
|
354
356
|
}
|