[Website] Add internal enterprise key reissue endpoint. (#21807)

Port of #21660 into the redone's enterprise backend.

- Extract getLicenseeFromStripeCustomer into the platform/enterprise
barrel, replacing the inline licensee-from-Stripe-customer block in the
activate route; cover its branches with a unit test.
- Add POST /api/enterprise/reissue: a support endpoint that re-signs an
enterprise key from a subscriptionId, guarded by a timing-safe compare
against ENTERPRISE_ADMIN_API_SECRET. Adapted to the redone — barrel
imports, node:crypto named imports, and the sibling routes' 503
configured-check guard — while keeping the original's generic
non-leaking 500.
- Document ENTERPRISE_ADMIN_API_SECRET in .env.example.
This commit is contained in:
Abdullah.
2026-06-19 11:56:00 +05:00
committed by GitHub
parent 2169e15162
commit 9eb90e72f9
6 changed files with 174 additions and 6 deletions
@@ -25,3 +25,7 @@ ENTERPRISE_JWT_PUBLIC_KEY=
# Optional: enterprise validity token lifetime in days (default 30).
# ENTERPRISE_VALIDITY_TOKEN_DURATION_DAYS=
# Shared secret guarding the internal enterprise key reissue endpoint
# (POST /api/enterprise/reissue), used to regenerate an enterprise key.
ENTERPRISE_ADMIN_API_SECRET=
@@ -1,6 +1,10 @@
import { NextResponse } from 'next/server';
import { getStripeClient, signEnterpriseKey } from '@/platform/enterprise';
import {
getLicenseeFromStripeCustomer,
getStripeClient,
signEnterpriseKey,
} from '@/platform/enterprise';
export const dynamic = 'force-dynamic';
@@ -67,11 +71,7 @@ export async function GET(request: Request) {
);
}
const customer = session.customer;
const licensee =
customer && typeof customer !== 'string' && !customer.deleted
? (customer.name ?? customer.email ?? 'Unknown')
: 'Unknown';
const licensee = getLicenseeFromStripeCustomer(session.customer);
const enterpriseKey = signEnterpriseKey(subscription.id, licensee);
@@ -0,0 +1,98 @@
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { NextResponse } from 'next/server';
import {
getLicenseeFromStripeCustomer,
getStripeClient,
signEnterpriseKey,
} from '@/platform/enterprise';
export const dynamic = 'force-dynamic';
function isSecretValid(providedSecret: string): boolean {
const expectedSecret = process.env.ENTERPRISE_ADMIN_API_SECRET;
if (!expectedSecret || !providedSecret) {
return false;
}
const comparisonKey = randomBytes(32);
const expectedDigest = createHmac('sha256', comparisonKey)
.update(expectedSecret)
.digest();
const providedDigest = createHmac('sha256', comparisonKey)
.update(providedSecret)
.digest();
return timingSafeEqual(expectedDigest, providedDigest);
}
export async function POST(request: Request) {
if (
!process.env.STRIPE_SECRET_KEY ||
!process.env.ENTERPRISE_JWT_PRIVATE_KEY ||
!process.env.ENTERPRISE_ADMIN_API_SECRET
) {
console.error(
'[enterprise-reissue] 503 — STRIPE_SECRET_KEY, ENTERPRISE_JWT_PRIVATE_KEY and/or ENTERPRISE_ADMIN_API_SECRET are not configured',
);
return NextResponse.json(
{ error: 'Enterprise key reissue is not configured.' },
{ status: 503 },
);
}
try {
let body: { subscriptionId?: unknown; secret?: unknown } = {};
try {
body = await request.json();
} catch {
body = {};
}
const secret = typeof body.secret === 'string' ? body.secret : '';
if (!isSecretValid(secret)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const subscriptionId =
typeof body.subscriptionId === 'string' ? body.subscriptionId : '';
if (!subscriptionId) {
return NextResponse.json(
{ error: 'Missing subscriptionId' },
{ status: 400 },
);
}
const stripe = getStripeClient();
const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
expand: ['customer'],
});
const licensee = getLicenseeFromStripeCustomer(subscription.customer);
const enterpriseKey = signEnterpriseKey(subscription.id, licensee);
const response = NextResponse.json({
enterpriseKey,
licensee,
subscriptionId: subscription.id,
subscriptionStatus: subscription.status,
});
response.headers.set('Cache-Control', 'no-store');
return response;
} catch (error: unknown) {
console.error('Enterprise key reissue failed', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}
@@ -0,0 +1,48 @@
import { getLicenseeFromStripeCustomer } from './get-licensee-from-stripe-customer';
type CustomerArg = Parameters<typeof getLicenseeFromStripeCustomer>[0];
const customer = (fields: Record<string, unknown>): CustomerArg =>
fields as unknown as CustomerArg;
describe('getLicenseeFromStripeCustomer', () => {
it('should return the customer name when present', () => {
expect(
getLicenseeFromStripeCustomer(
customer({ deleted: false, email: 'ops@acme.com', name: 'Acme Inc' }),
),
).toBe('Acme Inc');
});
it('should fall back to the email when the name is missing', () => {
expect(
getLicenseeFromStripeCustomer(
customer({ deleted: false, email: 'ops@acme.com', name: null }),
),
).toBe('ops@acme.com');
});
it('should return Unknown when both name and email are missing', () => {
expect(
getLicenseeFromStripeCustomer(
customer({ deleted: false, email: null, name: null }),
),
).toBe('Unknown');
});
it('should return Unknown for a deleted customer', () => {
expect(
getLicenseeFromStripeCustomer(
customer({ deleted: true, name: 'Acme Inc' }),
),
).toBe('Unknown');
});
it('should return Unknown for an unexpanded customer id string', () => {
expect(getLicenseeFromStripeCustomer('cus_123')).toBe('Unknown');
});
it('should return Unknown when there is no customer', () => {
expect(getLicenseeFromStripeCustomer(null)).toBe('Unknown');
});
});
@@ -0,0 +1,17 @@
import type Stripe from 'stripe';
type StripeCustomerField =
| string
| Stripe.Customer
| Stripe.DeletedCustomer
| null;
export function getLicenseeFromStripeCustomer(
customer: StripeCustomerField,
): string {
if (customer && typeof customer !== 'string' && !customer.deleted) {
return customer.name ?? customer.email ?? 'Unknown';
}
return 'Unknown';
}
@@ -1,4 +1,5 @@
export { getEnterprisePriceId } from './enterprise-price-id';
export { getLicenseeFromStripeCustomer } from './get-licensee-from-stripe-customer';
export { getStripeClient } from './stripe-client';
export { getSubscriptionCurrentPeriodEnd } from './subscription-current-period-end';
export { signEnterpriseKey } from './sign-enterprise-key';