npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ktds-axbd/harness-kit

v0.22.0

Published

AX BD Cloudflare Workers MSA scaffold + harness CLI (auth/CORS/D1/event-bus/middleware)

Readme

@ktds-axbd/harness-kit

AX BD MSA 서비스 그룹의 공통 기반 패키지. 새 Cloudflare Workers 서비스를 1분 내 생성하고, 인증/CORS/이벤트/ESLint를 일관되게 설정해요.

Quick Start

1. 새 서비스 생성

npx harness create gate-x --service-id gate-x --account-id <CF_ACCOUNT_ID>
cd gate-x
pnpm install

2. app.ts에 harness-kit 통합

import { Hono } from "hono";
import {
  createAuthMiddleware,
  createCorsMiddleware,
  errorHandler,
  rbac,
} from "@ktds-axbd/harness-kit";

const config = {
  serviceName: "gate-x",
  serviceId: "gate-x" as const,
  corsOrigins: ["https://fx.minu.best"],
  publicPaths: ["/api/auth/", "/api/openapi.json"],
};

const app = new Hono<{ Bindings: Env }>();

app.use("*", createCorsMiddleware(config));
app.use("*", createAuthMiddleware(config));
app.use("*", errorHandler());

// 관리자 전용 라우트
app.get("/api/admin", rbac("admin"), (c) => c.json({ ok: true }));

export default app;

3. 실행

pnpm dev    # wrangler dev (로컬)
pnpm test   # vitest

CLI

harness create <name>

새 서비스 scaffold를 생성해요.

harness create <name> [options]

Options:
  --service-id <id>    서비스 ID (기본: foundry-x)
  --account-id <id>    Cloudflare Account ID
  --db-name <name>     D1 Database 이름 (기본: <name>-db)
  -o, --output <dir>   출력 디렉토리 (기본: ./<name>)

유효한 service-id: | ID | 서비스 | |----|--------| | foundry-x | Foundry-X (발굴+형상화) | | gate-x | Gate-X (검증) | | launch-x | Launch-X (제품화+GTM) | | eval-x | Eval-X (평가) | | discovery-x | Discovery-X (수집) | | ai-foundry | AI Foundry (AI 에이전트) |

생성 파일:

<name>/
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── wrangler.toml
└── src/
    ├── index.ts      # Workers entry
    ├── app.ts        # Hono app + harness-kit 설정
    └── env.ts        # 환경 타입 정의

API Reference

Middleware

createAuthMiddleware(config)

JWT 검증 미들웨어. publicPaths에 등록된 경로는 건너뛰어요.

import { createAuthMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createAuthMiddleware({
  serviceName: "gate-x",
  serviceId: "gate-x",
  corsOrigins: ["https://fx.minu.best"],
  publicPaths: ["/api/auth/", "/api/openapi.json"],
  jwtAlgorithm: "HS256",  // 기본값
}));

JWT Payload 타입:

interface JwtPayload {
  sub: string;
  email: string;
  role: "admin" | "member" | "viewer";
  orgId?: string;
  orgRole?: "owner" | "admin" | "member" | "viewer";
  services?: Array<{ id: string; role: string }>;
  iat: number;
  exp: number;
}

createCorsMiddleware(config)

CORS 미들웨어. corsOrigins 기반으로 허용 Origin을 설정해요.

app.use("*", createCorsMiddleware(config));

허용 메서드: GET, POST, PUT, PATCH, DELETE, OPTIONS

rbac(minRole)

역할 기반 접근 제어. JWT payload의 role을 확인해요.

type Role = "admin" | "member" | "viewer";

// admin만 접근 가능
app.delete("/api/resource/:id", rbac("admin"), handler);

// member 이상 접근 가능
app.post("/api/resource", rbac("member"), handler);

역할 레벨: admin(3) > member(2) > viewer(1)

errorHandler()

표준 에러 응답 포맷을 처리해요.

app.use("*", errorHandler());

// HarnessError 직접 사용
import { HarnessError } from "@ktds-axbd/harness-kit";
throw new HarnessError("NOT_FOUND", "Resource not found", 404);

createStranglerMiddleware(config)

Strangler Fig 패턴 라우터. 서비스 이관 전/후 트래픽을 분기해요.

import { createStranglerMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createStranglerMiddleware({
  routes: [
    {
      pathPrefix: "/api/gate",
      serviceId: "gate-x",
      mode: "local",  // 이관 전: 모놀리스 처리
    },
    {
      pathPrefix: "/api/launch",
      serviceId: "launch-x",
      mode: "proxy",  // 이관 후: 외부 서비스 포워딩
      targetUrl: "https://launch-x.ktds-axbd.workers.dev",
    },
  ],
}));

createMultiTenantMiddleware(config?) — v0.2.0

JWT payload의 orgId를 검증하고 컨텍스트에 테넌트 정보를 주입해요.

import { createMultiTenantMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createMultiTenantMiddleware({
  bypassPaths: ["/health", "/api/public"],
  onMissingOrgId: (c) => c.json({ error: "Org required" }, 403),
}));

// 이후 핸들러에서
app.get("/api/data", (c) => {
  const orgId = c.get("orgId");     // string
  const orgRole = c.get("orgRole"); // string (default: "member")
  const userId = c.get("userId");   // string (= payload.sub)
  return c.json({ orgId, orgRole, userId });
});

Config options (v0.2.0): bypassPaths?, onMissingOrgId?

Config options (v0.3.0 추가): d1Binding?, d1Table? (default: "org_members"), d1Mode? ("optional" | "forced", default: "optional"), onMembershipDenied?

// v0.3.0: D1 membership 강제 검증
app.use("*", createMultiTenantMiddleware({
  d1Binding: "DB",          // Cloudflare D1 binding key
  d1Mode: "forced",         // D1 없으면 503, row 없으면 403
  d1Table: "org_members",   // 기본값, 커스터마이즈 가능
  onMembershipDenied: (c, reason) => c.json({ error: "Access denied", reason }, 403),
}));

D1 권장 schema:

CREATE TABLE IF NOT EXISTS org_members (
  user_id TEXT NOT NULL,
  org_id  TEXT NOT NULL,
  role    TEXT NOT NULL DEFAULT 'member',
  PRIMARY KEY (user_id, org_id)
);

