@ciromaciel/contract

中文文档
2.2.0发布于 2026/11/05MITES Module零依赖近 30 天下载 2230

A single response envelope, a closed catalogue of error codes, and helpers that make answering outside it impossible.

原始使用文档
文档目录12 节
  1. Installation
  2. Quick start
  3. API
  4. @ciromaciel/contract
  5. @ciromaciel/contract/codes
  6. @ciromaciel/contract/schemas
  7. @ciromaciel/contract/plan
  8. @ciromaciel/contract/webhooks
  9. @ciromaciel/contract/token
  10. @ciromaciel/contract/outbox
  11. @ciromaciel/contract/feedback
  12. Links

@ciromaciel/contract

One response envelope, a closed catalogue of error codes, and helpers that make answering outside it impossible.

This package is the executable norm: what services import in order to respond, and what tests import in order to check. The rules it enforces are described below.

Installation

bun add @ciromaciel/contract

zod is an optional peer, needed only by the /schemas entrypoint, and jose only by /token.

Quick start

import { ok, created, collection, failure } from '@ciromaciel/contract'
import { CODES } from '@ciromaciel/contract/codes'

return ok({ id, name })
return created({ id, name })
return failure(CODES.NOT_FOUND, 'project not found')

failure() derives the HTTP status from the code — there is no signature that lets you return NOT_FOUND with a 200.

API

@ciromaciel/contract

ExportWhat it does
ok(resource, { status, headers })One resource, at the root. No data, no success, no message.
created(resource, { headers })A resource that has just been born — 201.
collection(list, page, { status, headers }){ items, page: { limit, offset, total, hasMore } }. Without total, total and hasMore come back null — "nobody counted" is not the same as zero.
page({ limit, offset, total, extra })Builds the page object for collection, requiring a numeric total.
pagination(url, { defaultLimit, maxLimit })Reads limit/offset from a URL, with a ceiling.
failure(code, message, { details, headers, status })An error Response. Status comes from the code.
failureBody(code, message, details){ body, status } for frameworks that assemble the response themselves.
failureWithStatus(status, message, details, headers)For a global error handler that only has the HTTP number at hand.
CODES · STATUS_BY_CODE · RETRIABLE · ALIASESThe vocabulary and its tables.
isCode · toCode · codeFromStatusCode guards and conversions.

@ciromaciel/contract/codes

The closed enum: UNAUTHORIZED, FORBIDDEN, WRONG_AUDIENCE, INSUFFICIENT_SCOPE, NOT_FOUND, CONFLICT, NAME_TAKEN, EXPIRED, HAS_RESOURCES, VALIDATION_ERROR, UNPROCESSABLE, RATE_LIMITED, PAYLOAD_TOO_LARGE, PLAN_LIMIT, SUBSCRIPTION_REQUIRED, INTERNAL_ERROR, SERVICE_UNAVAILABLE, UPSTREAM_ERROR.

code is the part the client reads; message is for the human and its wording may change at any time. Renaming a code is a breaking change.

@ciromaciel/contract/schemas

import { errorSchema, collectionSchema, resourceSchema, pageSchema, codeSchema } from '@ciromaciel/contract/schemas'

Zod schemas for the same rules, from the reader's side. This path is separate on purpose: schemas.js imports Zod, and a worker that only needs to respond should not have to load a validator it never calls. While the index re-exported the schemas, the import was eager — asking for failure pulled Zod in, and the install broke with "Cannot find package 'zod'" for anyone who treated the peer as optional.

@ciromaciel/contract/plan

import { createPlanGate } from '@ciromaciel/contract/plan'

export const plan = createPlanGate({
    product: 'auth',
    resource: 'applications',
    tiers: [
        { slug: 'auth-applications-1', limit: 1 },
        { slug: 'auth-applications-5', limit: 5 },
        { slug: 'auth-applications-unlimited', limit: Infinity },
    ],
})

Also exports hasSlug, tierLimit, getPlanNotice, withPlanNotice, gateTelemetry, recordGateDecision and resetGateTelemetry. A product owns the prefix, the slugs and the wording of the notice; the cache, the fail-open, the product-prefix check and the choice between PLAN_LIMIT and SUBSCRIPTION_REQUIRED are the same reasoning everywhere.

@ciromaciel/contract/webhooks

import { webhookHeaders, verifyWebhook, generateSigningSecret } from '@ciromaciel/contract/webhooks'

const headers = await webhookHeaders(secret, { id: event.id, body }) // x-ccm-id, x-ccm-timestamp, x-ccm-signature
const result = await verifyWebhook([secret, previousSecret], { id, timestamp, signature, body })

