@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/dist/index.js
CHANGED
|
@@ -599,9 +599,8 @@ var AppPlugin = class {
|
|
|
599
599
|
pluginName: this.name,
|
|
600
600
|
version: this.version
|
|
601
601
|
});
|
|
602
|
-
const serviceName = `app.${appId}`;
|
|
603
602
|
const servicePayload = this.bundle.manifest ? { ...this.bundle.manifest, ...this.bundle } : this.bundle;
|
|
604
|
-
ctx.
|
|
603
|
+
ctx.getService("manifest").register(servicePayload);
|
|
605
604
|
};
|
|
606
605
|
this.start = async (ctx) => {
|
|
607
606
|
const sys = this.bundle.manifest || this.bundle;
|
|
@@ -809,6 +808,24 @@ var HttpDispatcher = class {
|
|
|
809
808
|
body: { success: false, error: { message, code, details } }
|
|
810
809
|
};
|
|
811
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* 404 Route Not Found — no route is registered for this path.
|
|
813
|
+
*/
|
|
814
|
+
routeNotFound(route) {
|
|
815
|
+
return {
|
|
816
|
+
status: 404,
|
|
817
|
+
body: {
|
|
818
|
+
success: false,
|
|
819
|
+
error: {
|
|
820
|
+
code: 404,
|
|
821
|
+
message: `Route Not Found: ${route}`,
|
|
822
|
+
type: "ROUTE_NOT_FOUND",
|
|
823
|
+
route,
|
|
824
|
+
hint: "No route is registered for this path. Check the API discovery endpoint for available routes."
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
812
829
|
ensureBroker() {
|
|
813
830
|
if (!this.kernel.broker) {
|
|
814
831
|
throw { statusCode: 500, message: "Kernel Broker not available" };
|
|
@@ -890,12 +907,14 @@ var HttpDispatcher = class {
|
|
|
890
907
|
const svcAvailable = (route, provider) => ({
|
|
891
908
|
enabled: true,
|
|
892
909
|
status: "available",
|
|
910
|
+
handlerReady: true,
|
|
893
911
|
route,
|
|
894
912
|
provider
|
|
895
913
|
});
|
|
896
914
|
const svcUnavailable = (name) => ({
|
|
897
915
|
enabled: false,
|
|
898
916
|
status: "unavailable",
|
|
917
|
+
handlerReady: false,
|
|
899
918
|
message: `Install a ${name} plugin to enable`
|
|
900
919
|
});
|
|
901
920
|
let locale = { default: "en", supported: ["en"], timezone: "UTC" };
|
|
@@ -928,7 +947,7 @@ var HttpDispatcher = class {
|
|
|
928
947
|
},
|
|
929
948
|
services: {
|
|
930
949
|
// Kernel-provided (always available via protocol implementation)
|
|
931
|
-
metadata: { enabled: true, status: "degraded", route: routes.metadata, provider: "kernel", message: "In-memory registry; DB persistence pending" },
|
|
950
|
+
metadata: { enabled: true, status: "degraded", handlerReady: true, route: routes.metadata, provider: "kernel", message: "In-memory registry; DB persistence pending" },
|
|
932
951
|
data: svcAvailable(routes.data, "kernel"),
|
|
933
952
|
// Plugin-provided — only available when a plugin registers the service
|
|
934
953
|
auth: hasAuth ? svcAvailable(routes.auth) : svcUnavailable("auth"),
|
|
@@ -1080,6 +1099,7 @@ var HttpDispatcher = class {
|
|
|
1080
1099
|
}
|
|
1081
1100
|
if (parts.length === 2) {
|
|
1082
1101
|
const [type, name] = parts;
|
|
1102
|
+
const packageId = query?.package || void 0;
|
|
1083
1103
|
if (method === "PUT" && body) {
|
|
1084
1104
|
const protocol = await this.resolveService("protocol");
|
|
1085
1105
|
if (protocol && typeof protocol.saveMetaItem === "function") {
|
|
@@ -1117,7 +1137,7 @@ var HttpDispatcher = class {
|
|
|
1117
1137
|
const protocol = await this.resolveService("protocol");
|
|
1118
1138
|
if (protocol && typeof protocol.getMetaItem === "function") {
|
|
1119
1139
|
try {
|
|
1120
|
-
const data = await protocol.getMetaItem({ type: singularType, name });
|
|
1140
|
+
const data = await protocol.getMetaItem({ type: singularType, name, packageId });
|
|
1121
1141
|
return { handled: true, response: this.success(data) };
|
|
1122
1142
|
} catch (e) {
|
|
1123
1143
|
}
|
|
@@ -1705,19 +1725,108 @@ var HttpDispatcher = class {
|
|
|
1705
1725
|
capitalize(s) {
|
|
1706
1726
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1707
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
|
|
1730
|
+
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
1731
|
+
*/
|
|
1732
|
+
async handleAI(subPath, method, body, query, _context) {
|
|
1733
|
+
let aiService;
|
|
1734
|
+
try {
|
|
1735
|
+
aiService = await this.resolveService("ai");
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
if (!aiService) {
|
|
1739
|
+
return {
|
|
1740
|
+
handled: true,
|
|
1741
|
+
response: {
|
|
1742
|
+
status: 404,
|
|
1743
|
+
body: { success: false, error: { message: "AI service is not configured", code: 404 } }
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
const fullPath = `/api/v1${subPath}`;
|
|
1748
|
+
const matchRoute = (pattern, path) => {
|
|
1749
|
+
const patternParts = pattern.split("/");
|
|
1750
|
+
const pathParts = path.split("/");
|
|
1751
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
1752
|
+
const params = {};
|
|
1753
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
1754
|
+
if (patternParts[i].startsWith(":")) {
|
|
1755
|
+
params[patternParts[i].substring(1)] = pathParts[i];
|
|
1756
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return params;
|
|
1761
|
+
};
|
|
1762
|
+
const routes = this.kernel.__aiRoutes;
|
|
1763
|
+
if (!routes) {
|
|
1764
|
+
return {
|
|
1765
|
+
handled: true,
|
|
1766
|
+
response: {
|
|
1767
|
+
status: 503,
|
|
1768
|
+
body: { success: false, error: { message: "AI service routes not yet initialized", code: 503 } }
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
for (const route of routes) {
|
|
1773
|
+
if (route.method !== method) continue;
|
|
1774
|
+
const params = matchRoute(route.path, fullPath);
|
|
1775
|
+
if (params === null) continue;
|
|
1776
|
+
const result = await route.handler({ body, params, query });
|
|
1777
|
+
if (result.stream && result.events) {
|
|
1778
|
+
return {
|
|
1779
|
+
handled: true,
|
|
1780
|
+
result: {
|
|
1781
|
+
type: "stream",
|
|
1782
|
+
contentType: result.vercelDataStream ? "text/plain; charset=utf-8" : "text/event-stream",
|
|
1783
|
+
events: result.events,
|
|
1784
|
+
vercelDataStream: result.vercelDataStream,
|
|
1785
|
+
headers: {
|
|
1786
|
+
"Content-Type": result.vercelDataStream ? "text/plain; charset=utf-8" : "text/event-stream",
|
|
1787
|
+
"Cache-Control": "no-cache",
|
|
1788
|
+
"Connection": "keep-alive"
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
handled: true,
|
|
1795
|
+
response: {
|
|
1796
|
+
status: result.status,
|
|
1797
|
+
body: result.body
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
handled: true,
|
|
1803
|
+
response: this.routeNotFound(subPath)
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1708
1806
|
/**
|
|
1709
1807
|
* Main Dispatcher Entry Point
|
|
1710
1808
|
* Routes the request to the appropriate handler based on path and precedence
|
|
1711
1809
|
*/
|
|
1712
|
-
async dispatch(method, path, body, query, context) {
|
|
1810
|
+
async dispatch(method, path, body, query, context, prefix) {
|
|
1713
1811
|
const cleanPath = path.replace(/\/$/, "");
|
|
1714
1812
|
if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
|
|
1715
|
-
const info = await this.getDiscoveryInfo("");
|
|
1813
|
+
const info = await this.getDiscoveryInfo(prefix ?? "");
|
|
1716
1814
|
return {
|
|
1717
1815
|
handled: true,
|
|
1718
1816
|
response: this.success(info)
|
|
1719
1817
|
};
|
|
1720
1818
|
}
|
|
1819
|
+
if (cleanPath === "/health" && method === "GET") {
|
|
1820
|
+
return {
|
|
1821
|
+
handled: true,
|
|
1822
|
+
response: this.success({
|
|
1823
|
+
status: "ok",
|
|
1824
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1825
|
+
version: "1.0.0",
|
|
1826
|
+
uptime: typeof process !== "undefined" ? process.uptime() : void 0
|
|
1827
|
+
})
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1721
1830
|
if (cleanPath.startsWith("/auth")) {
|
|
1722
1831
|
return this.handleAuth(cleanPath.substring(5), method, body, context);
|
|
1723
1832
|
}
|
|
@@ -1748,6 +1857,9 @@ var HttpDispatcher = class {
|
|
|
1748
1857
|
if (cleanPath.startsWith("/i18n")) {
|
|
1749
1858
|
return this.handleI18n(cleanPath.substring(5), method, query, context);
|
|
1750
1859
|
}
|
|
1860
|
+
if (cleanPath.startsWith("/ai")) {
|
|
1861
|
+
return this.handleAI(cleanPath, method, body, query, context);
|
|
1862
|
+
}
|
|
1751
1863
|
if (cleanPath === "/openapi.json" && method === "GET") {
|
|
1752
1864
|
const broker = this.ensureBroker();
|
|
1753
1865
|
try {
|
|
@@ -1758,7 +1870,10 @@ var HttpDispatcher = class {
|
|
|
1758
1870
|
}
|
|
1759
1871
|
const result = await this.handleApiEndpoint(cleanPath, method, body, query, context);
|
|
1760
1872
|
if (result.handled) return result;
|
|
1761
|
-
return {
|
|
1873
|
+
return {
|
|
1874
|
+
handled: true,
|
|
1875
|
+
response: this.routeNotFound(cleanPath)
|
|
1876
|
+
};
|
|
1762
1877
|
}
|
|
1763
1878
|
/**
|
|
1764
1879
|
* Handles Custom API Endpoints defined in metadata
|
|
@@ -1833,7 +1948,15 @@ function sendResult(result, res) {
|
|
|
1833
1948
|
return;
|
|
1834
1949
|
}
|
|
1835
1950
|
}
|
|
1836
|
-
res.status(404).json({
|
|
1951
|
+
res.status(404).json({
|
|
1952
|
+
success: false,
|
|
1953
|
+
error: {
|
|
1954
|
+
message: "Not Found",
|
|
1955
|
+
code: 404,
|
|
1956
|
+
type: "ROUTE_NOT_FOUND",
|
|
1957
|
+
hint: "No handler matched this request. Check the API discovery endpoint for available routes."
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1837
1960
|
}
|
|
1838
1961
|
function errorResponse(err, res) {
|
|
1839
1962
|
const code = err.statusCode || 500;
|
|
@@ -1865,6 +1988,14 @@ function createDispatcherPlugin(config = {}) {
|
|
|
1865
1988
|
server.get(`${prefix}/discovery`, async (_req, res) => {
|
|
1866
1989
|
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
1867
1990
|
});
|
|
1991
|
+
server.get(`${prefix}/health`, async (_req, res) => {
|
|
1992
|
+
try {
|
|
1993
|
+
const result = await dispatcher.dispatch("GET", "/health", void 0, {}, { request: _req });
|
|
1994
|
+
sendResult(result, res);
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
errorResponse(err, res);
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1868
1999
|
server.post(`${prefix}/auth/login`, async (req, res) => {
|
|
1869
2000
|
try {
|
|
1870
2001
|
const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });
|
|
@@ -2090,6 +2221,65 @@ function createDispatcherPlugin(config = {}) {
|
|
|
2090
2221
|
}
|
|
2091
2222
|
});
|
|
2092
2223
|
ctx.logger.info("Dispatcher bridge routes registered", { prefix });
|
|
2224
|
+
ctx.hook("ai:routes", async (routes) => {
|
|
2225
|
+
if (!server) return;
|
|
2226
|
+
for (const route of routes) {
|
|
2227
|
+
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
2228
|
+
const handler = async (req, res) => {
|
|
2229
|
+
try {
|
|
2230
|
+
const result = await route.handler({
|
|
2231
|
+
body: req.body,
|
|
2232
|
+
params: req.params,
|
|
2233
|
+
query: req.query
|
|
2234
|
+
});
|
|
2235
|
+
if (result.stream && result.events) {
|
|
2236
|
+
res.status(result.status);
|
|
2237
|
+
if (result.headers) {
|
|
2238
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
2239
|
+
res.header(k, v);
|
|
2240
|
+
}
|
|
2241
|
+
} else {
|
|
2242
|
+
res.header("Content-Type", "text/event-stream");
|
|
2243
|
+
res.header("Cache-Control", "no-cache");
|
|
2244
|
+
res.header("Connection", "keep-alive");
|
|
2245
|
+
}
|
|
2246
|
+
if (typeof res.write === "function" && typeof res.end === "function") {
|
|
2247
|
+
for await (const event of result.events) {
|
|
2248
|
+
res.write(typeof event === "string" ? event : `data: ${JSON.stringify(event)}
|
|
2249
|
+
|
|
2250
|
+
`);
|
|
2251
|
+
}
|
|
2252
|
+
res.end();
|
|
2253
|
+
} else {
|
|
2254
|
+
const events = [];
|
|
2255
|
+
for await (const event of result.events) {
|
|
2256
|
+
events.push(event);
|
|
2257
|
+
}
|
|
2258
|
+
res.json({ events });
|
|
2259
|
+
}
|
|
2260
|
+
} else {
|
|
2261
|
+
res.status(result.status);
|
|
2262
|
+
if (result.body !== void 0) {
|
|
2263
|
+
res.json(result.body);
|
|
2264
|
+
} else {
|
|
2265
|
+
res.end();
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
errorResponse(err, res);
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
const m = route.method.toLowerCase();
|
|
2273
|
+
if (m === "get" && typeof server.get === "function") {
|
|
2274
|
+
server.get(routePath, handler);
|
|
2275
|
+
} else if (m === "post" && typeof server.post === "function") {
|
|
2276
|
+
server.post(routePath, handler);
|
|
2277
|
+
} else if (m === "delete" && typeof server.delete === "function") {
|
|
2278
|
+
server.delete(routePath, handler);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
ctx.logger.info(`[Dispatcher] Registered ${routes.length} AI routes`);
|
|
2282
|
+
});
|
|
2093
2283
|
}
|
|
2094
2284
|
};
|
|
2095
2285
|
}
|