d1Mode 선택 가이드:

  • "optional" (기본): D1 없으면 JWT-only fallback (v0.2.0 동작과 동일)
  • "forced": D1 binding 필수, 없으면 503. 운영 보안 강화 시 사용

createCacheMiddleware(config) — v0.3.0

Cache-Control 헤더를 응답에 설정해요. CDN/브라우저 캐싱 정책을 미들웨어로 중앙 관리할 수 있어요.

import { createCacheMiddleware } from "@ktds-axbd/harness-kit";

// GET/HEAD 요청 5분 캐시
app.use("/api/static/*", createCacheMiddleware({
  maxAge: 300,               // browser cache 5분
  sMaxAge: 3600,             // CDN cache 1시간
  staleWhileRevalidate: 60,  // 재검증 중 stale 60초 허용
  scope: "public",           // "public" | "private" (default: "public")
  methods: ["GET", "HEAD"],  // (default)
}));

// 개인화 데이터: private 캐시
app.use("/api/profile", createCacheMiddleware({
  maxAge: 60,
  scope: "private",
}));

Config options: maxAge?, sMaxAge?, staleWhileRevalidate?, scope?, pathMatch?, methods?

동작 규칙:

  • 4xx/5xx 응답 → Cache-Control 헤더 미설정 (안전)
  • pathMatch 미일치 또는 methods 미일치 → 건너뜀

createCompressionMiddleware(config?) — v0.3.0

gzip/deflate 압축을 적용해요. hono/compress를 래핑하며 skipPaths로 스트리밍 경로를 제외할 수 있어요.

import { createCompressionMiddleware } from "@ktds-axbd/harness-kit";

// 전역 gzip 압축 (streaming 경로 제외)
app.use("*", createCompressionMiddleware({
  skipPaths: ["/api/stream", "/api/sse"],  // 건너뜀
  threshold: 1024,    // 최소 압축 크기 bytes (기본: 1024)
  encoding: "gzip",   // "gzip" | "deflate" (기본: Accept-Encoding 자동)
}));

Config options: threshold?, encoding?, skipPaths?

참고: Cloudflare Workers는 자동 gzip/brotli를 지원하지만, 이 미들웨어를 사용하면 명시적 제어(skipPaths, threshold)가 가능해요.


createRateLimitMiddleware(config) — v0.2.0

요청 수를 제한해요. KV namespace가 없으면 in-memory fallback을 사용해요.

import { createRateLimitMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createRateLimitMiddleware({
  limit: 100,        // 최대 요청 수
  windowSec: 60,     // 윈도우 (초)
  keyFn: (c) => c.req.header("CF-Connecting-IP") ?? "anon",
  kvBinding: "RATE_LIMIT_KV",  // optional: 분산 카운터
}));

Config options: limit (required), windowSec (required), keyFn?, kvBinding?


createRequestLoggerMiddleware(config?) — v0.2.0

요청/응답을 구조화된 JSON 또는 텍스트로 로깅해요. 민감 헤더는 자동 redact돼요.

import { createRequestLoggerMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createRequestLoggerMiddleware({
  format: "json",   // "json" | "text"
  logger: console.log,
  redactHeaders: ["authorization", "cookie", "x-api-key"],
}));
// 출력: {"method":"GET","path":"/api","status":200,"duration":12,"traceId":"...","timestamp":"..."}

Config options: format?, logger?, redactHeaders?


createTraceIdMiddleware(config?) — v0.2.0

요청 ID를 전파해요. 인입 헤더가 있으면 재사용하고, 없으면 UUID를 생성해요.

import { createTraceIdMiddleware } from "@ktds-axbd/harness-kit";

// 권장 ordering: traceId → logger → auth → tenant
app.use("*", createTraceIdMiddleware({ header: "x-trace-id" }));

app.get("/api", (c) => {
  const traceId = c.get("traceId"); // string
  return c.json({ traceId });
});

Config options: header? (default: "x-trace-id"), generator?


signJwt(payload, secret, opts?) — v0.6.0

JWT를 발급해요. createAuthMiddleware(verify)의 대칭 발급 함수예요.

import { signJwt, createAuthMiddleware } from "@ktds-axbd/harness-kit";

// OAuth callback 후 JWT 발급
const token = await signJwt(
  { sub: profile.id, email: profile.email, role: "member" },
  c.env.JWT_SECRET,
  { expiresIn: 3600 },  // 기본값
);

Parameters: payload: Omit<JwtPayload, "iat"|"exp">, secret: string, opts.expiresIn?: number (default: 3600s)


createOAuthRoutes(config) — v0.6.0

OAuth 2.0 소셜 로그인 full flow를 Hono sub-app 한 줄로 붙여요. GET /authorize (provider redirect) + GET /callback (code → JWT) 전체를 캡슐화해요.

import {
  createOAuthProvider,
  createOAuthRoutes,
  GOOGLE_PRESET,
} from "@ktds-axbd/harness-kit";

const app = new Hono<{ Bindings: Env }>();

const googleProvider = createOAuthProvider({
  provider: GOOGLE_PRESET,
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: "https://my-service.workers.dev/auth/callback",
  usePKCE: true,
});

// 한 줄로 OAuth 로그인 플로우 완성
app.route("/auth", createOAuthRoutes({
  provider: googleProvider,
  onSuccess: { redirectTo: "/" },  // 또는 "json"
}));

export default app;

OAuth flow:

  1. GET /auth/authorize → state cookie 저장 + 302 to Google
  2. GET /auth/callback?code=...&state=... → code 교환 → JWT 발급 → redirect or JSON

Config options: | 옵션 | 기본값 | 설명 | |------|--------|------| | provider (required) | — | OAuthProvider 인스턴스 | | jwtSecret? | c.env.JWT_SECRET 우선 | JWT 서명 키 | | jwtExpiresIn? | 3600 | Access token 만료 (초) | | refreshExpiresIn? | 604800 | Refresh token 만료 (초, 7d) | | refreshStore? | InMemoryRefreshTokenStore | Refresh token 상태 저장소 | | refreshCookieName? | "hk_refresh" | Refresh token cookie 이름 | | refreshSecret? | jwtSecret fallback | Refresh token 전용 서명키 (v0.8.0) | | refreshGraceMs? | 0 | Grace window (ms); 0 = strict revoke (v0.8.0) | | cookieName? | "hk_oauth_state" | state cookie 이름 | | usePKCE? | false | PKCE S256 활성화 | | mapProfile? | { sub, email, role: "member" } | profile → JWT payload 매핑 | | onSuccess? | "json" | "json" or { redirectTo: string } |

