All skills
simota avatar

/gateway

@e307415
by shingo imotasimota/agent-skills85 stars
15

Designing and reviewing APIs: OpenAPI spec generation, versioning strategy, breaking change detection, REST/GraphQL best practices. Use for API design or OpenAPI specs.

Use this Skill: https://skilld.dev/gh/simota/agent-skills/gateway

This session only. Nothing lands on disk.

referencemessagingwebhook-patterns.md

≈1.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Webhook Patterns

2026 note: webhook is the back-channel of choice for agentic / long-running AI work. Anthropic's Managed Agents and the broader 2026 "agentic SaaS" stack ship completion webhooks as the canonical way to notify external systems that an unattended multi-day run finished, an Evaluator Loop rubric passed, or a remediation action completed. Pair the patterns in this file with the receivers documented in mend/reference/safety-model.md and the incident-postmortem hooks in beacon/reference/incident-learning-postmortem.md. Treat agentic completion webhooks under the same signing / replay-protection / idempotency discipline as any other production webhook — the AI agent author does not get a discount on supply-chain hygiene.

HMAC-SHA256 Signature Verification

import crypto from 'node:crypto';

// Generic webhook signature verifier
function verifyWebhookSignature(
  payload: string | Buffer,
  signature: string,
  secret: string,
  algorithm: 'sha256' | 'sha1' = 'sha256',
): boolean {
  const expected = crypto
    .createHmac(algorithm, secret)
    .update(payload)
    .digest('hex');

  const sig = signature.replace(/^(sha256|sha1)=/, '');

  // Timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

Platform-Specific Verification

Platform Header Algorithm Format
Slack X-Slack-Signature HMAC-SHA256 v0=<hex> with timestamp
Discord X-Signature-Ed25519 Ed25519 Hex signature + timestamp
Stripe Stripe-Signature HMAC-SHA256 t=<ts>,v1=<hex>
GitHub X-Hub-Signature-256 HMAC-SHA256 sha256=<hex>
Telegram N/A (IP whitelist + secret token) Token-based Query param or header
LINE X-Line-Signature HMAC-SHA256 Base64
WhatsApp X-Hub-Signature-256 HMAC-SHA256 sha256=<hex>

Slack Verification (with timestamp)

function verifySlackRequest(
  body: string,
  timestamp: string,
  signature: string,
  signingSecret: string,
): boolean {
  // Reject requests older than 5 minutes (replay attack prevention)
  const fiveMinAgo = Math.floor(Date.now() / 1000) - 300;
  if (parseInt(timestamp) < fiveMinAgo) return false;

  const baseString = `v0:${timestamp}:${body}`;
  const hash = crypto
    .createHmac('sha256', signingSecret)
    .update(baseString)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(`v0=${hash}`),
    Buffer.from(signature),
  );
}

Idempotency

Idempotency Key Strategy

interface IdempotencyStore {
  exists(key: string): Promise<boolean>;
  set(key: string, result: unknown, ttlMs: number): Promise<void>;
  get(key: string): Promise<unknown | null>;
}

// Redis-based idempotency
class RedisIdempotencyStore implements IdempotencyStore {
  constructor(private redis: Redis) {}

  async exists(key: string): Promise<boolean> {
    return (await this.redis.exists(`idempotency:${key}`)) === 1;
  }

  async set(key: string, result: unknown, ttlMs: number): Promise<void> {
    await this.redis.set(
      `idempotency:${key}`,
      JSON.stringify(result),
      'PX',
      ttlMs,
    );
  }

  async get(key: string): Promise<unknown | null> {
    const data = await this.redis.get(`idempotency:${key}`);
    return data ? JSON.parse(data) : null;
  }
}

Idempotent Webhook Handler

