@objectstack/runtime 14.3.0 → 14.6.0

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.d.cts CHANGED
@@ -682,6 +682,19 @@ interface DispatcherPluginConfig {
682
682
  * where membership has not been seeded.
683
683
  */
684
684
  enforceProjectMembership?: boolean;
685
+ /**
686
+ * Reject anonymous requests to `auth: true` service routes (AI, etc.) with
687
+ * HTTP 401, mirroring the REST API's `requireAuth` gate. Must match the
688
+ * REST plugin's `api.requireAuth` so `/ai` and `/meta` stay in lockstep
689
+ * with `/data` — otherwise the AI routes' declared `auth: true` contract is
690
+ * never enforced and anonymous callers reach adapter/model status routes.
691
+ *
692
+ * Defaults to `false` (backward-compatible: previously nothing enforced
693
+ * `RouteDefinition.auth` here). Hosts pass their `api.requireAuth` through —
694
+ * the framework `serve` command and the cloud apps do so from the same
695
+ * stack `api` config the REST plugin reads.
696
+ */
697
+ requireAuth?: boolean;
685
698
  /**
686
699
  * Security response headers. When provided, every response routed
687
700
  * through this plugin gets the headers merged in (route-specific
@@ -955,6 +968,14 @@ interface HttpDispatcherOptions {
955
968
  * called on every scoped request so idle projects are evicted after TTL.
956
969
  */
957
970
  scopeManager?: EnvironmentScopeManager;
971
+ /**
972
+ * Reject anonymous requests to `auth: true` service routes (AI) and to the
973
+ * metadata catch-all with HTTP 401, mirroring the REST API's `requireAuth`
974
+ * gate. Matches {@link DispatcherPluginConfig.requireAuth}; the dispatcher
975
+ * plugin threads the host's `api.requireAuth` here. Defaults to `false`
976
+ * (backward-compatible — nothing enforced `RouteDefinition.auth` before).
977
+ */
978
+ requireAuth?: boolean;
958
979
  }
959
980
  /**
960
981
  * The HTTP dispatch engine — translates an inbound (method, path, body, ctx)
@@ -982,6 +1003,12 @@ declare class HttpDispatcher {
982
1003
  * `DispatcherConfig.enforceProjectMembership`).
983
1004
  */
984
1005
  private enforceMembership;
1006
+ /**
1007
+ * When `true`, `auth: true` AI routes and the metadata catch-all reject
1008
+ * anonymous callers with 401 (mirrors the REST `requireAuth` gate). Set
1009
+ * from {@link HttpDispatcherOptions.requireAuth}. Defaults to `false`.
1010
+ */
1011
+ private requireAuth;
985
1012
  /**
986
1013
  * In-memory cache of positive membership checks, keyed by
987
1014
  * `${environmentId}:${userId}`. Entries expire 60 seconds after insertion
@@ -1498,7 +1525,7 @@ declare class HttpDispatcher {
1498
1525
  * Handles Analytics requests
1499
1526
  * path: sub-path after /analytics/
1500
1527
  */
1501
- handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1528
+ handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1502
1529
  /**
1503
1530
  * Handles in-app notification requests (ADR-0030) — the
1504
1531
  * `/api/v1/notifications` surface backed by the messaging service's inbox
package/dist/index.d.ts CHANGED
@@ -682,6 +682,19 @@ interface DispatcherPluginConfig {
682
682
  * where membership has not been seeded.
683
683
  */
684
684
  enforceProjectMembership?: boolean;
685
+ /**
686
+ * Reject anonymous requests to `auth: true` service routes (AI, etc.) with
687
+ * HTTP 401, mirroring the REST API's `requireAuth` gate. Must match the
688
+ * REST plugin's `api.requireAuth` so `/ai` and `/meta` stay in lockstep
689
+ * with `/data` — otherwise the AI routes' declared `auth: true` contract is
690
+ * never enforced and anonymous callers reach adapter/model status routes.
691
+ *
692
+ * Defaults to `false` (backward-compatible: previously nothing enforced
693
+ * `RouteDefinition.auth` here). Hosts pass their `api.requireAuth` through —
694
+ * the framework `serve` command and the cloud apps do so from the same
695
+ * stack `api` config the REST plugin reads.
696
+ */
697
+ requireAuth?: boolean;
685
698
  /**
686
699
  * Security response headers. When provided, every response routed
687
700
  * through this plugin gets the headers merged in (route-specific
@@ -955,6 +968,14 @@ interface HttpDispatcherOptions {
955
968
  * called on every scoped request so idle projects are evicted after TTL.
956
969
  */
