Files
zeekay caad90bc04 rip: enterprise dirs + paywalls — full AGPL, no commercial-license code
Drop the dual-licensed `ee/` tree entirely. All features previously
locked behind the Hanzo Dataroom Commercial License are now part of the
AGPL-3.0 core and freely available — custom domains, branded viewing,
advanced analytics, watermarks, access controls, Q&A, AI vector stores,
workflows, SAML SSO, dataroom invitations, conversations, templates.

Layout changes:
* `ee/features/{ai,conversations,workflows,permissions,storage,templates,
  dataroom-invitations,access-notifications,security,conversions}/` →
  `features/<name>/` (AGPL).
* `ee/limits/` → `lib/billing/limits/` with every plan resolving to an
  unlimited profile (no users/links/documents/domains/datarooms cap,
  conversations and watermarks always on).
* `ee/stripe/` → `lib/billing/legacy/` (already a compat shim over
  `@hanzoai/commerce`; renewal-reminder webhook is now a no-op).
* `ee/features/billing/cancellation/` deleted. Pause/unpause/cancel/
  retention routes now compile against `lib/billing/cancellation.ts`
  stubs that return 410 Gone; `isTeamPaused`/`isTeamPausedById` in
  `lib/billing/paused.ts` always report active. `CancellationModal` is
  a no-op shim in `components/billing/`.
* `app/(ee)/api/*` route group folded into `app/api/*` (SAML auth,
  SCIM, AI chat, workflows, FAQ, link-upload).

Build: `npm run build` green.
2026-06-07 11:46:48 -07:00

114 lines
3.5 KiB
TypeScript

/**
* Hanzo Commerce webhook handler.
*
* Replaces /api/stripe/webhook. Hanzo Commerce emits the same logical event
* names as the legacy Stripe webhook (checkout.session.completed,
* customer.subscription.{updated,deleted}, invoice.upcoming,
* payment_intent.payment_failed) so the per-event business logic in
* ee/stripe/webhooks/* continues to apply unchanged.
*
* Configure Commerce to POST events here and set
* HANZO_COMMERCE_WEBHOOK_SECRET to the signing secret from
* commerce.hanzo.ai admin.
*
* TODO(stripe-rip): rename ee/stripe/webhooks/* -> ee/commerce/webhooks/*
* and update the imports below once the dataroom directory move ships.
*/
import { NextApiRequest, NextApiResponse } from "next";
import { processPaymentFailure } from "@/features/security";
import { stripeInstance } from "@/lib/billing/legacy";
import { checkoutSessionCompleted } from "@/lib/billing/legacy/webhooks/checkout-session-completed";
import { customerSubscriptionDeleted } from "@/lib/billing/legacy/webhooks/customer-subscription-deleted";
import { customerSubsciptionUpdated } from "@/lib/billing/legacy/webhooks/customer-subscription-updated";
import { invoiceUpcoming } from "@/lib/billing/legacy/webhooks/invoice-upcoming";
import { Readable } from "node:stream";
import { COMMERCE_WEBHOOK_SECRET } from "@/lib/commerce";
import { log } from "@/lib/utils";
export const config = {
supportsResponseStreaming: true,
api: {
bodyParser: false,
},
};
async function buffer(readable: Readable): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of readable) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
return Buffer.concat(chunks);
}
const relevantEvents = new Set([
"checkout.session.completed",
"customer.subscription.updated",
"customer.subscription.deleted",
"payment_intent.payment_failed",
"invoice.upcoming",
]);
export default async function webhookHandler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
res.setHeader("Allow", ["POST"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
const buf = await buffer(req);
const sig =
(req.headers["hanzo-commerce-signature"] as string | undefined) ??
(req.headers["stripe-signature"] as string | undefined);
if (!sig || !COMMERCE_WEBHOOK_SECRET) {
return res.status(400).send("Missing signature or secret");
}
let event;
try {
const c = stripeInstance();
event = c.webhooks.constructEvent(buf, sig, COMMERCE_WEBHOOK_SECRET);
} catch (err: any) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (!relevantEvents.has(event.type)) {
return res.status(400).send(`Unhandled event type: ${event.type}`);
}
try {
switch (event.type) {
case "checkout.session.completed":
await checkoutSessionCompleted(event);
break;
case "customer.subscription.updated":
await customerSubsciptionUpdated(event, res);
break;
case "customer.subscription.deleted":
await customerSubscriptionDeleted(event, res);
break;
case "payment_intent.payment_failed":
await processPaymentFailure(event);
break;
case "invoice.upcoming":
await invoiceUpcoming(event, res);
break;
}
} catch (error) {
await log({
message: `Commerce webhook failed. Error: ${error}`,
type: "error",
});
return res
.status(400)
.send("Webhook error: Webhook handler failed. View logs.");
}
return res.status(200).json({ received: true });
}