@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.d.cts CHANGED
@@ -1065,6 +1065,18 @@ declare class HttpDispatcher {
1065
1065
  * OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
1066
1066
  */
1067
1067
  private getMcpResourceMetadataUrl;
1068
+ /**
1069
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
1070
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
1071
+ * generic skill; only the connection URL is environment-specific).
1072
+ *
1073
+ * Served PUBLIC like `/discovery`: the content is generic agent
1074
+ * instructions plus a URL the caller already knows — no schema, no
1075
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
1076
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
1077
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
1078
+ */
1079
+ handleMcpSkill(method: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1068
1080
  /**
1069
1081
  * Normalise the inbound request into a Web-standard `Request` for the MCP
1070
1082
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -1501,6 +1513,21 @@ declare class HttpDispatcher {
1501
1513
  * POST /read/all → markAllRead
1502
1514
  */
1503
1515
  handleNotification(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1516
+ /**
1517
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
1518
+ * suggested audience bindings. A package's `isDefault: true` permission
1519
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
1520
+ * these routes let an admin see and resolve those suggestions. The
1521
+ * `security` service does the real gating (tenant-admin pre-check, and the
1522
+ * confirm write runs under the audience-anchor + delegated-admin gates
1523
+ * with the caller's execution context — never auto-bound, never system).
1524
+ *
1525
+ * Routes:
1526
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
1527
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
1528
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
1529
+ */
1530
+ handleSecurity(path: string, method: string, _body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1504
1531
  /**
1505
1532
  * Handles i18n requests
1506
1533
  * path: sub-path after /i18n/
package/dist/index.d.ts CHANGED
@@ -1065,6 +1065,18 @@ declare class HttpDispatcher {
1065
1065
  * OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
1066
1066
  */
1067
1067
  private getMcpResourceMetadataUrl;
1068
+ /**
1069
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
1070
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
1071
+ * generic skill; only the connection URL is environment-specific).
1072
+ *
1073
+ * Served PUBLIC like `/discovery`: the content is generic agent
1074
+ * instructions plus a URL the caller already knows — no schema, no
1075
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
1076
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
1077
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
1078
+ */
1079
+ handleMcpSkill(method: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1068
1080
  /**
1069
1081
  * Normalise the inbound request into a Web-standard `Request` for the MCP
1070
1082
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -1501,6 +1513,21 @@ declare class HttpDispatcher {
1501
1513
  * POST /read/all → markAllRead
1502
1514
  */
1503
1515
  handleNotification(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1516
+ /**
1517
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
1518
+ * suggested audience bindings. A package's `isDefault: true` permission
1519
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
1520
+ * these routes let an admin see and resolve those suggestions. The
1521
+ * `security` service does the real gating (tenant-admin pre-check, and the
1522
+ * confirm write runs under the audience-anchor + delegated-admin gates
1523
+ * with the caller's execution context — never auto-bound, never system).
1524
+ *
1525
+ * Routes:
1526
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
1527
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
1528
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
1529
+ */
1530
+ handleSecurity(path: string, method: string, _body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
1504
1531
  /**
1505
1532
  * Handles i18n requests
1506
1533
  * path: sub-path after /i18n/
package/dist/index.js CHANGED
@@ -2557,6 +2557,73 @@ var _HttpDispatcher = class _HttpDispatcher {
2557
2557
  return null;
2558
2558
  }
2559
2559
  }
2560
+ /**
2561
+ * `GET /mcp/skill` — the environment-customized portable Agent Skill
2562
+ * (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
2563
+ * generic skill; only the connection URL is environment-specific).
2564
+ *
2565
+ * Served PUBLIC like `/discovery`: the content is generic agent
2566
+ * instructions plus a URL the caller already knows — no schema, no
2567
+ * tenant data. Gated on the same default-on switch as the `/mcp` route
2568
+ * (404 when opted out, so the surface isn't advertised) and 501 when the
2569
+ * MCP plugin isn't loaded, mirroring `handleMcp`.
2570
+ */
2571
+ async handleMcpSkill(method, context) {
2572
+ if (!_HttpDispatcher.isMcpEnabled()) {
2573
+ return { handled: true, response: this.error("MCP server is not enabled for this environment", 404) };
2574
+ }
2575
+ if (method !== "GET") {
2576
+ return {
2577
+ handled: true,
2578
+ response: {
2579
+ status: 405,
2580
+ headers: { Allow: "GET" },
2581
+ body: { success: false, error: { message: "Method not allowed \u2014 use GET", code: 405 } }
2582
+ }
2583
+ };
2584
+ }
2585
+ const mcp = await this.resolveService("mcp", context.environmentId);
2586
+ if (!mcp || typeof mcp.renderSkill !== "function") {
2587
+ return { handled: true, response: this.error("MCP server is not available", 501) };
2588
+ }
2589
+ let mcpUrl;
2590
+ try {
2591
+ const authService = await this.resolveService("auth", context.environmentId);
2592
+ const url = authService?.getMcpResourceUrl?.();
2593
+ if (typeof url === "string" && url) mcpUrl = url;
2594
+ } catch {
2595
+ }
2596
+ if (!mcpUrl) {
2597
+ try {
2598
+ const webReq = this.toMcpWebRequest(context.request, void 0);
2599
+ const host = webReq?.headers.get("host");
2600
+ if (host) {
2601
+ const proto = webReq?.headers.get("x-forwarded-proto") || "http";
2602
+ mcpUrl = `${proto}://${host}/api/v1/mcp`;
2603
+ }
2604
+ } catch {
2605
+ }
2606
+ }
2607
+ const markdown = mcp.renderSkill({ mcpUrl });
2608
+ return {
2609
+ handled: true,
2610
+ result: {
2611
+ type: "stream",
2612
+ status: 200,
2613
+ contentType: "text/markdown; charset=utf-8",
2614
+ headers: {
2615
+ "content-type": "text/markdown; charset=utf-8",
2616
+ "content-disposition": 'inline; filename="SKILL.md"',
2617
+ // Same reasoning as /discovery (cloud#152): reflects mutable
2618
+ // runtime config (base URL), must never be edge-cached stale.
2619
+ "cache-control": "no-store"
2620
+ },
2621
+ events: (async function* () {
2622
+ yield markdown;
2623
+ })()
2624
+ }
2625
+ };
2626
+ }
2560
2627
  /**
2561
2628
  * Normalise the inbound request into a Web-standard `Request` for the MCP
2562
2629
  * transport. Accepts an already-Web `Request`, or a node/Hono-style req
@@ -3722,6 +3789,56 @@ var _HttpDispatcher = class _HttpDispatcher {
3722
3789
  }
3723
3790
  return { handled: false };
3724
3791
  }
3792
+ /**
3793
+ * Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
3794
+ * suggested audience bindings. A package's `isDefault: true` permission
3795
+ * set is an install-time SUGGESTION to bind it to the `everyone` position;
3796
+ * these routes let an admin see and resolve those suggestions. The
3797
+ * `security` service does the real gating (tenant-admin pre-check, and the
3798
+ * confirm write runs under the audience-anchor + delegated-admin gates
3799
+ * with the caller's execution context — never auto-bound, never system).
3800
+ *
3801
+ * Routes:
3802
+ * GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
3803
+ * POST /security/suggested-bindings/:id/confirm → create the anchor binding
3804
+ * POST /security/suggested-bindings/:id/dismiss → decline the suggestion
3805
+ */
3806
+ async handleSecurity(path, method, _body, query, context) {
3807
+ const service = await this.resolveService("security", context.environmentId);
3808
+ if (!service || typeof service.listAudienceBindingSuggestions !== "function") {
3809
+ return { handled: true, response: this.error("Security service not available", 503) };
3810
+ }
3811
+ const ec = context.executionContext;
3812
+ if (!ec?.userId && !ec?.isSystem) {
3813
+ return { handled: true, response: this.error("Authentication required", 401) };
3814
+ }
3815
+ const m = method.toUpperCase();
3816
+ const parts = path.split("/").filter(Boolean);
3817
+ if (parts[0] !== "suggested-bindings") return { handled: false };
3818
+ try {
3819
+ if (parts.length === 1 && m === "GET") {
3820
+ const status = query?.status ? String(query.status) : void 0;
3821
+ const packageId = query?.packageId ? String(query.packageId) : void 0;
3822
+ const result = await service.listAudienceBindingSuggestions(ec, { status, packageId });
3823
+ return { handled: true, response: this.success(result) };
3824
+ }
3825
+ if (parts.length === 3 && m === "POST") {
3826
+ const id = decodeURIComponent(parts[1]);
3827
+ if (parts[2] === "confirm") {
3828
+ const result = await service.confirmAudienceBindingSuggestion(ec, id);
3829
+ return { handled: true, response: this.success(result) };
3830
+ }
3831
+ if (parts[2] === "dismiss") {
3832
+ const result = await service.dismissAudienceBindingSuggestion(ec, id);
3833
+ return { handled: true, response: this.success(result) };
3834
+ }
3835
+ }
3836
+ return { handled: false };
3837
+ } catch (err) {
3838
+ const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
3839
+ return { handled: true, response: this.error(err?.message ?? "Security operation failed", status) };
3840
+ }
3841
+ }
3725
3842
  /**
3726
3843
  * Handles i18n requests
3727
3844
  * path: sub-path after /i18n/
@@ -4727,8 +4844,16 @@ var _HttpDispatcher = class _HttpDispatcher {
4727
4844
  return Array.isArray(rows) ? rows : rows?.value ?? [];
4728
4845
  }
4729
4846
  };
4730
- const userIdFromAuth = _context?.user?.id ?? _context?.userId ?? "system";
4731
- const userFromAuth = _context?.user ?? { id: userIdFromAuth, name: userIdFromAuth };
4847
+ const ec = _context?.executionContext;
4848
+ const userFromAuth = ec?.userId ? {
4849
+ id: ec.userId,
4850
+ name: ec.userId,
4851
+ email: ec.email,
4852
+ roles: Array.isArray(ec.positions) ? ec.positions : [],
4853
+ positions: Array.isArray(ec.positions) ? ec.positions : [],
4854
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
4855
+ tenantId: ec.tenantId
4856
+ } : { id: "system", name: "system", roles: [], positions: [], permissions: [] };
4732
4857
  const actionContext = {
4733
4858
  record,
4734
4859
  user: userFromAuth,
@@ -5110,6 +5235,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5110
5235
  if (cleanPath.startsWith("/data")) {
5111
5236
  return this.handleData(cleanPath.substring(5), method, body, query, context);
5112
5237
  }
5238
+ if (cleanPath === "/mcp/skill" || cleanPath.startsWith("/mcp/skill?")) {
5239
+ return this.handleMcpSkill(method, context);
5240
+ }
5113
5241
  if (cleanPath === "/mcp" || cleanPath.startsWith("/mcp/") || cleanPath.startsWith("/mcp?")) {
5114
5242
  return this.handleMcp(body, context);
5115
5243
  }
@@ -5137,6 +5265,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5137
5265
  if (cleanPath.startsWith("/notifications")) {
5138
5266
  return this.handleNotification(cleanPath.substring(14), method, body, query, context);
5139
5267
  }
5268
+ if (cleanPath === "/security" || cleanPath.startsWith("/security/")) {
5269
+ return this.handleSecurity(cleanPath.substring(9), method, body, query, context);
5270
+ }
5140
5271
  if (cleanPath.startsWith("/packages")) {
5141
5272
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5142
5273
  }
@@ -5855,6 +5986,14 @@ function createDispatcherPlugin(config = {}) {
5855
5986
  mountMcp("POST");
5856
5987
  mountMcp("GET");
5857
5988
  mountMcp("DELETE");
5989
+ server.get(`${prefix}/mcp/skill`, async (req, res) => {
5990
+ try {
5991
+ const result = await dispatcher.dispatch("GET", "/mcp/skill", req.body, req.query, { request: req });
5992
+ sendResult(result, res);
5993
+ } catch (err) {
5994
+ errorResponse(err, res);
5995
+ }
5996
+ });
5858
5997
  server.post(`${prefix}/keys`, async (req, res) => {
5859
5998
  try {
5860
5999
  const result = await dispatcher.dispatch("POST", "/keys", req.body, req.query, { request: req });
@@ -6159,9 +6298,7 @@ function createDispatcherPlugin(config = {}) {
6159
6298
  const registerActionRoutes = (base) => {
6160
6299
  server.post(`${base}/actions/:object/:action`, async (req, res) => {
6161
6300
  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);
6301
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}`, req.body, req.query, { request: req });
6165
6302
  sendResult(result, res);
6166
6303
  } catch (err) {
6167
6304
  errorResponse(err, res);
@@ -6169,9 +6306,7 @@ function createDispatcherPlugin(config = {}) {
6169
6306
  });
6170
6307
  server.post(`${base}/actions/:object/:action/:recordId`, async (req, res) => {
6171
6308
  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);
6309
+ const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}/${req.params.recordId}`, req.body, req.query, { request: req });
6175
6310
  sendResult(result, res);
6176
6311
  } catch (err) {
6177
6312
  errorResponse(err, res);