응답:

  • GET /authorize302 to provider
  • GET /callback 성공 → 200 { accessToken, refreshToken, token(deprecated alias), profile } (json) or 302 redirectTo + hk_token/hk_refresh cookies
  • code 누락 → 400, state 불일치 → 403, provider 오류 → 502
  • POST /refresh { refreshToken }200 { accessToken, refreshToken, token } (rotation) or 401 (invalid/reuse/revoked)

Refresh Token Rotation — v0.8.0 (RFC 9700)

RFC 9700 기반 stateful refresh token rotation + reuse detection + grace window 제공해요.

import {
  createOAuthProvider,
  createOAuthRoutes,
  InMemoryRefreshTokenStore,
  GOOGLE_PRESET,
} from "@ktds-axbd/harness-kit";

// 프로덕션: D1/KV adapter 주입 권장
// InMemoryRefreshTokenStore는 Workers single-isolate 환경에서 상태 유실 가능
const refreshStore = new InMemoryRefreshTokenStore();

app.route("/auth", createOAuthRoutes({
  provider: googleProvider,
  refreshStore,  // 생략 시 자동 InMemory 생성
  refreshExpiresIn: 604800,  // 7일 (기본값)
  refreshSecret: env.REFRESH_SECRET,  // v0.8.0: refresh 전용 서명키
  refreshGraceMs: 5000,              // v0.8.0: 5초 grace window (모바일/SPA 동시 요청 대응)
}));

Refresh flow:

  1. /callback{ accessToken, refreshToken } 발급 (각각 별도 서명키 사용 — v0.8.0)
  2. POST /auth/refresh { refreshToken } → 새 { accessToken, refreshToken } (1회용 회전)
  3. 동일 refresh token 재사용 → grace window 내라면 200 이전 신규 토큰 재반환 (idempotent) — v0.8.0
  4. grace 만료 후 재사용 → 401 { error: "refresh_token_reuse_detected" } + family 전체 revoke

Grace Window (v0.8.0):

  • 모바일/SPA 환경에서 동시 refresh 요청 시 정상 클라이언트가 false-positive revocation 당하는 문제 해소
  • refreshGraceMs=5000 (5초): 첫 회전 후 5초 내 동일 refresh token 재사용 → 이전에 발급된 새 token 재반환
  • refreshGraceMs=0 (기본): 즉시 revoke (v0.7.0 strict 동작과 동일)

refreshSecret 격리 (v0.8.0):

  • refreshSecret 설정 시 refresh token을 별도 키로 서명 → access token 서명키 노출이 refresh token을 위협하지 않음
  • 미설정 시 jwtSecret fallback (v0.7.0 backward compat)

⚠️ 프로덕션 주의사항: InMemoryRefreshTokenStore는 Workers isolate 재시작 시 상태 초기화. 프로덕션에서는 D1/KV adapter를 직접 구현하세요:

class D1RefreshTokenStore implements RefreshTokenStore {
  constructor(private db: D1Database) {}

  async isRotatedOrRevoked(jti: string): Promise<boolean> {
    const row = await this.db.prepare(
      "SELECT 1 FROM refresh_tokens WHERE jti = ? AND (rotated = 1 OR family_revoked = 1)"
    ).bind(jti).first();
    return row !== null;
  }
  // markRotated, revokeFamily, isFamilyRevoked ...
}

D1 유틸리티

import { getDb, runQuery, runExec } from "@ktds-axbd/harness-kit/d1";

// Workers fetch handler에서
export default {
  async fetch(req: Request, env: Env) {
    const db = getDb(env.DB);
    const rows = await runQuery<User>(db, "SELECT * FROM users WHERE id = ?", [id]);
    return Response.json(rows);
  },
};

Event Bus

D1 기반 도메인 이벤트 발행/구독이에요.

import { D1EventBus, NoopEventBus, createEvent } from "@ktds-axbd/harness-kit/events";

// 이벤트 발행
const bus = new D1EventBus(env.DB);
await bus.publish(
  createEvent("discovery.opportunity.created", "gate-x", {
    opportunityId: "opp-123",
    title: "AI 기반 BD 자동화",
  })
);

// 이벤트 구독
bus.subscribe("discovery.opportunity.created", async (event) => {
  console.log("New opportunity:", event.payload);
});

// 폴링으로 미처리 이벤트 처리 (Cron Trigger에서 사용)
await bus.poll("gate-x", 100);  // 최대 100건

D1 마이그레이션 필요:

-- 0114_domain_events.sql (Foundry-X DB에 이미 적용됨)
CREATE TABLE IF NOT EXISTS domain_events (
  id TEXT PRIMARY KEY,
  type TEXT NOT NULL,
  source TEXT NOT NULL,
  tenant_id TEXT NOT NULL DEFAULT 'default',
  payload TEXT NOT NULL,
  metadata TEXT,
  status TEXT NOT NULL DEFAULT 'pending',
  created_at TEXT NOT NULL
);

ESLint Plugin

서비스 간 직접 import를 방지해요.

// eslint.config.mjs
import { harnessKitPlugin } from "@ktds-axbd/harness-kit/eslint";

export default [
  {
    plugins: { "harness-kit": harnessKitPlugin },
    rules: {
      "harness-kit/no-cross-service-import": "error",
    },
    settings: {
      "harness-kit": {
        currentService: "gate-x",
        // 직접 import 금지 서비스 목록 (기본: 모든 ServiceId)
        forbiddenServices: ["foundry-x", "launch-x"],
      },
    },
  },
];

Resilience — 외부 호출 견고성 (./resilience, v0.15.0)

외부 API(AI 모델 등) 호출의 재시도·차단·대체 패턴. 무의존(AbortController + setTimeout).

import { withRetry, createCircuitBreaker, withFallback } from "@ktds-axbd/harness-kit/resilience";

