From global-plugin
Use when reviewing or editing a public HTTP API, webhook payload, event schema, or any boundary another team/service depends on. Do NOT use for internal intra-module calls (use `nestjs-service-boundary-guard`). Covers API versioning, breaking-change detection, schema evolution, webhook/event contracts, consumer migration.
npx claudepluginhub lgerard314/global-marketplace --plugin global-pluginThis skill is limited to using the following tools:
Prevent silent breaking changes across service boundaries. Governs every point where code crosses a team or service line: REST/HTTP APIs, webhooks, async event payloads, and machine-readable schema files. Prescribes how to distinguish additive from breaking changes, version and sign payloads, and keep contract tests wired in CI so breakage is caught before deployment.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Prevent silent breaking changes across service boundaries. Governs every point where code crosses a team or service line: REST/HTTP APIs, webhooks, async event payloads, and machine-readable schema files. Prescribes how to distinguish additive from breaking changes, version and sign payloads, and keep contract tests wired in CI so breakage is caught before deployment.
schemaVersion field; consumers tolerate unknown versions by logging and discarding, never crashing. — Why: producers and consumers deploy independently; a consumer receiving a version it has never seen must degrade gracefully rather than cascade-fail.Deprecation response header (RFC 8594) or a documented sunset date; silent removals are forbidden. — Why: silent removal gives consumers zero time to adapt and makes postmortems harder because the change is invisible in the API response.| Thought | Reality |
|---|---|
| "Small rename, no one uses that field" | Someone does — field removal is one of the most common sources of silent production breakage across service boundaries. |
| "Webhook validation is slow, skip the signature check" | Skipping verification turns the endpoint into an open command channel; replay attacks cost one HTTP request. |
| "We have a version in the URL, we can handle breaks later" | A version number without a migration plan is cosmetic — consumers on the old version are still broken the moment semantics change under them. |
Bad:
// BEFORE
interface OrderCreatedEvent {
orderId: string;
total: number; // was a number
}
// AFTER — type change breaks any consumer that stored or compared the value
interface OrderCreatedEvent {
orderId: string;
total: string; // changed to string — silent breakage for typed consumers
}
Good:
// BEFORE
interface OrderCreatedEvent {
orderId: string;
total: number;
}
// AFTER — additive change: new optional field, existing fields untouched
interface OrderCreatedEvent {
orderId: string;
total: number;
currency?: string; // optional new field; consumers that ignore unknown fields are unaffected
}
Bad:
// Trusting that the request came from the right sender based on URL path alone
app.post('/webhooks/stripe', express.json(), async (req, res) => {
const event = req.body; // no verification — anyone who knows this URL can POST
await processStripeEvent(event);
res.sendStatus(200);
});
Good:
import crypto from 'node:crypto';
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
const TOLERANCE_SECONDS = 300; // reject replays older than 5 minutes
app.post(
'/webhooks/stripe',
express.raw({ type: 'application/json' }), // raw body needed for HMAC
(req, res) => {
const sigHeader = req.headers['stripe-signature'] as string | undefined;
if (!sigHeader) return res.status(400).send('Missing signature header');
const [timestampPart, v1Part] = sigHeader.split(',');
const timestamp = parseInt(timestampPart.replace('t=', ''), 10);
const receivedSig = v1Part.replace('v1=', '');
// Replay-attack guard
const nowSeconds = Math.floor(Date.now() / 1000);
if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) {
return res.status(400).send('Timestamp outside tolerance window');
}
// Recompute the expected HMAC-SHA256
const signedPayload = `${timestamp}.${req.body}`;
const expectedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Constant-time comparison to prevent timing attacks
const sigBuffer = Buffer.from(receivedSig, 'hex');
const expectedBuffer = Buffer.from(expectedSig, 'hex');
if (
sigBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expectedBuffer)
) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
res.sendStatus(200); // ack before async processing to avoid webhook retries
processStripeEvent(event).catch(console.error);
},
);
Bad:
// Producer — no version field; consumers cannot distinguish old from new shape
async function publishOrderShipped(orderId: string, carrier: string) {
await eventBus.publish('order.shipped', { orderId, carrier });
}
// Consumer — crashes when shape changes
async function handleOrderShipped(raw: unknown) {
const event = raw as { orderId: string; carrier: string }; // assertion, no guard
await sendShippingEmail(event.orderId, event.carrier);
}
Good:
import { z } from 'zod';
// Discriminated union keyed on schemaVersion
const OrderShippedV1 = z.object({
schemaVersion: z.literal(1),
orderId: z.string().uuid(),
carrier: z.string(),
});
const OrderShippedV2 = z.object({
schemaVersion: z.literal(2),
orderId: z.string().uuid(),
carrier: z.string(),
trackingUrl: z.string().url(), // new field in v2
});
const OrderShippedEvent = z.discriminatedUnion('schemaVersion', [
OrderShippedV1,
OrderShippedV2,
]);
type OrderShippedEvent = z.infer<typeof OrderShippedEvent>;
// Consumer — tolerates unknown versions gracefully
async function handleOrderShipped(raw: unknown) {
const parsed = OrderShippedEvent.safeParse(raw);
if (!parsed.success) {
// Unknown or malformed version — log and discard, do not crash
logger.warn({ raw, error: parsed.error }, 'Unrecognised order.shipped schema; skipping');
return;
}
const event = parsed.data;
const url = event.schemaVersion === 2 ? event.trackingUrl : undefined;
await sendShippingEmail(event.orderId, event.carrier, url);
}
Definitively breaking:
number → string, string → string[]).total now excludes tax when it previously included it).Additive (non-breaking when consumers ignore unknown fields):
else/default branch for unknown values; consumers with exhaustive switches will fail.Behavioural (hard to classify automatically — requires human review):
Before claiming a change is additive, answer: does every known consumer use a tolerant reader pattern (e.g., struct tags with omitempty, Zod with .passthrough(), OpenAPI additionalProperties: true)?
For the schema-first workflow (OpenAPI spec example, openapi-typescript / redocly / oasdiff tooling, PR procedure), see references/patterns.md — OpenAPI workflow section.
Webhook endpoints are public — signature verification is the only defence.
HMAC-SHA256 pattern: producer signs the raw body concatenated with a timestamp; consumer recomputes the HMAC and compares via a constant-time function to prevent timing-oracle attacks.
// producer.ts — sign before sending
import crypto from 'node:crypto';
export function buildWebhookHeaders(
rawBody: string,
secret: string,
): Record<string, string> {
const timestamp = Math.floor(Date.now() / 1000).toString();
const signedPayload = `${timestamp}.${rawBody}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return {
'Content-Type': 'application/json',
'X-Webhook-Timestamp': timestamp,
'X-Webhook-Signature': `v1=${signature}`,
};
}
The consumer implementation is shown in the Good vs bad section above.
Secret rotation: Rotate webhook secrets without downtime by accepting signatures from both the old and the new secret during a brief overlap window (e.g., 30 minutes).
Schema registry: For high-volume systems, use a schema registry (AWS Glue Schema Registry, Confluent, or a Git-tracked JSON Schema file). Producers validate against the registry before publishing; consumers validate on receipt.
Migration window procedure:
HTTP API deprecations:
Deprecation and Sunset headers (RFC 8594) to every response from a deprecated endpoint.description field for the deprecated operation.// NestJS interceptor — attaches deprecation headers automatically
@Injectable()
export class DeprecationInterceptor implements NestInterceptor {
constructor(
private readonly sunsetDate: string, // e.g. "Mon, 01 Sep 2026 00:00:00 GMT"
private readonly successorPath: string,
private readonly logger: Logger,
) {}
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest<Request>();
this.logger.warn({
msg: 'Deprecated endpoint called',
path: req.path,
method: req.method,
caller: req.headers['x-service-name'] ?? 'unknown',
});
return next.handle().pipe(
tap(() => {
const res = context.switchToHttp().getResponse<Response>();
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', this.sunsetDate);
res.setHeader('Link', `<${this.successorPath}>; rel="successor-version"`);
}),
);
}
}
Event deprecations:
contract.deprecated meta-event when an event type or schema version enters deprecation. Include the event type, current version, last-supported version, and sunset timestamp.For Pact (consumer-driven) and schema-driven generated-client test patterns plus CI requirements, see references/patterns.md — Contract testing section.
queue-and-retry-safety for delivery semantics (at-least-once, DLQ, visibility timeout); resilience-and-error-handling for consumer retry patterns and circuit-breaker configuration; auth-and-permissions-safety for endpoint authorisation and API key management.architecture-guard's package dependency direction (module-level, compile-time); nestjs-service-boundary-guard's internal intra-module boundary enforcement.Produce a markdown report with these sections:
schemaVersion + deprecation headers over silent removal.schemaVersion; consumers tolerate unknown versions