@telorun/kernel 0.2.4 → 0.2.6

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.
Files changed (192) hide show
  1. package/dist/base-definition.d.ts +14 -0
  2. package/dist/base-definition.d.ts.map +1 -0
  3. package/dist/base-definition.js +17 -0
  4. package/dist/base-definition.js.map +1 -0
  5. package/dist/boot-context-registry.d.ts.map +1 -1
  6. package/dist/boot-context-registry.js +6 -6
  7. package/dist/boot-context-registry.js.map +1 -1
  8. package/dist/capabilities/capabilities/component.yaml +2 -1
  9. package/dist/capabilities/capabilities/executable.yaml +2 -1
  10. package/dist/capabilities/capabilities/handler.yaml +2 -1
  11. package/dist/capabilities/capabilities/listener.yaml +2 -1
  12. package/dist/capabilities/capabilities/provider.yaml +2 -1
  13. package/dist/capabilities/capabilities/template.yaml +2 -1
  14. package/dist/capabilities/capabilities/type.yaml +2 -1
  15. package/dist/capabilities/component.d.ts +3 -0
  16. package/dist/capabilities/component.d.ts.map +1 -0
  17. package/dist/capabilities/component.js +4 -0
  18. package/dist/capabilities/component.js.map +1 -0
  19. package/dist/capabilities/component.yaml +1 -1
  20. package/dist/capabilities/executable.d.ts +3 -0
  21. package/dist/capabilities/executable.d.ts.map +1 -0
  22. package/dist/capabilities/executable.js +5 -0
  23. package/dist/capabilities/executable.js.map +1 -0
  24. package/dist/capabilities/executable.yaml +1 -1
  25. package/dist/capabilities/handler.d.ts +3 -0
  26. package/dist/capabilities/handler.d.ts.map +1 -0
  27. package/dist/capabilities/handler.js +4 -0
  28. package/dist/capabilities/handler.js.map +1 -0
  29. package/dist/capabilities/handler.yaml +1 -1
  30. package/dist/capabilities/invokable.d.ts +3 -0
  31. package/dist/capabilities/invokable.d.ts.map +1 -0
  32. package/dist/capabilities/invokable.js +5 -0
  33. package/dist/capabilities/invokable.js.map +1 -0
  34. package/dist/capabilities/listener.d.ts +3 -0
  35. package/dist/capabilities/listener.d.ts.map +1 -0
  36. package/dist/capabilities/listener.js +5 -0
  37. package/dist/capabilities/listener.js.map +1 -0
  38. package/dist/capabilities/listener.yaml +1 -1
  39. package/dist/capabilities/mount.d.ts +3 -0
  40. package/dist/capabilities/mount.d.ts.map +1 -0
  41. package/dist/capabilities/mount.js +5 -0
  42. package/dist/capabilities/mount.js.map +1 -0
  43. package/dist/capabilities/provider.d.ts +3 -0
  44. package/dist/capabilities/provider.d.ts.map +1 -0
  45. package/dist/capabilities/provider.js +8 -0
  46. package/dist/capabilities/provider.js.map +1 -0
  47. package/dist/capabilities/provider.yaml +1 -1
  48. package/dist/capabilities/runnable.d.ts +3 -0
  49. package/dist/capabilities/runnable.d.ts.map +1 -0
  50. package/dist/capabilities/runnable.js +5 -0
  51. package/dist/capabilities/runnable.js.map +1 -0
  52. package/dist/capabilities/service.d.ts +3 -0
  53. package/dist/capabilities/service.d.ts.map +1 -0
  54. package/dist/capabilities/service.js +5 -0
  55. package/dist/capabilities/service.js.map +1 -0
  56. package/dist/capabilities/template.d.ts +3 -0
  57. package/dist/capabilities/template.d.ts.map +1 -0
  58. package/dist/capabilities/template.js +5 -0
  59. package/dist/capabilities/template.js.map +1 -0
  60. package/dist/capabilities/template.yaml +1 -1
  61. package/dist/capabilities/type.d.ts +3 -0
  62. package/dist/capabilities/type.d.ts.map +1 -0
  63. package/dist/capabilities/type.js +5 -0
  64. package/dist/capabilities/type.js.map +1 -0
  65. package/dist/capabilities/type.yaml +1 -1
  66. package/dist/controller-loader.d.ts +1 -1
  67. package/dist/controller-loader.d.ts.map +1 -1
  68. package/dist/controller-loader.js +6 -3
  69. package/dist/controller-loader.js.map +1 -1
  70. package/dist/controller-registry.d.ts +1 -6
  71. package/dist/controller-registry.d.ts.map +1 -1
  72. package/dist/controller-registry.js +11 -22
  73. package/dist/controller-registry.js.map +1 -1
  74. package/dist/controllers/capability/capability-controller.d.ts +0 -5
  75. package/dist/controllers/capability/capability-controller.d.ts.map +1 -1
  76. package/dist/controllers/capability/capability-controller.js +1 -5
  77. package/dist/controllers/capability/capability-controller.js.map +1 -1
  78. package/dist/controllers/module/import-controller.d.ts +35 -0
  79. package/dist/controllers/module/import-controller.d.ts.map +1 -0
  80. package/dist/controllers/module/import-controller.js +113 -0
  81. package/dist/controllers/module/import-controller.js.map +1 -0
  82. package/dist/controllers/module/module-controller.d.ts +2 -15
  83. package/dist/controllers/module/module-controller.d.ts.map +1 -1
  84. package/dist/controllers/module/module-controller.js +9 -90
  85. package/dist/controllers/module/module-controller.js.map +1 -1
  86. package/dist/controllers/resource-definition/resource-definition-controller.d.ts +3 -4
  87. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  88. package/dist/controllers/resource-definition/resource-definition-controller.js +17 -11
  89. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  90. package/dist/controllers/resource-definition/resource-template-controller.d.ts +12 -0
  91. package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -0
  92. package/dist/controllers/resource-definition/resource-template-controller.js +112 -0
  93. package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -0
  94. package/dist/evaluation-context.d.ts +91 -0
  95. package/dist/evaluation-context.d.ts.map +1 -0
  96. package/dist/evaluation-context.js +220 -0
  97. package/dist/evaluation-context.js.map +1 -0
  98. package/dist/execution-context.d.ts +13 -0
  99. package/dist/execution-context.d.ts.map +1 -0
  100. package/dist/execution-context.js +14 -0
  101. package/dist/execution-context.js.map +1 -0
  102. package/dist/index.d.ts +1 -4
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +0 -3
  105. package/dist/index.js.map +1 -1
  106. package/dist/kernel.d.ts +31 -42
  107. package/dist/kernel.d.ts.map +1 -1
  108. package/dist/kernel.js +315 -371
  109. package/dist/kernel.js.map +1 -1
  110. package/dist/loader.d.ts +11 -10
  111. package/dist/loader.d.ts.map +1 -1
  112. package/dist/loader.js +50 -112
  113. package/dist/loader.js.map +1 -1
  114. package/dist/manifest-adapters/http-adapter.d.ts.map +1 -1
  115. package/dist/manifest-adapters/http-adapter.js +3 -1
  116. package/dist/manifest-adapters/http-adapter.js.map +1 -1
  117. package/dist/manifest-adapters/local-file-adapter.d.ts +7 -5
  118. package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
  119. package/dist/manifest-adapters/local-file-adapter.js +28 -24
  120. package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
  121. package/dist/manifest-adapters/manifest-adapter.d.ts +2 -0
  122. package/dist/manifest-adapters/manifest-adapter.d.ts.map +1 -1
  123. package/dist/manifest-adapters/registry-adapter.d.ts.map +1 -1
  124. package/dist/manifest-adapters/registry-adapter.js +3 -1
  125. package/dist/manifest-adapters/registry-adapter.js.map +1 -1
  126. package/dist/manifest-schemas.d.ts +61 -49
  127. package/dist/manifest-schemas.d.ts.map +1 -1
  128. package/dist/manifest-schemas.js +58 -37
  129. package/dist/manifest-schemas.js.map +1 -1
  130. package/dist/module-context-registry.d.ts +48 -0
  131. package/dist/module-context-registry.d.ts.map +1 -0
  132. package/dist/module-context-registry.js +91 -0
  133. package/dist/module-context-registry.js.map +1 -0
  134. package/dist/module-context.d.ts +31 -0
  135. package/dist/module-context.d.ts.map +1 -0
  136. package/dist/module-context.js +67 -0
  137. package/dist/module-context.js.map +1 -0
  138. package/dist/registry.d.ts +1 -2
  139. package/dist/registry.d.ts.map +1 -1
  140. package/dist/registry.js +3 -3
  141. package/dist/registry.js.map +1 -1
  142. package/dist/resource-context.d.ts +27 -10
  143. package/dist/resource-context.d.ts.map +1 -1
  144. package/dist/resource-context.js +100 -44
  145. package/dist/resource-context.js.map +1 -1
  146. package/dist/schema-valiator.d.ts.map +1 -1
  147. package/dist/schema-valiator.js +16 -3
  148. package/dist/schema-valiator.js.map +1 -1
  149. package/dist/snapshot-serializer.d.ts +1 -2
  150. package/dist/snapshot-serializer.d.ts.map +1 -1
  151. package/dist/snapshot-serializer.js.map +1 -1
  152. package/dist/types.d.ts +3 -0
  153. package/dist/types.d.ts.map +1 -1
  154. package/dist/types.js.map +1 -1
  155. package/package.json +11 -9
  156. package/src/boot-context-registry.ts +169 -0
  157. package/src/controller-loader.ts +299 -0
  158. package/src/controller-registry.ts +191 -0
  159. package/src/controllers/module/import-controller.ts +143 -0
  160. package/src/controllers/module/module-controller.ts +16 -0
  161. package/src/controllers/resource-definition/resource-definition-controller.ts +86 -0
  162. package/src/controllers/resource-definition/resource-definition.json +18 -0
  163. package/src/controllers/resource-definition/resource-template-controller.ts +138 -0
  164. package/src/event-stream.ts +121 -0
  165. package/src/events.ts +99 -0
  166. package/src/index.ts +7 -0
  167. package/src/kernel.ts +647 -0
  168. package/src/loader.ts +134 -0
  169. package/src/manifest-adapters/local-file-adapter.ts +62 -0
  170. package/src/manifest-adapters/manifest-adapter.ts +35 -0
  171. package/src/manifest-schemas.ts +85 -0
  172. package/src/registry.ts +137 -0
  173. package/src/resource-context.ts +267 -0
  174. package/src/resource-uri.ts +200 -0
  175. package/src/schema-valiator.ts +68 -0
  176. package/dist/cli.d.ts +0 -3
  177. package/dist/cli.d.ts.map +0 -1
  178. package/dist/cli.js +0 -109
  179. package/dist/cli.js.map +0 -1
  180. package/dist/expressions.d.ts +0 -20
  181. package/dist/expressions.d.ts.map +0 -1
  182. package/dist/expressions.js +0 -253
  183. package/dist/expressions.js.map +0 -1
  184. package/dist/src/controllers/module/module.yaml +0 -32
  185. package/dist/template-definition.d.ts +0 -38
  186. package/dist/template-definition.d.ts.map +0 -1
  187. package/dist/template-definition.js +0 -26
  188. package/dist/template-definition.js.map +0 -1
  189. package/dist/template-expander.d.ts +0 -19
  190. package/dist/template-expander.d.ts.map +0 -1
  191. package/dist/template-expander.js +0 -425
  192. package/dist/template-expander.js.map +0 -1