// 지수 백오프 + jitter + retryable predicate
const result = await withRetry(() => fetch(url), {
  maxAttempts: 3,
  baseDelayMs: 200,
  jitter: true,
  isRetryable: (e) => e instanceof TypeError, // 네트워크 오류만 재시도
});

// 회로 차단기 — closed→open→half-open
const breaker = createCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000, halfOpenMaxCalls: 2 });
const data = await breaker.execute(() => callExternalApi());

// 폴백 체인 — primary 실패 시 fallback
const value = await withFallback(() => primaryModel(), () => fallbackModel());

Webhook 서명 검증 (./middleware, v0.15.0)

수신 webhook의 HMAC 서명을 검증하는 미들웨어. GitHub/Slack/Stripe 프리셋 + Web Crypto timing-safe 비교.

import { createWebhookVerifyMiddleware } from "@ktds-axbd/harness-kit/middleware";

// GitHub (X-Hub-Signature-256)
app.use("/webhooks/github", createWebhookVerifyMiddleware({ provider: "github", secret: c.env.GH_WEBHOOK_SECRET }));
// 검증 실패 시 401, 성공 시 다음 핸들러로 (raw body 보존)

KV Store (./kv, v0.15.0)

Cloudflare KV 바인딩 helper. namespace prefix + JSON + TTL. ./d1 대칭.

import { createKVStore } from "@ktds-axbd/harness-kit/kv";

const kv = createKVStore(c.env.CACHE, { prefix: "session:" });
await kv.setJSON("user-123", { name: "..." }, { ttlSeconds: 3600 });
const user = await kv.getJSON<User>("user-123"); // prefix 자동 부착, miss 시 null

getOrFetch cache-aside (v0.21.0+) — 캐시 hit 시 반환, miss 시 fetcher 실행 후 저장하고 결과 반환. raw JSON(envelope 없음) + prefix·TTL 적용.

const profile = await kv.getOrFetch<Profile>(
  "user-123",
  async () => fetchProfileFromD1("user-123"), // miss 시에만 실행
  3600, // TTL(초)
);

Health/Readiness probe (./middleware, v0.16.0)

liveness(즉시 200) + readiness(의존성 체크 콜백). 로드밸런서·Cloudflare 헬스체크·k8s 스타일 probe.

import { createHealthMiddleware } from "@ktds-axbd/harness-kit/middleware";

const health = createHealthMiddleware({
  checks: [
    { name: "d1", check: async () => { await c.env.DB.prepare("SELECT 1").first(); } },
    { name: "kv", check: async () => { await c.env.CACHE.get("ping"); } },
  ],
  // livenessPath: "/health" (기본), readinessPath: "/ready" (기본)
});
app.route("/", health); // GET /health → 200, GET /ready → 200 또는 503 + 실패 항목

API Key auth (./middleware, v0.16.0)

M2M 인증. 발급·검증·rotation. 원본 키는 1회만 반환, KV에는 SHA-256 해시만 저장.

import { createApiKeyMiddleware, issueApiKey, rotateApiKey } from "@ktds-axbd/harness-kit/middleware";

// 발급 (원본 키는 이때 1회만 노출)
const { apiKey, id } = await issueApiKey({ kv: c.env.KEYS, owner: "svc-a", options: { scopes: ["read"] } });

// 검증 미들웨어 (X-API-Key 헤더)
app.use("/api/*", createApiKeyMiddleware({ kvBinding: "KEYS", bypassPaths: ["/api/health"] }));
// 통과 시 c.get("apiKeyId")·c.get("owner")·c.get("scopes"), 실패 시 401

// rotation (신규 발급 + 기존 revoke)
const rotated = await rotateApiKey({ kv: c.env.KEYS, id, owner: "svc-a" });

Pagination (./pagination, v0.16.0)

cursor·offset 양 모드 순수 유틸. ./d1와는 SQL fragment 반환으로 연동(직접 import 없음).

import { parsePaginationParams, buildPageResponse, buildD1PaginationClause } from "@ktds-axbd/harness-kit/pagination";

const params = parsePaginationParams(c.req.query()); // { limit, cursor?, offset? } (limit max 100 clamp)
const { clause, binds } = buildD1PaginationClause({ ...params, orderBy: "id" });
const rows = await c.env.DB.prepare(`SELECT * FROM items ${clause}`).bind(...binds).all();
return c.json(buildPageResponse(rows.results, { ...params, cursorField: "id" })); // { data, pageInfo: { hasNextPage, nextCursor } }

R2 Store (./r2, v0.16.0)

Cloudflare R2 바인딩 helper. ./kv 대칭. presigned URL은 R2 바인딩 미지원(Worker proxy 패턴 별도).

import { createR2Store } from "@ktds-axbd/harness-kit/r2";

const r2 = createR2Store(c.env.BUCKET, { prefix: "uploads/" });
await r2.put("a.json", JSON.stringify(data), { httpMetadata: { contentType: "application/json" } });
const obj = await r2.getJSON<Data>("a.json"); // prefix 자동 부착
const page = await r2.list({ prefix: "2026/", limit: 100 }); // { objects, truncated, cursor }

Queue (./queue, v0.16.0)

Cloudflare Queue producer + consumer helper.

import { createQueueProducer, createQueueConsumer } from "@ktds-axbd/harness-kit/queue";

// Producer
const q = createQueueProducer(c.env.MY_QUEUE);
await q.send({ jobId: 1 });
await q.sendBatch([{ jobId: 2 }, { jobId: 3 }], { delaySeconds: 30 });

// Consumer (메시지별 ack 성공 / retry throw 격리)
export const queue = createQueueConsumer<Job>(async (body) => { await process(body); });

Structured Logger (./middleware, v0.17.0)

요청별 구조화 JSON 로거. traceId 자동 바인딩. 핸들러에서 getLogger(c)로 호출.

import { createLoggerMiddleware, getLogger } from "@ktds-axbd/harness-kit/middleware";

app.use("*", createTraceIdMiddleware());
app.use("*", createLoggerMiddleware({ service: "gate-x", minLevel: "info", redact: ["password"] }));

app.get("/api/data", (c) => {
  getLogger(c).info("fetch data", { userId: c.get("userId") }); // { level, msg, traceId, service, userId, timestamp }
  return c.json({ ok: true });
});

