diff --git a/apps/host-selfhost/src/auth/better-auth.test.ts b/apps/host-selfhost/src/auth/better-auth.test.ts
index eebc68e5c..968a6902d 100644
--- a/apps/host-selfhost/src/auth/better-auth.test.ts
+++ b/apps/host-selfhost/src/auth/better-auth.test.ts
@@ -89,6 +89,47 @@ test("sign-up issues a bearer token and resolves to a per-user org-pinned identi
expect(body.organization!.id).toBeTruthy();
});
+test("self-host API keys are not capped by Better Auth's default request limit", async () => {
+ const inviteCode = await mintInviteCode(handler);
+ const signUp = await handler(
+ new Request(`${BASE}/api/auth/sign-up/email`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ email: "key-user@test.local",
+ password: "member-password-123",
+ name: "Key User",
+ inviteCode,
+ }),
+ }),
+ );
+ expect(signUp.status).toBe(200);
+ const token = signUp.headers.get("set-auth-token");
+ expect(token).toBeTruthy();
+
+ const createKey = await handler(
+ new Request(`${BASE}/api/account/api-keys`, {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({ name: "MCP bootstrap" }),
+ }),
+ );
+ expect(createKey.status).toBe(200);
+ const keyBody = (await createKey.json()) as { value: string };
+
+ for (let i = 0; i < 12; i++) {
+ const me = await handler(
+ new Request(`${BASE}/api/account/me`, {
+ headers: { "x-api-key": keyBody.value },
+ }),
+ );
+ expect(me.status).toBe(200);
+ }
+});
+
test("an unauthenticated request is rejected with 401", async () => {
const res = await handler(new Request("http://localhost/api/account/me"));
expect(res.status).toBe(401);
diff --git a/apps/host-selfhost/src/auth/better-auth.ts b/apps/host-selfhost/src/auth/better-auth.ts
index 578304090..7a7fc861a 100644
--- a/apps/host-selfhost/src/auth/better-auth.ts
+++ b/apps/host-selfhost/src/auth/better-auth.ts
@@ -83,7 +83,7 @@ const makeAuthOptions = (url: string, organizationId: string, gate?: SignupGate)
plugins: [
organization(),
admin(),
- apiKey({ enableSessionForAPIKeys: true }),
+ apiKey({ enableSessionForAPIKeys: true, rateLimit: { enabled: false } }),
bearer(),
mcp({ loginPage: "/login" }),
],
diff --git a/apps/host-selfhost/web/routeTree.gen.ts b/apps/host-selfhost/web/routeTree.gen.ts
index f185a4149..a53ccbec3 100644
--- a/apps/host-selfhost/web/routeTree.gen.ts
+++ b/apps/host-selfhost/web/routeTree.gen.ts
@@ -18,8 +18,10 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespace'
import { Route as ResumeExecutionIdRouteImport } from './routes/resume.$executionId'
import { Route as JoinCodeRouteImport } from './routes/join.$code'
+import { Route as IntegrationsNamespaceRouteImport } from './routes/integrations.$namespace'
import { Route as SourcesAddPluginKeyRouteImport } from './routes/sources.add.$pluginKey'
import { Route as PluginsPluginIdSplatRouteImport } from './routes/plugins.$pluginId.$'
+import { Route as IntegrationsAddPluginKeyRouteImport } from './routes/integrations.add.$pluginKey'
const ToolsRoute = ToolsRouteImport.update({
id: '/tools',
@@ -66,6 +68,11 @@ const JoinCodeRoute = JoinCodeRouteImport.update({
path: '/join/$code',
getParentRoute: () => rootRouteImport,
} as any)
+const IntegrationsNamespaceRoute = IntegrationsNamespaceRouteImport.update({
+ id: '/integrations/$namespace',
+ path: '/integrations/$namespace',
+ getParentRoute: () => rootRouteImport,
+} as any)
const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({
id: '/sources/add/$pluginKey',
path: '/sources/add/$pluginKey',
@@ -76,6 +83,12 @@ const PluginsPluginIdSplatRoute = PluginsPluginIdSplatRouteImport.update({
path: '/plugins/$pluginId/$',
getParentRoute: () => rootRouteImport,
} as any)
+const IntegrationsAddPluginKeyRoute =
+ IntegrationsAddPluginKeyRouteImport.update({
+ id: '/integrations/add/$pluginKey',
+ path: '/integrations/add/$pluginKey',
+ getParentRoute: () => rootRouteImport,
+ } as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -84,9 +97,11 @@ export interface FileRoutesByFullPath {
'/policies': typeof PoliciesRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
+ '/integrations/$namespace': typeof IntegrationsNamespaceRoute
'/join/$code': typeof JoinCodeRoute
'/resume/$executionId': typeof ResumeExecutionIdRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
+ '/integrations/add/$pluginKey': typeof IntegrationsAddPluginKeyRoute
'/plugins/$pluginId/$': typeof PluginsPluginIdSplatRoute
'/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
}
@@ -97,9 +112,11 @@ export interface FileRoutesByTo {
'/policies': typeof PoliciesRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
+ '/integrations/$namespace': typeof IntegrationsNamespaceRoute
'/join/$code': typeof JoinCodeRoute
'/resume/$executionId': typeof ResumeExecutionIdRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
+ '/integrations/add/$pluginKey': typeof IntegrationsAddPluginKeyRoute
'/plugins/$pluginId/$': typeof PluginsPluginIdSplatRoute
'/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
}
@@ -111,9 +128,11 @@ export interface FileRoutesById {
'/policies': typeof PoliciesRoute
'/secrets': typeof SecretsRoute
'/tools': typeof ToolsRoute
+ '/integrations/$namespace': typeof IntegrationsNamespaceRoute
'/join/$code': typeof JoinCodeRoute
'/resume/$executionId': typeof ResumeExecutionIdRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
+ '/integrations/add/$pluginKey': typeof IntegrationsAddPluginKeyRoute
'/plugins/$pluginId/$': typeof PluginsPluginIdSplatRoute
'/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
}
@@ -126,9 +145,11 @@ export interface FileRouteTypes {
| '/policies'
| '/secrets'
| '/tools'
+ | '/integrations/$namespace'
| '/join/$code'
| '/resume/$executionId'
| '/sources/$namespace'
+ | '/integrations/add/$pluginKey'
| '/plugins/$pluginId/$'
| '/sources/add/$pluginKey'
fileRoutesByTo: FileRoutesByTo
@@ -139,9 +160,11 @@ export interface FileRouteTypes {
| '/policies'
| '/secrets'
| '/tools'
+ | '/integrations/$namespace'
| '/join/$code'
| '/resume/$executionId'
| '/sources/$namespace'
+ | '/integrations/add/$pluginKey'
| '/plugins/$pluginId/$'
| '/sources/add/$pluginKey'
id:
@@ -152,9 +175,11 @@ export interface FileRouteTypes {
| '/policies'
| '/secrets'
| '/tools'
+ | '/integrations/$namespace'
| '/join/$code'
| '/resume/$executionId'
| '/sources/$namespace'
+ | '/integrations/add/$pluginKey'
| '/plugins/$pluginId/$'
| '/sources/add/$pluginKey'
fileRoutesById: FileRoutesById
@@ -166,9 +191,11 @@ export interface RootRouteChildren {
PoliciesRoute: typeof PoliciesRoute
SecretsRoute: typeof SecretsRoute
ToolsRoute: typeof ToolsRoute
+ IntegrationsNamespaceRoute: typeof IntegrationsNamespaceRoute
JoinCodeRoute: typeof JoinCodeRoute
ResumeExecutionIdRoute: typeof ResumeExecutionIdRoute
SourcesNamespaceRoute: typeof SourcesNamespaceRoute
+ IntegrationsAddPluginKeyRoute: typeof IntegrationsAddPluginKeyRoute
PluginsPluginIdSplatRoute: typeof PluginsPluginIdSplatRoute
SourcesAddPluginKeyRoute: typeof SourcesAddPluginKeyRoute
}
@@ -238,6 +265,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof JoinCodeRouteImport
parentRoute: typeof rootRouteImport
}
+ '/integrations/$namespace': {
+ id: '/integrations/$namespace'
+ path: '/integrations/$namespace'
+ fullPath: '/integrations/$namespace'
+ preLoaderRoute: typeof IntegrationsNamespaceRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/sources/add/$pluginKey': {
id: '/sources/add/$pluginKey'
path: '/sources/add/$pluginKey'
@@ -252,6 +286,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PluginsPluginIdSplatRouteImport
parentRoute: typeof rootRouteImport
}
+ '/integrations/add/$pluginKey': {
+ id: '/integrations/add/$pluginKey'
+ path: '/integrations/add/$pluginKey'
+ fullPath: '/integrations/add/$pluginKey'
+ preLoaderRoute: typeof IntegrationsAddPluginKeyRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
@@ -262,9 +303,11 @@ const rootRouteChildren: RootRouteChildren = {
PoliciesRoute: PoliciesRoute,
SecretsRoute: SecretsRoute,
ToolsRoute: ToolsRoute,
+ IntegrationsNamespaceRoute: IntegrationsNamespaceRoute,
JoinCodeRoute: JoinCodeRoute,
ResumeExecutionIdRoute: ResumeExecutionIdRoute,
SourcesNamespaceRoute: SourcesNamespaceRoute,
+ IntegrationsAddPluginKeyRoute: IntegrationsAddPluginKeyRoute,
PluginsPluginIdSplatRoute: PluginsPluginIdSplatRoute,
SourcesAddPluginKeyRoute: SourcesAddPluginKeyRoute,
}
diff --git a/apps/host-selfhost/web/routes/integrations.$namespace.tsx b/apps/host-selfhost/web/routes/integrations.$namespace.tsx
new file mode 100644
index 000000000..49a458104
--- /dev/null
+++ b/apps/host-selfhost/web/routes/integrations.$namespace.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { IntegrationDetailPage } from "@executor-js/react/pages/integration-detail";
+
+export const Route = createFileRoute("/integrations/$namespace")({
+ component: () => {
+ const { namespace } = Route.useParams();
+ return ;
+ },
+});
diff --git a/apps/host-selfhost/web/routes/integrations.add.$pluginKey.tsx b/apps/host-selfhost/web/routes/integrations.add.$pluginKey.tsx
new file mode 100644
index 000000000..cdf2b8a8a
--- /dev/null
+++ b/apps/host-selfhost/web/routes/integrations.add.$pluginKey.tsx
@@ -0,0 +1,22 @@
+import { Schema } from "effect";
+import { createFileRoute } from "@tanstack/react-router";
+import { AddIntegrationPage } from "@executor-js/react/pages/integration-add";
+
+const SearchParams = Schema.toStandardSchemaV1(
+ Schema.Struct({
+ url: Schema.optional(Schema.String),
+ preset: Schema.optional(Schema.String),
+ namespace: Schema.optional(Schema.String),
+ }),
+);
+
+export const Route = createFileRoute("/integrations/add/$pluginKey")({
+ validateSearch: SearchParams,
+ component: () => {
+ const { pluginKey } = Route.useParams();
+ const { url, preset, namespace } = Route.useSearch();
+ return (
+
+ );
+ },
+});
diff --git a/apps/host-selfhost/web/routes/sources.$namespace.tsx b/apps/host-selfhost/web/routes/sources.$namespace.tsx
index b8fcd190b..2df401d30 100644
--- a/apps/host-selfhost/web/routes/sources.$namespace.tsx
+++ b/apps/host-selfhost/web/routes/sources.$namespace.tsx
@@ -1,9 +1,9 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { IntegrationDetailPage } from "@executor-js/react/pages/integration-detail";
+import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/sources/$namespace")({
- component: () => {
- const { namespace } = Route.useParams();
- return ;
+ beforeLoad: ({ params }) => {
+ const { namespace } = params;
+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TanStack Router redirects are modeled as thrown values
+ throw redirect({ to: "/integrations/$namespace", params: { namespace } });
},
});
diff --git a/apps/host-selfhost/web/routes/sources.add.$pluginKey.tsx b/apps/host-selfhost/web/routes/sources.add.$pluginKey.tsx
index b984e7994..ec809b401 100644
--- a/apps/host-selfhost/web/routes/sources.add.$pluginKey.tsx
+++ b/apps/host-selfhost/web/routes/sources.add.$pluginKey.tsx
@@ -1,6 +1,5 @@
import { Schema } from "effect";
-import { createFileRoute } from "@tanstack/react-router";
-import { AddIntegrationPage } from "@executor-js/react/pages/integration-add";
+import { createFileRoute, redirect } from "@tanstack/react-router";
const SearchParams = Schema.toStandardSchemaV1(
Schema.Struct({
@@ -12,11 +11,9 @@ const SearchParams = Schema.toStandardSchemaV1(
export const Route = createFileRoute("/sources/add/$pluginKey")({
validateSearch: SearchParams,
- component: () => {
- const { pluginKey } = Route.useParams();
- const { url, preset, namespace } = Route.useSearch();
- return (
-
- );
+ beforeLoad: ({ params, search }) => {
+ const { pluginKey } = params;
+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TanStack Router redirects are modeled as thrown values
+ throw redirect({ to: "/integrations/add/$pluginKey", params: { pluginKey }, search });
},
});