@wahlu/public-api-contracts
v0.7.3
Published
Executable public HTTP wire contracts for the Wahlu API
Downloads
124
Readme
@wahlu/public-api-contracts
Executable, domain-free HTTP wire contracts for Wahlu's public API. The package can be consumed in browser or server builds and has no framework, database, provider, job, CLI, MCP, or UI dependency.
Contract status: this source models the 22-operation public contract. The package is ESM-only and targets Node.js 18+ or a modern browser runtime. Cursor signing remains server-only even though consumer schemas are browser-compatible.
npm install @wahlu/public-api-contractsPublic exports
- Strict Zod schemas and inferred types for success/error envelopes, response metadata, structured issues, bounded error details, links, cursor pagination, rate limits, request IDs, idempotency keys, and canonical UTC timestamps.
createSuccessEnvelopeSchemaandcursorResponseMetadataSchemaare cursor-only producer contracts. Consumers can usecreateConsumerSuccessEnvelopeSchemaduring migration; an existing offset endpoint must opt intocreateLegacySuccessEnvelopeSchemaexplicitly.coreErrorPolicy,createPublicError, producer schema factories, and safe status helpers. The consumer schema accepts future bounded uppercase domain codes; producers derive status and retryability from the immutable core registry or an explicit domain registry. Unregistered producer errors collapse to a minimal, non-retryableINTERNAL_ERRORrather than exposing the attempted code, details, or message.resolveOrGenerateRequestIdfor the server policy that accepts valid caller IDs and replaces invalid values instead of turning them into a public error.toUtcTimestamp,requireUtcTimestamp, and generictoSafePublicStatusboundary helpers.encodeCursoranddecodeCursorfor versioned HMAC-SHA-256 cursor tokens.- Domain-free operation registry primitives.
definePublicOperationvalidates and freezes complete endpoint descriptors;createOperationRegistryrejects duplicate IDs and route shapes and returns a deterministically ordered, typed registry. Candidate/released filters never reorder operations. - Canonical released schemas and operation descriptors for 22 operations: agent context, brand target discovery, non-mutating live target-bound TikTok privacy reads, explicit target privacy refresh, public platform capabilities, Media upload sessions, remote Media import, bounded brand-scoped Media listing, one-item Media read, explicit reviewed Media repair derivatives, bounded content-item listing, one-item content read, draft creation, precise existing-draft TikTok privacy update, write-free scheduling preflight, bounded date-range Schedule listing, held Schedule creation, one-item Schedule read, bounded publish receipt lookup, exact receipt-bound provider cleanup, guarded rescheduling, and auditable unsent cancellation. Their registered HTTP routes, SDK, CLI/MCP adapters and shared raw/SDK acceptance scenarios conform to the canonical success metadata envelope.
- Pure deterministic OpenAPI 3.1 projection.
generateReleasedOpenApiDocumentfilters candidates, projects exact descriptor statuses/auth/scopes/examples and protocol-bodyless responses, whilestringifyOpenApiDocumentproduces stable reviewable bytes. The committed released artifact isdocs/openapi/wahlu-public-api-v1.openapi.json.
The independent OpenAPI 3.1 structure gate is:
yarn workspace @wahlu/public-api-contracts validate:openapiIt pins Redocly CLI through npx so validation does not add a package dependency or change the
repository lockfile.
Operation registry contract
The registry is the developer-authored source for API, OpenAPI, SDK, and reference work. Exactly
22 descriptors are released and generate the committed released OpenAPI artifact: five discovery
operations, Media upload sessions, remote Media import, bounded brand-scoped Media listing,
one-item Media read, explicit reviewed repair
derivatives, bounded content listing, one-item content read, draft creation, precise TikTok privacy update and write-free scheduling
preflight, held Schedule creation and one-item Schedule read, bounded publish receipt lookup, and
exact receipt-bound provider cleanup, guarded rescheduling, and auditable unsent cancellation.
Execution, approval, retry, polling, and /publish-runs operations remain absent. A descriptor must
declare its stable ID and release state, canonical method/path template, tags and prose, authentication
and exact scopes, effect/risk/authority policy, idempotency behaviour, request schema slots, success
schema, allowed canonical errors, validated JSON examples, links, and deprecation state.
Registry construction enforces several cross-field invariants:
- path, query, and header parameters are strict named Zod objects; template parameter names and path schema fields match exactly;
- GET/HEAD are read-only, PUT/PATCH/DELETE cannot claim to be reads, and a naturally idempotent PUT may return an unchanged success. Released non-PUT mutations require explicit idempotency except the explicitly registered bounded target-refresh POST action, whose medium-risk write has a strict empty body and no content, Schedule, or provider-publishing effect;
- an
externaleffect is deliberately narrower than a generic write: it is high-risk, requires API-key authentication, names at least one effect-authority scope already present in the operation scopes, and always requires idempotency. A publishing descriptor can therefore namepublish:execute, while a different external effect can use its own explicit authority without making this package domain-specific; - header/body idempotency uses the exported canonical field schemas and
resolveOperationIdempotencyKey. When both sources are allowed, at least one is required for a required operation and two supplied values must match. Replay returns a stable result through the explicitly declaredreplay.response_status; it is not misleadingly described as returning the original wire response, and validation requires that status to select a declared response schema; - examples parse through every declared request/success/error schema, contain bounded plain JSON, and are cloned and deeply frozen. Zod schema objects remain unfrozen and reusable;
- one primary 2xx response may declare exact alternate expected 2xx statuses or conditional-GET 304.
Statuses are unique, every expected status has a matching schema-validated example, canonical error
statuses cannot be reclassified as expected responses, and protocol-bodyless 204/205/304 responses
use the exported
bodylessResponseSchema. A 304 also requires the canonical optionalIf-None-Matchrequest field. This models both ETag revalidation and create/replay201/200variation without weakening error validation; - deprecated replacement IDs resolve inside the registry, cannot form deprecated chains, and a released operation cannot point to a candidate replacement.
Zod schemas are a trusted developer-time contract boundary, not untrusted request input. The schema
check accepts real Zod 3 first-party types structurally as well as through instanceof, so schemas
created by another installed Zod 3 module instance remain usable. It rejects minimal duck types and
requires direct strict ZodObject shapes where OpenAPI needs named path/query/header parameters.
Consumers should still share one compatible Zod 3 version where possible.
The released capabilities operation models If-None-Match and its bodyless 304 directly.
OpenAPI's global-header layer combines operation-specific ETag/cache headers on successful
representations with request-ID and rate-limit headers on every response. It does not claim cache or
idempotency-replay headers on errors, and it documents Retry-After only for the guaranteed 429
rate-limit response.
Cursor security contract
The codec requires an explicit signing secret containing at least 32 bytes and uses Web Crypto, so
the main package does not import Node-only crypto modules. Cursor encoding and authenticated
decoding are server-only operations: never expose or bundle the signing secret into browser code.
Browser consumers should treat cursor tokens as opaque strings and use cursorTokenSchema when
validation is needed. A cursor authenticates its version,
resource, ordering identity, sort value, final ID tie-break, optional expiry, and a keyed digest of
the caller's canonical named scope fields. Raw workspace/brand scope strings are therefore not
embedded in the payload, and structured encoding avoids delimiter collisions.
Decode always authenticates the token before parsing the payload. Wrong-resource, wrong-order,
cross-scope, malformed, expired, and tampered tokens all throw InvalidCursorError with code
INVALID_CURSOR and the same safe public message. The reason property is for internal metrics and
tests only; public adapters must not expose it.
const cursor = await encodeCursor(
{
resource: "schedules",
order: "scheduled_at:asc,id:asc",
scope: { workspace_id: workspaceId, brand_id: brandId },
sortValue: schedule.scheduled_at,
id: schedule.id
},
process.env.PUBLIC_CURSOR_SIGNING_SECRET
);
const position = await decodeCursor(cursor, process.env.PUBLIC_CURSOR_SIGNING_SECRET, {
resource: "schedules",
order: "scheduled_at:asc,id:asc",
scope: { workspace_id: workspaceId, brand_id: brandId }
});The payload is opaque and tamper-evident, not encrypted. Do not place secrets or provider payloads in sort values or IDs. Error-detail producers remain responsible for allowlisting and redacting values before validating them against the bounded public schema.
responseMetadataSchema.pagination temporarily accepts a strict legacy offset variant as well as
the canonical cursor shape. New collections should validate directly with cursorPaginationSchema;
the offset schema exists only so response-envelope migration does not break live V1 collections.
