Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/app/api/tenant/provision/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/app/api/tenant/squadhub/restart/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
};
28 changes: 28 additions & 0 deletions apps/web/src/app/api/tenant/squadhub/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
};
29 changes: 29 additions & 0 deletions apps/web/src/app/api/tenant/squadhub/status/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
};
51 changes: 51 additions & 0 deletions apps/web/src/lib/api/tenant-auth.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
4 changes: 2 additions & 2 deletions packages/plugins/src/defaults/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { DevProvisioner } from "./provisioner";
export { DevLifecycle } from "./lifecycle";
export { DefaultSquadhubProvisioner } from "./squadhub-provisioner";
export { DefaultSquadhubLifecycle } from "./squadhub-lifecycle";
27 changes: 0 additions & 27 deletions packages/plugins/src/defaults/lifecycle.ts

This file was deleted.

27 changes: 0 additions & 27 deletions packages/plugins/src/defaults/provisioner.ts

This file was deleted.

20 changes: 20 additions & 0 deletions packages/plugins/src/defaults/squadhub-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -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<void> {}

async stop(): Promise<void> {}

async destroy(): Promise<void> {}

async getStatus(): Promise<SquadhubStatus> {
return { running: true, healthy: true };
}
}
24 changes: 24 additions & 0 deletions packages/plugins/src/defaults/squadhub-provisioner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {
SquadhubProvisioner,
ProvisionResult,
ProvisioningStatus,
} from "../interfaces/squadhub-provisioner";

/**
* Default squadhub provisioner — reads connection from environment variables.
* Override with a cloud implementation to provision real infrastructure.
*/
export class DefaultSquadhubProvisioner implements SquadhubProvisioner {
async provision(): Promise<ProvisionResult> {
return {
squadhubUrl: process.env.SQUADHUB_URL ?? "http://localhost:18790",
squadhubToken: process.env.SQUADHUB_TOKEN ?? "",
};
}

async getProvisioningStatus(): Promise<ProvisioningStatus> {
return { status: "active" };
}

async deprovision(): Promise<void> {}
}
6 changes: 3 additions & 3 deletions packages/plugins/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type { PluginMap } from "./registry";

// Interfaces
export type {
TenantProvisioner,
SquadhubProvisioner,
ProvisionParams,
ProvisionResult,
ProvisioningStatus,
Expand All @@ -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";
6 changes: 3 additions & 3 deletions packages/plugins/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProvisionResult>;

Expand Down
26 changes: 14 additions & 12 deletions packages/plugins/src/registry.spec.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
16 changes: 8 additions & 8 deletions packages/plugins/src/registry.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down