Cron/Scheduled handler (./cron, v0.17.0)

Workers cron 트리거 래퍼. name별 라우팅 + job 에러 격리.

import { createScheduledHandler } from "@ktds-axbd/harness-kit/cron";

export const scheduled = createScheduledHandler({
  jobs: [
    { name: "hourly-sync", cron: "0 * * * *", handler: async (ctx) => { await sync(ctx.env); } },
    { name: "daily-report", cron: "0 0 * * *", handler: async (ctx) => { await report(ctx.env); } },
  ],
}); // event.cron으로 매칭 job 실행, 1개 throw가 타 job 미차단, ctx.waitUntil

schedule predicate (v0.22.0): 단일 cron 안에서 시간/조건 제약을 선언적으로 표현. 예를 들어 6시간마다 도는 cron에 백업 job만 UTC 18시에 실행하고 싶을 때, handler 내부 if (getUTCHours() !== 18) return 대신:

createScheduledHandler({
  jobs: [
    { name: "recon", cron: "0 */6 * * *", handler: reconJob }, // 매 실행
    {
      name: "backup",
      cron: "0 */6 * * *",
      schedule: () => new Date().getUTCHours() === 18, // UTC 18시에만
      handler: backupJob,
    },
  ],
}); // schedule false면 cron 매칭이어도 skip + logger.info("cron skip")

타입 export (v0.22.0): ScheduledEventLike/ExecutionContextLike를 export해, 반환값을 export const scheduled = ...로 내보낼 때 declaration emit의 private name leak(TS4023/TS4082)을 방지. Cloudflare 표준 타입(ScheduledController/ExecutionContext)도 구조적으로 호환돼요.

Metrics scrape endpoint (./metrics, v0.17.0)

Prometheus pull(/metrics) 엔드포인트. otel push와 별개.

import { createMetricsRegistry, createMetricsEndpoint } from "@ktds-axbd/harness-kit/metrics";

const metrics = createMetricsRegistry();
const reqCount = metrics.counter("http_requests_total", "총 요청 수", ["method"]);
reqCount.inc({ method: "GET" });

app.route("/", createMetricsEndpoint(metrics)); // GET /metrics → Prometheus text format

WebSocket/DO helper (./websocket, v0.17.0)

WebSocket 업그레이드 + 룸 broadcast(Durable Object 호환).

import { createWebSocketUpgrade, createBroadcastHub } from "@ktds-axbd/harness-kit/websocket";

const hub = createBroadcastHub();

app.get("/ws", (c) =>
  createWebSocketUpgrade({           // non-WS 요청 시 426
    onOpen: (ws) => hub.add(ws),
    onMessage: (ws, msg) => hub.broadcast(msg),  // 전체 broadcast
    onClose: (ws) => hub.remove(ws),             // 자동 remove
  })(c.req.raw),                     // 101 Response 반환
);

Validation (./middleware, v0.18.0)

duck-typed schema(zod·valibot 호환) 요청 검증. zod 미import = dependency-free.

import { createValidatorMiddleware, getValidated } from "@ktds-axbd/harness-kit/middleware";
import { z } from "zod"; // 소비자가 제공 (harness-kit는 zod 비의존)

const schema = z.object({ name: z.string(), age: z.number() });

app.post("/users", createValidatorMiddleware("json", schema), (c) => {
  const body = getValidated<{ name: string; age: number }>(c, "json"); // 검증된 타입 데이터
  return c.json({ ok: true, body });
}); // 검증 실패 시 400 + { issues: [...] }

Config/Secrets (./config, v0.18.0)

Workers env 타입 로더. required fail-fast + transform + secret 마스킹.

import { createConfig } from "@ktds-axbd/harness-kit/config";

const config = createConfig(c.env, {
  API_URL: { required: true },
  MAX_RETRIES: { default: "3", transform: Number },
  JWT_SECRET: { required: true, secret: true }, // 로그에서 *** 마스킹
});
config.get("API_URL");        // string (없으면 startup throw)
config.get("MAX_RETRIES");    // 3 (number)
console.log(config.toJSON()); // { ..., JWT_SECRET: "***" }

Service Binding/RPC (./service-binding, v0.18.0)

MSA Worker-to-Worker service binding 호출 클라이언트.

import { createServiceClient } from "@ktds-axbd/harness-kit/service-binding";

const agent = createServiceClient(c.env.AGENT_SVC, {  // Cloudflare service binding (Fetcher)
  basePath: "/api/agent",
  headers: { authorization: c.req.header("authorization") ?? "" }, // auth 전파
});
const result = await agent.post<{ id: string }>("/run", { task: "x" }); // 타입 JSON, non-2xx → throw

Observability — OTel export (v0.9.0)

createTraceIdMiddleware + createRequestLoggerMiddleware가 만든 데이터를 OpenTelemetry SDK 의존성 없이 OTLP/JSON wire format으로 collector에 직접 전송해요 (fetch 기반, bundle footprint 추가 ≈ 0).

import {
  createOtelTraceMiddleware,
  exportMetrics,
  buildRequestCountMetric,
  buildDurationHistogramMetric,
} from "@ktds-axbd/harness-kit/otel";

// 요청당 span을 OTLP collector로 전송 (trace-id + method/path/status/duration → span attributes)
app.use("*", createOtelTraceMiddleware({
  endpoint: "https://collector.example.com",  // POST {endpoint}/v1/traces
  serviceName: "my-service",
  headers: { "x-api-key": env.OTEL_API_KEY },
  resourceAttributes: { "deployment.environment": "production" },
}));

Signals: traces(요청 span) + metrics(request count sum + duration histogram). exportTraces/exportMetrics가 OTLP/JSON payload를 /v1/traces·/v1/metrics로 POST하고, span flush는 ctx.waitUntil로 응답 지연 없이 처리해요.

