@ayronforge/haversack

中文文档
0.12.0发布于 2026/10/22MITTypeScriptES Module构建来源已验证近 30 天下载 1901

Typed TypeScript toolbox: Effect services for backend integrations plus focused React bindings for PostHog feature flags

原始使用文档
文档目录12 节
  1. What's inside
  2. Schemas
  3. Email
  4. PostHog
  5. Stripe
  6. Auth
  7. Cloudflare
  8. Contracts and AWS
  9. Testing
  10. Environment configuration
  11. Contributing
  12. License

Haversack

A haversack is the bag you pack once and carry everywhere.

Every new Effect project starts by rebuilding the same things: an email service wrapped around Resend, feature flags that fail open, a Stripe client with proper tracing spans, a rate limiter on Durable Objects, CPF validation with real check digits. Haversack is that code — extracted from production apps, written as idiomatic Effect from end to end, and packed into one package so you never write it twice.

Everything is a typed Effect service. No loose async functions, no untyped errors, no global config. Each capability is a Context.Service with tagged errors, tracing spans, and a Layer you compose like any other — config in, service out, test layers included.

Import only what you use. Each module is an independent subpath export, and heavy SDKs (Stripe, Resend, posthog-js, ...) are optional peer dependencies: if you never import /stripe, you never install Stripe.

bun add @ayronforge/haversack effect

Haversack is built on Effect 4 (effect@^4.0.0).

What's inside

ModuleWhat it gives you
/schemaSchema building blocks: URLs, slugs, phone numbers, Brazilian documents (CPF, CNPJ, CEP)
/emailResend-backed email service with react-email rendering and contact sync
/posthogFail-open server analytics and flags, plus typed React flag policy over the official PostHog provider
/stripeStripe SDK wrapper with per-operation tracing spans and webhook verification
/auth/workos, /auth/clerkAuth SDK client layers with redacted config and tagged errors
/cfWorkers primitives: rate limiter, distributed lock, queues, Analytics Engine, R2, Workflows adapter
/contracts, /awsVendor-neutral blob storage port with R2 and S3 implementations
/testingTest helpers: service runners, ExecutionContext fakes, SDK stubs
/errors, /utilsTagged error primitives, error normalization, and pure helpers

Schemas

Reusable Effect Schema building blocks with canonical storage values and separate presentation formatters.

import { Schema } from "effect";
import {
  CnpjFromString,
  CpfFromString,
  EmailAddressFromString,
  EndpointUrlFromString,
  formatCnpj,
  formatCpf,
  PhoneNumberFromString,
  SlugFromString,
  UuidFromString,
} from "@ayronforge/haversack/schema";

const cpf = Schema.decodeUnknownSync(CpfFromString)("529.982.247-25");
// "52998224725" — canonical digits in both decoded and encoded forms
formatCpf(cpf); // "529.982.247-25"

const cnpj = Schema.decodeUnknownSync(CnpjFromString)("11.222.333/0001-81");
// "11222333000181" — canonical digits in both decoded and encoded forms
formatCnpj(cnpj); // "11.222.333/0001-81"

const phone = Schema.decodeUnknownSync(PhoneNumberFromString({ defaultCountry: "BR" }))(
  "(11) 98765-4321",
);
// "+5511987654321" — canonical E.164

const email = Schema.decodeUnknownSync(EmailAddressFromString())("[email protected]");
// "[email protected]" — domain normalized, local part preserved by default

const host = Schema.decodeUnknownSync(EndpointUrlFromString)("https://api.example.com/");
// "https://api.example.com" — normalized, rejects query strings and fragments

const slug = Schema.decodeUnknownSync(SlugFromString)("Café com Leite!");
// "cafe-com-leite"

const uuid = Schema.decodeUnknownSync(UuidFromString)("0198A3FC-9DB1-7BD5-8A1E-2F6A1F4C9D10");
// "0198a3fc-9db1-7bd5-8a1e-2f6a1f4c9d10" — strict, canonical UUID

CepLookup.layerViaCep({ fetch }) resolves an already parsed Cep without capturing global I/O and distinguishes not-found, unavailable, and malformed responses. PhoneNumberFromString parses international or explicitly country-scoped national input through libphonenumber-js.

