@telorun/kernel 0.12.0 → 0.13.2
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/LICENSE +2 -2
- package/dist/application-env.d.ts +24 -0
- package/dist/application-env.d.ts.map +1 -0
- package/dist/application-env.js +156 -0
- package/dist/application-env.js.map +1 -0
- package/dist/controller-loaders/npm-loader.d.ts +32 -8
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +74 -101
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +3 -2
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +23 -25
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +3 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts +5 -0
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +67 -6
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/internal-context.d.ts +25 -0
- package/dist/internal-context.d.ts.map +1 -0
- package/dist/internal-context.js +2 -0
- package/dist/internal-context.js.map +1 -0
- package/dist/kernel.d.ts +21 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +109 -5
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +7 -23
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +18 -8
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/manifest-sources/analysis-stamp.d.ts +25 -0
- package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -0
- package/dist/manifest-sources/analysis-stamp.js +151 -0
- package/dist/manifest-sources/analysis-stamp.js.map +1 -0
- package/dist/resource-context.d.ts +2 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +28 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-validator.d.ts +28 -0
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +161 -1
- package/dist/schema-validator.js.map +1 -1
- package/package.json +9 -6
- package/src/application-env.ts +216 -0
- package/src/controller-loaders/npm-loader.ts +78 -103
- package/src/controllers/module/import-controller.ts +33 -36
- package/src/controllers/resource-definition/resource-definition-controller.ts +6 -0
- package/src/controllers/resource-definition/resource-template-controller.ts +95 -7
- package/src/internal-context.ts +25 -0
- package/src/kernel.ts +130 -5
- package/src/manifest-schemas.ts +31 -11
- package/src/manifest-sources/analysis-stamp.ts +169 -0
- package/src/resource-context.ts +34 -0
- package/src/schema-validator.ts +178 -2
- package/dist/generated/runtime-deps.json +0 -6
package/src/schema-validator.ts
CHANGED
|
@@ -1,22 +1,104 @@
|
|
|
1
1
|
import { evaluate } from "@marcbachmann/cel-js";
|
|
2
2
|
import { DataValidator, RuntimeError, TypeRule } from "@telorun/sdk";
|
|
3
|
-
import AjvModule from "ajv";
|
|
3
|
+
import AjvModule, { type ValidateFunction } from "ajv";
|
|
4
|
+
import standaloneCodeMod from "ajv/dist/standalone/index.js";
|
|
4
5
|
import addFormats from "ajv-formats";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import * as path from "node:path";
|
|
5
10
|
import { formatAjvErrors } from "./manifest-schemas.js";
|
|
11
|
+
import { ManifestRootSchema } from "@telorun/templating";
|
|
6
12
|
|
|
7
13
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
14
|
+
// AJV's standalone subpath is CJS — the default export shows up as either
|
|
15
|
+
// the function itself or `.default` depending on how the bundler/loader
|
|
16
|
+
// rewrites it. Normalise once.
|
|
17
|
+
const standaloneCode: (...args: any[]) => string =
|
|
18
|
+
(standaloneCodeMod as any).default ?? (standaloneCodeMod as any);
|
|
19
|
+
|
|
20
|
+
/** `require` resolved from this file's URL — used to satisfy `ajv/dist/...`
|
|
21
|
+
* / `ajv-formats/...` imports embedded in standalone-compiled validators
|
|
22
|
+
* loaded back off disk. Anchored here so it always resolves through the
|
|
23
|
+
* kernel package's node_modules, regardless of where the cache file
|
|
24
|
+
* lives on disk. */
|
|
25
|
+
const kernelRequire = createRequire(import.meta.url);
|
|
26
|
+
|
|
27
|
+
/** Resolved AJV + ajv-formats versions, baked into every cache key so a
|
|
28
|
+
* pnpm/npm install that upgrades either package invalidates all stale
|
|
29
|
+
* `<hash>.cjs` files automatically. Standalone-compiled validators
|
|
30
|
+
* embed `require("ajv/dist/runtime/...")` — running a validator built
|
|
31
|
+
* against an older AJV against the current runtime is undefined
|
|
32
|
+
* behaviour, so the version pin must be part of the hash, not a manual
|
|
33
|
+
* bump. Falls back to walking up from the package's main entry when
|
|
34
|
+
* the dependency restricts subpath access via `exports`. */
|
|
35
|
+
function readDepVersion(spec: string): string {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = kernelRequire(`${spec}/package.json`);
|
|
38
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
39
|
+
} catch {
|
|
40
|
+
// restricted exports — try the filesystem walk below
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const entry = kernelRequire.resolve(spec);
|
|
44
|
+
let dir = path.dirname(entry);
|
|
45
|
+
while (dir !== path.dirname(dir)) {
|
|
46
|
+
const candidate = path.join(dir, "package.json");
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
|
|
49
|
+
const expectedName = spec.split("/").slice(0, spec.startsWith("@") ? 2 : 1).join("/");
|
|
50
|
+
if (typeof pkg.name === "string" && pkg.name === expectedName) {
|
|
51
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// keep walking — not at the package root yet
|
|
55
|
+
}
|
|
56
|
+
dir = path.dirname(dir);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// package not installed
|
|
60
|
+
}
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
const AJV_VERSION = readDepVersion("ajv");
|
|
64
|
+
const AJV_FORMATS_VERSION = readDepVersion("ajv-formats");
|
|
65
|
+
const VALIDATOR_RUNTIME_TAG = `ajv@${AJV_VERSION}+ajv-formats@${AJV_FORMATS_VERSION}`;
|
|
66
|
+
|
|
67
|
+
const SHA256_HEADER_PATTERN = /^\/\/ sha256:([0-9a-f]{64})\n/;
|
|
68
|
+
|
|
69
|
+
/** Verify a cached validator file's SHA-256 integrity header and return
|
|
70
|
+
* the body when the digest matches. Returns `null` on any mismatch /
|
|
71
|
+
* malformed header — the caller treats that as a cache miss and
|
|
72
|
+
* recompiles + overwrites the file. */
|
|
73
|
+
function verifyAndExtractBody(text: string): string | null {
|
|
74
|
+
const match = text.match(SHA256_HEADER_PATTERN);
|
|
75
|
+
if (!match) return null;
|
|
76
|
+
const body = text.slice(match[0].length);
|
|
77
|
+
const actual = createHash("sha256").update(body).digest("hex");
|
|
78
|
+
return actual === match[1] ? body : null;
|
|
79
|
+
}
|
|
8
80
|
|
|
9
81
|
export class SchemaValidator {
|
|
10
82
|
private ajv: InstanceType<typeof Ajv>;
|
|
11
83
|
private typeRules = new Map<string, TypeRule[]>();
|
|
12
84
|
private rawSchemas = new Map<string, object>();
|
|
13
85
|
private compiledValidators = new WeakMap<object, DataValidator>();
|
|
86
|
+
private cacheDir: string | undefined;
|
|
87
|
+
/** Tracks (schema-hash → in-memory compiled validator) so two distinct
|
|
88
|
+
* but content-equal schema objects share one compile across the kernel
|
|
89
|
+
* process — `compiledValidators` is keyed by object identity and would
|
|
90
|
+
* miss those cases. */
|
|
91
|
+
private hashCache = new Map<string, DataValidator>();
|
|
14
92
|
|
|
15
93
|
constructor() {
|
|
16
94
|
this.ajv = new Ajv({
|
|
17
95
|
strict: false,
|
|
18
96
|
removeAdditional: false,
|
|
19
97
|
useDefaults: true,
|
|
98
|
+
// Required for `standaloneCode` extraction — tells AJV to keep the
|
|
99
|
+
// generated validator's source available rather than wrapping it
|
|
100
|
+
// through `new Function`. The cost at compile time is negligible.
|
|
101
|
+
code: { source: true },
|
|
20
102
|
});
|
|
21
103
|
addFormats.default(this.ajv);
|
|
22
104
|
for (const kw of [
|
|
@@ -33,6 +115,10 @@ export class SchemaValidator {
|
|
|
33
115
|
]) {
|
|
34
116
|
this.ajv.addKeyword(kw);
|
|
35
117
|
}
|
|
118
|
+
// Register the shared manifest root so module schemas can
|
|
119
|
+
// `$ref: "telo://manifest#/$defs/ResourceRef"` without each manifest
|
|
120
|
+
// bundling its own copy. Mirrors the analyzer's createAjv().
|
|
121
|
+
this.ajv.addSchema(ManifestRootSchema);
|
|
36
122
|
}
|
|
37
123
|
|
|
38
124
|
addSchema(name: string, schema: object): void {
|
|
@@ -54,6 +140,19 @@ export class SchemaValidator {
|
|
|
54
140
|
return this.typeRules.get(name);
|
|
55
141
|
}
|
|
56
142
|
|
|
143
|
+
/** Enable the on-disk validator cache rooted at `dir`. Compiled AJV
|
|
144
|
+
* validators are written as standalone CJS modules keyed by content
|
|
145
|
+
* hash, so subsequent process invocations skip the ≈2–10 ms AJV
|
|
146
|
+
* codegen for each unseen schema. Safe to call before or after
|
|
147
|
+
* `compile()` — already-compiled in-memory entries are unaffected.
|
|
148
|
+
* The caller is responsible for choosing a writable directory; the
|
|
149
|
+
* kernel anchors this under `<entry-dir>/.telo/manifests/__validators/`
|
|
150
|
+
* so it lives next to the manifest cache and rides along in
|
|
151
|
+
* `COPY --from=build /srv /srv` Docker images. */
|
|
152
|
+
setCacheDir(dir: string | undefined): void {
|
|
153
|
+
this.cacheDir = dir;
|
|
154
|
+
}
|
|
155
|
+
|
|
57
156
|
compile(schema: any): DataValidator {
|
|
58
157
|
if (schema && typeof schema === "object") {
|
|
59
158
|
const cached = this.compiledValidators.get(schema as object);
|
|
@@ -85,7 +184,20 @@ export class SchemaValidator {
|
|
|
85
184
|
},
|
|
86
185
|
}
|
|
87
186
|
: normalized;
|
|
88
|
-
|
|
187
|
+
|
|
188
|
+
const hash = createHash("sha256")
|
|
189
|
+
.update(JSON.stringify({ runtime: VALIDATOR_RUNTIME_TAG, schema: injected }))
|
|
190
|
+
.digest("hex")
|
|
191
|
+
.slice(0, 32);
|
|
192
|
+
const cachedByHash = this.hashCache.get(hash);
|
|
193
|
+
if (cachedByHash) {
|
|
194
|
+
if (schema && typeof schema === "object") {
|
|
195
|
+
this.compiledValidators.set(schema as object, cachedByHash);
|
|
196
|
+
}
|
|
197
|
+
return cachedByHash;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const validate = this.compileAjvOrLoadCached(injected, hash);
|
|
89
201
|
|
|
90
202
|
const validator = {
|
|
91
203
|
validate: (data: any) => {
|
|
@@ -102,6 +214,7 @@ export class SchemaValidator {
|
|
|
102
214
|
},
|
|
103
215
|
};
|
|
104
216
|
|
|
217
|
+
this.hashCache.set(hash, validator);
|
|
105
218
|
if (schema && typeof schema === "object") {
|
|
106
219
|
this.compiledValidators.set(schema as object, validator);
|
|
107
220
|
}
|
|
@@ -109,6 +222,69 @@ export class SchemaValidator {
|
|
|
109
222
|
return validator;
|
|
110
223
|
}
|
|
111
224
|
|
|
225
|
+
/** Load `<cacheDir>/<hash>.cjs` if present, else compile via AJV and
|
|
226
|
+
* persist as standalone CJS. Cached files start with a
|
|
227
|
+
* `// sha256:<hex>\n` header covering the rest of the file; a
|
|
228
|
+
* mismatch (truncated write, FS corruption, tampering inside a baked
|
|
229
|
+
* Docker image) is treated as a cache miss and the validator is
|
|
230
|
+
* recompiled — and overwritten — so the cache self-heals. The cached
|
|
231
|
+
* body is wrapped so its embedded `require("ajv/...")` /
|
|
232
|
+
* `require("ajv-formats/...")` calls resolve against the kernel
|
|
233
|
+
* package; the cache file lives outside any `node_modules` tree, so a
|
|
234
|
+
* bare `require()` from its own path would fail. Read/write failures
|
|
235
|
+
* surface to stderr but never abort compilation. */
|
|
236
|
+
private compileAjvOrLoadCached(
|
|
237
|
+
schema: any,
|
|
238
|
+
hash: string,
|
|
239
|
+
): ValidateFunction {
|
|
240
|
+
const cacheDir = this.cacheDir;
|
|
241
|
+
if (cacheDir) {
|
|
242
|
+
const cachePath = path.join(cacheDir, `${hash}.cjs`);
|
|
243
|
+
try {
|
|
244
|
+
const text = fs.readFileSync(cachePath, "utf-8");
|
|
245
|
+
const body = verifyAndExtractBody(text);
|
|
246
|
+
if (body !== null) {
|
|
247
|
+
const factory = new Function(
|
|
248
|
+
"require",
|
|
249
|
+
"module",
|
|
250
|
+
"exports",
|
|
251
|
+
`${body}\nreturn module.exports;`,
|
|
252
|
+
);
|
|
253
|
+
const mod: { exports: any } = { exports: {} };
|
|
254
|
+
const loaded = factory(kernelRequire, mod, mod.exports);
|
|
255
|
+
if (typeof loaded === "function") {
|
|
256
|
+
return loaded as ValidateFunction;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Header missing / mismatched / non-function export — fall
|
|
260
|
+
// through and recompile. The write step below overwrites the
|
|
261
|
+
// stale file with a fresh hash header.
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
`[telo:kernel] validator cache load failed (${hash}): ${err instanceof Error ? err.message : String(err)}\n`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const validate = this.ajv.compile(schema) as ValidateFunction;
|
|
272
|
+
if (cacheDir) {
|
|
273
|
+
try {
|
|
274
|
+
const body = standaloneCode(this.ajv, validate);
|
|
275
|
+
const integrity = createHash("sha256").update(body).digest("hex");
|
|
276
|
+
const payload = `// sha256:${integrity}\n${body}`;
|
|
277
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
278
|
+
fs.writeFileSync(path.join(cacheDir, `${hash}.cjs`), payload, "utf-8");
|
|
279
|
+
} catch (err) {
|
|
280
|
+
process.stderr.write(
|
|
281
|
+
`[telo:kernel] validator cache write failed (${hash}): ${err instanceof Error ? err.message : String(err)}\n`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return validate;
|
|
286
|
+
}
|
|
287
|
+
|
|
112
288
|
composeWithRules(base: DataValidator, typeName: string, rules: TypeRule[]): DataValidator {
|
|
113
289
|
return {
|
|
114
290
|
validate: (data: any) => {
|