@hachej/boring-mcp 0.1.82 → 0.1.83

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.
@@ -358,6 +358,7 @@ interface RegisterBoringMcpRoutesOptions {
358
358
  provider?: ManagedConnectorProvider;
359
359
  env?: NodeJS.ProcessEnv;
360
360
  transport?: McpTransportClient;
361
+ resolveTrustedWorkspaceId?: (request: FastifyRequest) => string | undefined;
361
362
  /**
362
363
  * Behavior when boring-mcp is disabled for this deployment.
363
364
  * - `skip` (default): do not register the routes at all (client sees 404).
@@ -1812,10 +1812,10 @@ function createBoringMcpAppAgentToolsForRequest(app, ctx, options) {
1812
1812
  if (!userId) return [];
1813
1813
  return createBoringMcpAppAgentTools(app, { workspaceId: ctx.workspaceId, userId }, options);
1814
1814
  }
1815
- async function requireBoringMcpActor(app, request) {
1815
+ async function requireBoringMcpActor(app, request, admittedWorkspaceId) {
1816
1816
  const body = asRecord(request.body);
1817
1817
  const query = asRecord(request.query);
1818
- const workspaceId = validateWorkspaceId(
1818
+ const workspaceId = admittedWorkspaceId ?? validateWorkspaceId(
1819
1819
  request.headers["x-boring-workspace-id"] ?? body.workspaceId ?? query.workspaceId,
1820
1820
  request.id
1821
1821
  );
@@ -1831,6 +1831,33 @@ function sourceActionRateLimitKey(request) {
1831
1831
  const workspaceId = typeof request.headers["x-boring-workspace-id"] === "string" ? request.headers["x-boring-workspace-id"] : "unknown-workspace";
1832
1832
  return `${request.user?.id ?? request.ip}:${workspaceId}`;
1833
1833
  }
1834
+ function d1HostScopeViolation(request) {
1835
+ throw routeError(421, ERROR_CODES.D1_HOST_SCOPE_VIOLATION, ERROR_CODES.D1_HOST_SCOPE_VIOLATION, request.id);
1836
+ }
1837
+ function presentedWorkspaceIds(request) {
1838
+ const presented = [];
1839
+ const append = (value) => {
1840
+ if (Array.isArray(value)) presented.push(...value.length > 0 ? value : [value]);
1841
+ else presented.push(value);
1842
+ };
1843
+ let foundRawHeader = false;
1844
+ for (let index = 0; index < request.raw.rawHeaders.length; index += 2) {
1845
+ if (request.raw.rawHeaders[index]?.toLowerCase() !== "x-boring-workspace-id") continue;
1846
+ foundRawHeader = true;
1847
+ append(request.raw.rawHeaders[index + 1]?.split(","));
1848
+ }
1849
+ if (!foundRawHeader) {
1850
+ for (const [key, value] of Object.entries(request.headers)) {
1851
+ if (key.toLowerCase() === "x-boring-workspace-id") append(value);
1852
+ }
1853
+ }
1854
+ for (const value of [request.query, request.body]) {
1855
+ if (value && typeof value === "object" && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "workspaceId")) {
1856
+ append(value.workspaceId);
1857
+ }
1858
+ }
1859
+ return presented;
1860
+ }
1834
1861
  function sourceIdFromBody(body, requestId) {
1835
1862
  const sourceId = parseString(asRecord(body).sourceId);
1836
1863
  if (!sourceId) throw routeError(400, ERROR_CODES.VALIDATION_FAILED, "sourceId is required", requestId);
@@ -1850,6 +1877,29 @@ function registerBoringMcpRoutes(app, options) {
1850
1877
  };
1851
1878
  const routeTransport = options.transport ?? createComposioMcpTransportFor(options.config, options.env);
1852
1879
  const routeGate = new InMemoryMcpRateBudgetGate({ maxCalls: 100, maxToolCalls: 10, windowMs: 6e4 });
1880
+ const admittedWorkspaceIds = /* @__PURE__ */ new WeakMap();
1881
+ const trustedWorkspaceId = (request) => options.resolveTrustedWorkspaceId?.(request);
1882
+ const sourceRouteRateLimitKey = (request) => {
1883
+ const trusted = trustedWorkspaceId(request);
1884
+ if (trusted === void 0) return sourceActionRateLimitKey(request);
1885
+ const userId = request.user?.id;
1886
+ if (!userId) throw routeError(401, ERROR_CODES.UNAUTHORIZED, "Authentication required", request.id);
1887
+ return `${userId}:${trusted}`;
1888
+ };
1889
+ const admitTrustedWorkspace = async (request) => {
1890
+ const trusted = trustedWorkspaceId(request);
1891
+ if (trusted === void 0) return;
1892
+ for (const presented of presentedWorkspaceIds(request)) {
1893
+ let normalized;
1894
+ try {
1895
+ normalized = validateWorkspaceId(presented, request.id);
1896
+ } catch {
1897
+ d1HostScopeViolation(request);
1898
+ }
1899
+ if (normalized !== trusted) d1HostScopeViolation(request);
1900
+ }
1901
+ admittedWorkspaceIds.set(request, trusted);
1902
+ };
1853
1903
  function adapterFor(actor) {
1854
1904
  const registry = createUserSettingsMcpSourceRegistry(app, actor);
1855
1905
  return {
@@ -1865,18 +1915,22 @@ function registerBoringMcpRoutes(app, options) {
1865
1915
  hardening: { gate: routeGate, timeoutMs: 3e4 }
1866
1916
  });
1867
1917
  }
1868
- app.get("/api/v1/boring-mcp/sources", async (request) => {
1918
+ app.get("/api/v1/boring-mcp/sources", {
1919
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceRouteRateLimitKey, allowList: (request) => trustedWorkspaceId(request) === void 0 } },
1920
+ preHandler: admitTrustedWorkspace
1921
+ }, async (request) => {
1869
1922
  assertEnabled(request.id);
1870
- const actor = await requireBoringMcpActor(app, request);
1923
+ const actor = await requireBoringMcpActor(app, request, admittedWorkspaceIds.get(request));
1871
1924
  const { registry } = adapterFor(actor);
1872
1925
  const sources = await registry.listSources(actor);
1873
1926
  return { sourceStatuses: sources.map(createMcpSourceStatusPayload) };
1874
1927
  });
1875
1928
  app.post("/api/v1/boring-mcp/connect", {
1876
- config: { rateLimit: { ...SOURCE_CONNECT_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1929
+ config: { rateLimit: { ...SOURCE_CONNECT_RATE_LIMIT, keyGenerator: sourceRouteRateLimitKey } },
1930
+ preHandler: admitTrustedWorkspace
1877
1931
  }, async (request, reply) => {
1878
1932
  assertEnabled(request.id);
1879
- const actor = await requireBoringMcpActor(app, request);
1933
+ const actor = await requireBoringMcpActor(app, request, admittedWorkspaceIds.get(request));
1880
1934
  const { adapter } = adapterFor(actor);
1881
1935
  const provider = providerFromBody(request.body, request.id);
1882
1936
  const result = await withMcpHttpErrors(request.id, () => adapter.startConnect(actor, { provider }));
@@ -1885,27 +1939,30 @@ function registerBoringMcpRoutes(app, options) {
1885
1939
  return { status, connectUrl };
1886
1940
  });
1887
1941
  app.post("/api/v1/boring-mcp/refresh", {
1888
- config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1942
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceRouteRateLimitKey } },
1943
+ preHandler: admitTrustedWorkspace
1889
1944
  }, async (request) => {
1890
1945
  assertEnabled(request.id);
1891
- const actor = await requireBoringMcpActor(app, request);
1946
+ const actor = await requireBoringMcpActor(app, request, admittedWorkspaceIds.get(request));
1892
1947
  const { adapter } = adapterFor(actor);
1893
1948
  const result = await withMcpHttpErrors(request.id, () => adapter.refreshStatus(actor, sourceIdFromBody(request.body, request.id)));
1894
1949
  return { status: result };
1895
1950
  });
1896
1951
  app.post("/api/v1/boring-mcp/disconnect", {
1897
- config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1952
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceRouteRateLimitKey } },
1953
+ preHandler: admitTrustedWorkspace
1898
1954
  }, async (request) => {
1899
1955
  assertEnabled(request.id);
1900
- const actor = await requireBoringMcpActor(app, request);
1956
+ const actor = await requireBoringMcpActor(app, request, admittedWorkspaceIds.get(request));
1901
1957
  const status = await withMcpHttpErrors(request.id, () => adapterFor(actor).adapter.disconnectSource(actor, sourceIdFromBody(request.body, request.id)));
1902
1958
  return { status };
1903
1959
  });