의존성 0: @microlabs/otel-cf-workers·@opentelemetry/* 등 SDK 미사용 — OTLP/JSON(resourceSpans/resourceMetrics) 구조를 직접 구성. peerDependency 추가 없음.

W3C Trace Context — traceparent 전파 (v0.10.0)

인바운드 traceparent 헤더를 파싱하여 분산 트레이싱의 parent-child span 연결을 지원해요.

import {
  createOtelTraceMiddleware,
  getTraceparent,
  parseTraceparent,
  buildTraceparent,
} from "@ktds-axbd/harness-kit/otel";

// 1. Middleware 등록 — 인바운드 traceparent 자동 파싱
app.use("*", createOtelTraceMiddleware({ endpoint: "https://collector.example.com", serviceName: "svc-a" }));

// 2. 아웃바운드 fetch에 traceparent 전파 (downstream service와 trace 연결)
app.get("/api/data", async (c) => {
  const res = await fetch("https://svc-b.example.com/api/items", {
    headers: { traceparent: getTraceparent(c) ?? "" },
  });
  return c.json(await res.json());
});

// 3. 직접 파싱 / 생성 유틸
const parsed = parseTraceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
// → { traceId: "4bf92f...", parentSpanId: "00f067...", flags: "01" }
// all-zero traceId/spanId, 잘못된 포맷 → null

const header = buildTraceparent("4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7");
// → "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"

인바운드 traceparent가 유효하면 span이 기존 traceId를 이어받고 parentSpanId가 설정되어 parent-child 관계가 형성돼요. 유효하지 않거나 없으면 새 root span이 생성돼요.

OTLP Logs export — log-trace correlation (v0.11.0)

세 번째 OTLP signal인 logs를 추가해요. createOtelTraceMiddlewareincludeLogs 옵션(기본 true)으로 요청당 LogRecord 1개를 collector로 전송하며, 같은 요청 span의 traceId/spanId를 담아 **trace와 자동 상관(correlation)**돼요.

import {
  createOtelTraceMiddleware,
  exportLogs,
  buildLogRecord,
} from "@ktds-axbd/harness-kit/otel";

// includeLogs 기본 true — traces·metrics와 동일 미들웨어에서 요청당 LogRecord 발행
app.use("*", createOtelTraceMiddleware({
  endpoint: "https://collector.example.com",  // POST {endpoint}/v1/logs
  serviceName: "my-service",
  includeLogs: true,   // 기본값 (includeMetrics와 동형, false로 끄기 가능)
}));

LogRecord: body "METHOD path STATUS", severity는 status 기반 매핑(5xx → ERROR(17) · 4xx → WARN(13) · 그 외 → INFO(9)). traceId/spanId가 설정되어 collector backend(Tempo/Loki 등)에서 trace ↔ log 연결이 가능해요.

Signals (3종): traces(요청 span) + metrics(count sum + duration histogram) + logs(요청당 LogRecord). 각각 exportTraces/exportMetrics/exportLogs/v1/traces·/v1/metrics·/v1/logs로 OTLP/JSON POST하고, flush는 모두 ctx.waitUntil로 응답 지연 없이 처리해요. 의존성 0 유지 (manual OTLP/JSON, resourceLogs/scopeLogs/logRecords 구조 직접 구성).


Per-Request 테넌트 라우팅 (v0.12.0 — 멀티테넌트 관측)

createMultiTenantMiddlewarecreateOtelTraceMiddleware를 연동하면 요청마다 orgId를 동적으로 추출해 traces·metrics·logs 3 signal 전부를 별도 테넌트 백엔드로 라우팅해요. Tempo·Mimir·Loki 멀티테넌시를 단일 Collector 인스턴스로 구현할 수 있어요.

통합 예제

import { createHarness } from "@ktds-axbd/harness-kit";
import { createMultiTenantMiddleware, createOtelTraceMiddleware } from "@ktds-axbd/harness-kit";

const config = { serviceName: "my-service", serviceId: "my-service", corsOrigins: ["*"] };
const app = createHarness(config);

// 1. 테넌트 컨텍스트 추출 (X-Org-ID 헤더 → c.set("orgId", ...))
app.use("*", createMultiTenantMiddleware({
  headerName: "X-Org-ID",    // 소비자가 보내는 헤더
  contextKey: "orgId",       // c.get("orgId")로 접근
  defaultOrgId: "default",
}));

// 2. OTel 트레이싱 + 테넌트 라우팅
app.use("*", createOtelTraceMiddleware(config, {
  endpoint: "http://localhost:4318",  // OTel Collector
  tenantContextKey: "orgId",          // createMultiTenantMiddleware의 contextKey와 일치
  tenantHeaderName: "X-Scope-OrgID", // Grafana 스택 표준 헤더 (Mimir·Loki·Tempo)
  defaultOrgId: "default",           // 테넌트 컨텍스트 없을 때 fallback (헤더 생략)
}));

동작 원리

| 단계 | 설명 | |------|------| | 요청 수신 | createMultiTenantMiddlewareX-Org-ID 헤더를 읽어 c.set("orgId", orgId) | | flush 시점 | ctx.waitUntil로 비동기 실행 — c.get("orgId")로 orgId 획득 | | 헤더 주입 | withTenantHeader(config, orgId, "X-Scope-OrgID")로 각 signal의 HTTP 헤더에 주입 | | 테넌트 격리 | Tempo: X-Scope-OrgID → Mimir: X-Scope-OrgID → Loki: X-Scope-OrgID |

No-context Fallback

tenantContextKey로 조회된 값이 없으면:

  • defaultOrgId가 설정된 경우 → X-Scope-OrgID: defaultOrgId 주입
  • defaultOrgId도 없으면 → 테넌트 헤더 생략 (단일 테넌트 동작)

Grafana 관측 환경 (v0.12.0 — dashboards-as-code)

examples/observability/ 디렉토리에 Grafana as-code provisioning 파일이 포함돼 있어요. docker compose up 한 번으로 완전한 관측 스택을 재현할 수 있어요.

npm 설치 후 node_modules/@ktds-axbd/harness-kit/examples/observability/에서 바로 사용할 수 있어요.

포함 서비스

| 서비스 | 이미지 | 포트 | 역할 | |--------|--------|------|------| | Grafana | grafana/grafana:11.3.1 | :3000 | 대시보드 + datasource 관리 | | Tempo | grafana/tempo:2.6.1 | :3200 / :4317 | 분산 추적 | | Loki | grafana/loki:3.2.0 | :3100 | 로그 수집 | | Prometheus | prom/prometheus:v2.53.0 | :9090 | 메트릭 수집 | | Mimir | grafana/mimir:2.14.0 | :9009 | 멀티테넌트 메트릭 | | OTel Collector | otel/opentelemetry-collector-contrib:latest | :4318 | 신호 수집·라우팅 |

로컬 실행

# npm 설치 후
cd node_modules/@ktds-axbd/harness-kit/examples/observability
docker compose up -d

# Grafana UI: http://localhost:3000  (anonymous admin)
# RED 대시보드: http://localhost:3000/d/harness-kit-red

기존 Grafana에 붙이기

grafana/provisioning/ 파일을 기존 Grafana 인스턴스의 provisioning 디렉토리에 복사하면 Tempo·Loki·Prometheus datasource와 RED 대시보드를 자동으로 로드해요.

cp -r examples/observability/grafana/provisioning /etc/grafana/provisioning
cp -r examples/observability/grafana/dashboards /etc/grafana/dashboards
# Grafana 재시작

HarnessConfig 타입

interface HarnessConfig {
  serviceName: string;          // 서비스 표시명
  serviceId: ServiceId;         // 서비스 식별자
  corsOrigins: string[];        // 허용 CORS Origin 목록
  publicPaths?: string[];       // 인증 불필요 경로 (prefix 매칭)
  jwtAlgorithm?: string;        // JWT 알고리즘 (기본: "HS256")
}


OAuth 2.0 Provider

Workers 환경(fetch + crypto.subtle)에서 Google/GitHub 소셜 로그인을 처리하는 helper예요.

기본 흐름

import { createOAuthProvider, generatePKCE, generateState, verifyState } from "@ktds-axbd/harness-kit";

const oauth = createOAuthProvider({
  provider: "google",  // "google" | "github" | GenericProviderSpec
  clientId: env.GOOGLE_CLIENT_ID,
  clientSecret: env.GOOGLE_CLIENT_SECRET,
  redirectUri: "https://myapp.example.com/callback",
  usePKCE: true,
});

// Step 1: Authorization URL 생성
const pkce = await generatePKCE();
const state = generateState();
const { url } = oauth.getAuthorizationUrl({ state, codeVerifier: pkce.verifier });
// state/verifier를 KV에 저장 후 redirect

// Step 2: Callback — code 교환
const tokens = await oauth.exchangeCode({ code, codeVerifier: storedVerifier });

// Step 3: 사용자 정보
const profile = await oauth.getUserInfo(tokens.accessToken);
// profile: { id, email, name, avatarUrl?, raw }

JwtPayload 연동

const payload = { sub: profile.id, email: profile.email ?? "", role: "member" as const };

Feature Flags — OpenFeature 어댑터 (v0.12.0)

./flags 모듈은 의존성 없는 feature-flag 평가(boolean·string·number·JSON + deterministic rollout + targeting)를 제공하며, HarnessKitProviderOpenFeature SDK에 연결할 수 있어요. Cloudflare Flagship GA 시 FlagshipProvider로 교체 가능한 표준 어댑터예요.

설치 (OpenFeature 어댑터 사용 시)

@openfeature/server-sdkoptional peerDependency예요 — 어댑터를 쓸 때만 설치하면 돼요.

npm install @openfeature/server-sdk

사용 예

import { OpenFeature } from "@openfeature/server-sdk";
import { HarnessKitProvider, StaticFlagProvider } from "@ktds-axbd/harness-kit/flags";

// 1) harness FlagProvider 정의 (Static 또는 KVFlagProvider)
const flagProvider = new StaticFlagProvider({
  "new-dashboard": {
    key: "new-dashboard",
    defaultValue: false,
    rules: [{ when: { orgId: "org-123" }, value: true, variant: "treatment" }],
  },
});

// 2) OpenFeature SDK에 harness-kit Provider 등록
await OpenFeature.setProviderAndWait(new HarnessKitProvider(flagProvider));

// 3) 표준 OpenFeature client API로 평가
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("new-dashboard", false, {
  targetingKey: userId, // → harness EvaluationContext.userId
  orgId: "org-123",     // → harness EvaluationContext.orgId
});

HarnessKitProvider는 4종 resolve(Boolean/String/Number/Object)를 지원하며, harness ResolutionDetails(value·reason·variant)를 OpenFeature 표준 형식으로 전파해요. 어댑터를 거치지 않고 isEnabled(c, key)·getFlag(c, key) 미들웨어 헬퍼를 직접 쓸 수도 있어요.


createSecurityHeadersMiddleware(config?) (F715)

보안 헤더 주입 미들웨어. CSP·HSTS·X-Frame-Options·X-Content-Type-Options·Referrer-Policy를 응답에 추가해요.

import { createSecurityHeadersMiddleware } from "@ktds-axbd/harness-kit";
// 또는 subpath: import { createSecurityHeadersMiddleware } from "@ktds-axbd/harness-kit/middleware";

app.use("*", createSecurityHeadersMiddleware({
  csp: "default-src 'self'; script-src 'self' 'nonce-{n}'", // false로 비활성화
  hsts: { maxAge: 63072000, includeSubDomains: true },
  xFrameOptions: "DENY",          // "SAMEORIGIN" | false
  xContentTypeOptions: true,       // nosniff (기본값)
  referrerPolicy: "strict-origin-when-cross-origin", // false로 비활성화
}));

기본값: CSP default-src 'self', X-Frame-Options DENY, nosniff true, Referrer-Policy strict-origin-when-cross-origin. HSTS는 기본 비활성화.

조건부 CSP (v0.19.0+)csp에 콜백을 전달하면 응답 컨텍스트 기반으로 CSP를 동적 결정해요. HTML 문서(예: Swagger UI)는 제외하고 JSON API에만 strict CSP를 적용하는 등 HTML+JSON 혼재 표면에 유용해요. 콜백이 false/undefined를 반환하면 CSP 헤더를 설정하지 않아요.

app.use("*", createSecurityHeadersMiddleware({
  // JSON 응답만 strict CSP, HTML(Swagger UI 등)은 제외
  csp: (c) => (c.res.headers.get("Content-Type") ?? "").includes("text/html")
    ? false
    : "default-src 'none'",
}));

createTimeoutMiddleware(config) (F715)

요청 타임아웃 미들웨어. 핸들러가 timeoutMs 초과 시 504 Gateway Timeout을 반환해요.

import { createTimeoutMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createTimeoutMiddleware({
  timeoutMs: 5000,
  message: "Request timed out",   // 기본: "Gateway Timeout"
  skipPaths: ["/health", "/api/stream"],
}));

createBodyLimitMiddleware(config) (F715)

요청 바디 크기 제한 미들웨어. Content-Length 초과 시 413 Payload Too Large를 반환해요. (DoS 방어)

import { createBodyLimitMiddleware } from "@ktds-axbd/harness-kit";

app.use("*", createBodyLimitMiddleware({
  maxBytes: 10_000_000,           // JSON 기본 10MB
  message: "File too large",
  methods: ["POST", "PUT", "PATCH"], // 기본값
  skipPaths: ["/api/health"],      // v0.19.0+ — 한도 완전 면제 경로
  pathLimits: [                    // v0.20.0+ — prefix별 차등 한도
    { prefix: "/api/files/", maxBytes: 50_000_000 },        // 업로드 50MB
    { prefix: "/api/launch/object-store/", maxBytes: 50_000_000 },
  ],
}));

Content-Length 헤더가 없으면 passthrough. GET/DELETE 등은 기본 제한 제외. skipPaths (v0.19.0+) — 업로드 등 큰 body가 정상인 경로를 한도 검사에서 완전 면제해요. pathLimits (v0.20.0+) — 경로 prefix별 차등 한도를 적용해요(예: JSON 10MB, 업로드 50MB). 여러 prefix 매칭 시 가장 긴(구체적인) prefix가 우선(longest-prefix-match), skipPaths와 병존(skipPaths 먼저 검사). skipPaths로 업로드를 면제하면 ceiling이 사라지는 문제를 pathLimits로 해소(업로드도 한도 유지).

createIdempotencyMiddleware(config?) (F716)

POST/PATCH 중복 실행 방지 미들웨어. Idempotency-Key 헤더 기반으로 첫 요청 응답을 저장하고, 동일 키 재요청 시 replay해요. in-flight 중 동일 키 요청은 409를 반환해요.

import { createIdempotencyMiddleware } from "@ktds-axbd/harness-kit";
// 또는 subpath: import { createIdempotencyMiddleware } from "@ktds-axbd/harness-kit/middleware";

app.use("*", createIdempotencyMiddleware({
  kvBinding: "IDEMPOTENCY_KV",    // Cloudflare KV namespace binding (필수, 없으면 passthrough)
  ttlSec: 86400,                  // 캐시 TTL (기본: 86400초 = 24시간)
  keyHeader: "Idempotency-Key",   // 헤더 이름 (기본값)
  methods: ["POST", "PATCH"],     // 적용 메서드 (기본값)
}));

동작 요약:

  • kvBinding 미설정 → passthrough (idempotency 비활성화)
  • Idempotency-Key 헤더 없음 → passthrough
  • 첫 요청 → handler 실행, KV에 응답 저장
  • 동일 키 재요청 → 저장된 응답 replay (handler 미실행)
  • in-flight 중 동일 키 → 409 Conflict
  • TTL 만료 → 키 소멸, 새 요청으로 처리

Subpath Exports

// middleware subpath (F715+)
import { createSecurityHeadersMiddleware, createTimeoutMiddleware, createBodyLimitMiddleware, createIdempotencyMiddleware } from "@ktds-axbd/harness-kit/middleware";

// oauth subpath (F715+)
import { createOAuthProvider, createOAuthRoutes } from "@ktds-axbd/harness-kit/oauth";

Resilience — 외부 호출 견고성 (v0.15.0+)

./resilience subpath export — 무의존(AbortController + setTimeout), Cloudflare Workers 호환.

import { withRetry, createCircuitBreaker, withFallback } from '@ktds-axbd/harness-kit/resilience'

withRetry

지수 백오프 + jitter + AbortSignal 취소 지원.

const result = await withRetry(
  () => fetchExternalAPI(),
  {
    maxAttempts: 5,       // 최대 시도 횟수
    baseDelayMs: 100,     // 기본 지연 ms (기본값: 100)
    maxDelayMs: 10000,    // 최대 지연 ms (기본값: 10000)
    jitter: true,         // 지터 활성화 (기본값: true, thundering herd 방지)
    signal: abortSignal,  // 취소 신호 (선택)
    retryable: (err) => err instanceof NetworkError, // 재시도 가능 여부 (선택)
  }
)

createCircuitBreaker

closed → open → half-open 상태머신. cascade failure 차단.

const cb = createCircuitBreaker({
  failureThreshold: 5,    // open 전환 실패 횟수
  resetTimeoutMs: 10000,  // open → half-open 대기 ms
  halfOpenMaxCalls: 1,    // half-open 허용 호출 수 (기본값: 1)
})

try {
  const result = await cb.execute(() => callExternalService())
} catch (err) {
  if (err instanceof CircuitOpenError) {
    // 회로 열림 — 빠른 실패
  }
}

cb.getState() // 'closed' | 'open' | 'half-open'
cb.reset()    // 강제 CLOSED 복귀

withFallback

primary 실패 시 fallback chain 순차 실행.

const result = await withFallback(
  () => callPrimaryAPI(),       // 1순위
  () => callSecondaryAPI(),     // 2순위 (primary 실패 시)
  () => Promise.resolve(cache), // 최종 fallback
)

조합 패턴

const cb = createCircuitBreaker({ failureThreshold: 3, resetTimeoutMs: 5000 })

const result = await withFallback(
  () => withRetry(() => cb.execute(() => callAI()), { maxAttempts: 3 }),
  () => Promise.resolve(cachedResponse),
)

버전 정보

| 버전 | 변경사항 | |------|---------| | 0.13.0 | production middleware 3종(security-headers·timeout·body-limit) + ./middleware·./oauth subpath exports | | 0.12.0 | OpenFeature 어댑터 — HarnessKitProvider (@openfeature/server-sdk optional peerDep) | | 0.5.0 | OAuth 2.0 provider (Google/GitHub preset + PKCE S256 + state CSRF) | | 0.1.0 | 초기 릴리스 — Phase 20 AX BD MSA 재조정 |