@objectstack/runtime 4.0.1 → 4.0.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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +10 -0
- package/dist/index.cjs +198 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +198 -8
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/app-plugin.test.ts +8 -3
- package/src/app-plugin.ts +4 -8
- package/src/dispatcher-plugin.ts +107 -2
- package/src/http-dispatcher.root.test.ts +5 -2
- package/src/http-dispatcher.ts +157 -12
- package/vitest.config.ts +25 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/runtime@4.0.
|
|
2
|
+
> @objectstack/runtime@4.0.2 build /home/runner/work/framework/framework/packages/runtime
|
|
3
3
|
> tsup --config ../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
13
|
+
[32mESM[39m [1mdist/index.js [22m[32m94.32 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m204.99 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 155ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m96.91 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.cjs.map [22m[32m205.06 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 156ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 3326ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m26.00 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m26.00 KB[39m
|
package/CHANGELOG.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -646,9 +646,8 @@ var AppPlugin = class {
|
|
|
646
646
|
pluginName: this.name,
|
|
647
647
|
version: this.version
|
|
648
648
|
});
|
|
649
|
-
const serviceName = `app.${appId}`;
|
|
650
649
|
const servicePayload = this.bundle.manifest ? { ...this.bundle.manifest, ...this.bundle } : this.bundle;
|
|
651
|
-
ctx.
|
|
650
|
+
ctx.getService("manifest").register(servicePayload);
|
|
652
651
|
};
|
|
653
652
|
this.start = async (ctx) => {
|
|
654
653
|
const sys = this.bundle.manifest || this.bundle;
|
|
@@ -856,6 +855,24 @@ var HttpDispatcher = class {
|
|
|
856
855
|
body: { success: false, error: { message, code, details } }
|
|
857
856
|
};
|
|
858
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* 404 Route Not Found — no route is registered for this path.
|
|
860
|
+
*/
|
|
861
|
+
routeNotFound(route) {
|
|
862
|
+
return {
|
|
863
|
+
status: 404,
|
|
864
|
+
body: {
|
|
865
|
+
success: false,
|
|
866
|
+
error: {
|
|
867
|
+
code: 404,
|
|
868
|
+
message: `Route Not Found: ${route}`,
|
|
869
|
+
type: "ROUTE_NOT_FOUND",
|
|
870
|
+
route,
|
|
871
|
+
hint: "No route is registered for this path. Check the API discovery endpoint for available routes."
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
859
876
|
ensureBroker() {
|
|
860
877
|
if (!this.kernel.broker) {
|
|
861
878
|
throw { statusCode: 500, message: "Kernel Broker not available" };
|
|
@@ -937,12 +954,14 @@ var HttpDispatcher = class {
|
|
|
937
954
|
const svcAvailable = (route, provider) => ({
|
|
938
955
|
enabled: true,
|
|
939
956
|
status: "available",
|
|
957
|
+
handlerReady: true,
|
|
940
958
|
route,
|
|
941
959
|
provider
|
|
942
960
|
});
|
|
943
961
|
const svcUnavailable = (name) => ({
|
|
944
962
|
enabled: false,
|
|
945
963
|
status: "unavailable",
|
|
964
|
+
handlerReady: false,
|
|
946
965
|
message: `Install a ${name} plugin to enable`
|
|
947
966
|
});
|
|
948
967
|
let locale = { default: "en", supported: ["en"], timezone: "UTC" };
|
|
@@ -975,7 +994,7 @@ var HttpDispatcher = class {
|
|
|
975
994
|
},
|
|
976
995
|
services: {
|
|
977
996
|
// Kernel-provided (always available via protocol implementation)
|
|
978
|
-
metadata: { enabled: true, status: "degraded", route: routes.metadata, provider: "kernel", message: "In-memory registry; DB persistence pending" },
|
|
997
|
+
metadata: { enabled: true, status: "degraded", handlerReady: true, route: routes.metadata, provider: "kernel", message: "In-memory registry; DB persistence pending" },
|
|
979
998
|
data: svcAvailable(routes.data, "kernel"),
|
|
980
999
|
// Plugin-provided — only available when a plugin registers the service
|
|
981
1000
|
auth: hasAuth ? svcAvailable(routes.auth) : svcUnavailable("auth"),
|
|
@@ -1127,6 +1146,7 @@ var HttpDispatcher = class {
|
|
|
1127
1146
|
}
|
|
1128
1147
|
if (parts.length === 2) {
|
|
1129
1148
|
const [type, name] = parts;
|
|
1149
|
+
const packageId = query?.package || void 0;
|
|
1130
1150
|
if (method === "PUT" && body) {
|
|
1131
1151
|
const protocol = await this.resolveService("protocol");
|
|
1132
1152
|
if (protocol && typeof protocol.saveMetaItem === "function") {
|
|
@@ -1164,7 +1184,7 @@ var HttpDispatcher = class {
|
|
|
1164
1184
|
const protocol = await this.resolveService("protocol");
|
|
1165
1185
|
if (protocol && typeof protocol.getMetaItem === "function") {
|
|
1166
1186
|
try {
|
|
1167
|
-
const data = await protocol.getMetaItem({ type: singularType, name });
|
|
1187
|
+
const data = await protocol.getMetaItem({ type: singularType, name, packageId });
|
|
1168
1188
|
return { handled: true, response: this.success(data) };
|
|
1169
1189
|
} catch (e) {
|
|
1170
1190
|
}
|
|
@@ -1752,19 +1772,108 @@ var HttpDispatcher = class {
|
|
|
1752
1772
|
capitalize(s) {
|
|
1753
1773
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1754
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
|
|
1777
|
+
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
1778
|
+
*/
|
|
1779
|
+
async handleAI(subPath, method, body, query, _context) {
|
|
1780
|
+
let aiService;
|
|
1781
|
+
try {
|
|
1782
|
+
aiService = await this.resolveService("ai");
|
|
1783
|
+
} catch {
|
|
1784
|
+
}
|
|
1785
|
+
if (!aiService) {
|
|
1786
|
+
return {
|
|
1787
|
+
handled: true,
|
|
1788
|
+
response: {
|
|
1789
|
+
status: 404,
|
|
1790
|
+
body: { success: false, error: { message: "AI service is not configured", code: 404 } }
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
const fullPath = `/api/v1${subPath}`;
|
|
1795
|
+
const matchRoute = (pattern, path) => {
|
|
1796
|
+
const patternParts = pattern.split("/");
|
|
1797
|
+
const pathParts = path.split("/");
|
|
1798
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
1799
|
+
const params = {};
|
|
1800
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
1801
|
+
if (patternParts[i].startsWith(":")) {
|
|
1802
|
+
params[patternParts[i].substring(1)] = pathParts[i];
|
|
1803
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return params;
|
|
1808
|
+
};
|
|
1809
|
+
const routes = this.kernel.__aiRoutes;
|
|
1810
|
+
if (!routes) {
|
|
1811
|
+
return {
|
|
1812
|
+
handled: true,
|
|
1813
|
+
response: {
|
|
1814
|
+
status: 503,
|
|
1815
|
+
body: { success: false, error: { message: "AI service routes not yet initialized", code: 503 } }
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
for (const route of routes) {
|
|
1820
|
+
if (route.method !== method) continue;
|
|
1821
|
+
const params = matchRoute(route.path, fullPath);
|
|
1822
|
+
if (params === null) continue;
|
|
1823
|
+
const result = await route.handler({ body, params, query });
|
|
1824
|
+
if (result.stream && result.events) {
|
|
1825
|
+
return {
|
|
1826
|
+
handled: true,
|
|
1827
|
+
result: {
|
|
1828
|
+
type: "stream",
|
|
1829
|
+
contentType: result.vercelDataStream ? "text/plain; charset=utf-8" : "text/event-stream",
|
|
1830
|
+
events: result.events,
|
|
1831
|
+
vercelDataStream: result.vercelDataStream,
|
|
1832
|
+
headers: {
|
|
1833
|
+
"Content-Type": result.vercelDataStream ? "text/plain; charset=utf-8" : "text/event-stream",
|
|
1834
|
+
"Cache-Control": "no-cache",
|
|
1835
|
+
"Connection": "keep-alive"
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
return {
|
|
1841
|
+
handled: true,
|
|
1842
|
+
response: {
|
|
1843
|
+
status: result.status,
|
|
1844
|
+
body: result.body
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
handled: true,
|
|
1850
|
+
response: this.routeNotFound(subPath)
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1755
1853
|
/**
|
|
1756
1854
|
* Main Dispatcher Entry Point
|
|
1757
1855
|
* Routes the request to the appropriate handler based on path and precedence
|
|
1758
1856
|
*/
|
|
1759
|
-
async dispatch(method, path, body, query, context) {
|
|
1857
|
+
async dispatch(method, path, body, query, context, prefix) {
|
|
1760
1858
|
const cleanPath = path.replace(/\/$/, "");
|
|
1761
1859
|
if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
|
|
1762
|
-
const info = await this.getDiscoveryInfo("");
|
|
1860
|
+
const info = await this.getDiscoveryInfo(prefix ?? "");
|
|
1763
1861
|
return {
|
|
1764
1862
|
handled: true,
|
|
1765
1863
|
response: this.success(info)
|
|
1766
1864
|
};
|
|
1767
1865
|
}
|
|
1866
|
+
if (cleanPath === "/health" && method === "GET") {
|
|
1867
|
+
return {
|
|
1868
|
+
handled: true,
|
|
1869
|
+
response: this.success({
|
|
1870
|
+
status: "ok",
|
|
1871
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1872
|
+
version: "1.0.0",
|
|
1873
|
+
uptime: typeof process !== "undefined" ? process.uptime() : void 0
|
|
1874
|
+
})
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1768
1877
|
if (cleanPath.startsWith("/auth")) {
|
|
1769
1878
|
return this.handleAuth(cleanPath.substring(5), method, body, context);
|
|
1770
1879
|
}
|
|
@@ -1795,6 +1904,9 @@ var HttpDispatcher = class {
|
|
|
1795
1904
|
if (cleanPath.startsWith("/i18n")) {
|
|
1796
1905
|
return this.handleI18n(cleanPath.substring(5), method, query, context);
|
|
1797
1906
|
}
|
|
1907
|
+
if (cleanPath.startsWith("/ai")) {
|
|
1908
|
+
return this.handleAI(cleanPath, method, body, query, context);
|
|
1909
|
+
}
|
|
1798
1910
|
if (cleanPath === "/openapi.json" && method === "GET") {
|
|
1799
1911
|
const broker = this.ensureBroker();
|
|
1800
1912
|
try {
|
|
@@ -1805,7 +1917,10 @@ var HttpDispatcher = class {
|
|
|
1805
1917
|
}
|
|
1806
1918
|
const result = await this.handleApiEndpoint(cleanPath, method, body, query, context);
|
|
1807
1919
|
if (result.handled) return result;
|
|
1808
|
-
return {
|
|
1920
|
+
return {
|
|
1921
|
+
handled: true,
|
|
1922
|
+
response: this.routeNotFound(cleanPath)
|
|
1923
|
+
};
|
|
1809
1924
|
}
|
|
1810
1925
|
/**
|
|
1811
1926
|
* Handles Custom API Endpoints defined in metadata
|
|
@@ -1880,7 +1995,15 @@ function sendResult(result, res) {
|
|
|
1880
1995
|
return;
|
|
1881
1996
|
}
|
|
1882
1997
|
}
|
|
1883
|
-
res.status(404).json({
|
|
1998
|
+
res.status(404).json({
|
|
1999
|
+
success: false,
|
|
2000
|
+
error: {
|
|
2001
|
+
message: "Not Found",
|
|
2002
|
+
code: 404,
|
|
2003
|
+
type: "ROUTE_NOT_FOUND",
|
|
2004
|
+
hint: "No handler matched this request. Check the API discovery endpoint for available routes."
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
1884
2007
|
}
|
|
1885
2008
|
function errorResponse(err, res) {
|
|
1886
2009
|
const code = err.statusCode || 500;
|
|
@@ -1912,6 +2035,14 @@ function createDispatcherPlugin(config = {}) {
|
|
|
1912
2035
|
server.get(`${prefix}/discovery`, async (_req, res) => {
|
|
1913
2036
|
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
1914
2037
|
});
|
|
2038
|
+
server.get(`${prefix}/health`, async (_req, res) => {
|
|
2039
|
+
try {
|
|
2040
|
+
const result = await dispatcher.dispatch("GET", "/health", void 0, {}, { request: _req });
|
|
2041
|
+
sendResult(result, res);
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
errorResponse(err, res);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
1915
2046
|
server.post(`${prefix}/auth/login`, async (req, res) => {
|
|
1916
2047
|
try {
|
|
1917
2048
|
const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });
|
|
@@ -2137,6 +2268,65 @@ function createDispatcherPlugin(config = {}) {
|
|
|
2137
2268
|
}
|
|
2138
2269
|
});
|
|
2139
2270
|
ctx.logger.info("Dispatcher bridge routes registered", { prefix });
|
|
2271
|
+
ctx.hook("ai:routes", async (routes) => {
|
|
2272
|
+
if (!server) return;
|
|
2273
|
+
for (const route of routes) {
|
|
2274
|
+
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
2275
|
+
const handler = async (req, res) => {
|
|
2276
|
+
try {
|
|
2277
|
+
const result = await route.handler({
|
|
2278
|
+
body: req.body,
|
|
2279
|
+
params: req.params,
|
|
2280
|
+
query: req.query
|
|
2281
|
+
});
|
|
2282
|
+
if (result.stream && result.events) {
|
|
2283
|
+
res.status(result.status);
|
|
2284
|
+
if (result.headers) {
|
|
2285
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
2286
|
+
res.header(k, v);
|
|
2287
|
+
}
|
|
2288
|
+
} else {
|
|
2289
|
+
res.header("Content-Type", "text/event-stream");
|
|
2290
|
+
res.header("Cache-Control", "no-cache");
|
|
2291
|
+
res.header("Connection", "keep-alive");
|
|
2292
|
+
}
|
|
2293
|
+
if (typeof res.write === "function" && typeof res.end === "function") {
|
|
2294
|
+
for await (const event of result.events) {
|
|
2295
|
+
res.write(typeof event === "string" ? event : `data: ${JSON.stringify(event)}
|
|
2296
|
+
|
|
2297
|
+
`);
|
|
2298
|
+
}
|
|
2299
|
+
res.end();
|
|
2300
|
+
} else {
|
|
2301
|
+
const events = [];
|
|
2302
|
+
for await (const event of result.events) {
|
|
2303
|
+
events.push(event);
|
|
2304
|
+
}
|
|
2305
|
+
res.json({ events });
|
|
2306
|
+
}
|
|
2307
|
+
} else {
|
|
2308
|
+
res.status(result.status);
|
|
2309
|
+
if (result.body !== void 0) {
|
|
2310
|
+
res.json(result.body);
|
|
2311
|
+
} else {
|
|
2312
|
+
res.end();
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
} catch (err) {
|
|
2316
|
+
errorResponse(err, res);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
const m = route.method.toLowerCase();
|
|
2320
|
+
if (m === "get" && typeof server.get === "function") {
|
|
2321
|
+
server.get(routePath, handler);
|
|
2322
|
+
} else if (m === "post" && typeof server.post === "function") {
|
|
2323
|
+
server.post(routePath, handler);
|
|
2324
|
+
} else if (m === "delete" && typeof server.delete === "function") {
|
|
2325
|
+
server.delete(routePath, handler);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
ctx.logger.info(`[Dispatcher] Registered ${routes.length} AI routes`);
|
|
2329
|
+
});
|
|
2140
2330
|
}
|
|
2141
2331
|
};
|
|
2142
2332
|
}
|