1904
1960
  app.post("/api/v1/boring-mcp/tools", {
1905
- config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1961
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceRouteRateLimitKey } },
1962
+ preHandler: admitTrustedWorkspace
1906
1963
  }, async (request) => {
1907
1964
  assertEnabled(request.id);
1908
- const actor = await requireBoringMcpActor(app, request);
1965
+ const actor = await requireBoringMcpActor(app, request, admittedWorkspaceIds.get(request));
1909
1966
  const body = asRecord(request.body);
1910
1967
  const result = await withMcpHttpErrors(request.id, () => handlersFor(actor).searchTools(actor, {
1911
1968
  sourceId: sourceIdFromBody(request.body, request.id),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-mcp",
3
- "version": "0.1.82",
3
+ "version": "0.1.83",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -49,8 +49,8 @@
49
49
  "fastify": "^5.9.0",
50
50
  "react": "^18.0.0 || ^19.0.0",
51
51
  "react-dom": "^18.0.0 || ^19.0.0",
52
- "@hachej/boring-core": "0.1.82",
53
- "@hachej/boring-workspace": "0.1.82"
52
+ "@hachej/boring-core": "0.1.83",
53
+ "@hachej/boring-workspace": "0.1.83"
54
54
  },
55
55
  "devDependencies": {
56
56
  "fastify": "^5.9.0",
@@ -66,8 +66,8 @@
66
66
  "tsup": "^8.4.0",
67
67
  "typescript": "~6.0.3",
68
68
  "vitest": "^4.1.9",
69
- "@hachej/boring-core": "0.1.82",
70
- "@hachej/boring-workspace": "0.1.82"
69
+ "@hachej/boring-core": "0.1.83",
70
+ "@hachej/boring-workspace": "0.1.83"
71
71
  },
72
72
  "dependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.29.0"