Email

A provider port with a Resend adapter. Send pre-rendered HTML or a react-email element, and sync contacts to segments and topics.

import { Effect, Layer, Redacted } from "effect";
import { EmailConfig, EmailLive, EmailService } from "@ayronforge/haversack/email";

const config = EmailConfig.layer({
  apiKey: Redacted.make(process.env.RESEND_API_KEY!),
  from: "Acme <[email protected]>",
});

const program = Effect.gen(function* () {
  const email = yield* EmailService;
  yield* email.send({
    to: "[email protected]",
    subject: "Welcome",
    html: "<p>Hello!</p>",
    idempotencyKey: "welcome-user-1",
  });
}).pipe(Effect.provide(EmailLive.pipe(Layer.provide(config))));

PostHog

Server-side capture and feature flags run over fetch without an SDK and work in Workers. Server delivery and evaluation are fail-open: failures log and fall back rather than break a request. The transport is Effect's FetchHttpClient.Fetch reference (defaults to globalThis.fetch); tests and hosts inject their own with Effect.provideService(FetchHttpClient.Fetch, fn).

import { Effect, Layer, Redacted } from "effect";
import { FeatureFlags, PostHogAnalytics, PostHogConfig } from "@ayronforge/haversack/posthog";

const config = PostHogConfig.layer({
  projectToken: Redacted.make(process.env.POSTHOG_TOKEN!),
});

const program = Effect.gen(function* () {
  const analytics = yield* PostHogAnalytics;
  yield* analytics.track({ event: "signup", distinctId: "user_1" });

  const flags = yield* FeatureFlags;
  return yield* flags.isEnabled(
    { key: "new-checkout", fallback: false },
    { distinctId: "user_1" },
  );
}).pipe(
  Effect.provide(PostHogAnalytics.layer.pipe(Layer.provide(config))),
  Effect.provide(FeatureFlags.layer.pipe(Layer.provide(config))),
);

React feature-flag policy lives in @ayronforge/haversack/posthog/react and composes directly with the official @posthog/react provider. useFeatureFlag subscribes through useSyncExternalStore; FeatureGate adds typed fallback and pending rendering without owning SDK initialization, capture, or identity.

Stripe

An SDK wrapper where every call is named: the operation becomes the tracing span (stripe.checkout.sessions.create) and error metadata.

import { Effect, Layer, Redacted } from "effect";
import { StripeClient, StripeConfig } from "@ayronforge/haversack/stripe";

const config = StripeConfig.layer({
  secretKey: Redacted.make(process.env.STRIPE_SECRET_KEY!),
  webhookSecret: Redacted.make(process.env.STRIPE_WEBHOOK_SECRET!),
});

const program = Effect.gen(function* () {
  const stripe = yield* StripeClient;
  const session = yield* stripe.use("checkout.sessions.create", (sdk) =>
    sdk.checkout.sessions.create({ mode: "subscription", line_items: [/* ... */] }),
  );
  return session.url;
}).pipe(Effect.provide(StripeClient.layer.pipe(Layer.provide(config))));

verifyWebhook(payload, signature) validates webhook signatures with tagged errors. makeStripeClientFromSdk is the seam for injecting a fake SDK in tests.

Auth

Thin client layers for WorkOS (/auth/workos) and Clerk (/auth/clerk), following the same provider pattern: config with redacted keys, use with spans, tagged errors. Clerk additionally exposes useWebhook for verified webhook events.

Cloudflare

Workers primitives, all binding-oriented — you pass bindings explicitly, the library never reads a global env.

  • RequestRateLimiter — fixed-window and token-bucket over effect/persistence, with layerMemory for tests and layerDurableObject(namespace) for cross-isolate limits. The namespace points to a caller-owned Durable Object implementing RateLimiterRpc; the application owns persistence, migrations and cleanup. Fail-open on store failures.
  • DistributedLock / withLock(key, effect) — acquireUseRelease lifecycle over a caller-owned Durable Object implementing the typed lease RPC contract.
  • makeQueueClientService(queue) — queue producer publishing in Cloudflare's 100-message batches.
  • AnalyticsEngine — the strict Analytics Engine SQL API with Schema-decoded rows. Non-successful responses remain typed query errors; the library does not infer undocumented Cloudflare error semantics from response text. Requests go through the FetchHttpClient.Fetch reference, so no global fetch is captured.
  • makeR2BlobStorageLayer(bucket) / R2BlobPresignerLive — the blob storage contract over R2 (see Contracts below).
  • @ayronforge/haversack/cf/workflow — makeCloudflareWorkflowEngineLayer({ workflow, step }) adapts effect/workflow to Cloudflare Workflows: activities map to step.do with retries, durable deferreds to sendEvent/waitForEvent, durable clocks to step.sleep.

