@objectstack/runtime 13.0.0 → 14.5.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
@@ -1065,6 +1092,18 @@ declare class HttpDispatcher {
1065
1092
  * OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
1066
1093
  */
1067
1094
  private getMcpResourceMetadataUrl;
1095
+ /**
1096
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
1097
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
1098
+ * generic skill; only the connection URL is environment-specific).
1099
+ *
1100
+ * Served PUBLIC like `/discovery`: the content is generic agent
1101
+ * instructions plus a URL the caller already knows — no schema, no
1102
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
1103
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
1104
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
1105
+ */
1106
+ handleMcpSkill(method: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1068
1107
  /**
1069
1108
  * Normalise the inbound request into a Web-standard `Request` for the MCP
1070
1109
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -1486,7 +1525,7 @@ declare class HttpDispatcher {
1486
1525
  * Handles Analytics requests
1487
1526
  * path: sub-path after /analytics/
1488
1527
  */
1489
- handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1528
+ handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1490
1529
  /**
1491
1530
  * Handles in-app notification requests (ADR-0030) — the
1492
1531
  * `/api/v1/notifications` surface backed by the messaging service's inbox
@@ -1501,6 +1540,21 @@ declare class HttpDispatcher {
1501
1540
  * POST /read/all → markAllRead
1502
1541
  */
1503
1542
  handleNotification(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1543
+ /**
1544
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
1545
+ * suggested audience bindings. A package's `isDefault: true` permission
1546
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
1547
+ * these routes let an admin see and resolve those suggestions. The
1548
+ * `security` service does the real gating (tenant-admin pre-check, and the
1549
+ * confirm write runs under the audience-anchor + delegated-admin gates
1550
+ * with the caller's execution context — never auto-bound, never system).
1551
+ *
1552
+ * Routes:
1553
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
1554
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
1555
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
1556
+ */
1557
+ handleSecurity(path: string, method: string, _body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1504
1558
  /**
1505
1559
  * Handles i18n requests
1506
1560
  * path: sub-path after /i18n/
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
@@ -1065,6 +1092,18 @@ declare class HttpDispatcher {
1065
1092
  * OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
1066
1093
  */
1067
1094
  private getMcpResourceMetadataUrl;
1095
+ /**
1096
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
1097
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
1098
+ * generic skill; only the connection URL is environment-specific).
1099
+ *
1100
+ * Served PUBLIC like `/discovery`: the content is generic agent
1101
+ * instructions plus a URL the caller already knows — no schema, no
1102
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
1103
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
1104
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
1105
+ */
1106
+ handleMcpSkill(method: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1068
1107
  /**
1069
1108
  * Normalise the inbound request into a Web-standard `Request` for the MCP
1070
1109
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -1486,7 +1525,7 @@ declare class HttpDispatcher {
1486
1525
  * Handles Analytics requests
1487
1526
  * path: sub-path after /analytics/
1488
1527
  */
1489
- handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1528
+ handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1490
1529
  /**
1491
1530
  * Handles in-app notification requests (ADR-0030) — the
1492
1531
  * `/api/v1/notifications` surface backed by the messaging service's inbox
@@ -1501,6 +1540,21 @@ declare class HttpDispatcher {
1501
1540
  * POST /read/all → markAllRead
1502
1541
  */
1503
1542
  handleNotification(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1543
+ /**
1544
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
1545
+ * suggested audience bindings. A package's `isDefault: true` permission
1546
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
1547
+ * these routes let an admin see and resolve those suggestions. The
1548
+ * `security` service does the real gating (tenant-admin pre-check, and the
1549
+ * confirm write runs under the audience-anchor + delegated-admin gates
1550
+ * with the caller's execution context — never auto-bound, never system).
1551
+ *
1552
+ * Routes:
1553
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
1554
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
1555
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
1556
+ */
1557
+ handleSecurity(path: string, method: string, _body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1504
1558
  /**
1505
1559
  * Handles i18n requests
1506
1560
  * path: sub-path after /i18n/
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
  }
@@ -2557,6 +2567,73 @@ var _HttpDispatcher = class _HttpDispatcher {
2557
2567
  return null;
2558
2568
  }
2559
2569
  }
2570
+ /**
2571
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
2572
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
2573
+ * generic skill; only the connection URL is environment-specific).
2574
+ *
2575
+ * Served PUBLIC like `/discovery`: the content is generic agent
2576
+ * instructions plus a URL the caller already knows — no schema, no
2577
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
2578
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
2579
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
2580
+ */
2581
+ async handleMcpSkill(method, context) {
2582
+ if (!_HttpDispatcher.isMcpEnabled()) {
2583
+ return { handled: true, response: this.error("MCP server is not enabled for this environment", 404) };
2584
+ }
2585
+ if (method !== "GET") {
2586
+ return {
2587
+ handled: true,
2588
+ response: {
2589
+ status: 405,
2590
+ headers: { Allow: "GET" },
2591
+ body: { success: false, error: { message: "Method not allowed \u2014 use GET", code: 405 } }
2592
+ }
2593
+ };
2594
+ }
2595
+ const mcp = await this.resolveService("mcp", context.environmentId);
2596
+ if (!mcp || typeof mcp.renderSkill !== "function") {
2597
+ return { handled: true, response: this.error("MCP server is not available", 501) };
2598
+ }
2599
+ let mcpUrl;
2600
+ try {
2601
+ const authService = await this.resolveService("auth", context.environmentId);
2602
+ const url = authService?.getMcpResourceUrl?.();
2603
+ if (typeof url === "string" && url) mcpUrl = url;
2604
+ } catch {
2605
+ }
2606
+ if (!mcpUrl) {
2607
+ try {
2608
+ const webReq = this.toMcpWebRequest(context.request, void 0);
2609
+ const host = webReq?.headers.get("host");
2610
+ if (host) {
2611
+ const proto = webReq?.headers.get("x-forwarded-proto") || "http";
2612
+ mcpUrl = `${proto}://${host}/api/v1/mcp`;
2613
+ }
2614
+ } catch {
2615
+ }
2616
+ }
2617
+ const markdown = mcp.renderSkill({ mcpUrl });
2618
+ return {
2619
+ handled: true,
2620
+ result: {
2621
+ type: "stream",
2622
+ status: 200,
2623
+ contentType: "text/markdown; charset=utf-8",
2624
+ headers: {
2625
+ "content-type": "text/markdown; charset=utf-8",
2626
+ "content-disposition": 'inline; filename="SKILL.md"',
2627
+ // Same reasoning as /discovery (cloud#152): reflects mutable
2628
+ // runtime config (base URL), must never be edge-cached stale.
2629
+ "cache-control": "no-store"
2630
+ },
2631
+ events: (async function* () {
2632
+ yield markdown;
2633
+ })()
2634
+ }
2635
+ };
2636
+ }
2560
2637
  /**
2561
2638
  * Normalise the inbound request into a Web-standard `Request` for the MCP
2562
2639
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -3364,6 +3441,15 @@ var _HttpDispatcher = class _HttpDispatcher {
3364
3441
  * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
3365
3442
  */
3366
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
+ }
3367
3453
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
3368
3454
  if (parts[0] === "types") {
3369
3455
  const protocol = await this.resolveService("protocol");
@@ -3663,13 +3749,13 @@ var _HttpDispatcher = class _HttpDispatcher {
3663
3749
  * Handles Analytics requests
3664
3750
  * path: sub-path after /analytics/
3665
3751
  */
3666
- async handleAnalytics(path, method, body, _context) {
3752
+ async handleAnalytics(path, method, body, context) {
3667
3753
  const analyticsService = await this.getService(CoreServiceName.enum.analytics);
3668
3754
  if (!analyticsService) return { handled: false };
3669
3755
  const m = method.toUpperCase();
3670
3756
  const subPath = path.replace(/^\/+/, "");
3671
3757
  if (subPath === "query" && m === "POST") {
3672
- const result = await analyticsService.query(body);
3758
+ const result = await analyticsService.query(body, context?.executionContext);
3673
3759
  return { handled: true, response: this.success(result) };
3674
3760
  }
3675
3761
  if (subPath === "meta" && m === "GET") {
@@ -3677,7 +3763,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3677
3763
  return { handled: true, response: this.success(result) };
3678
3764
  }
3679
3765
  if (subPath === "sql" && m === "POST") {
3680
- const result = await analyticsService.generateSql(body);
3766
+ const result = await analyticsService.generateSql(body, context?.executionContext);
3681
3767
  return { handled: true, response: this.success(result) };
3682
3768
  }
3683
3769
  return { handled: false };
@@ -3722,6 +3808,56 @@ var _HttpDispatcher = class _HttpDispatcher {
3722
3808
  }
3723
3809
  return { handled: false };
3724
3810
  }
3811
+ /**
3812
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
3813
+ * suggested audience bindings. A package's `isDefault: true` permission
3814
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
3815
+ * these routes let an admin see and resolve those suggestions. The
3816
+ * `security` service does the real gating (tenant-admin pre-check, and the
3817
+ * confirm write runs under the audience-anchor + delegated-admin gates
3818
+ * with the caller's execution context — never auto-bound, never system).
3819
+ *
3820
+ * Routes:
3821
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
3822
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
3823
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
3824
+ */
3825
+ async handleSecurity(path, method, _body, query, context) {
3826
+ const service = await this.resolveService("security", context.environmentId);
3827
+ if (!service || typeof service.listAudienceBindingSuggestions !== "function") {
3828
+ return { handled: true, response: this.error("Security service not available", 503) };
3829
+ }
3830
+ const ec = context.executionContext;
3831
+ if (!ec?.userId && !ec?.isSystem) {
3832
+ return { handled: true, response: this.error("Authentication required", 401) };
3833
+ }
3834
+ const m = method.toUpperCase();
3835
+ const parts = path.split("/").filter(Boolean);
3836
+ if (parts[0] !== "suggested-bindings") return { handled: false };
3837
+ try {
3838
+ if (parts.length === 1 && m === "GET") {
3839
+ const status = query?.status ? String(query.status) : void 0;
3840
+ const packageId = query?.packageId ? String(query.packageId) : void 0;
3841
+ const result = await service.listAudienceBindingSuggestions(ec, { status, packageId });
3842
+ return { handled: true, response: this.success(result) };
3843
+ }
3844
+ if (parts.length === 3 && m === "POST") {
3845
+ const id = decodeURIComponent(parts[1]);
3846
+ if (parts[2] === "confirm") {
3847
+ const result = await service.confirmAudienceBindingSuggestion(ec, id);
3848
+ return { handled: true, response: this.success(result) };
3849
+ }
3850
+ if (parts[2] === "dismiss") {
3851
+ const result = await service.dismissAudienceBindingSuggestion(ec, id);
3852
+ return { handled: true, response: this.success(result) };
3853
+ }
3854
+ }
3855
+ return { handled: false };
3856
+ } catch (err) {
3857
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
3858
+ return { handled: true, response: this.error(err?.message ?? "Security operation failed", status) };
3859
+ }
3860
+ }
3725
3861
  /**
3726
3862
  * Handles i18n requests
3727
3863
  * path: sub-path after /i18n/
@@ -4727,8 +4863,16 @@ var _HttpDispatcher = class _HttpDispatcher {
4727
4863
  return Array.isArray(rows) ? rows : rows?.value ?? [];
4728
4864
  }
4729
4865
  };
4730
- const userIdFromAuth = _context?.user?.id ?? _context?.userId ?? "system";
4731
- const userFromAuth = _context?.user ?? { id: userIdFromAuth, name: userIdFromAuth };
4866
+ const ec = _context?.executionContext;
4867
+ const userFromAuth = ec?.userId ? {
4868
+ id: ec.userId,
4869
+ name: ec.userId,
4870
+ email: ec.email,
4871
+ roles: Array.isArray(ec.positions) ? ec.positions : [],
4872
+ positions: Array.isArray(ec.positions) ? ec.positions : [],
4873
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
4874
+ tenantId: ec.tenantId
4875
+ } : { id: "system", name: "system", roles: [], positions: [], permissions: [] };
4732
4876
  const actionContext = {
4733
4877
  record,
4734
4878
  user: userFromAuth,
@@ -4807,6 +4951,15 @@ var _HttpDispatcher = class _HttpDispatcher {
4807
4951
  if (route.method !== method) continue;
4808
4952
  const params = matchRoute(route.path, fullPath);
4809
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
+ }
4810
4963
  const ec = context.executionContext;
4811
4964
  const user = ec?.userId ? {
4812
4965
  userId: ec.userId,
@@ -5110,6 +5263,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5110
5263
  if (cleanPath.startsWith("/data")) {
5111
5264
  return this.handleData(cleanPath.substring(5), method, body, query, context);
5112
5265
  }
5266
+ if (cleanPath === "/mcp/skill" || cleanPath.startsWith("/mcp/skill?")) {
5267
+ return this.handleMcpSkill(method, context);
5268
+ }
5113
5269
  if (cleanPath === "/mcp" || cleanPath.startsWith("/mcp/") || cleanPath.startsWith("/mcp?")) {
5114
5270
  return this.handleMcp(body, context);
5115
5271
  }
@@ -5137,6 +5293,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5137
5293
  if (cleanPath.startsWith("/notifications")) {
5138
5294
  return this.handleNotification(cleanPath.substring(14), method, body, query, context);
5139
5295
  }
5296
+ if (cleanPath === "/security" || cleanPath.startsWith("/security/")) {
5297
+ return this.handleSecurity(cleanPath.substring(9), method, body, query, context);
5298
+ }
5140
5299
  if (cleanPath.startsWith("/packages")) {
5141
5300
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5142
5301
  }
@@ -5533,7 +5692,7 @@ function resolveErrorReporter(ctx, override) {
5533
5692
  }
5534
5693
 
5535
5694
  // src/dispatcher-plugin.ts
5536
- function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5695
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser, requireAuth = false) {
5537
5696
  const handler = async (req, res) => {
5538
5697
  try {
5539
5698
  let user;
@@ -5543,6 +5702,17 @@ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUs
5543
5702
  } catch {
5544
5703
  }
5545
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
+ }
5546
5716
  const result = await route.handler({
5547
5717
  body: req.body,
5548
5718
  params: req.params,
@@ -5726,8 +5896,10 @@ function createDispatcherPlugin(config = {}) {
5726
5896
  if (!server) return;
5727
5897
  const kernel = ctx.getKernel();
5728
5898
  const enforceMembership = config.enforceProjectMembership ?? (config.scoping?.enableProjectScoping ?? false);
5899
+ const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? false;
5729
5900
  const dispatcher = new HttpDispatcher(kernel, void 0, {
5730
- enforceProjectMembership: enforceMembership
5901
+ enforceProjectMembership: enforceMembership,
5902
+ requireAuth
5731
5903
  });
5732
5904
  const prefix = config.prefix || "/api/v1";
5733
5905
  const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
@@ -5855,6 +6027,14 @@ function createDispatcherPlugin(config = {}) {
5855
6027
  mountMcp("POST");
5856
6028
  mountMcp("GET");
5857
6029
  mountMcp("DELETE");
6030
+ server.get(`${prefix}/mcp/skill`, async (req, res) => {
6031
+ try {
6032
+ const result = await dispatcher.dispatch("GET", "/mcp/skill", req.body, req.query, { request: req });
6033
+ sendResult(result, res);
6034
+ } catch (err) {
6035
+ errorResponse(err, res);
6036
+ }
6037
+ });
5858
6038
  server.post(`${prefix}/keys`, async (req, res) => {
5859
6039
  try {
5860
6040
  const result = await dispatcher.dispatch("POST", "/keys", req.body, req.query, { request: req });
@@ -6159,9 +6339,7 @@ function createDispatcherPlugin(config = {}) {
6159
6339
  const registerActionRoutes = (base) => {
6160
6340
  server.post(`${base}/actions/:object/:action`, async (req, res) => {
6161
6341
  try {
6162
- const ctx2 = { request: req };
6163
- if (req.params?.environmentId) ctx2.environmentId = req.params.environmentId;
6164
- const result = await dispatcher.handleActions(`/${req.params.object}/${req.params.action}`, "POST", req.body, ctx2);
6342
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}`, req.body, req.query, { request: req });
6165
6343
  sendResult(result, res);
6166
6344
  } catch (err) {
6167
6345
  errorResponse(err, res);
@@ -6169,9 +6347,7 @@ function createDispatcherPlugin(config = {}) {
6169
6347
  });
6170
6348
  server.post(`${base}/actions/:object/:action/:recordId`, async (req, res) => {
6171
6349
  try {
6172
- const ctx2 = { request: req };
6173
- if (req.params?.environmentId) ctx2.environmentId = req.params.environmentId;
6174
- const result = await dispatcher.handleActions(`/${req.params.object}/${req.params.action}/${req.params.recordId}`, "POST", req.body, ctx2);
6350
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}/${req.params.recordId}`, req.body, req.query, { request: req });
6175
6351
  sendResult(result, res);
6176
6352
  } catch (err) {
6177
6353
  errorResponse(err, res);
@@ -6233,11 +6409,11 @@ function createDispatcherPlugin(config = {}) {
6233
6409
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6234
6410
  let count = 0;
6235
6411
  if (enableProjectScoping && projectResolution === "required") {
6236
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6412
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
6237
6413
  } else {
6238
- if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6414
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser, requireAuth)) count++;
6239
6415
  if (enableProjectScoping) {
6240
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6416
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
6241
6417
  }
6242
6418
  }
6243
6419
  return count;