@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/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.registerService(serviceName, servicePayload);
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 { handled: false };
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({ success: false, error: { message: "Not Found", code: 404 } });
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
  }