Contracts and AWS

@ayronforge/haversack/contracts defines vendor-neutral ports — currently BlobStorage (put/get/delete/exists/list) and BlobPresigner. Code against the port; pick the implementation per runtime:

import { Effect } from "effect";
import { BlobStorage } from "@ayronforge/haversack/contracts";
import { makeR2BlobStorageLayer } from "@ayronforge/haversack/cf";
import { S3BlobStorageLive, S3Config } from "@ayronforge/haversack/aws";

const program = Effect.gen(function* () {
  const storage = yield* BlobStorage;
  yield* storage.put("reports/q3.pdf", bytes, { contentType: "application/pdf" });
  const report = yield* storage.get("reports/q3.pdf", { maxBytes: 10_000_000 });
});

// In a Worker:  Effect.provide(program, makeR2BlobStorageLayer(env.BUCKET))
// Elsewhere:    Effect.provide(program, S3BlobStorageLive) + S3Config.layer({ ... })

BlobPresigner.presignPut signs the Content-Type and, when you pass contentLength, the exact body size too. Browsers send Content-Length automatically, so an upload whose body is any other size is rejected by the storage before it is written:

const url = yield* presigner.presignPut({
  key: "uploads/avatar.png",
  contentType: "image/png",
  contentLength: file.size,
  expiresInSeconds: 600,
});

Testing

  • runWithService(tag, layer) — a Promise-based runner for exercising one service in tests without repeating provide/run boilerplate.
  • testExecutionContext() — a fake Cloudflare ExecutionContext whose drainWaitUntil() awaits fire-and-forget work.
  • testStub<T>(partial) — the sanctioned escape hatch for faking SDK clients in tests. Never use it in production code.
  • applyTestEnvDefaults(record) — fills missing process.env keys.

Environment configuration

Haversack deliberately contains no env-var handling. Use @ayronforge/envil for type-safe environment validation and feed the decoded values into the config layers.

Contributing

bun install
bun run typecheck
bun test
bun run build   # bundles with tsdown and smoke-tests every entrypoint

Issues and pull requests are welcome. If you're adding a new integration, follow the house style: every capability is a Context.Service with a config layer, tagged errors, and tracing spans — and multi-vendor capabilities get a port in /contracts with one implementation per vendor.

License

MIT

版本与发布记录

@ayronforge/haversack 共发布 13 个版本,首次发布于 2026/08/09;过去 12 个月发布了 13 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.12.0latest2026/10/2216 天前
0.11.02026/10/021 个月前
0.10.02026/09/191 个月前
0.9.02026/09/022 个月前
0.8.02026/09/012 个月前
0.7.02026/08/152 个月前

关于 @ayronforge/haversack 的常见问题

@ayronforge/haversack 是什么?

Typed TypeScript toolbox: Effect services for backend integrations plus focused React bindings for PostHog feature flags

如何安装 @ayronforge/haversack?

在项目目录运行 npm install @ayronforge/haversack,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(^4.0.0)、libphonenumber-js(^1.12.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ayronforge/haversack 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ayronforge/haversack 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ayronforge/haversack 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ayronforge/haversack 可以商用吗?

@ayronforge/haversack 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ayronforge/haversack 的下载量有多少?

根据 npm 官方下载统计,@ayronforge/haversack 在 2026/10/08 – 2026/11/06 这 30 天内被下载 1,901 次,日均约 63 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ayronforge/haversack 还在维护吗?

最新版本 0.12.0 发布于 2026/10/22(16 天前);至今共发布 13 个版本,其中过去 12 个月发布了 13 个;首次发布于 2026/08/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。