@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.cjs CHANGED
@@ -2161,7 +2161,7 @@ function safeGet(ctx, name) {
2161
2161
  var import_core5 = require("@objectstack/core");
2162
2162
  var import_types3 = require("@objectstack/types");
2163
2163
  var import_system = require("@objectstack/spec/system");
2164
- var import_ai = require("@objectstack/spec/ai");
2164
+ var import_ai2 = require("@objectstack/spec/ai");
2165
2165
  var import_shared2 = require("@objectstack/spec/shared");
2166
2166
  init_package_state_store();
2167
2167
 
@@ -2195,6 +2195,7 @@ function checkApiExposure(def, action) {
2195
2195
  }
2196
2196
 
2197
2197
  // src/security/resolve-execution-context.ts
2198
+ var import_ai = require("@objectstack/spec/ai");
2198
2199
  var import_core3 = require("@objectstack/core");
2199
2200
  function extractJwtBearer(headers) {
2200
2201
  const auth = headers.get("authorization");
@@ -2255,7 +2256,15 @@ async function resolveExecutionContext(opts) {
2255
2256
  isSystem: false
2256
2257
  };
2257
2258
  if (authz.userId) {
2258
- ctx.principalKind = "human";
2259
+ if (oauthPrincipal?.clientId) {
2260
+ ctx.principalKind = "agent";
2261
+ ctx.onBehalfOf = { userId: authz.userId, principalKind: "human" };
2262
+ ctx.permissions = (0, import_ai.scopesToAgentPermissionSets)(oauthPrincipal.scopes);
2263
+ ctx.positions = [];
2264
+ ctx.systemPermissions = oauthPrincipal.scopes?.includes(import_ai.MCP_OAUTH_SCOPE_ACTIONS) ? authz.systemPermissions ?? [] : [];
2265
+ } else {
2266
+ ctx.principalKind = "human";
2267
+ }
2259
2268
  } else {
2260
2269
  ctx.principalKind = "guest";
2261
2270
  ctx.positions = ["guest"];
@@ -2331,6 +2340,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2331
2340
  }
2332
2341
  };
2333
2342
  this.enforceMembership = options?.enforceProjectMembership ?? true;
2343
+ this.requireAuth = options?.requireAuth ?? false;
2334
2344
  this.kernelResolver = options?.kernelResolver ?? resolveService("kernel-resolver");
2335
2345
  this.scopeManager = options?.scopeManager ?? resolveService("scope-manager");
2336
2346
  }
@@ -2548,14 +2558,14 @@ var _HttpDispatcher = class _HttpDispatcher {
2548
2558
  return { handled: true, response };
2549
2559
  }
2550
2560
  const grantedScopes = Array.isArray(ec.oauthScopes) ? ec.oauthScopes : void 0;
2551
- if (grantedScopes && !grantedScopes.some((s) => import_ai.MCP_OAUTH_SCOPES.includes(s))) {
2561
+ if (grantedScopes && !grantedScopes.some((s) => import_ai2.MCP_OAUTH_SCOPES.includes(s))) {
2552
2562
  const resourceMetadataUrl = await this.getMcpResourceMetadataUrl(context);
2553
2563
  const response = this.error(
2554
- `Forbidden: the access token grants none of the MCP scopes (${import_ai.MCP_OAUTH_SCOPES.join(", ")})`,
2564
+ `Forbidden: the access token grants none of the MCP scopes (${import_ai2.MCP_OAUTH_SCOPES.join(", ")})`,
2555
2565
  403
2556
2566
  );
2557
2567
  response.headers = {
2558
- "WWW-Authenticate": `Bearer error="insufficient_scope", scope="${import_ai.MCP_OAUTH_SCOPES.join(" ")}"` + (resourceMetadataUrl ? `, resource_metadata="${resourceMetadataUrl}"` : "")
2568
+ "WWW-Authenticate": `Bearer error="insufficient_scope", scope="${import_ai2.MCP_OAUTH_SCOPES.join(" ")}"` + (resourceMetadataUrl ? `, resource_metadata="${resourceMetadataUrl}"` : "")
2559
2569
  };
2560
2570
  return { handled: true, response };
2561
2571
  }
@@ -2622,6 +2632,73 @@ var _HttpDispatcher = class _HttpDispatcher {
2622
2632
  return null;
2623
2633
  }
2624
2634
  }