@@ -0,0 +1,169 @@
1
+ import { RuntimeError } from "@telorun/sdk";
2
+
3
+ /** Stored entry for a single context provider */
4
+ interface ProviderEntry {
5
+ kind: string;
6
+ name: string;
7
+ module: string;
8
+ /**
9
+ * If defined, only resources whose `"${kind}/${name}"` is in this list may
10
+ * access this provider's context. `undefined` means unrestricted.
11
+ * An empty array means no consumer is allowed.
12
+ */
13
+ grants: string[] | undefined;
14
+ context: Record<string, unknown>;
15
+ }
16
+
17
+ /**
18
+ * Accumulates ContextProvider contributions during the AOT boot initialization
19
+ * multi-pass loop and enforces RBAC visibility rules.
20
+ *
21
+ * Lifecycle:
22
+ * 1. A provider resource is created + init()'d → kernel calls register().
23
+ * 2. Before a consumer resource is created → kernel calls buildContext() to
24
+ * obtain a per-consumer CEL evaluation context.
25
+ * 3. buildContext() enforces grants and scans the raw manifest for unauthorized
26
+ * references, throwing ERR_VISIBILITY_DENIED on violations.
27
+ */
28
+ export class BootContextRegistry {
29
+ private providers: ProviderEntry[] = [];
30
+
31
+ /**
32
+ * Register a new context provider after its init() has completed.
33
+ * Called by the kernel for every resource instance that passes isContextProvider().
34
+ */
35
+ register(
36
+ kind: string,
37
+ name: string,
38
+ module: string,
39
+ grants: string[] | undefined,
40
+ context: Record<string, unknown>,
41
+ ): void {
42
+ this.providers.push({ kind, name, module, grants, context });
43
+ }
44
+
45
+ /**
46
+ * Returns true if at least one provider has been registered.
47
+ * Used by the kernel to skip context resolution when there are no providers yet.
48
+ */
49
+ hasProviders(): boolean {
50
+ return this.providers.length > 0;
51
+ }
52
+
53
+ /**
54
+ * Build a CEL evaluation context for a specific consumer resource, enforcing
55
+ * RBAC grants.
56
+ *
57
+ * For each registered provider:
58
+ * - `grants === undefined` → unrestricted, always included.
59
+ * - `grants` defined and consumer key in grants → included.
60
+ * - `grants` defined and consumer key NOT in grants:
61
+ * - Scan the raw manifest JSON for `${{ ... }}` expressions that reference
62
+ * the provider's namespace prefix (`"${kind}.${name}."`).
63
+ * - If a reference is found → throw ERR_VISIBILITY_DENIED immediately.
64
+ * - If no reference → silently exclude the provider from the context.
65
+ *
66
+ * CEL context structure mirrors the kind hierarchy:
67
+ * kind="Config", name="database" → { Config: { database: { host, port } } }
68
+ * kind="MyApp.Secrets", name="vault" → { MyApp: { Secrets: { vault: { ... } } } }
69
+ *
70
+ * @param consumerKind - kind of the resource being initialized (e.g. "MyApp.Api.Server")
71
+ * @param consumerName - name of the resource being initialized (e.g. "api")
72
+ * @param rawManifest - the unresolved manifest object (used for namespace reference scan)
73
+ */
74
+ buildContext(
75
+ consumerKind: string,
76
+ consumerName: string,
77
+ rawManifest: Record<string, unknown>,
78
+ ): Record<string, unknown> {
79
+ const consumerKey = `${consumerKind}/${consumerName}`;
80
+ const celContext: Record<string, unknown> = {};
81
+ const manifestJson = JSON.stringify(rawManifest);
82
+
83
+ for (const provider of this.providers) {
84
+ const isGranted = provider.grants === undefined || provider.grants.includes(consumerKey);
85
+
86
+ if (!isGranted) {
87
+ const namespacePrefix = buildProviderNamespacePrefix(provider.kind, provider.name);
88
+ if (manifestReferencesNamespace(manifestJson, namespacePrefix)) {
89
+ throw new RuntimeError(
90
+ "ERR_VISIBILITY_DENIED",
91
+ `Resource "${consumerKind}/${consumerName}" references context from ` +
92
+ `provider "${provider.kind}/${provider.name}" but is not listed in its grants. ` +
93
+ `Add "${consumerKey}" to the provider's grants field to allow access.`,
94
+ );
95
+ }
96
+ // Not referenced and not granted — silently exclude
97
+ continue;
98
+ }
99
+
100
+ mergeProviderIntoContext(celContext, provider.kind, provider.name, provider.context);
101
+ }
102
+
103
+ return celContext;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Build the namespace prefix string that appears inside a `${{ }}` expression
109
+ * when a consumer references this provider.
110
+ *
111
+ * The trailing dot ensures we match the start of a property access without
112
+ * false-matching a shared prefix substring:
113
+ * kind="Config", name="database" → "Config.database."
114
+ * kind="MyApp.Secrets", name="vault" → "MyApp.Secrets.vault."
115
+ */
116
+ function buildProviderNamespacePrefix(kind: string, name: string): string {
117
+ return `${kind}.${name}.`;
118
+ }
119
+
120
+ /**
121
+ * Scan serialized manifest JSON for any `${{ expression }}` block that contains
122
+ * the provider's namespace prefix.
123
+ *
124
+ * Using JSON.stringify() as the scan target covers all nested fields uniformly.
125
+ * The approach cannot produce false positives from field names or non-template
126
+ * values because `${{` is not valid JSON syntax outside a string value.
127
+ */
128
+ function manifestReferencesNamespace(manifestJson: string, namespacePrefix: string): boolean {
129
+ let searchFrom = 0;
130
+ while (true) {
131
+ const start = manifestJson.indexOf("${{", searchFrom);
132
+ if (start === -1) break;
133
+ const end = manifestJson.indexOf("}}", start + 3);
134
+ if (end === -1) break;
135
+ const expression = manifestJson.slice(start + 3, end);
136
+ if (expression.includes(namespacePrefix)) {
137
+ return true;
138
+ }
139
+ searchFrom = end + 2;
140
+ }
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Place provider.context into the CEL context object under the path
146
+ * defined by the provider's kind segments and name.
147
+ *
148
+ * kind="Config", name="database":
149
+ * context.Config = { database: { host, port } }
150
+ *
151
+ * kind="MyApp.Api", name="server":
152
+ * context.MyApp = { Api: { server: { ... } } }
153
+ */
154
+ function mergeProviderIntoContext(
155
+ celContext: Record<string, unknown>,
156
+ kind: string,
157
+ name: string,
158
+ providerData: Record<string, unknown>,
159
+ ): void {
160
+ const kindSegments = kind.split(".");
161
+ let cursor = celContext;
162
+ for (const segment of kindSegments) {
163
+ if (typeof cursor[segment] !== "object" || cursor[segment] === null) {
164
+ cursor[segment] = {};
165
+ }
166
+ cursor = cursor[segment] as Record<string, unknown>;
167
+ }
168
+ cursor[name] = providerData;
169
+ }
@@ -0,0 +1,299 @@
1
+ import { ControllerInstance, RuntimeError } from "@telorun/sdk";
2
+ import { execFile } from "child_process";
3
+ import { createHash } from "crypto";
4
+ import * as fs from "fs/promises";
5
+ import * as os from "os";
6
+ import { PackageURL } from "packageurl-js";
7
+ import * as path from "path";
8
+ import { promisify } from "util";
9
+
10
+ const homedir = os.homedir();
11
+ const cacheRoot = path.join(homedir, ".cache", "telo");
12
+ const npmCacheRoot = path.join(cacheRoot, "npm");
13
+ const isBun = typeof (globalThis as any).Bun !== "undefined";
14
+
15
+ export class ControllerLoader {
16
+ /**
17
+ * Load controller instance from URI in format:
18
+ *
19
+ * <runtime>:<registry>:<path>@<version-spec>
20
+ */
21
+ async load(purlCandidates: string[], baseUri: string): Promise<ControllerInstance> {
22
+ if (!purlCandidates || purlCandidates.length === 0) {
23
+ throw new RuntimeError("ERR_CONTROLLER_NOT_FOUND", "Missing controller PURL candidates");
24
+ }
25
+ const purl = purlCandidates.find((p) => p.startsWith("pkg:npm"));
26
+ if (!purl) {
27
+ throw new RuntimeError(
28
+ "ERR_CONTROLLER_NOT_FOUND",
29
+ "Controller PURL candidates not applicable",
30
+ );
31
+ }
32
+ const [type, namespace, name, versionSpec, qualifiers, entry] = PackageURL.parseString(purl);
33
+
34
+ const localPath = (qualifiers as any)?.get("local_path");
35
+ const cacheKey = createHash("sha256").update(purlCandidates[0]).digest("hex").slice(0, 12);
36
+ const installDir = path.join(npmCacheRoot, cacheKey);
37
+
38
+ let packageRoot: string;
39
+ const isLocalManifest =
40
+ baseUri && !baseUri.startsWith("http://") && !baseUri.startsWith("https://");
41
+ if (localPath && isLocalManifest) {
42
+ const baseUriPath = baseUri.startsWith("file://") ? baseUri.slice("file://".length) : baseUri;
43
+ const manifestDir = path.dirname(baseUriPath);
44
+ const resolvedLocalPath = path.resolve(manifestDir, localPath);
45
+ if (await this.pathExists(resolvedLocalPath)) {
46
+ packageRoot = resolvedLocalPath;
47
+ } else {
48
+ const nodeModulesPath = await this.findInNodeModules(`${namespace}/${name}`);
49
+ if (nodeModulesPath) {
50
+ packageRoot = nodeModulesPath;
51
+ } else {
52
+ await this.ensureNpmPackageInstalled(installDir, `${namespace}/${name}@${versionSpec}`);
53
+ packageRoot = this.getInstalledPackageRoot(installDir, `${namespace}/${name}`);
54
+ }
55
+ }
56
+ } else {
57
+ const nodeModulesPath = await this.findInNodeModules(`${namespace}/${name}`);
58
+ if (nodeModulesPath) {
59
+ packageRoot = nodeModulesPath;
60
+ } else {
61
+ await this.ensureNpmPackageInstalled(installDir, `${namespace}/${name}@${versionSpec}`);
62
+ packageRoot = this.getInstalledPackageRoot(installDir, `${namespace}/${name}`);
63
+ }
64
+ }
65
+
66
+ const entryFile = await this.resolvePackageEntry(packageRoot, entry ? `./${entry}` : ".");
67
+ const instance = await import(entryFile);
68
+ if (!instance || (!instance.create && !instance.register)) {
69
+ throw new Error(
70
+ `Invalid controller loaded from "${purlCandidates[0]}": missing create or register function`,
71
+ );
72
+ }
73
+ return instance;
74
+ }
75
+
76
+ private async ensureNpmPackageInstalled(installDir: string, packageSpec: string): Promise<void> {
77
+ const packageName = this.getPackageName(
78
+ packageSpec.startsWith(".") || path.isAbsolute(packageSpec)
79
+ ? await this.getLocalPackageName(packageSpec)
80
+ : packageSpec,
81
+ );
82
+ const packageRoot = this.getInstalledPackageRoot(installDir, packageName);
83
+ const packageJsonPath = path.join(packageRoot, "package.json");
84
+ if (await this.pathExists(packageJsonPath)) {
85
+ return;
86
+ }
87
+
88
+ await fs.mkdir(installDir, { recursive: true });
89
+ const rootPackageJson = path.join(installDir, "package.json");
90
+ if (!(await this.pathExists(rootPackageJson))) {
91
+ await fs.writeFile(
92
+ rootPackageJson,
93
+ JSON.stringify({ name: "telo-cache", private: true }, null, 2),
94
+ );
95
+ }
96
+
97
+ const execFileAsync = promisify(execFile);
98
+ const args = [
99
+ "install",
100
+ "--no-audit",
101
+ "--no-fund",
102
+ "--silent",
103
+ "--prefix",
104
+ installDir,
105
+ packageSpec,
106
+ ];
107
+
108
+ await execFileAsync("npm", args);
109
+ }
110
+
111
+ private getPackageName(packageSpec: string): string {
112
+ if (packageSpec.startsWith("@")) {
113
+ const lastAt = packageSpec.lastIndexOf("@");
114
+ return lastAt > 0 ? packageSpec.slice(0, lastAt) : packageSpec;
115
+ }
116
+ const [name] = packageSpec.split("@");
117
+ return name;
118
+ }
119
+
120
+ private getInstalledPackageRoot(installDir: string, packageName: string): string {
121
+ const nameParts = packageName.split("/");
122
+ return path.join(installDir, "node_modules", ...nameParts);
123
+ }
124
+
125
+ private async getLocalPackageName(packagePath: string): Promise<string> {
126
+ const packageJsonPath = path.join(packagePath, "package.json");
127
+ if (!(await this.pathExists(packageJsonPath))) {
128
+ throw new Error(`Local package missing package.json: ${packagePath}`);
129
+ }
130
+ const content = await fs.readFile(packageJsonPath, "utf8");
131
+ const parsed = JSON.parse(content);
132
+ if (!parsed?.name) {
133
+ throw new Error(`Local package missing name in package.json: ${packagePath}`);
134
+ }
135
+ return parsed.name;
136
+ }
137
+
138
+ private async resolvePackageEntry(
139
+ packageRoot: string,
140
+ entry: string,
141
+ packageName?: string,
142
+ ): Promise<string> {
143
+ const packageJsonPath = path.join(packageRoot, "package.json");
144
+ let resolvedPackageName = packageName;
145
+ let packageJson: any = null;
146
+ if (!resolvedPackageName && (await this.pathExists(packageJsonPath))) {
147
+ const content = await fs.readFile(packageJsonPath, "utf8");
148
+ try {
149
+ packageJson = JSON.parse(content);
150
+ resolvedPackageName = packageJson?.name;
151
+ } catch {
152
+ resolvedPackageName = packageName;
153
+ }
154
+ } else if (await this.pathExists(packageJsonPath)) {
155
+ try {
156
+ packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
157
+ } catch {
158
+ packageJson = null;
159
+ }
160
+ }
161
+
162
+ const entryValue = entry.trim();
163
+ const exportTarget = this.resolvePackageExportTarget(packageJson?.exports, entryValue);
164
+ if (exportTarget) {
165
+ const resolved = path.resolve(packageRoot, exportTarget);
166
+ if (await this.pathExists(resolved)) {
167
+ return this.resolveForRuntime(resolved, packageRoot);
168
+ }
169
+ if (!path.extname(resolved)) {
170
+ const withJs = `${resolved}.js`;
171
+ if (await this.pathExists(withJs)) {
172
+ return withJs;
173
+ }
174
+ }
175
+ }
176
+ if ((entryValue === "." || entryValue === "./") && packageJson) {
177
+ const mainFields = ["module", "main"];
178
+ for (const field of mainFields) {
179
+ const target = packageJson[field];
180
+ if (typeof target === "string") {
181
+ const resolved = path.resolve(packageRoot, target);
182
+ if (await this.pathExists(resolved)) {
183
+ return this.resolveForRuntime(resolved, packageRoot);
184
+ }
185
+ if (!path.extname(resolved)) {
186
+ const withJs = `${resolved}.js`;
187
+ if (await this.pathExists(withJs)) {
188
+ return withJs;
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ const directPath = path.resolve(packageRoot, entryValue);
196
+ if (await this.pathExists(directPath)) {
197
+ return this.resolveForRuntime(directPath, packageRoot);
198
+ }
199
+ if (!path.extname(directPath)) {
200
+ const withJs = `${directPath}.js`;
201
+ if (await this.pathExists(withJs)) {
202
+ return withJs;
203
+ }
204
+ }
205
+
206
+ throw new Error(`Controller entry "${entryValue}" could not be resolved in ${packageRoot}`);
207
+ }
208
+
209
+ private resolvePackageExportTarget(exportsField: any, entry: string): string | null {
210
+ if (!exportsField) {
211
+ return null;
212
+ }
213
+
214
+ const key = entry === "." || entry === "./" ? "." : entry;
215
+ const target = exportsField[key];
216
+ return this.resolveExportTargetValue(target);
217
+ }
218
+
219
+ private resolveExportTargetValue(target: any): string | null {
220
+ if (!target) {
221
+ return null;
222
+ }
223
+ if (typeof target === "string") {
224
+ return target;
225
+ }
226
+ if (Array.isArray(target)) {
227
+ for (const item of target) {
228
+ const resolved = this.resolveExportTargetValue(item);
229
+ if (resolved) {
230
+ return resolved;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ if (typeof target === "object") {
236
+ const preferredKeys = isBun
237
+ ? ["bun", "import", "default", "require"]
238
+ : ["import", "default", "require"];
239
+ for (const key of preferredKeys) {
240
+ if (target[key]) {
241
+ const resolved = this.resolveExportTargetValue(target[key]);
242
+ if (resolved) {
243
+ return resolved;
244
+ }
245
+ }
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * For Node.js, resolve .ts paths to their compiled .js equivalents in dist/.
253
+ * Bun can load .ts directly, so it returns the path unchanged.
254
+ */
255
+ private async resolveForRuntime(resolvedPath: string, packageRoot: string): Promise<string> {
256
+ if (isBun || !resolvedPath.endsWith(".ts")) {
257
+ return resolvedPath;
258
+ }
259
+ // Try dist/ equivalent: src/foo.ts -> dist/foo.js
260
+ const relative = path.relative(packageRoot, resolvedPath);
261
+ const distEquivalent = path.resolve(
262
+ packageRoot,
263
+ relative.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
264
+ );
265
+ if (await this.pathExists(distEquivalent)) {
266
+ return distEquivalent;
267
+ }
268
+ // Fallback: same location but .js
269
+ const jsPath = resolvedPath.replace(/\.ts$/, ".js");
270
+ if (await this.pathExists(jsPath)) {
271
+ return jsPath;
272
+ }
273
+ return resolvedPath;
274
+ }
275
+
276
+ private async findInNodeModules(packageName: string): Promise<string | null> {
277
+ const nameParts = packageName.split("/");
278
+ const candidates = [
279
+ path.join(process.cwd(), "node_modules", ...nameParts),
280
+ path.join(process.cwd(), "node_modules", ".pnpm", "node_modules", ...nameParts),
281
+ ];
282
+ for (const candidate of candidates) {
283
+ const packageJsonPath = path.join(candidate, "package.json");
284
+ if (await this.pathExists(packageJsonPath)) {
285
+ return candidate;
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+
291
+ private async pathExists(filePath: string): Promise<boolean> {
292
+ try {
293
+ await fs.access(filePath);
294
+ return true;
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+ }
@@ -0,0 +1,191 @@
1
+ import { ControllerInstance, ResourceDefinition, RuntimeResource } from "@telorun/sdk";
2
+ import * as path from "path";
3
+
4
+ /**
5
+ * ControllerRegistry: Manages controller loading and dispatch
6
+ * Maps fully-qualified resource kinds to their controller implementations
7
+ */
8
+ export class ControllerRegistry {
9
+ private controllersByKind: Map<string, ControllerInstance> = new Map();
10
+ private definitionsByKind: Map<string, ResourceDefinition> = new Map();
11
+ private controllerLoaders: Map<string, () => Promise<ControllerInstance>> = new Map();
12
+ /**
13
+ * Register a controller definition
14
+ */
15
+ registerDefinition(
16
+ definition: ResourceDefinition,
17
+ // baseDir?: string,
18
+ // namespace?: string | null,
19
+ ): void {
20
+ // Construct fully qualified kind: Namespace.Name
21
+ // Only add namespace if name is not already qualified (doesn't contain a dot)
22
+ const namespace = definition.metadata.module;
23
+ const baseDir = null;
24
+ const name = definition.metadata.name;
25
+ const kind = namespace && !name.includes(".") ? `${namespace}.${name}` : name;
26
+
27
+ this.definitionsByKind.set(kind, definition);
28
+
29
+ // If definition has controllers, register loader for them
30
+ if (definition.controllers && definition.controllers.length > 0 && baseDir) {
31
+ this.registerControllerLoader(kind, definition, baseDir);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get a controller instance for a kind
37
+ * Lazy-loads controller code on first access
38
+ * Throws if controller not found
39
+ */
40
+ getController(kind: string): ControllerInstance {
41
+ // Return cached instance if available
42
+ if (this.controllersByKind.has(kind)) {
43
+ return this.controllersByKind.get(kind)!;
44
+ }
45
+
46
+ // Load controller if loader is registered
47
+ // const loader = this.controllerLoaders.get(kind);
48
+ // if (loader) {
49
+ // const controller = await loader();
50
+ // this.controllersByKind.set(kind, controller);
51
+ // return controller;
52
+ // }
53
+ return {
54
+ schema: { type: "object", additionalProperties: false },
55
+ };
56
+ // throw new Error(`No controller registered for kind: ${kind}`);
57
+ }
58
+
59
+ /**
60
+ * Safe get - returns undefined if controller not found
61
+ */
62
+ getControllerOrUndefined(kind: string): ControllerInstance | undefined {
63
+ // Return cached instance if available
64
+ if (this.controllersByKind.has(kind)) {
65
+ return this.controllersByKind.get(kind);
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ /**
71
+ * Check if a controller exists for this kind (definition or directly registered)
72
+ */
73
+ hasController(kind: string): boolean {
74
+ return this.controllersByKind.has(kind) || this.definitionsByKind.has(kind);
75
+ }
76
+
77
+ /**
78
+ * Get definition for a kind
79
+ */
80
+ getDefinition(kind: string): ResourceDefinition | undefined {
81
+ return this.definitionsByKind.get(kind);
82
+ }
83
+
84
+ /**
85
+ * Get all registered kinds
86
+ */
87
+ getKinds(): string[] {
88
+ return Array.from(this.definitionsByKind.keys());
89
+ }
90
+
91
+ getControllerKinds(): string[] {
92
+ return Array.from(this.controllersByKind.keys());
93
+ }
94
+
95
+ /**
96
+ * Create a resource instance using its controller
97
+ */
98
+ async create(kind: string, resource: RuntimeResource, ctx: any): Promise<any | null> {
99
+ const controller = this.getController(kind);
100
+ if (!controller || !controller.create) {
101
+ return null;
102
+ }
103
+ return controller.create(resource, ctx);
104
+ }
105
+
106
+ /**
107
+ * Register a controller for a kind
108
+ */
109
+ registerController(kind: string, controller: ControllerInstance): void {
110
+ if (!this.definitionsByKind.has(kind)) {
111
+ throw new Error(`Cannot register controller for kind ${kind} without definition`);
112
+ }
113
+ // Ensure controller has schema from definition
114
+ const definition = this.definitionsByKind.get(kind);
115
+ const wrappedController: ControllerInstance = {
116
+ ...controller,
117
+ schema: controller.schema ?? definition?.schema,
118
+ inputSchema: controller.inputSchema,
119
+ outputSchema: controller.outputSchema,
120
+ };
121
+ this.controllersByKind.set(kind, wrappedController);
122
+ }
123
+
124
+ /**
125
+ * Private: Register controller loader
126
+ */
127
+ private registerControllerLoader(
128
+ kind: string,
129
+ definition: ResourceDefinition,
130
+ moduleDir: string,
131
+ ): void {
132
+ const controllerDef = definition.controllers?.[0]; // Use first matching controller for now
133
+ if (!controllerDef) return;
134
+
135
+ this.controllerLoaders.set(kind, async () => {
136
+ const modulePath = path.resolve(moduleDir, controllerDef.entry);
137
+ const moduleRuntime = await import(modulePath);
138
+ const exported = moduleRuntime.default || moduleRuntime.Module || moduleRuntime;
139
+
140
+ const registerFn =
141
+ typeof moduleRuntime.register === "function"
142
+ ? moduleRuntime.register
143
+ : typeof exported === "function" && !this.isModuleClass(exported)
144
+ ? exported
145
+ : null;
146
+
147
+ const createFn =
148
+ typeof moduleRuntime.create === "function"
149
+ ? moduleRuntime.create
150
+ : typeof exported?.create === "function"
151
+ ? exported.create
152
+ : null;
153
+
154
+ const executeFn =
155
+ typeof moduleRuntime.execute === "function"
156
+ ? moduleRuntime.execute
157
+ : typeof exported?.execute === "function"
158
+ ? exported.execute
159
+ : null;
160
+
161
+ const compileFn =
162
+ typeof moduleRuntime.compile === "function"
163
+ ? moduleRuntime.compile
164
+ : typeof exported?.compile === "function"
165
+ ? exported.compile
166
+ : null;
167
+
168
+ if (!registerFn && !executeFn && !createFn && !compileFn) {
169
+ throw new Error(`Controller for "${kind}" exports no usable handlers`);
170
+ }
171
+
172
+ if (!definition.schema) {
173
+ throw new Error(`Definition for "${kind}" does not have schema`);
174
+ }
175
+
176
+ return {
177
+ register: registerFn ?? undefined,
178
+ create: createFn ?? undefined,
179
+ execute: executeFn ?? undefined,
180
+ compile: compileFn ?? undefined,
181
+ schema: definition.schema,
182
+ };
183
+ });
184
+ }
185
+
186
+ private isModuleClass(obj: any): boolean {
187
+ return (
188
+ typeof obj === "function" && (obj.name === "Controller" || obj.toString().includes("class"))
189
+ );
190
+ }
191
+ }