@cosmicdrift/kumiko-guards 0.351.0 → 0.352.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.352.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.352.0",
|
|
32
32
|
"ts-morph": "^28.0.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.352.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "New No-Temporal-Polyfill-Import guard reports direct temporal-polyfill value imports outside kumiko-types",
|
|
6
|
+
"detail": "Scans source and test files (ts, tsx). Flags named, default, namespace and side-effect imports, re-exports, import() and require of \"temporal-polyfill\" or its subpaths, and prints the replacement import from @cosmicdrift/kumiko-types/temporal. Type-only imports and triple-slash type references pass."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.352.0",
|
|
10
|
+
"type": "breaking",
|
|
11
|
+
"title": "upgrade-state guard fails on open manual upgrade steps",
|
|
12
|
+
"detail": "Every entry in the marker's pendingManual list now produces a guard violation until it is resolved.",
|
|
13
|
+
"migration": "Run `kumiko-upgrade --resolve <id> --reason \"<what you did>\"` for each open step once you applied it, or add `--not-applicable` when it does not concern your repo. The guard passes when no manual step is left open."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.351.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Guard: finds value imports from `temporal-polyfill` outside kumiko-types.
|
|
4
|
+
*
|
|
5
|
+
* Since 0.351.0 `@cosmicdrift/kumiko-types/temporal` is the single source of
|
|
6
|
+
* the Temporal API: native `globalThis.Temporal` wins, the polyfill is only a
|
|
7
|
+
* fallback. A module that imports `Temporal` straight from `temporal-polyfill`
|
|
8
|
+
* gets the polyfill classes while framework-made values are native ones, so
|
|
9
|
+
* `instanceof Temporal.Instant` and `z.instanceof(Temporal.Instant)` reject
|
|
10
|
+
* them ("expected Instant, received Instant"). Side-effect imports such as
|
|
11
|
+
* `temporal-polyfill/global` are flagged too: they can put a second
|
|
12
|
+
* implementation on the global.
|
|
13
|
+
*
|
|
14
|
+
* Not flagged: `import type` (erased at runtime) and the
|
|
15
|
+
* `/// <reference types="temporal-polyfill/global" />` directive.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun guards/guard-no-temporal-polyfill-import.ts
|
|
19
|
+
*
|
|
20
|
+
* Exit 1 on violations, 0 when clean.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { type ImportDeclaration, Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
24
|
+
import {
|
|
25
|
+
type AstGuard,
|
|
26
|
+
findRepoRootFor,
|
|
27
|
+
type GuardViolation,
|
|
28
|
+
relFromRepoRoot,
|
|
29
|
+
runStandalone,
|
|
30
|
+
type ScanSpec,
|
|
31
|
+
} from "./_lib/guard-kit";
|
|
32
|
+
import { type RepoRoot, resolveRepoRoots } from "./_lib/roots";
|
|
33
|
+
|
|
34
|
+
const SCAN: ScanSpec = {
|
|
35
|
+
scope: "source+tests",
|
|
36
|
+
extensions: ["ts", "tsx"],
|
|
37
|
+
frameworkWithin: ["packages/*/src/**", "samples/**"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const REPLACEMENT_IMPORT = 'import { Temporal } from "@cosmicdrift/kumiko-types/temporal";';
|
|
41
|
+
|
|
42
|
+
// The one module that resolves native-vs-polyfill; repo-scoped so an app file
|
|
43
|
+
// at the same relative path does not inherit the exception.
|
|
44
|
+
const TEMPORAL_SOURCE = { repo: "kumiko-framework", relPath: "packages/types/src/temporal.ts" };
|
|
45
|
+
|
|
46
|
+
export function isTemporalSingleSource(repoName: string | undefined, relPath: string): boolean {
|
|
47
|
+
return repoName === TEMPORAL_SOURCE.repo && relPath === TEMPORAL_SOURCE.relPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPolyfillSpecifier(specifier: string): boolean {
|
|
51
|
+
return specifier === "temporal-polyfill" || specifier.startsWith("temporal-polyfill/");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isTypeOnlyImport(decl: ImportDeclaration): boolean {
|
|
55
|
+
if (decl.isTypeOnly()) return true;
|
|
56
|
+
const named = decl.getNamedImports();
|
|
57
|
+
const hasValueBinding =
|
|
58
|
+
decl.getDefaultImport() !== undefined || decl.getNamespaceImport() !== undefined;
|
|
59
|
+
return !hasValueBinding && named.length > 0 && named.every((n) => n.isTypeOnly());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type PolyfillImport = { readonly line: number; readonly specifier: string };
|
|
63
|
+
|
|
64
|
+
function findPolyfillValueImports(sf: SourceFile): PolyfillImport[] {
|
|
65
|
+
const found: PolyfillImport[] = [];
|
|
66
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
67
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
68
|
+
if (isPolyfillSpecifier(specifier) && !isTypeOnlyImport(decl)) {
|
|
69
|
+
found.push({ line: decl.getStartLineNumber(), specifier });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const decl of sf.getExportDeclarations()) {
|
|
73
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
74
|
+
if (specifier !== undefined && isPolyfillSpecifier(specifier) && !decl.isTypeOnly()) {
|
|
75
|
+
found.push({ line: decl.getStartLineNumber(), specifier });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
79
|
+
const callee = call.getExpression();
|
|
80
|
+
const isRequireCall = Node.isIdentifier(callee) && callee.getText() === "require";
|
|
81
|
+
const isDynamicImport = callee.getKind() === SyntaxKind.ImportKeyword;
|
|
82
|
+
if (!isRequireCall && !isDynamicImport) continue;
|
|
83
|
+
const arg = call.getArguments()[0];
|
|
84
|
+
if (arg && Node.isStringLiteral(arg) && isPolyfillSpecifier(arg.getLiteralValue())) {
|
|
85
|
+
found.push({ line: call.getStartLineNumber(), specifier: arg.getLiteralValue() });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const guard: AstGuard = {
|
|
92
|
+
name: "No-Temporal-Polyfill-Import Guard",
|
|
93
|
+
scan: SCAN,
|
|
94
|
+
hint: `Import Temporal from the kumiko single source instead: ${REPLACEMENT_IMPORT} A direct "temporal-polyfill" import creates a second Temporal implementation, so instanceof and z.instanceof(Temporal.Instant) reject framework-made values.`,
|
|
95
|
+
run(files, roots: readonly RepoRoot[] = resolveRepoRoots()) {
|
|
96
|
+
const violations: GuardViolation[] = [];
|
|
97
|
+
for (const sf of files) {
|
|
98
|
+
const file = sf.getFilePath();
|
|
99
|
+
const rel = relFromRepoRoot(file, roots);
|
|
100
|
+
if (isTemporalSingleSource(findRepoRootFor(file, roots)?.name, rel)) continue;
|
|
101
|
+
for (const hit of findPolyfillValueImports(sf)) {
|
|
102
|
+
violations.push({
|
|
103
|
+
file: rel,
|
|
104
|
+
line: hit.line,
|
|
105
|
+
message: `[${hit.specifier}] direct temporal-polyfill import — use ${REPLACEMENT_IMPORT}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { violations };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (import.meta.main) runStandalone(guard);
|
|
@@ -36,11 +36,18 @@ const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
|
|
|
36
36
|
const RESTRICTED_SYMBOLS = new Set([
|
|
37
37
|
"getUnscopedAggregateStreamMaxVersion",
|
|
38
38
|
"getUnscopedAggregateStreamTenant",
|
|
39
|
+
// Appends any event type without the feature-ownership check of ctx.appendEvent and without
|
|
40
|
+
// an audited unsafeRaw escape hatch, so new callers need an explicit allowlist entry.
|
|
41
|
+
"appendEventInTenantDb",
|
|
42
|
+
"getStreamVersionInTenantDb",
|
|
39
43
|
]);
|
|
40
44
|
|
|
41
45
|
const ALLOWLIST = [
|
|
42
46
|
/^packages\/framework\/src\/event-store\/event-store\.ts$/,
|
|
43
47
|
/^packages\/framework\/src\/event-store\/index\.ts$/,
|
|
48
|
+
/^packages\/framework\/src\/event-store\/tenant-db-append\.ts$/,
|
|
49
|
+
/^packages\/bundled-features\/src\/cap-counter\/cap-reservation\.ts$/,
|
|
50
|
+
/^packages\/bundled-features\/src\/user-data-rights-defaults\/hooks\/delivery-attempt\.userdata-hook\.ts$/,
|
|
44
51
|
/^packages\/bundled-features\/src\/tenant\/seeding\.ts$/,
|
|
45
52
|
/^packages\/bundled-features\/src\/tier-engine\/feature\.ts$/,
|
|
46
53
|
];
|
|
@@ -112,7 +119,7 @@ function findRestrictedSymbolReferences(sf: SourceFile): Violation[] {
|
|
|
112
119
|
export const guard: AstGuard = {
|
|
113
120
|
name: "Restricted-Symbols Guard",
|
|
114
121
|
scan: SCAN,
|
|
115
|
-
hint: 'getUnscopedAggregateStream{MaxVersion,Tenant} is an existence oracle for foreign tenants — only seed-/system-internal code may reference them. New caller needed? Extend the allowlist in guard-restricted-symbols.ts, with a reason. Known gap: `export * from "...event-store"` is not detected (named imports, re-exports, and namespace property access are covered).',
|
|
122
|
+
hint: 'getUnscopedAggregateStream{MaxVersion,Tenant} is an existence oracle for foreign tenants — only seed-/system-internal code may reference them. appendEventInTenantDb/getStreamVersionInTenantDb bypass the feature-ownership check of ctx.appendEvent — only framework-internal coordination code may use them. New caller needed? Extend the allowlist in guard-restricted-symbols.ts, with a reason. Known gap: `export * from "...event-store"` is not detected (named imports, re-exports, and namespace property access are covered).',
|
|
116
123
|
run(files, roots = resolveRepoRoots()) {
|
|
117
124
|
const violations: Array<{ file: string; line: number; message: string }> = [];
|
|
118
125
|
|
|
@@ -125,7 +132,7 @@ export const guard: AstGuard = {
|
|
|
125
132
|
violations.push({
|
|
126
133
|
file: rel,
|
|
127
134
|
line: v.line,
|
|
128
|
-
message: `[${v.symbol}]
|
|
135
|
+
message: `[${v.symbol}] restricted event-store primitive referenced outside allowlist`,
|
|
129
136
|
});
|
|
130
137
|
}
|
|
131
138
|
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* `@cosmicdrift/kumiko-cli` or `@cosmicdrift/kumiko-dev-server`) and records
|
|
8
8
|
* the version it was applied at. This guard re-runs `kumiko-upgrade --from <marker version> --json` and
|
|
9
9
|
* fails if any changelog entries are still pending — meaning the marker is
|
|
10
|
-
* stale and the repo hasn't run the upgrade since.
|
|
10
|
+
* stale and the repo hasn't run the upgrade since. It also fails once per open
|
|
11
|
+
* `pendingManual` step the marker still carries (breaking changes without a
|
|
12
|
+
* codemod that nobody resolved yet).
|
|
11
13
|
*
|
|
12
14
|
* Single-repo only, like `guard-upgrade-state.ts` in infra/guards but
|
|
13
15
|
* without that package's multi-repo `resolveRepoRoots()` scan loop — this
|
|
@@ -31,7 +33,16 @@ import {
|
|
|
31
33
|
const MARKER_REL = ".kumiko/upgrade-state.json";
|
|
32
34
|
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
33
35
|
|
|
34
|
-
type
|
|
36
|
+
type PendingManualStep = {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly version: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type UpgradeMarker = {
|
|
43
|
+
readonly version: string;
|
|
44
|
+
readonly pendingManual: readonly PendingManualStep[];
|
|
45
|
+
};
|
|
35
46
|
|
|
36
47
|
type PendingEntry = {
|
|
37
48
|
readonly version: string;
|
|
@@ -71,6 +82,25 @@ export function isUpgradeJson(value: unknown): value is UpgradeJson {
|
|
|
71
82
|
);
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
export function isPendingManualStep(value: unknown): value is PendingManualStep {
|
|
86
|
+
if (!value || typeof value !== "object") return false;
|
|
87
|
+
const v = value as Record<string, unknown>;
|
|
88
|
+
return (
|
|
89
|
+
typeof v["id"] === "string" &&
|
|
90
|
+
v["id"].length > 0 &&
|
|
91
|
+
typeof v["version"] === "string" &&
|
|
92
|
+
typeof v["title"] === "string"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readPendingManual(parsed: unknown): readonly PendingManualStep[] {
|
|
97
|
+
const raw =
|
|
98
|
+
parsed && typeof parsed === "object"
|
|
99
|
+
? (parsed as Record<string, unknown>)["pendingManual"]
|
|
100
|
+
: undefined;
|
|
101
|
+
return Array.isArray(raw) ? raw.filter(isPendingManualStep) : [];
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
export function readMarker(root: string): UpgradeMarker | { error: string } {
|
|
75
105
|
const markerPath = join(root, MARKER_REL);
|
|
76
106
|
if (!existsSync(markerPath)) {
|
|
@@ -93,7 +123,7 @@ export function readMarker(root: string): UpgradeMarker | { error: string } {
|
|
|
93
123
|
error: `${MARKER_REL} is missing a valid "version" field (expected semver x.y.z[-pre][+build]) — broken marker file`,
|
|
94
124
|
};
|
|
95
125
|
}
|
|
96
|
-
return { version };
|
|
126
|
+
return { version, pendingManual: readPendingManual(parsed) };
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
/** Pending changelog entries turned into guard violations — pure, testable without a subprocess. */
|
|
@@ -109,6 +139,20 @@ export function pendingViolations(json: UpgradeJson, markerVersion: string): Gua
|
|
|
109
139
|
}));
|
|
110
140
|
}
|
|
111
141
|
|
|
142
|
+
/** Open manual upgrade steps from the marker, one violation each. */
|
|
143
|
+
export function pendingManualViolations(
|
|
144
|
+
pendingManual: readonly PendingManualStep[],
|
|
145
|
+
): GuardViolation[] {
|
|
146
|
+
return pendingManual.map((step) => ({
|
|
147
|
+
file: MARKER_REL,
|
|
148
|
+
line: 1,
|
|
149
|
+
message:
|
|
150
|
+
`Open manual upgrade step ${step.id} (${step.version}): ${step.title}. ` +
|
|
151
|
+
`Resolve it with \`kumiko-upgrade --resolve ${step.id} --reason "<what you did>"\` ` +
|
|
152
|
+
`or mark it with \`--not-applicable\`.`,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
112
156
|
export function resolveKumikoUpgradeBin(
|
|
113
157
|
cwd: string,
|
|
114
158
|
pathEnv: string | undefined = process.env["PATH"],
|
|
@@ -204,16 +248,17 @@ export const check: RepoCheck = {
|
|
|
204
248
|
notApplicable: false,
|
|
205
249
|
};
|
|
206
250
|
}
|
|
251
|
+
const manualViolations = pendingManualViolations(marker.pendingManual);
|
|
207
252
|
const result = await runKumikoUpgrade(marker.version, rootAbsPath);
|
|
208
253
|
if (!result.ok) {
|
|
209
254
|
return {
|
|
210
|
-
violations: [{ file: MARKER_REL, line: 1, message: result.error }],
|
|
255
|
+
violations: [{ file: MARKER_REL, line: 1, message: result.error }, ...manualViolations],
|
|
211
256
|
matchedFiles: 1,
|
|
212
257
|
notApplicable: false,
|
|
213
258
|
};
|
|
214
259
|
}
|
|
215
260
|
return {
|
|
216
|
-
violations: pendingViolations(result.json, marker.version),
|
|
261
|
+
violations: [...pendingViolations(result.json, marker.version), ...manualViolations],
|
|
217
262
|
matchedFiles: 1,
|
|
218
263
|
notApplicable: false,
|
|
219
264
|
};
|
package/src/run-guards.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { guard as loadallEvents } from "./guard-loadall-events";
|
|
|
42
42
|
import { guard as noDateApi } from "./guard-no-date-api";
|
|
43
43
|
import { guard as noDirectFs } from "./guard-no-direct-fs";
|
|
44
44
|
import { guard as noLogicInViews } from "./guard-no-logic-in-views";
|
|
45
|
+
import { guard as noTemporalPolyfillImport } from "./guard-no-temporal-polyfill-import";
|
|
45
46
|
import { guard as openToAllReason } from "./guard-open-to-all-reason";
|
|
46
47
|
import { guard as piiAnnotations } from "./guard-pii-annotations";
|
|
47
48
|
import { guard as preEsPatterns } from "./guard-pre-es-patterns";
|
|
@@ -72,6 +73,7 @@ export const GUARDS = [
|
|
|
72
73
|
htmlEscape,
|
|
73
74
|
crossFeatureImports,
|
|
74
75
|
noDateApi,
|
|
76
|
+
noTemporalPolyfillImport,
|
|
75
77
|
restrictedSymbols,
|
|
76
78
|
fakeTests,
|
|
77
79
|
noLogicInViews,
|