2635
+ /**
2636
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
2637
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
2638
+ * generic skill; only the connection URL is environment-specific).
2639
+ *
2640
+ * Served PUBLIC like `/discovery`: the content is generic agent
2641
+ * instructions plus a URL the caller already knows — no schema, no
2642
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
2643
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
2644
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
2645
+ */
2646
+ async handleMcpSkill(method, context) {
2647
+ if (!_HttpDispatcher.isMcpEnabled()) {
2648
+ return { handled: true, response: this.error("MCP server is not enabled for this environment", 404) };
2649
+ }
2650
+ if (method !== "GET") {
2651
+ return {
2652
+ handled: true,
2653
+ response: {
2654
+ status: 405,
2655
+ headers: { Allow: "GET" },
2656
+ body: { success: false, error: { message: "Method not allowed \u2014 use GET", code: 405 } }
2657
+ }
2658
+ };
2659
+ }
2660
+ const mcp = await this.resolveService("mcp", context.environmentId);
2661
+ if (!mcp || typeof mcp.renderSkill !== "function") {
2662
+ return { handled: true, response: this.error("MCP server is not available", 501) };
2663
+ }
2664
+ let mcpUrl;
2665
+ try {
2666
+ const authService = await this.resolveService("auth", context.environmentId);
2667
+ const url = authService?.getMcpResourceUrl?.();
2668
+ if (typeof url === "string" && url) mcpUrl = url;
2669
+ } catch {
2670
+ }
2671
+ if (!mcpUrl) {
2672
+ try {
2673
+ const webReq = this.toMcpWebRequest(context.request, void 0);
2674
+ const host = webReq?.headers.get("host");
2675
+ if (host) {
2676
+ const proto = webReq?.headers.get("x-forwarded-proto") || "http";
2677
+ mcpUrl = `${proto}://${host}/api/v1/mcp`;
2678
+ }
2679
+ } catch {
2680
+ }
2681
+ }
2682
+ const markdown = mcp.renderSkill({ mcpUrl });
2683
+ return {
2684
+ handled: true,
2685
+ result: {
2686
+ type: "stream",
2687
+ status: 200,
2688
+ contentType: "text/markdown; charset=utf-8",
2689
+ headers: {
2690
+ "content-type": "text/markdown; charset=utf-8",
2691
+ "content-disposition": 'inline; filename="SKILL.md"',
2692
+ // Same reasoning as /discovery (cloud#152): reflects mutable
2693
+ // runtime config (base URL), must never be edge-cached stale.
2694
+ "cache-control": "no-store"
2695
+ },
2696
+ events: (async function* () {
2697
+ yield markdown;
2698
+ })()
2699
+ }
2700
+ };
2701
+ }
2625
2702
  /**
2626
2703
  * Normalise the inbound request into a Web-standard `Request` for the MCP
2627
2704
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -3429,6 +3506,15 @@ var _HttpDispatcher = class _HttpDispatcher {
3429
3506
  * Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
3430
3507
  */
