@cosmicdrift/kumiko-framework 0.320.0 → 0.322.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 +4 -4
- package/src/__tests__/schema-cli.integration.test.ts +11 -0
- package/src/__tests__/upgrade-cli.test.ts +50 -3
- package/src/api/__tests__/client-ip.test.ts +25 -0
- package/src/api/__tests__/extra-routes.integration.test.ts +42 -0
- package/src/api/client-ip.ts +27 -0
- package/src/api/extra-route.ts +3 -0
- package/src/api/index.ts +7 -1
- package/src/api/request-context.ts +23 -1
- package/src/api/server.ts +3 -1
- package/src/changes.json +60 -0
- package/src/db/event-store-executor-write.ts +8 -0
- package/src/db/queries/event-store-transfer.ts +73 -0
- package/src/engine/__tests__/entity-handlers.test.ts +28 -0
- package/src/engine/__tests__/steps-webhook-send.test.ts +2 -2
- package/src/engine/entity-handlers.ts +20 -1
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/alias-reexport.ts +1 -0
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/cycle-a.ts +1 -0
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/cycle-b.ts +1 -0
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/entry.ts +3 -0
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/local-rebind.ts +3 -0
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/origin.ts +1 -0
- package/src/engine/feature-ast/__tests__/fixtures/resolve-import/star-barrel.ts +1 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +4 -4
- package/src/engine/feature-ast/__tests__/resolve-import.test.ts +138 -0
- package/src/engine/feature-ast/extractors/index.ts +4 -0
- package/src/engine/feature-ast/extractors/resolve-import.ts +187 -0
- package/src/engine/feature-ast/extractors/shared.ts +26 -19
- package/src/engine/feature-ast/parse.ts +13 -20
- package/src/engine/index.ts +3 -0
- package/src/engine/registry-state.ts +2 -1
- package/src/engine/steps/webhook-send.ts +4 -2
- package/src/engine/types/index.ts +2 -0
- package/src/errors/i18n/de.yaml +14 -0
- package/src/errors/i18n/en.yaml +13 -0
- package/src/errors/reasons.ts +5 -0
- package/src/event-store/__tests__/transfer.integration.test.ts +236 -0
- package/src/event-store/index.ts +6 -0
- package/src/event-store/transfer.ts +84 -0
- package/src/i18n/__tests__/i18n.test.ts +35 -0
- package/src/i18n/__tests__/mail-registry.test.ts +19 -0
- package/src/i18n/index.ts +11 -4
- package/src/i18n/mail-registry.ts +13 -7
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +3 -1
- package/src/pipeline/__tests__/irreversible-operation-gate.integration.test.ts +249 -0
- package/src/pipeline/dispatch-shared.ts +33 -4
- package/src/pipeline/index.ts +4 -0
- package/src/pipeline/irreversible-operation-gate.ts +33 -0
- package/src/pipeline/multi-stream-apply-context.ts +4 -0
- package/src/schema-cli.ts +5 -1
- package/src/ui-types/__tests__/plural.test.ts +108 -0
- package/src/ui-types/index.ts +6 -0
- package/src/ui-types/plural.ts +69 -0
- package/src/upgrade-cli.ts +58 -18
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ORIGIN_VALUE = "origin-value" as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./alias-reexport";
|
|
@@ -2799,10 +2799,10 @@ defineFeature("showpony", (r) => {
|
|
|
2799
2799
|
|
|
2800
2800
|
test("an imported registrar-wrapper stays invisible when its file can't be resolved (in-memory project, #1008)", () => {
|
|
2801
2801
|
// parseInline uses an in-memory ts-morph Project — "./register/screens"
|
|
2802
|
-
// isn't a real file, so
|
|
2803
|
-
//
|
|
2804
|
-
//
|
|
2805
|
-
//
|
|
2802
|
+
// isn't a real file, so resolveModuleFile() returns undefined and the
|
|
2803
|
+
// wrapper is skipped like any other unresolvable call. Real cross-file
|
|
2804
|
+
// resolution against on-disk files is covered by the parseFeatureFile
|
|
2805
|
+
// fixture tests below.
|
|
2806
2806
|
expect(result.patterns.some((p) => p.kind === "screen" || p.kind === "nav")).toBe(false);
|
|
2807
2807
|
});
|
|
2808
2808
|
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Project, type SourceFile } from "ts-morph";
|
|
4
|
+
import {
|
|
5
|
+
findImportBindingForLocalName,
|
|
6
|
+
resolveExportedVariable,
|
|
7
|
+
resolveModuleFile,
|
|
8
|
+
} from "../extractors/resolve-import";
|
|
9
|
+
import { parseFeatureFile, parseSourceFile } from "../parse";
|
|
10
|
+
|
|
11
|
+
function loadFixture(relPath: string): SourceFile {
|
|
12
|
+
const project = new Project({
|
|
13
|
+
skipAddingFilesFromTsConfig: true,
|
|
14
|
+
skipFileDependencyResolution: true,
|
|
15
|
+
});
|
|
16
|
+
return project.addSourceFileAtPath(resolve(__dirname, relPath));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("resolveModuleFile", () => {
|
|
20
|
+
test("resolves a relative specifier to the real source file", () => {
|
|
21
|
+
const entry = loadFixture("fixtures/resolve-import/entry.ts");
|
|
22
|
+
const resolved = resolveModuleFile(entry, "./star-barrel");
|
|
23
|
+
expect(String(resolved?.getFilePath())).toBe(
|
|
24
|
+
resolve(__dirname, "fixtures/resolve-import/star-barrel.ts"),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns undefined for an unresolvable package", () => {
|
|
29
|
+
const entry = loadFixture("fixtures/resolve-import/entry.ts");
|
|
30
|
+
const resolved = resolveModuleFile(entry, "@totally-unresolvable/not-a-real-package");
|
|
31
|
+
expect(resolved).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("findImportBindingForLocalName", () => {
|
|
36
|
+
test("follows an aliased named import to the module's original export name", () => {
|
|
37
|
+
const entry = loadFixture("fixtures/resolve-import/entry.ts");
|
|
38
|
+
const binding = findImportBindingForLocalName(entry, "RENAMED");
|
|
39
|
+
expect(binding).toEqual({ moduleSpecifier: "./star-barrel", importedName: "ALIASED" });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("returns undefined for a name with no import declaration", () => {
|
|
43
|
+
const entry = loadFixture("fixtures/resolve-import/entry.ts");
|
|
44
|
+
expect(findImportBindingForLocalName(entry, "NOT_IMPORTED")).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("resolveExportedVariable", () => {
|
|
49
|
+
test("follows a cross-file aliased re-export (export { X as Y } from ...)", () => {
|
|
50
|
+
const aliasFile = loadFixture("fixtures/resolve-import/alias-reexport.ts");
|
|
51
|
+
const decl = resolveExportedVariable(aliasFile, "ALIASED");
|
|
52
|
+
expect(decl?.getName()).toBe("ORIGIN_VALUE");
|
|
53
|
+
expect(decl?.getInitializer()?.getText()).toContain("origin-value");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("follows an export * barrel to a nested aliased re-export", () => {
|
|
57
|
+
const barrelFile = loadFixture("fixtures/resolve-import/star-barrel.ts");
|
|
58
|
+
const decl = resolveExportedVariable(barrelFile, "ALIASED");
|
|
59
|
+
expect(decl?.getName()).toBe("ORIGIN_VALUE");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("follows a local re-export of an imported binding (import + export {} with no from)", () => {
|
|
63
|
+
const localRebindFile = loadFixture("fixtures/resolve-import/local-rebind.ts");
|
|
64
|
+
const decl = resolveExportedVariable(localRebindFile, "LOCAL_EXPORT");
|
|
65
|
+
expect(decl?.getName()).toBe("ORIGIN_VALUE");
|
|
66
|
+
expect(String(decl?.getSourceFile().getFilePath())).toBe(
|
|
67
|
+
resolve(__dirname, "fixtures/resolve-import/origin.ts"),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("terminates instead of looping forever on a mutual export * cycle", () => {
|
|
72
|
+
const cycleFile = loadFixture("fixtures/resolve-import/cycle-a.ts");
|
|
73
|
+
const decl = resolveExportedVariable(cycleFile, "NOTHING_EXPORTED_HERE");
|
|
74
|
+
expect(decl).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns undefined for a name that is never exported anywhere in the chain", () => {
|
|
78
|
+
const barrelFile = loadFixture("fixtures/resolve-import/star-barrel.ts");
|
|
79
|
+
expect(resolveExportedVariable(barrelFile, "DOES_NOT_EXIST")).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("same-file shadowing (regression for the getSymbol()/getDefinitionNodes() removal)", () => {
|
|
84
|
+
function makeSourceFile(content: string): SourceFile {
|
|
85
|
+
const project = new Project({
|
|
86
|
+
skipAddingFilesFromTsConfig: true,
|
|
87
|
+
skipFileDependencyResolution: true,
|
|
88
|
+
useInMemoryFileSystem: true,
|
|
89
|
+
});
|
|
90
|
+
return project.createSourceFile("shadow.ts", content);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
test("an inner const declared inside setup() shadows a same-named outer const", () => {
|
|
94
|
+
const sf = makeSourceFile(`
|
|
95
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
96
|
+
|
|
97
|
+
const NAME = "outer";
|
|
98
|
+
|
|
99
|
+
defineFeature("f", (r) => {
|
|
100
|
+
const NAME = "inner";
|
|
101
|
+
r.extendsRegistrar(NAME, {});
|
|
102
|
+
});
|
|
103
|
+
`);
|
|
104
|
+
const result = parseSourceFile(sf);
|
|
105
|
+
expect(result.errors).toEqual([]);
|
|
106
|
+
expect(result.patterns).toMatchObject([{ kind: "extendsRegistrar", extensionName: "inner" }]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a function parameter shadowing an outer const yields an unresolvable reference, not the outer value", () => {
|
|
110
|
+
const sf = makeSourceFile(`
|
|
111
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
112
|
+
|
|
113
|
+
const NAME = "outer";
|
|
114
|
+
|
|
115
|
+
defineFeature("f", (r) => {
|
|
116
|
+
function wrap(NAME) {
|
|
117
|
+
r.extendsRegistrar(NAME, {});
|
|
118
|
+
}
|
|
119
|
+
wrap("shadowed-by-param");
|
|
120
|
+
});
|
|
121
|
+
`);
|
|
122
|
+
const result = parseSourceFile(sf);
|
|
123
|
+
expect(result.patterns).toEqual([]);
|
|
124
|
+
expect(result.errors).toEqual([expect.objectContaining({ methodName: "extendsRegistrar" })]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("parseFeatureFile resolves cross-file constants without a Program", () => {
|
|
129
|
+
test("sessions' imported event-name constant resolves to its real value", () => {
|
|
130
|
+
const result = parseFeatureFile(
|
|
131
|
+
resolve(__dirname, "../../../../../bundled-features/src/sessions/feature.ts"),
|
|
132
|
+
);
|
|
133
|
+
const defineEvent = result.patterns.find((p) => p.kind === "defineEvent");
|
|
134
|
+
// SESSION_REVOKED_EVENT_SHORT is imported from ./session-revoked-event —
|
|
135
|
+
// only resolves through the cross-file resolver this change replaces.
|
|
136
|
+
expect(defineEvent).toMatchObject({ eventName: "session-revoked" });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Syntactic cross-file resolution for feature-ast identifier extractors.
|
|
2
|
+
// Deliberately avoids ts-morph's symbol/type-checker APIs (getSymbol,
|
|
3
|
+
// getDefinitionNodes, getModuleSpecifierSourceFile, ...) — those build a full
|
|
4
|
+
// TypeScript program on first use, which against this repo's workspace
|
|
5
|
+
// packages pulls in ~1000 source files and costs seconds instead of
|
|
6
|
+
// milliseconds.
|
|
7
|
+
import type {
|
|
8
|
+
Node,
|
|
9
|
+
ParameterDeclaration,
|
|
10
|
+
SourceFile,
|
|
11
|
+
Statement,
|
|
12
|
+
VariableDeclaration,
|
|
13
|
+
} from "ts-morph";
|
|
14
|
+
import { SyntaxKind, ts } from "ts-morph";
|
|
15
|
+
|
|
16
|
+
const MODULE_RESOLUTION_OPTIONS: ts.CompilerOptions = {
|
|
17
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
18
|
+
module: ts.ModuleKind.ESNext,
|
|
19
|
+
target: ts.ScriptTarget.ESNext,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const MAX_RESOLUTION_DEPTH = 24;
|
|
23
|
+
|
|
24
|
+
export function resolveModuleFile(fromFile: SourceFile, specifier: string): SourceFile | undefined {
|
|
25
|
+
const project = fromFile.getProject();
|
|
26
|
+
// The project's own host, not ts.sys: it honours an in-memory file system
|
|
27
|
+
// (Designer) and exists outside Node.
|
|
28
|
+
const resolved = ts.resolveModuleName(
|
|
29
|
+
specifier,
|
|
30
|
+
fromFile.getFilePath(),
|
|
31
|
+
MODULE_RESOLUTION_OPTIONS,
|
|
32
|
+
project.getModuleResolutionHost(),
|
|
33
|
+
);
|
|
34
|
+
const resolvedFileName = resolved.resolvedModule?.resolvedFileName;
|
|
35
|
+
if (!resolvedFileName) return undefined;
|
|
36
|
+
// A declaration file has no initializers to read. Installed packages that
|
|
37
|
+
// ship .ts sources (the @cosmicdrift/* packages in a consumer repo) stay
|
|
38
|
+
// resolvable, as they were via the type checker.
|
|
39
|
+
if (resolvedFileName.endsWith(".d.ts")) return undefined;
|
|
40
|
+
return fromFile.getProject().addSourceFileAtPathIfExists(resolvedFileName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function findImportBindingForLocalName(
|
|
44
|
+
sourceFile: SourceFile,
|
|
45
|
+
localName: string,
|
|
46
|
+
): { readonly moduleSpecifier: string; readonly importedName: string } | undefined {
|
|
47
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
48
|
+
const named = importDecl
|
|
49
|
+
.getNamedImports()
|
|
50
|
+
.find((ni) => (ni.getAliasNode()?.getText() ?? ni.getName()) === localName);
|
|
51
|
+
if (!named) continue;
|
|
52
|
+
return {
|
|
53
|
+
moduleSpecifier: importDecl.getModuleSpecifierValue(),
|
|
54
|
+
importedName: named.getName(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `visited` keys on `${filePath}#${name}`, not path alone — aliasing can
|
|
62
|
+
* legally re-export a different name from the same file — and doubles as
|
|
63
|
+
* the cycle guard; `depth` backstops pathological non-cyclic chains.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveExportedVariable(
|
|
66
|
+
file: SourceFile,
|
|
67
|
+
exportedName: string,
|
|
68
|
+
visited: Set<string> = new Set(),
|
|
69
|
+
depth = 0,
|
|
70
|
+
): VariableDeclaration | undefined {
|
|
71
|
+
if (depth > MAX_RESOLUTION_DEPTH) return undefined;
|
|
72
|
+
const key = `${file.getFilePath()}#${exportedName}`;
|
|
73
|
+
if (visited.has(key)) return undefined;
|
|
74
|
+
visited.add(key);
|
|
75
|
+
|
|
76
|
+
const directDecl = file.getVariableDeclaration(exportedName);
|
|
77
|
+
if (directDecl?.getVariableStatement()?.hasExportKeyword()) return directDecl;
|
|
78
|
+
|
|
79
|
+
const namedExportMatch = resolveViaNamedExportSpecifier(file, exportedName, visited, depth);
|
|
80
|
+
if (namedExportMatch) return namedExportMatch;
|
|
81
|
+
|
|
82
|
+
return resolveViaStarExport(file, exportedName, visited, depth);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveViaNamedExportSpecifier(
|
|
86
|
+
file: SourceFile,
|
|
87
|
+
exportedName: string,
|
|
88
|
+
visited: Set<string>,
|
|
89
|
+
depth: number,
|
|
90
|
+
): VariableDeclaration | undefined {
|
|
91
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
92
|
+
const specifier = exportDecl
|
|
93
|
+
.getNamedExports()
|
|
94
|
+
.find((spec) => (spec.getAliasNode()?.getText() ?? spec.getName()) === exportedName);
|
|
95
|
+
if (!specifier) continue;
|
|
96
|
+
const originalName = specifier.getName();
|
|
97
|
+
|
|
98
|
+
if (!exportDecl.hasModuleSpecifier()) {
|
|
99
|
+
const resolved = resolveLocalExportBinding(file, originalName, visited, depth);
|
|
100
|
+
if (resolved) return resolved;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const moduleSpecifierValue = exportDecl.getModuleSpecifierValue();
|
|
105
|
+
if (!moduleSpecifierValue) continue;
|
|
106
|
+
const target = resolveModuleFile(file, moduleSpecifierValue);
|
|
107
|
+
if (!target) continue;
|
|
108
|
+
const resolved = resolveExportedVariable(target, originalName, visited, depth + 1);
|
|
109
|
+
if (resolved) return resolved;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveLocalExportBinding(
|
|
115
|
+
file: SourceFile,
|
|
116
|
+
originalName: string,
|
|
117
|
+
visited: Set<string>,
|
|
118
|
+
depth: number,
|
|
119
|
+
): VariableDeclaration | undefined {
|
|
120
|
+
const localDecl = file.getVariableDeclaration(originalName);
|
|
121
|
+
if (localDecl) return localDecl;
|
|
122
|
+
const binding = findImportBindingForLocalName(file, originalName);
|
|
123
|
+
if (!binding) return undefined;
|
|
124
|
+
const target = resolveModuleFile(file, binding.moduleSpecifier);
|
|
125
|
+
if (!target) return undefined;
|
|
126
|
+
return resolveExportedVariable(target, binding.importedName, visited, depth + 1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveViaStarExport(
|
|
130
|
+
file: SourceFile,
|
|
131
|
+
exportedName: string,
|
|
132
|
+
visited: Set<string>,
|
|
133
|
+
depth: number,
|
|
134
|
+
): VariableDeclaration | undefined {
|
|
135
|
+
for (const exportDecl of file.getExportDeclarations()) {
|
|
136
|
+
if (!exportDecl.hasModuleSpecifier()) continue;
|
|
137
|
+
if (exportDecl.getNamedExports().length > 0) continue;
|
|
138
|
+
if (exportDecl.getNamespaceExport()) continue; // `export * as ns from "./m"` is not a re-export target.
|
|
139
|
+
const moduleSpecifierValue = exportDecl.getModuleSpecifierValue();
|
|
140
|
+
if (!moduleSpecifierValue) continue;
|
|
141
|
+
const target = resolveModuleFile(file, moduleSpecifierValue);
|
|
142
|
+
if (!target) continue;
|
|
143
|
+
const resolved = resolveExportedVariable(target, exportedName, visited, depth + 1);
|
|
144
|
+
if (resolved) return resolved;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A function parameter of the same name shadows and stops the search — it has no statically readable initializer. */
|
|
150
|
+
export function findScopedVariableDeclaration(
|
|
151
|
+
identifier: Node,
|
|
152
|
+
name: string,
|
|
153
|
+
): VariableDeclaration | undefined {
|
|
154
|
+
for (const ancestor of [identifier, ...identifier.getAncestors()]) {
|
|
155
|
+
const params = getShadowingParameters(ancestor);
|
|
156
|
+
if (params?.some((p) => p.getName() === name)) return undefined;
|
|
157
|
+
|
|
158
|
+
const statements = getDirectStatements(ancestor);
|
|
159
|
+
if (!statements) continue;
|
|
160
|
+
for (const stmt of statements) {
|
|
161
|
+
const varStmt = stmt.asKind(SyntaxKind.VariableStatement);
|
|
162
|
+
if (!varStmt) continue;
|
|
163
|
+
const decl = varStmt
|
|
164
|
+
.getDeclarationList()
|
|
165
|
+
.getDeclarations()
|
|
166
|
+
.find((d) => d.getName() === name);
|
|
167
|
+
if (decl) return decl;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getShadowingParameters(node: Node): readonly ParameterDeclaration[] | undefined {
|
|
174
|
+
const fn =
|
|
175
|
+
node.asKind(SyntaxKind.ArrowFunction) ??
|
|
176
|
+
node.asKind(SyntaxKind.FunctionExpression) ??
|
|
177
|
+
node.asKind(SyntaxKind.FunctionDeclaration);
|
|
178
|
+
return fn?.getParameters();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getDirectStatements(node: Node): readonly Statement[] | undefined {
|
|
182
|
+
const block = node.asKind(SyntaxKind.Block);
|
|
183
|
+
if (block) return block.getStatements();
|
|
184
|
+
const sourceFile = node.asKind(SyntaxKind.SourceFile);
|
|
185
|
+
if (sourceFile) return sourceFile.getStatements();
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
@@ -2,6 +2,12 @@ import type { CallExpression, Node, ObjectLiteralExpression } from "ts-morph";
|
|
|
2
2
|
import { SyntaxKind } from "ts-morph";
|
|
3
3
|
import { isPlainObject } from "../../../utils/is-plain-object";
|
|
4
4
|
import type { ParseError } from "../parse";
|
|
5
|
+
import {
|
|
6
|
+
findImportBindingForLocalName,
|
|
7
|
+
findScopedVariableDeclaration,
|
|
8
|
+
resolveExportedVariable,
|
|
9
|
+
resolveModuleFile,
|
|
10
|
+
} from "./resolve-import";
|
|
5
11
|
|
|
6
12
|
export type ExtractOutput<TPattern> =
|
|
7
13
|
| { readonly kind: "pattern"; readonly pattern: TPattern }
|
|
@@ -200,28 +206,31 @@ function unwrapLiteralInitializer(node: Node): string | undefined {
|
|
|
200
206
|
/**
|
|
201
207
|
* Resolves a bare Identifier to the string value of its declaration's
|
|
202
208
|
* initializer (`export const EXT_TENANT_DATA = "tenant-data" as const`),
|
|
203
|
-
* following imports
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* function/class/type definition or an unresolvable import
|
|
207
|
-
*
|
|
209
|
+
* following imports syntactically across files and packages against a
|
|
210
|
+
* real-filesystem Project without ever building a TypeScript program (see
|
|
211
|
+
* resolve-import.ts). Only descends into VariableDeclaration initializers; a
|
|
212
|
+
* function/class/type definition or an unresolvable import yields undefined,
|
|
213
|
+
* never a throw.
|
|
208
214
|
*/
|
|
209
215
|
function resolveIdentifierToStringLiteral(identifier: Node): string | undefined {
|
|
210
216
|
const id = identifier.asKind(SyntaxKind.Identifier);
|
|
211
217
|
if (!id) return undefined;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const def of defs) {
|
|
219
|
-
const init = def.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
|
|
220
|
-
if (!init) continue;
|
|
221
|
-
const value = unwrapLiteralInitializer(init);
|
|
218
|
+
const name = id.getText();
|
|
219
|
+
|
|
220
|
+
const scoped = findScopedVariableDeclaration(id, name);
|
|
221
|
+
if (scoped) {
|
|
222
|
+
const init = scoped.getInitializer();
|
|
223
|
+
const value = init ? unwrapLiteralInitializer(init) : undefined;
|
|
222
224
|
if (value !== undefined) return value;
|
|
223
225
|
}
|
|
224
|
-
|
|
226
|
+
|
|
227
|
+
const binding = findImportBindingForLocalName(id.getSourceFile(), name);
|
|
228
|
+
if (!binding) return undefined;
|
|
229
|
+
const targetFile = resolveModuleFile(id.getSourceFile(), binding.moduleSpecifier);
|
|
230
|
+
if (!targetFile) return undefined;
|
|
231
|
+
const decl = resolveExportedVariable(targetFile, binding.importedName);
|
|
232
|
+
const init = decl?.getInitializer();
|
|
233
|
+
return init ? unwrapLiteralInitializer(init) : undefined;
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
/**
|
|
@@ -279,9 +288,7 @@ export function resolveSameFileObjectLiteral(node: Node): ObjectLiteralExpressio
|
|
|
279
288
|
if (direct) return direct;
|
|
280
289
|
const identifier = node.asKind(SyntaxKind.Identifier);
|
|
281
290
|
if (!identifier) return undefined;
|
|
282
|
-
const
|
|
283
|
-
const fromSymbol = valueDecl?.asKind(SyntaxKind.VariableDeclaration);
|
|
284
|
-
const varDecl = fromSymbol ?? node.getSourceFile().getVariableDeclaration(identifier.getText());
|
|
291
|
+
const varDecl = findScopedVariableDeclaration(identifier, identifier.getText());
|
|
285
292
|
return varDecl?.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
286
293
|
}
|
|
287
294
|
|
|
@@ -73,7 +73,9 @@ import {
|
|
|
73
73
|
extractWorkspace,
|
|
74
74
|
extractWriteHandler,
|
|
75
75
|
findFunctionLiteral,
|
|
76
|
+
findImportBindingForLocalName,
|
|
76
77
|
readObjectPropertyInitializer,
|
|
78
|
+
resolveModuleFile,
|
|
77
79
|
resolveSameFileObjectLiteral,
|
|
78
80
|
} from "./extractors";
|
|
79
81
|
import type { FeaturePattern, UnknownPattern } from "./patterns";
|
|
@@ -296,13 +298,13 @@ function walkSetupCallback(
|
|
|
296
298
|
/**
|
|
297
299
|
* Resolves a bare call like `registerShowPonyScreens(r)` to the callee's
|
|
298
300
|
* body + its own parameter name for the registrar slot. The callee may be
|
|
299
|
-
* declared in the same file
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
301
|
+
* declared in the same file, or imported by name from another file —
|
|
302
|
+
* resolved syntactically via resolveModuleFile, which only works against a
|
|
303
|
+
* real-filesystem Project; the in-memory Designer Project has no files to
|
|
304
|
+
* resolve against and returns undefined there, same as any other
|
|
305
|
+
* unresolvable reference. Does not follow re-exports in the target file — a
|
|
306
|
+
* wrapper function is expected to be declared directly in the module it's
|
|
307
|
+
* imported from.
|
|
306
308
|
*/
|
|
307
309
|
function resolveRegistrarWrapperCall(
|
|
308
310
|
call: CallExpression,
|
|
@@ -328,21 +330,12 @@ function resolveRegistrarWrapperCall(
|
|
|
328
330
|
const local = resolveWrapperDeclarationInFile(callSourceFile, name, argIndex);
|
|
329
331
|
if (local) return { ...local, sourceFile: callSourceFile };
|
|
330
332
|
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
d.getNamedImports().some((ni) => (ni.getAliasNode()?.getText() ?? ni.getName()) === name),
|
|
335
|
-
);
|
|
336
|
-
if (!importDecl) return undefined;
|
|
337
|
-
const exportedName =
|
|
338
|
-
importDecl
|
|
339
|
-
.getNamedImports()
|
|
340
|
-
.find((ni) => (ni.getAliasNode()?.getText() ?? ni.getName()) === name)
|
|
341
|
-
?.getName() ?? name;
|
|
342
|
-
const targetFile = importDecl.getModuleSpecifierSourceFile();
|
|
333
|
+
const binding = findImportBindingForLocalName(callSourceFile, name);
|
|
334
|
+
if (!binding) return undefined;
|
|
335
|
+
const targetFile = resolveModuleFile(callSourceFile, binding.moduleSpecifier);
|
|
343
336
|
if (!targetFile) return undefined; // Unresolvable: in-memory project, external package, or missing file.
|
|
344
337
|
|
|
345
|
-
const imported = resolveWrapperDeclarationInFile(targetFile,
|
|
338
|
+
const imported = resolveWrapperDeclarationInFile(targetFile, binding.importedName, argIndex);
|
|
346
339
|
return imported ? { ...imported, sourceFile: targetFile } : undefined;
|
|
347
340
|
}
|
|
348
341
|
|
package/src/engine/index.ts
CHANGED
|
@@ -479,6 +479,7 @@ export type {
|
|
|
479
479
|
PersonalAnnotationsNoFind,
|
|
480
480
|
PersonalSubject,
|
|
481
481
|
PlatformComponent,
|
|
482
|
+
PluralForms,
|
|
482
483
|
PostDeleteHookFn,
|
|
483
484
|
PostSaveHookFn,
|
|
484
485
|
PreDeleteHookFn,
|
|
@@ -528,8 +529,10 @@ export type {
|
|
|
528
529
|
TenantId,
|
|
529
530
|
TextFieldDef,
|
|
530
531
|
ToolbarAction,
|
|
532
|
+
TranslationEntry,
|
|
531
533
|
TranslationKeys,
|
|
532
534
|
TranslationsDef,
|
|
535
|
+
TranslationValue,
|
|
533
536
|
TreeAction,
|
|
534
537
|
TreeActionDef,
|
|
535
538
|
TreeActionsHandle,
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
SecretKeyDefinition,
|
|
39
39
|
StoreTableDef,
|
|
40
40
|
StreamHandlerDef,
|
|
41
|
+
TranslationValue,
|
|
41
42
|
TreeActionDef,
|
|
42
43
|
WorkspaceDefinition,
|
|
43
44
|
WriteHandlerDef,
|
|
@@ -212,7 +213,7 @@ export type RegistryState = {
|
|
|
212
213
|
extensionSelectorMap: Map<string, string>;
|
|
213
214
|
allReferenceData: ReferenceDataDef[];
|
|
214
215
|
allConfigSeeds: ConfigSeedDef[];
|
|
215
|
-
mergedTranslations: Record<string, Record<string,
|
|
216
|
+
mergedTranslations: Record<string, Record<string, TranslationValue>>;
|
|
216
217
|
metricMap: Map<string, FeatureMetricDef & { readonly featureName: string }>;
|
|
217
218
|
secretKeyMap: Map<string, SecretKeyDefinition>;
|
|
218
219
|
projectionMap: Map<string, ProjectionDefinition>;
|
|
@@ -25,9 +25,11 @@ export {
|
|
|
25
25
|
|
|
26
26
|
type WebhookHttpMethod = "POST" | "PUT" | "PATCH";
|
|
27
27
|
|
|
28
|
+
// Name inside the tenant-owned secrets namespace
|
|
29
|
+
// `step-dispatcher:webhook-auth.<secret>` (secrets feature).
|
|
28
30
|
type WebhookAuth =
|
|
29
|
-
| { readonly kind: "bearer"; readonly
|
|
30
|
-
| { readonly kind: "header"; readonly name: string; readonly
|
|
31
|
+
| { readonly kind: "bearer"; readonly secret: string }
|
|
32
|
+
| { readonly kind: "header"; readonly name: string; readonly secret: string };
|
|
31
33
|
|
|
32
34
|
type WebhookSendArgs = {
|
|
33
35
|
readonly url: StepResolver<string>;
|
|
@@ -42,6 +42,7 @@ export type {
|
|
|
42
42
|
NotificationDefinition,
|
|
43
43
|
NotificationRecipientFn,
|
|
44
44
|
NotificationTemplateFn,
|
|
45
|
+
PluralForms,
|
|
45
46
|
ReferenceDataDef,
|
|
46
47
|
RegistrarExtensionDef,
|
|
47
48
|
RegistrarExtensionHooks,
|
|
@@ -50,6 +51,7 @@ export type {
|
|
|
50
51
|
TranslationEntry,
|
|
51
52
|
TranslationKeys,
|
|
52
53
|
TranslationsDef,
|
|
54
|
+
TranslationValue,
|
|
53
55
|
UiExtensionDef,
|
|
54
56
|
} from "@cosmicdrift/kumiko-types/config";
|
|
55
57
|
export type {
|
package/src/errors/i18n/de.yaml
CHANGED
|
@@ -159,6 +159,20 @@ public_intake_required:
|
|
|
159
159
|
Wurzel-Handler `access: { roles: [..., "anonymous"], personalData: "public-intake" }`
|
|
160
160
|
deklarieren.
|
|
161
161
|
|
|
162
|
+
irreversible_operation_requires_high_risk:
|
|
163
|
+
endUser: |
|
|
164
|
+
Diese Aktion ist nicht erlaubt.
|
|
165
|
+
Wende dich an einen Administrator wenn du glaubst, dass das ein Fehler ist.
|
|
166
|
+
developer: |
|
|
167
|
+
`AccessDeniedError`: der direkt aufgerufene Einstiegs-Handler führt ein
|
|
168
|
+
hartes Delete oder `forget` aus (`details.operation`), aber sein
|
|
169
|
+
aufgelöstes Agent-Risiko (`details.resolvedRisk`) liegt unter `"high"`.
|
|
170
|
+
`details.handler` nennt den Einstiegs-Handler — eine Delegation über
|
|
171
|
+
`ctx.write`/`writeAs` aus einem Handler mit niedrigerem Risiko erbt das
|
|
172
|
+
deklarierte Risiko des inneren Handlers nicht.
|
|
173
|
+
|
|
174
|
+
`agent: { risk: "high" }` am genannten Handler deklarieren.
|
|
175
|
+
|
|
162
176
|
agent.tool_not_allowed:
|
|
163
177
|
endUser: |
|
|
164
178
|
Dieses Tool ist im aktuellen Modus nicht verfügbar.
|
package/src/errors/i18n/en.yaml
CHANGED
|
@@ -153,6 +153,19 @@ public_intake_required:
|
|
|
153
153
|
`access: { roles: [..., "anonymous"], personalData: "public-intake" }` on
|
|
154
154
|
the root handler if the anonymous intake is intended.
|
|
155
155
|
|
|
156
|
+
irreversible_operation_requires_high_risk:
|
|
157
|
+
endUser: |
|
|
158
|
+
This action isn't allowed.
|
|
159
|
+
Please contact an administrator if you believe this is a mistake.
|
|
160
|
+
developer: |
|
|
161
|
+
`AccessDeniedError`: the directly-dispatched entry handler performs a hard
|
|
162
|
+
delete or `forget` (`details.operation`) but its resolved agent risk
|
|
163
|
+
(`details.resolvedRisk`) is below `"high"`. `details.handler` names the
|
|
164
|
+
entry handler — delegating the call via `ctx.write`/`writeAs` from a
|
|
165
|
+
lower-risk handler does not inherit an inner handler's declared risk.
|
|
166
|
+
|
|
167
|
+
Declare `agent: { risk: "high" }` on the named handler.
|
|
168
|
+
|
|
156
169
|
agent.tool_not_allowed:
|
|
157
170
|
endUser: |
|
|
158
171
|
This tool is not available in the current mode.
|
package/src/errors/reasons.ts
CHANGED
|
@@ -63,6 +63,11 @@ export const FrameworkReasons = {
|
|
|
63
63
|
// AccessDeniedError: a write under an anonymous root touched a personal-data field
|
|
64
64
|
// without the root handler declaring access.personalData: "public-intake".
|
|
65
65
|
publicIntakeRequired: "public_intake_required",
|
|
66
|
+
|
|
67
|
+
// AccessDeniedError: the directly-dispatched entry handler performs a hard
|
|
68
|
+
// delete/forget but its agent.risk resolves below "high" — delegation via
|
|
69
|
+
// ctx.write/writeAs from a lower-risk handler does not inherit the gate.
|
|
70
|
+
irreversibleOperationRequiresHighRisk: "irreversible_operation_requires_high_risk",
|
|
66
71
|
} as const;
|
|
67
72
|
|
|
68
73
|
export type FrameworkReason = (typeof FrameworkReasons)[keyof typeof FrameworkReasons];
|