957
970
  scopeManager?: EnvironmentScopeManager;
971
+ /**
972
+ * Reject anonymous requests to `auth: true` service routes (AI) and to the
973
+ * metadata catch-all with HTTP 401, mirroring the REST API's `requireAuth`
974
+ * gate. Matches {@link DispatcherPluginConfig.requireAuth}; the dispatcher
975
+ * plugin threads the host's `api.requireAuth` here. Defaults to `false`
976
+ * (backward-compatible — nothing enforced `RouteDefinition.auth` before).
977
+ */
978
+ requireAuth?: boolean;
958
979
  }
959
980
  /**
960
981
  * The HTTP dispatch engine — translates an inbound (method, path, body, ctx)
@@ -982,6 +1003,12 @@ declare class HttpDispatcher {
982
1003
  * `DispatcherConfig.enforceProjectMembership`).
983
1004
  */
984
1005
  private enforceMembership;
1006
+ /**
1007
+ * When `true`, `auth: true` AI routes and the metadata catch-all reject
1008
+ * anonymous callers with 401 (mirrors the REST `requireAuth` gate). Set
1009
+ * from {@link HttpDispatcherOptions.requireAuth}. Defaults to `false`.
1010
+ */
1011
+ private requireAuth;
985
1012
  /**
986
1013
  * In-memory cache of positive membership checks, keyed by
987
1014
  * `${environmentId}:${userId}`. Entries expire 60 seconds after insertion
@@ -1498,7 +1525,7 @@ declare class HttpDispatcher {
1498
1525
  * Handles Analytics requests
1499
1526
  * path: sub-path after /analytics/
1500
1527
  */
1501
- handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1528
+ handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1502
1529
  /**
1503
1530
  * Handles in-app notification requests (ADR-0030) — the
1504
1531
  * `/api/v1/notifications` surface backed by the messaging service's inbox
package/dist/index.js CHANGED
@@ -2119,6 +2119,7 @@ function checkApiExposure(def, action) {
2119
2119
  }
2120
2120
 
2121
2121
  // src/security/resolve-execution-context.ts
2122
+ import { scopesToAgentPermissionSets, MCP_OAUTH_SCOPE_ACTIONS } from "@objectstack/spec/ai";
2122
2123
  import {
2123
2124
  resolveAuthzContext,
2124
2125
  resolveLocalizationContext
@@ -2182,7 +2183,15 @@ async function resolveExecutionContext(opts) {
2182
2183
  isSystem: false
2183
2184
  };
2184
2185
  if (authz.userId) {
2185
- ctx.principalKind = "human";
2186
+ if (oauthPrincipal?.clientId) {
2187
+ ctx.principalKind = "agent";
2188
+ ctx.onBehalfOf = { userId: authz.userId, principalKind: "human" };
2189
+ ctx.permissions = scopesToAgentPermissionSets(oauthPrincipal.scopes);
2190
+ ctx.positions = [];
2191
+ ctx.systemPermissions = oauthPrincipal.scopes?.includes(MCP_OAUTH_SCOPE_ACTIONS) ? authz.systemPermissions ?? [] : [];
2192
+ } else {
2193
+ ctx.principalKind = "human";
2194
+ }
2186
2195
  } else {
2187
2196
  ctx.principalKind = "guest";
2188
2197
  ctx.positions = ["guest"];
@@ -2266,6 +2275,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2266
2275
  }
2267
2276
  };
2268
2277
  this.enforceMembership = options?.enforceProjectMembership ?? true;
2278
+ this.requireAuth = options?.requireAuth ?? false;
2269
2279
  this.kernelResolver = options?.kernelResolver ?? resolveService("kernel-resolver");
2270
2280
  this.scopeManager = options?.scopeManager ?? resolveService("scope-manager");
2271
2281
  }
@@ -3431,6 +3441,15 @@ var _HttpDispatcher = class _HttpDispatcher {
3431
3441
  * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
3432
3442
  */