3431
3508
  async handleMetadata(path, _context, method, body, query) {
3509
+ if (this.requireAuth) {
3510
+ const ec = _context.executionContext;
3511
+ if (!ec?.userId && !ec?.isSystem) {
3512
+ return {
3513
+ handled: true,
3514
+ response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
3515
+ };
3516
+ }
3517
+ }
3432
3518
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
3433
3519
  if (parts[0] === "types") {
3434
3520
  const protocol = await this.resolveService("protocol");
@@ -3728,13 +3814,13 @@ var _HttpDispatcher = class _HttpDispatcher {
3728
3814
  * Handles Analytics requests
3729
3815
  * path: sub-path after /analytics/
3730
3816
  */
3731
- async handleAnalytics(path, method, body, _context) {
3817
+ async handleAnalytics(path, method, body, context) {
3732
3818
  const analyticsService = await this.getService(import_system.CoreServiceName.enum.analytics);
3733
3819
  if (!analyticsService) return { handled: false };
3734
3820
  const m = method.toUpperCase();
3735
3821
  const subPath = path.replace(/^\/+/, "");
3736
3822
  if (subPath === "query" && m === "POST") {
3737
- const result = await analyticsService.query(body);
3823
+ const result = await analyticsService.query(body, context?.executionContext);
3738
3824
  return { handled: true, response: this.success(result) };
3739
3825
  }
3740
3826
  if (subPath === "meta" && m === "GET") {
@@ -3742,7 +3828,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3742
3828
  return { handled: true, response: this.success(result) };
3743
3829
  }
3744
3830
  if (subPath === "sql" && m === "POST") {
3745
- const result = await analyticsService.generateSql(body);
3831
+ const result = await analyticsService.generateSql(body, context?.executionContext);
3746
3832
  return { handled: true, response: this.success(result) };
3747
3833
  }
3748
3834
  return { handled: false };
@@ -3787,6 +3873,56 @@ var _HttpDispatcher = class _HttpDispatcher {
3787
3873
  }
3788
3874
  return { handled: false };
3789
3875
  }
3876
+ /**
3877
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
3878
+ * suggested audience bindings. A package's `isDefault: true` permission
3879
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
3880
+ * these routes let an admin see and resolve those suggestions. The
3881
+ * `security` service does the real gating (tenant-admin pre-check, and the
3882
+ * confirm write runs under the audience-anchor + delegated-admin gates
3883
+ * with the caller's execution context — never auto-bound, never system).
3884
+ *
3885
+ * Routes:
3886
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
3887
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
3888
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
3889
+ */
3890
+ async handleSecurity(path, method, _body, query, context) {
3891
+ const service = await this.resolveService("security", context.environmentId);
3892
+ if (!service || typeof service.listAudienceBindingSuggestions !== "function") {
3893
+ return { handled: true, response: this.error("Security service not available", 503) };
3894
+ }
3895
+ const ec = context.executionContext;
3896
+ if (!ec?.userId && !ec?.isSystem) {
3897
+ return { handled: true, response: this.error("Authentication required", 401) };
3898
+ }
3899
+ const m = method.toUpperCase();
3900
+ const parts = path.split("/").filter(Boolean);
3901
+ if (parts[0] !== "suggested-bindings") return { handled: false };
3902
+ try {
3903
+ if (parts.length === 1 && m === "GET") {
3904
+ const status = query?.status ? String(query.status) : void 0;
3905
+ const packageId = query?.packageId ? String(query.packageId) : void 0;
3906
+ const result = await service.listAudienceBindingSuggestions(ec, { status, packageId });
3907
+ return { handled: true, response: this.success(result) };
3908
+ }
3909
+ if (parts.length === 3 && m === "POST") {
3910
+ const id = decodeURIComponent(parts[1]);
3911
+ if (parts[2] === "confirm") {
3912
+ const result = await service.confirmAudienceBindingSuggestion(ec, id);
3913
+ return { handled: true, response: this.success(result) };
3914
+ }
3915
+ if (parts[2] === "dismiss") {
3916
+ const result = await service.dismissAudienceBindingSuggestion(ec, id);
3917
+ return { handled: true, response: this.success(result) };
3918
+ }
3919
+ }
3920
+ return { handled: false };
3921
+ } catch (err) {
3922
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
3923
+ return { handled: true, response: this.error(err?.message ?? "Security operation failed", status) };
3924
+ }
3925
+ }
3790
3926
  /**
3791
3927
  * Handles i18n requests
3792
3928
  * path: sub-path after /i18n/
@@ -4792,8 +4928,16 @@ var _HttpDispatcher = class _HttpDispatcher {
4792
4928
  return Array.isArray(rows) ? rows : rows?.value ?? [];
4793
4929
  }
4794
4930
  };
4795
- const userIdFromAuth = _context?.user?.id ?? _context?.userId ?? "system";
4796
- const userFromAuth = _context?.user ?? { id: userIdFromAuth, name: userIdFromAuth };
4931
+ const ec = _context?.executionContext;
4932
+ const userFromAuth = ec?.userId ? {
4933
+ id: ec.userId,
4934
+ name: ec.userId,
4935
+ email: ec.email,
4936
+ roles: Array.isArray(ec.positions) ? ec.positions : [],
4937
+ positions: Array.isArray(ec.positions) ? ec.positions : [],
4938
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
4939
+ tenantId: ec.tenantId
4940
+ } : { id: "system", name: "system", roles: [], positions: [], permissions: [] };
4797
4941
  const actionContext = {
4798
4942
  record,
4799
4943
  user: userFromAuth,
@@ -4872,6 +5016,15 @@ var _HttpDispatcher = class _HttpDispatcher {
4872
5016
  if (route.method !== method) continue;
4873
5017
  const params = matchRoute(route.path, fullPath);
4874
5018
  if (params === null) continue;
5019
+ if (this.requireAuth && route.auth !== false) {
5020
+ const gec = context.executionContext;
5021
+ if (!gec?.userId && !gec?.isSystem) {
5022
+ return {
5023
+ handled: true,
5024
+ response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
5025
+ };
5026
+ }
5027
+ }
4875
5028
  const ec = context.executionContext;
4876
5029
  const user = ec?.userId ? {
4877
5030
  userId: ec.userId,
@@ -5175,6 +5328,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5175
5328
  if (cleanPath.startsWith("/data")) {
5176
5329
  return this.handleData(cleanPath.substring(5), method, body, query, context);
5177
5330
  }
5331
+ if (cleanPath === "/mcp/skill" || cleanPath.startsWith("/mcp/skill?")) {
5332
+ return this.handleMcpSkill(method, context);
5333
+ }
5178
5334
  if (cleanPath === "/mcp" || cleanPath.startsWith("/mcp/") || cleanPath.startsWith("/mcp?")) {
5179
5335
  return this.handleMcp(body, context);
5180
5336
  }
@@ -5202,6 +5358,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5202
5358
  if (cleanPath.startsWith("/notifications")) {
5203
5359
  return this.handleNotification(cleanPath.substring(14), method, body, query, context);
5204
5360
  }
5361
+ if (cleanPath === "/security" || cleanPath.startsWith("/security/")) {
5362
+ return this.handleSecurity(cleanPath.substring(9), method, body, query, context);
5363
+ }
5205
5364
  if (cleanPath.startsWith("/packages")) {
5206
5365
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5207
5366
  }
@@ -5588,7 +5747,7 @@ function resolveErrorReporter(ctx, override) {
5588
5747
  }
5589
5748
 
5590
5749
  // src/dispatcher-plugin.ts
5591
- function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5750
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser, requireAuth = false) {
5592
5751
  const handler = async (req, res) => {
5593
5752
  try {
5594
5753
  let user;
@@ -5598,6 +5757,17 @@ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUs
5598
5757
  } catch {
5599
5758
  }
5600
5759
  }
5760
+ if (requireAuth && route.auth !== false && !user) {
5761
+ res.status(401);
5762
+ if (securityHeaders) {
5763
+ for (const [k, v] of Object.entries(securityHeaders)) res.header(k, v);
5764
+ }
5765
+ res.json({
5766
+ error: "unauthenticated",
5767
+ message: "Authentication is required to access this endpoint."
5768
+ });
5769
+ return;
5770
+ }
5601
5771
  const result = await route.handler({
5602
5772
  body: req.body,
5603
5773
  params: req.params,
@@ -5781,8 +5951,10 @@ function createDispatcherPlugin(config = {}) {
5781
5951
  if (!server) return;
5782
5952
  const kernel = ctx.getKernel();
5783
5953
  const enforceMembership = config.enforceProjectMembership ?? (config.scoping?.enableProjectScoping ?? false);
5954
+ const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? false;
5784
5955
  const dispatcher = new HttpDispatcher(kernel, void 0, {
5785
- enforceProjectMembership: enforceMembership
5956
+ enforceProjectMembership: enforceMembership,
5957
+ requireAuth
5786
5958
  });
5787
5959
  const prefix = config.prefix || "/api/v1";
5788
5960
  const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
@@ -5910,6 +6082,14 @@ function createDispatcherPlugin(config = {}) {
5910
6082
  mountMcp("POST");
5911
6083
  mountMcp("GET");
5912
6084
  mountMcp("DELETE");
6085
+ server.get(`${prefix}/mcp/skill`, async (req, res) => {
6086
+ try {
6087
+ const result = await dispatcher.dispatch("GET", "/mcp/skill", req.body, req.query, { request: req });
6088
+ sendResult(result, res);
6089
+ } catch (err) {
6090
+ errorResponse(err, res);
6091
+ }
6092
+ });
5913
6093
  server.post(`${prefix}/keys`, async (req, res) => {
5914
6094
  try {
5915
6095
  const result = await dispatcher.dispatch("POST", "/keys", req.body, req.query, { request: req });
@@ -6214,9 +6394,7 @@ function createDispatcherPlugin(config = {}) {
6214
6394
  const registerActionRoutes = (base) => {
6215
6395
  server.post(`${base}/actions/:object/:action`, async (req, res) => {
6216
6396
  try {
6217
- const ctx2 = { request: req };
6218
- if (req.params?.environmentId) ctx2.environmentId = req.params.environmentId;
6219
- const result = await dispatcher.handleActions(`/${req.params.object}/${req.params.action}`, "POST", req.body, ctx2);
6397
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}`, req.body, req.query, { request: req });
6220
6398
  sendResult(result, res);
6221
6399
  } catch (err) {
6222
6400
  errorResponse(err, res);
@@ -6224,9 +6402,7 @@ function createDispatcherPlugin(config = {}) {
6224
6402
  });
6225
6403
  server.post(`${base}/actions/:object/:action/:recordId`, async (req, res) => {
6226
6404
  try {
6227
- const ctx2 = { request: req };
6228
- if (req.params?.environmentId) ctx2.environmentId = req.params.environmentId;
6229
- const result = await dispatcher.handleActions(`/${req.params.object}/${req.params.action}/${req.params.recordId}`, "POST", req.body, ctx2);
6405
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}/${req.params.recordId}`, req.body, req.query, { request: req });
6230
6406
  sendResult(result, res);
6231
6407
  } catch (err) {
6232
6408
  errorResponse(err, res);
@@ -6288,11 +6464,11 @@ function createDispatcherPlugin(config = {}) {
6288
6464
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6289
6465
  let count = 0;
6290
6466
  if (enableProjectScoping && projectResolution === "required") {
6291
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6467
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
6292
6468
  } else {
6293
- if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6469
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser, requireAuth)) count++;
6294
6470
  if (enableProjectScoping) {
6295
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6471
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
6296
6472
  }
6297
6473
  }
6298
6474
  return count;