From 8675a4a9636c46adfb04314b8e62432e3aa17e19 Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Wed, 18 Feb 2026 12:07:54 +0200 Subject: [PATCH 1/2] refactor: rename plugins to squadhub-provisioner/squadhub-lifecycle and add lifecycle API routes --- .../web/src/app/api/tenant/provision/route.ts | 2 +- .../app/api/tenant/squadhub/restart/route.ts | 28 ++++++++++ apps/web/src/app/api/tenant/squadhub/route.ts | 28 ++++++++++ .../app/api/tenant/squadhub/status/route.ts | 29 +++++++++++ apps/web/src/lib/api/tenant-auth.ts | 51 +++++++++++++++++++ packages/plugins/src/defaults/index.ts | 4 +- packages/plugins/src/defaults/lifecycle.ts | 27 ---------- .../src/defaults/squadhub-lifecycle.ts | 20 ++++++++ ...provisioner.ts => squadhub-provisioner.ts} | 12 ++--- packages/plugins/src/index.ts | 6 +-- packages/plugins/src/interfaces/index.ts | 6 +-- .../{lifecycle.ts => squadhub-lifecycle.ts} | 0 ...provisioner.ts => squadhub-provisioner.ts} | 2 +- packages/plugins/src/registry.spec.ts | 26 +++++----- packages/plugins/src/registry.ts | 16 +++--- 15 files changed, 193 insertions(+), 64 deletions(-) create mode 100644 apps/web/src/app/api/tenant/squadhub/restart/route.ts create mode 100644 apps/web/src/app/api/tenant/squadhub/route.ts create mode 100644 apps/web/src/app/api/tenant/squadhub/status/route.ts create mode 100644 apps/web/src/lib/api/tenant-auth.ts delete mode 100644 packages/plugins/src/defaults/lifecycle.ts create mode 100644 packages/plugins/src/defaults/squadhub-lifecycle.ts rename packages/plugins/src/defaults/{provisioner.ts => squadhub-provisioner.ts} (54%) rename packages/plugins/src/interfaces/{lifecycle.ts => squadhub-lifecycle.ts} (100%) rename packages/plugins/src/interfaces/{provisioner.ts => squadhub-provisioner.ts} (95%) diff --git a/apps/web/src/app/api/tenant/provision/route.ts b/apps/web/src/app/api/tenant/provision/route.ts index 7298027..f05ff3a 100644 --- a/apps/web/src/app/api/tenant/provision/route.ts +++ b/apps/web/src/app/api/tenant/provision/route.ts @@ -63,7 +63,7 @@ export const POST = async (request: NextRequest) => { } else { // 4. Create tenant + provision via plugin await loadPlugins(); - const provisioner = getPlugin("provisioner"); + const provisioner = getPlugin("squadhub-provisioner"); // Create tenant record (or use existing non-active one) const tenantIdToProvision = existingTenant diff --git a/apps/web/src/app/api/tenant/squadhub/restart/route.ts b/apps/web/src/app/api/tenant/squadhub/restart/route.ts new file mode 100644 index 0000000..8712be8 --- /dev/null +++ b/apps/web/src/app/api/tenant/squadhub/restart/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { loadPlugins, getPlugin } from "@clawe/plugins"; +import { getAuthenticatedTenant } from "@/lib/api/tenant-auth"; + +/** + * POST /api/tenant/squadhub/restart + * + * Restart the current user's squadhub service. + * Dev: no-op. Cloud: forces new ECS task deployment. + */ +export const POST = async (request: NextRequest) => { + const result = await getAuthenticatedTenant(request); + if ("error" in result) return result.error; + + const { tenant } = result; + + try { + await loadPlugins(); + const lifecycle = getPlugin("squadhub-lifecycle"); + await lifecycle.restart(tenant._id); + + return NextResponse.json({ ok: true }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +}; diff --git a/apps/web/src/app/api/tenant/squadhub/route.ts b/apps/web/src/app/api/tenant/squadhub/route.ts new file mode 100644 index 0000000..c893085 --- /dev/null +++ b/apps/web/src/app/api/tenant/squadhub/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { loadPlugins, getPlugin } from "@clawe/plugins"; +import { getAuthenticatedTenant } from "@/lib/api/tenant-auth"; + +/** + * DELETE /api/tenant/squadhub + * + * Destroy the current user's squadhub service permanently. + * Dev: no-op. Cloud: deletes ECS service + EFS access point + CloudMap entry. + */ +export const DELETE = async (request: NextRequest) => { + const result = await getAuthenticatedTenant(request); + if ("error" in result) return result.error; + + const { tenant } = result; + + try { + await loadPlugins(); + const lifecycle = getPlugin("squadhub-lifecycle"); + await lifecycle.destroy(tenant._id); + + return NextResponse.json({ ok: true }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +}; diff --git a/apps/web/src/app/api/tenant/squadhub/status/route.ts b/apps/web/src/app/api/tenant/squadhub/status/route.ts new file mode 100644 index 0000000..c7cead3 --- /dev/null +++ b/apps/web/src/app/api/tenant/squadhub/status/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { loadPlugins, getPlugin } from "@clawe/plugins"; +import { getAuthenticatedTenant } from "@/lib/api/tenant-auth"; + +/** + * GET /api/tenant/squadhub/status + * + * Check the health/status of the current user's squadhub service. + * Dev: always returns { running: true, healthy: true }. + * Cloud: checks ECS service running count + task health. + */ +export const GET = async (request: NextRequest) => { + const result = await getAuthenticatedTenant(request); + if ("error" in result) return result.error; + + const { tenant } = result; + + try { + await loadPlugins(); + const lifecycle = getPlugin("squadhub-lifecycle"); + const status = await lifecycle.getStatus(tenant._id); + + return NextResponse.json(status); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +}; diff --git a/apps/web/src/lib/api/tenant-auth.ts b/apps/web/src/lib/api/tenant-auth.ts new file mode 100644 index 0000000..7e0bd7b --- /dev/null +++ b/apps/web/src/lib/api/tenant-auth.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { ConvexHttpClient } from "convex/browser"; +import { api } from "@clawe/backend"; + +/** + * Extract the auth token and resolve the tenant for the current user. + * Returns a Convex client (with auth set) and the tenant record, + * or a NextResponse error if auth/tenant resolution fails. + */ +export async function getAuthenticatedTenant(request: NextRequest) { + const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; + if (!convexUrl) { + return { + error: NextResponse.json( + { error: "NEXT_PUBLIC_CONVEX_URL not configured" }, + { status: 500 }, + ), + }; + } + + const authHeader = request.headers.get("authorization"); + const authToken = authHeader?.startsWith("Bearer ") + ? authHeader.slice(7) + : null; + + if (!authToken) { + return { + error: NextResponse.json( + { error: "Missing Authorization header" }, + { status: 401 }, + ), + }; + } + + const convex = new ConvexHttpClient(convexUrl); + convex.setAuth(authToken); + + const tenant = await convex.query(api.tenants.getForCurrentUser, {}); + + if (!tenant) { + return { + error: NextResponse.json( + { error: "No tenant found for current user" }, + { status: 404 }, + ), + }; + } + + return { convex, tenant }; +} diff --git a/packages/plugins/src/defaults/index.ts b/packages/plugins/src/defaults/index.ts index 3e6d452..c9ce3c0 100644 --- a/packages/plugins/src/defaults/index.ts +++ b/packages/plugins/src/defaults/index.ts @@ -1,2 +1,2 @@ -export { DevProvisioner } from "./provisioner"; -export { DevLifecycle } from "./lifecycle"; +export { DefaultSquadhubProvisioner } from "./squadhub-provisioner"; +export { DefaultSquadhubLifecycle } from "./squadhub-lifecycle"; diff --git a/packages/plugins/src/defaults/lifecycle.ts b/packages/plugins/src/defaults/lifecycle.ts deleted file mode 100644 index a54708b..0000000 --- a/packages/plugins/src/defaults/lifecycle.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { - SquadhubLifecycle, - SquadhubStatus, -} from "../interfaces/lifecycle"; - -/** - * Dev/self-hosted lifecycle manager. - * All operations are no-ops — user manages squadhub via docker compose. - * Always reports healthy. - */ -export class DevLifecycle implements SquadhubLifecycle { - async restart(): Promise { - // No-op — user manually restarts docker. - } - - async stop(): Promise { - // No-op. - } - - async destroy(): Promise { - // No-op. - } - - async getStatus(): Promise { - return { running: true, healthy: true }; - } -} diff --git a/packages/plugins/src/defaults/squadhub-lifecycle.ts b/packages/plugins/src/defaults/squadhub-lifecycle.ts new file mode 100644 index 0000000..0b2be82 --- /dev/null +++ b/packages/plugins/src/defaults/squadhub-lifecycle.ts @@ -0,0 +1,20 @@ +import type { + SquadhubLifecycle, + SquadhubStatus, +} from "../interfaces/squadhub-lifecycle"; + +/** + * Default squadhub lifecycle — all operations are no-ops. + * Override with a cloud implementation to manage real infrastructure. + */ +export class DefaultSquadhubLifecycle implements SquadhubLifecycle { + async restart(): Promise {} + + async stop(): Promise {} + + async destroy(): Promise {} + + async getStatus(): Promise { + return { running: true, healthy: true }; + } +} diff --git a/packages/plugins/src/defaults/provisioner.ts b/packages/plugins/src/defaults/squadhub-provisioner.ts similarity index 54% rename from packages/plugins/src/defaults/provisioner.ts rename to packages/plugins/src/defaults/squadhub-provisioner.ts index 9f59579..d072981 100644 --- a/packages/plugins/src/defaults/provisioner.ts +++ b/packages/plugins/src/defaults/squadhub-provisioner.ts @@ -1,15 +1,14 @@ import type { - TenantProvisioner, + SquadhubProvisioner, ProvisionResult, ProvisioningStatus, -} from "../interfaces/provisioner"; +} from "../interfaces/squadhub-provisioner"; /** - * Dev/self-hosted provisioner. - * Reads SQUADHUB_URL and SQUADHUB_TOKEN from environment variables. - * Returns immediately — no infrastructure to create. + * Default squadhub provisioner — reads connection from environment variables. + * Override with a cloud implementation to provision real infrastructure. */ -export class DevProvisioner implements TenantProvisioner { +export class DefaultSquadhubProvisioner implements SquadhubProvisioner { async provision(): Promise { return { squadhubUrl: process.env.SQUADHUB_URL ?? "http://localhost:18790", @@ -22,6 +21,5 @@ export class DevProvisioner implements TenantProvisioner { } async deprovision(): Promise { - // No-op in dev — user manages squadhub via docker compose. } } diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts index edfc4b5..4327143 100644 --- a/packages/plugins/src/index.ts +++ b/packages/plugins/src/index.ts @@ -4,7 +4,7 @@ export type { PluginMap } from "./registry"; // Interfaces export type { - TenantProvisioner, + SquadhubProvisioner, ProvisionParams, ProvisionResult, ProvisioningStatus, @@ -13,5 +13,5 @@ export type { } from "./interfaces"; // Dev defaults (for testing and direct use) -export { DevProvisioner } from "./defaults/provisioner"; -export { DevLifecycle } from "./defaults/lifecycle"; +export { DefaultSquadhubProvisioner } from "./defaults/squadhub-provisioner"; +export { DefaultSquadhubLifecycle } from "./defaults/squadhub-lifecycle"; diff --git a/packages/plugins/src/interfaces/index.ts b/packages/plugins/src/interfaces/index.ts index fd00872..16e6b95 100644 --- a/packages/plugins/src/interfaces/index.ts +++ b/packages/plugins/src/interfaces/index.ts @@ -1,8 +1,8 @@ export type { - TenantProvisioner, + SquadhubProvisioner, ProvisionParams, ProvisionResult, ProvisioningStatus, -} from "./provisioner"; +} from "./squadhub-provisioner"; -export type { SquadhubLifecycle, SquadhubStatus } from "./lifecycle"; +export type { SquadhubLifecycle, SquadhubStatus } from "./squadhub-lifecycle"; diff --git a/packages/plugins/src/interfaces/lifecycle.ts b/packages/plugins/src/interfaces/squadhub-lifecycle.ts similarity index 100% rename from packages/plugins/src/interfaces/lifecycle.ts rename to packages/plugins/src/interfaces/squadhub-lifecycle.ts diff --git a/packages/plugins/src/interfaces/provisioner.ts b/packages/plugins/src/interfaces/squadhub-provisioner.ts similarity index 95% rename from packages/plugins/src/interfaces/provisioner.ts rename to packages/plugins/src/interfaces/squadhub-provisioner.ts index 0fbb553..49e7e65 100644 --- a/packages/plugins/src/interfaces/provisioner.ts +++ b/packages/plugins/src/interfaces/squadhub-provisioner.ts @@ -17,7 +17,7 @@ export interface ProvisioningStatus { message?: string; } -export interface TenantProvisioner { +export interface SquadhubProvisioner { /** Create infrastructure for a new tenant and return connection details. */ provision(params: ProvisionParams): Promise; diff --git a/packages/plugins/src/registry.spec.ts b/packages/plugins/src/registry.spec.ts index d083e40..fc9b190 100644 --- a/packages/plugins/src/registry.spec.ts +++ b/packages/plugins/src/registry.spec.ts @@ -1,7 +1,9 @@ import { describe, it, expect, beforeEach } from "vitest"; import { loadPlugins, hasPlugin, getPlugin } from "./registry"; -import { DevProvisioner } from "./defaults/provisioner"; -import { DevLifecycle } from "./defaults/lifecycle"; +import type { SquadhubProvisioner } from "./interfaces/squadhub-provisioner"; +import type { SquadhubLifecycle } from "./interfaces/squadhub-lifecycle"; +import { DefaultSquadhubProvisioner } from "./defaults/squadhub-provisioner"; +import { DefaultSquadhubLifecycle } from "./defaults/squadhub-lifecycle"; describe("registry", () => { describe("loadPlugins", () => { @@ -13,22 +15,22 @@ describe("registry", () => { describe("getPlugin", () => { it("returns dev provisioner by default", () => { - const provisioner = getPlugin("provisioner"); - expect(provisioner).toBeInstanceOf(DevProvisioner); + const provisioner = getPlugin("squadhub-provisioner"); + expect(provisioner).toBeInstanceOf(DefaultSquadhubProvisioner); }); it("returns dev lifecycle by default", () => { - const lifecycle = getPlugin("lifecycle"); - expect(lifecycle).toBeInstanceOf(DevLifecycle); + const lifecycle = getPlugin("squadhub-lifecycle"); + expect(lifecycle).toBeInstanceOf(DefaultSquadhubLifecycle); }); }); }); -describe("DevProvisioner", () => { - let provisioner: DevProvisioner; +describe("DefaultSquadhubProvisioner", () => { + let provisioner: SquadhubProvisioner; beforeEach(() => { - provisioner = new DevProvisioner(); + provisioner = new DefaultSquadhubProvisioner(); }); it("provision returns env-based connection", async () => { @@ -54,11 +56,11 @@ describe("DevProvisioner", () => { }); }); -describe("DevLifecycle", () => { - let lifecycle: DevLifecycle; +describe("DefaultSquadhubLifecycle", () => { + let lifecycle: SquadhubLifecycle; beforeEach(() => { - lifecycle = new DevLifecycle(); + lifecycle = new DefaultSquadhubLifecycle(); }); it("restart is a no-op", async () => { diff --git a/packages/plugins/src/registry.ts b/packages/plugins/src/registry.ts index ea69e2f..0eb45d8 100644 --- a/packages/plugins/src/registry.ts +++ b/packages/plugins/src/registry.ts @@ -1,16 +1,16 @@ -import type { TenantProvisioner } from "./interfaces/provisioner"; -import type { SquadhubLifecycle } from "./interfaces/lifecycle"; -import { DevProvisioner } from "./defaults/provisioner"; -import { DevLifecycle } from "./defaults/lifecycle"; +import type { SquadhubProvisioner } from "./interfaces/squadhub-provisioner"; +import type { SquadhubLifecycle } from "./interfaces/squadhub-lifecycle"; +import { DefaultSquadhubProvisioner } from "./defaults/squadhub-provisioner"; +import { DefaultSquadhubLifecycle } from "./defaults/squadhub-lifecycle"; export interface PluginMap { - provisioner: TenantProvisioner; - lifecycle: SquadhubLifecycle; + "squadhub-provisioner": SquadhubProvisioner; + "squadhub-lifecycle": SquadhubLifecycle; } let plugins: PluginMap = { - provisioner: new DevProvisioner(), - lifecycle: new DevLifecycle(), + "squadhub-provisioner": new DefaultSquadhubProvisioner(), + "squadhub-lifecycle": new DefaultSquadhubLifecycle(), }; let pluginsLoaded = false; From 99a2cdd7438c2f187c79c6860936d2f189e652b8 Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Wed, 18 Feb 2026 12:09:49 +0200 Subject: [PATCH 2/2] fix --- packages/plugins/src/defaults/squadhub-provisioner.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/plugins/src/defaults/squadhub-provisioner.ts b/packages/plugins/src/defaults/squadhub-provisioner.ts index d072981..c51a9bd 100644 --- a/packages/plugins/src/defaults/squadhub-provisioner.ts +++ b/packages/plugins/src/defaults/squadhub-provisioner.ts @@ -20,6 +20,5 @@ export class DefaultSquadhubProvisioner implements SquadhubProvisioner { return { status: "active" }; } - async deprovision(): Promise { - } + async deprovision(): Promise {} }