async function handleWebhookIdempotent(
  eventId: string,
  handler: () => Promise<unknown>,
  store: IdempotencyStore,
  ttlMs = 24 * 60 * 60 * 1000, // 24 hours
): Promise<{ result: unknown; deduplicated: boolean }> {
  // Check if already processed
  const existing = await store.get(eventId);
  if (existing !== null) {
    return { result: existing, deduplicated: true };
  }

  // Process and store result
  const result = await handler();
  await store.set(eventId, result, ttlMs);
  return { result, deduplicated: false };
}

Retry Strategy

Exponential Backoff with Jitter

interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
  jitter: boolean;
}

const DEFAULT_RETRY: RetryConfig = {
  maxRetries: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  jitter: true,
};

function calculateDelay(attempt: number, config: RetryConfig): number {
  const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
  const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);

  if (!config.jitter) return cappedDelay;

  // Full jitter: random between 0 and cappedDelay
  return Math.random() * cappedDelay;
}

async function withRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig = DEFAULT_RETRY,
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      if (attempt < config.maxRetries) {
        const delay = calculateDelay(attempt, config);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError!;
}

Retry Decision Matrix

Error Type Retry? Strategy
2xx No Success
400 Bad Request No Fix payload
401 Unauthorized No Refresh token, retry once
403 Forbidden No Check permissions
404 Not Found No Fix endpoint
408 Timeout Yes Exponential backoff
429 Too Many Requests Yes Respect Retry-After header
500 Server Error Yes Exponential backoff
502/503/504 Yes Exponential backoff
Network Error Yes Exponential backoff
DNS Error No Fix configuration

Dead Letter Queue (DLQ)

interface DeadLetterEntry {
  id: string;
  originalEvent: unknown;
  error: string;
  attempts: number;
  firstFailedAt: Date;
  lastFailedAt: Date;
  metadata: {
    platform: string;
    eventType: string;
    webhookId: string;
  };
}

interface DeadLetterQueue {
  push(entry: DeadLetterEntry): Promise<void>;
  peek(limit: number): Promise<DeadLetterEntry[]>;
  retry(id: string): Promise<boolean>;
  discard(id: string): Promise<void>;
  stats(): Promise<{ total: number; byPlatform: Record<string, number> }>;
}

Webhook Handler Middleware Chain

type WebhookMiddleware = (
  ctx: WebhookContext,
  next: () => Promise<void>,
) => Promise<void>;

interface WebhookContext {
  rawBody: Buffer;
  headers: Record<string, string>;
  event: unknown;
  platform: PlatformType;
  verified: boolean;
  idempotencyKey?: string;
  metadata: Record<string, unknown>;
}

// Standard middleware chain
const webhookPipeline: WebhookMiddleware[] = [
  signatureVerification,   // 1. Verify HMAC signature
  timestampValidation,     // 2. Reject stale requests
  rateLimiting,            // 3. Per-source rate limit
  idempotencyCheck,        // 4. Deduplicate events
  eventParsing,            // 5. Parse and normalize
  messageRouting,          // 6. Route to handler
  errorHandling,           // 7. Catch and DLQ
];

Webhook Health & Monitoring

interface WebhookMetrics {
  received: number;
  processed: number;
  failed: number;
  deduplicated: number;
  averageLatencyMs: number;
  dlqSize: number;
}

// Health endpoint response
interface WebhookHealth {
  status: 'healthy' | 'degraded' | 'unhealthy';
  uptime: number;
  lastEventAt: Date | null;
  metrics: WebhookMetrics;
  platforms: Record<PlatformType, {
    connected: boolean;
    lastEventAt: Date | null;
    errorRate: number;
  }>;
}

Source: SKILL.md on GitHub

No alerts13d5 checks · Risk SAFE
  • Gen Agent Trust Hub13d

    The skill is a comprehensive API design specialist focusing on OpenAPI, GraphQL, and REST best practices. It provides detailed guidance on versioning, security (OWASP Top 10), and error handling without any malicious code or unsafe operations.

  • Socket13d

    No alerts

  • Snyk13d

    Risk: LOW · No issues

  • Runlayer6mo

    2/14 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at e307415. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 2 weeks ago

README badge

README badge for simota/agent-skills/gateway