One protocol for every webhook of the stack: Standard Webhooks with the x-ccm prefix. The signature is v1,<base64 HMAC-SHA256 of "id.timestamp.body">, the key is the base64 after whsec_, and a timestamp more than five minutes off is refused. verifyWebhook takes several secrets so a rotation has a grace period, and answers with a reason (stale_timestamp, bad_signature, …) instead of a bare boolean. Also exports signWebhook, secretPrefixOf and SIGNATURE_TOLERANCE_SECONDS.

@ciromaciel/contract/token

import { createTokenVerifier } from '@ciromaciel/contract/token'

const tokens = createTokenVerifier({ authUrl: 'https://auth.worker.myinfrastructure.click' })

await tokens.verifySession(bearer) // REST routes
await tokens.verifyAccessToken(bearer, { audience: 'https://hoster.worker.myinfrastructure.click/mcp' }) // MCP server

The Auth signs sessions and OAuth access tokens with the same key and issuer; only the claims tell them apart. verifySession accepts a person's session and refuses an access token (access_token_not_accepted): a token consented for one resource and one scope must never act as the whole account on a REST route. verifyAccessToken accepts only an access token whose aud is this server (wrong_audience otherwise, RFC 8707) and returns its scopes.

Both return { ok: true, subject, email, name, scopes, claims } or { ok: false, reason }, never throw on a bad credential, and accept RS256 with the riligar-auth issuer only. jose (5 or 6) is an optional peer, needed only by this entrypoint.

@ciromaciel/contract/outbox

import { OUTBOX_SQL, envelope, enqueueEvent, deliverDue, releaseHeld, retryDelivery } from '@ciromaciel/contract/outbox'

The delivery queue every product shares. Each event is a row in outbox before it leaves, one per subscribed endpoint, signed with ./webhooks. A 2xx is sent; 401 and 403 are held (probed hourly, released at once by releaseHeld when the secret changes); 410 is failed; anything else retries after 1 min, 5 min, 30 min, 2 h and 12 h. OUTBOX_SQL is the schema to put in the product's first migration. The product supplies a D1-shaped db and decryptSecret(ciphertext).

@ciromaciel/contract/feedback

import { createFeedback, FEEDBACK_SQL } from '@ciromaciel/contract/feedback'

"Sugerir ou relatar", the corner button of every panel (FeedbackWidget in @ciromaciel/dashboard-kit), on the worker side. createFeedback({ product, prefix, error, requirePerson, locate, fields }) returns handleRoutes (GET/POST /api/feedback, behind the login), handleHooks (the Tasks' signed answers at /feedback/tasks and the build job's news at /feedback/build) and runCron (resend what the Tasks did not take; resend and then report what the triage never took). A feedback is addressed by its full UUID and only by it (2.0.0, 25/10/2026): its thread is <product>:<uuid> (the workKey of feedback.created), the build's news are { id, status, note? }, and a prefix, a code, a slug or an unknown id answers the same 404. The Tasks' thread itself goes by its own UUID (2.1.0): the feedback.job event carries workId (from the webhook's workId), never the key. <prefix>-<6 hex> (DK-1A2B3C) is the label people read in titles, never an address. FEEDBACK_SQL is the table; the product adds its own columns (deck_id) and names them in locate().columns. Only a person sends, and locate names only the account's own things. Env: FEEDBACK_URL, FEEDBACK_SECRET, TASKS_WEBHOOK_SECRET, BUILD_SECRET. FAILURE_OPTIONS are the choices of a job that stopped (1.5.0): each one the Ciro picks becomes the job that answers it (repair, retry, revert, ship, revise, build); the ones that move main share the product's deploy group.

MIT © Ciro Cesar Maciel

版本与发布记录

@ciromaciel/contract 共发布 14 个版本,首次发布于 2026/09/20;过去 12 个月发布了 14 个版本。

版本发布日期距今
2.2.0latest2026/11/054 天前
2.1.02026/10/2615 天前
2.0.02026/10/2615 天前
1.5.02026/10/1525 天前
1.4.02026/10/1526 天前
1.3.02026/10/081 个月前

关于 @ciromaciel/contract 的常见问题

@ciromaciel/contract 是什么?

A single response envelope, a closed catalogue of error codes, and helpers that make answering outside it impossible.

如何安装 @ciromaciel/contract?

在项目目录运行 npm install @ciromaciel/contract,或使用 pnpm add / yarn add / bun add。

@ciromaciel/contract 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/ciromaciel__contract 是否存在。

@ciromaciel/contract 支持 ESM 还是 CommonJS?

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

@ciromaciel/contract 可以商用吗?

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

@ciromaciel/contract 的下载量有多少?

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

@ciromaciel/contract 还在维护吗?

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