@objectstack/runtime 13.0.0 → 14.3.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
@@ -2622,6 +2622,73 @@ var _HttpDispatcher = class _HttpDispatcher {
2622
2622
  return null;
2623
2623
  }
2624
2624
  }
2625
+ /**
2626
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
2627
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
2628
+ * generic skill; only the connection URL is environment-specific).
2629
+ *
2630
+ * Served PUBLIC like `/discovery`: the content is generic agent
2631
+ * instructions plus a URL the caller already knows — no schema, no
2632
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
2633
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
2634
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
2635
+ */
2636
+ async handleMcpSkill(method, context) {
2637
+ if (!_HttpDispatcher.isMcpEnabled()) {
2638
+ return { handled: true, response: this.error("MCP server is not enabled for this environment", 404) };
2639
+ }
2640
+ if (method !== "GET") {
2641
+ return {
2642
+ handled: true,
2643
+ response: {
2644
+ status: 405,
2645
+ headers: { Allow: "GET" },
2646
+ body: { success: false, error: { message: "Method not allowed \u2014 use GET", code: 405 } }
2647
+ }
2648
+ };
2649
+ }
2650
+ const mcp = await this.resolveService("mcp", context.environmentId);
2651
+ if (!mcp || typeof mcp.renderSkill !== "function") {
2652
+ return { handled: true, response: this.error("MCP server is not available", 501) };
2653
+ }
2654
+ let mcpUrl;
2655
+ try {
2656
+ const authService = await this.resolveService("auth", context.environmentId);
2657
+ const url = authService?.getMcpResourceUrl?.();
2658
+ if (typeof url === "string" && url) mcpUrl = url;
2659
+ } catch {
2660
+ }
2661
+ if (!mcpUrl) {
2662
+ try {
2663
+ const webReq = this.toMcpWebRequest(context.request, void 0);
2664
+ const host = webReq?.headers.get("host");
2665
+ if (host) {
2666
+ const proto = webReq?.headers.get("x-forwarded-proto") || "http";
2667
+ mcpUrl = `${proto}://${host}/api/v1/mcp`;
2668
+ }
2669
+ } catch {
2670
+ }
2671
+ }
2672
+ const markdown = mcp.renderSkill({ mcpUrl });
2673
+ return {
2674
+ handled: true,
2675
+ result: {
2676
+ type: "stream",
2677
+ status: 200,
2678
+ contentType: "text/markdown; charset=utf-8",
2679
+ headers: {
2680
+ "content-type": "text/markdown; charset=utf-8",
2681
+ "content-disposition": 'inline; filename="SKILL.md"',
2682
+ // Same reasoning as /discovery (cloud#152): reflects mutable
2683
+ // runtime config (base URL), must never be edge-cached stale.
2684
+ "cache-control": "no-store"
2685
+ },
2686
+ events: (async function* () {
2687
+ yield markdown;
2688
+ })()
2689
+ }
2690
+ };
2691
+ }
2625
2692
  /**
2626
2693
  * Normalise the inbound request into a Web-standard `Request` for the MCP
2627
2694
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -3787,6 +3854,56 @@ var _HttpDispatcher = class _HttpDispatcher {
3787
3854
  }
3788
3855
  return { handled: false };
3789
3856
  }
3857
+ /**
3858
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
3859
+ * suggested audience bindings. A package's `isDefault: true` permission
3860
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
3861
+ * these routes let an admin see and resolve those suggestions. The
3862
+ * `security` service does the real gating (tenant-admin pre-check, and the
3863
+ * confirm write runs under the audience-anchor + delegated-admin gates
3864
+ * with the caller's execution context — never auto-bound, never system).
3865
+ *
3866
+ * Routes:
3867
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
3868
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
3869
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
3870
+ */
3871
+ async handleSecurity(path, method, _body, query, context) {
3872
+ const service = await this.resolveService("security", context.environmentId);
3873
+ if (!service || typeof service.listAudienceBindingSuggestions !== "function") {
3874
+ return { handled: true, response: this.error("Security service not available", 503) };
3875
+ }
3876
+ const ec = context.executionContext;
3877
+ if (!ec?.userId && !ec?.isSystem) {
3878
+ return { handled: true, response: this.error("Authentication required", 401) };
3879
+ }
3880
+ const m = method.toUpperCase();
3881
+ const parts = path.split("/").filter(Boolean);
3882
+ if (parts[0] !== "suggested-bindings") return { handled: false };
3883
+ try {
3884
+ if (parts.length === 1 && m === "GET") {
3885
+ const status = query?.status ? String(query.status) : void 0;
3886
+ const packageId = query?.packageId ? String(query.packageId) : void 0;
3887
+ const result = await service.listAudienceBindingSuggestions(ec, { status, packageId });
3888
+ return { handled: true, response: this.success(result) };
3889
+ }
3890
+ if (parts.length === 3 && m === "POST") {
3891
+ const id = decodeURIComponent(parts[1]);
3892
+ if (parts[2] === "confirm") {
3893
+ const result = await service.confirmAudienceBindingSuggestion(ec, id);
3894
+ return { handled: true, response: this.success(result) };
3895
+ }
3896
+ if (parts[2] === "dismiss") {
3897
+ const result = await service.dismissAudienceBindingSuggestion(ec, id);
3898
+ return { handled: true, response: this.success(result) };
3899
+ }
3900
+ }
3901
+ return { handled: false };
3902
+ } catch (err) {
3903
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
3904
+ return { handled: true, response: this.error(err?.message ?? "Security operation failed", status) };
3905
+ }
3906
+ }
3790
3907
  /**
3791
3908
  * Handles i18n requests
3792
3909
  * path: sub-path after /i18n/
@@ -4792,8 +4909,16 @@ var _HttpDispatcher = class _HttpDispatcher {
4792
4909
  return Array.isArray(rows) ? rows : rows?.value ?? [];
4793
4910
  }
4794
4911
  };
4795
- const userIdFromAuth = _context?.user?.id ?? _context?.userId ?? "system";
4796
- const userFromAuth = _context?.user ?? { id: userIdFromAuth, name: userIdFromAuth };
4912
+ const ec = _context?.executionContext;
4913
+ const userFromAuth = ec?.userId ? {
4914
+ id: ec.userId,
4915
+ name: ec.userId,
4916
+ email: ec.email,
4917
+ roles: Array.isArray(ec.positions) ? ec.positions : [],
4918
+ positions: Array.isArray(ec.positions) ? ec.positions : [],
4919
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
4920
+ tenantId: ec.tenantId
4921
+ } : { id: "system", name: "system", roles: [], positions: [], permissions: [] };
4797
4922
  const actionContext = {
4798
4923
  record,
4799
4924
  user: userFromAuth,
@@ -5175,6 +5300,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5175
5300
  if (cleanPath.startsWith("/data")) {
5176
5301
  return this.handleData(cleanPath.substring(5), method, body, query, context);
5177
5302
  }
5303
+ if (cleanPath === "/mcp/skill" || cleanPath.startsWith("/mcp/skill?")) {
5304
+ return this.handleMcpSkill(method, context);
5305
+ }
5178
5306
  if (cleanPath === "/mcp" || cleanPath.startsWith("/mcp/") || cleanPath.startsWith("/mcp?")) {
5179
5307
  return this.handleMcp(body, context);
5180
5308
  }
@@ -5202,6 +5330,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5202
5330
  if (cleanPath.startsWith("/notifications")) {
5203
5331
  return this.handleNotification(cleanPath.substring(14), method, body, query, context);
5204
5332
  }
5333
+ if (cleanPath === "/security" || cleanPath.startsWith("/security/")) {
5334
+ return this.handleSecurity(cleanPath.substring(9), method, body, query, context);
5335
+ }
5205
5336
  if (cleanPath.startsWith("/packages")) {
5206
5337
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5207
5338
  }
@@ -5910,6 +6041,14 @@ function createDispatcherPlugin(config = {}) {
5910
6041
  mountMcp("POST");
5911
6042
  mountMcp("GET");
5912
6043
  mountMcp("DELETE");
6044
+ server.get(`${prefix}/mcp/skill`, async (req, res) => {
6045
+ try {
6046
+ const result = await dispatcher.dispatch("GET", "/mcp/skill", req.body, req.query, { request: req });
6047
+ sendResult(result, res);
6048
+ } catch (err) {
6049
+ errorResponse(err, res);
6050
+ }
6051
+ });
5913
6052
  server.post(`${prefix}/keys`, async (req, res) => {
5914
6053
  try {
5915
6054
  const result = await dispatcher.dispatch("POST", "/keys", req.body, req.query, { request: req });
@@ -6214,9 +6353,7 @@ function createDispatcherPlugin(config = {}) {
6214
6353
  const registerActionRoutes = (base) => {
6215
6354
  server.post(`${base}/actions/:object/:action`, async (req, res) => {
6216
6355
  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);
6356
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}`, req.body, req.query, { request: req });
6220
6357
  sendResult(result, res);
6221
6358
  } catch (err) {
6222
6359
  errorResponse(err, res);
@@ -6224,9 +6361,7 @@ function createDispatcherPlugin(config = {}) {
6224
6361
  });
6225
6362
  server.post(`${base}/actions/:object/:action/:recordId`, async (req, res) => {
6226
6363
  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);
6364
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}/${req.params.recordId}`, req.body, req.query, { request: req });
6230
6365
  sendResult(result, res);
6231
6366
  } catch (err) {
6232
6367
  errorResponse(err, res);