3433
3443
  async handleMetadata(path, _context, method, body, query) {
3444
+ if (this.requireAuth) {
3445
+ const ec = _context.executionContext;
3446
+ if (!ec?.userId && !ec?.isSystem) {
3447
+ return {
3448
+ handled: true,
3449
+ response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
3450
+ };
3451
+ }
3452
+ }
3434
3453
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
3435
3454
  if (parts[0] === "types") {
3436
3455
  const protocol = await this.resolveService("protocol");
@@ -3730,13 +3749,13 @@ var _HttpDispatcher = class _HttpDispatcher {
3730
3749
  * Handles Analytics requests
3731
3750
  * path: sub-path after /analytics/
3732
3751
  */
3733
- async handleAnalytics(path, method, body, _context) {
3752
+ async handleAnalytics(path, method, body, context) {
3734
3753
  const analyticsService = await this.getService(CoreServiceName.enum.analytics);
3735
3754
  if (!analyticsService) return { handled: false };
3736
3755
  const m = method.toUpperCase();
3737
3756
  const subPath = path.replace(/^\/+/, "");
3738
3757
  if (subPath === "query" && m === "POST") {
3739
- const result = await analyticsService.query(body);
3758
+ const result = await analyticsService.query(body, context?.executionContext);
3740
3759
  return { handled: true, response: this.success(result) };
3741
3760
  }
3742
3761
  if (subPath === "meta" && m === "GET") {
@@ -3744,7 +3763,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3744
3763
  return { handled: true, response: this.success(result) };
3745
3764
  }
3746
3765
  if (subPath === "sql" && m === "POST") {
3747
- const result = await analyticsService.generateSql(body);
3766
+ const result = await analyticsService.generateSql(body, context?.executionContext);
3748
3767
  return { handled: true, response: this.success(result) };
3749
3768
  }
3750
3769
  return { handled: false };
@@ -4932,6 +4951,15 @@ var _HttpDispatcher = class _HttpDispatcher {
4932
4951
  if (route.method !== method) continue;
4933
4952
  const params = matchRoute(route.path, fullPath);
4934
4953
  if (params === null) continue;
4954
+ if (this.requireAuth && route.auth !== false) {
4955
+ const gec = context.executionContext;
4956
+ if (!gec?.userId && !gec?.isSystem) {
4957
+ return {
4958
+ handled: true,
4959
+ response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
4960
+ };
4961
+ }
4962
+ }
4935
4963
  const ec = context.executionContext;
4936
4964
  const user = ec?.userId ? {
4937
4965
  userId: ec.userId,
@@ -5664,7 +5692,7 @@ function resolveErrorReporter(ctx, override) {
5664
5692
  }
5665
5693
 
5666
5694
  // src/dispatcher-plugin.ts
5667
- function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5695
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser, requireAuth = false) {
5668
5696
  const handler = async (req, res) => {
5669
5697
  try {
5670
5698
  let user;
@@ -5674,6 +5702,17 @@ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUs
5674
5702
  } catch {
5675
5703
  }
5676
5704
  }
5705
+ if (requireAuth && route.auth !== false && !user) {
5706
+ res.status(401);
5707
+ if (securityHeaders) {
5708
+ for (const [k, v] of Object.entries(securityHeaders)) res.header(k, v);
5709
+ }
5710
+ res.json({
5711
+ error: "unauthenticated",
5712
+ message: "Authentication is required to access this endpoint."
5713
+ });
5714
+ return;
5715
+ }
5677
5716
  const result = await route.handler({
5678
5717
  body: req.body,
5679
5718
  params: req.params,
@@ -5857,8 +5896,10 @@ function createDispatcherPlugin(config = {}) {
5857
5896
  if (!server) return;
5858
5897
  const kernel = ctx.getKernel();
5859
5898
  const enforceMembership = config.enforceProjectMembership ?? (config.scoping?.enableProjectScoping ?? false);
5899
+ const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? false;
5860
5900
  const dispatcher = new HttpDispatcher(kernel, void 0, {
5861
- enforceProjectMembership: enforceMembership
5901
+ enforceProjectMembership: enforceMembership,
5902
+ requireAuth
5862
5903
  });
5863
5904
  const prefix = config.prefix || "/api/v1";
5864
5905
  const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
@@ -6368,11 +6409,11 @@ function createDispatcherPlugin(config = {}) {
6368
6409
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6369
6410
  let count = 0;
6370
6411
  if (enableProjectScoping && projectResolution === "required") {
6371
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6412
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
6372
6413
  } else {
6373
- if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6414
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser, requireAuth)) count++;
6374
6415
  if (enableProjectScoping) {
6375
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6416
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
6376
6417
  }
6377
6418
  }
6378
6419
  return count;