@tokenops/sdk/kernel is the common vocabulary for application boundaries. It has no Node-only APIs and works in browsers, workers, and Node 22+, but it does use the platform globals crypto and performance. Install zod@^4.1.11: it is a mandatory runtime peer for this subpath, not an optional integration.
Ids, time, and clocks#
| API | Contract |
|---|---|
newId / isId / IdSchema | UUID-v4 ids with one of the published ID_PREFIXES, such as org, dst, drw, or evt. |
parseUnixSeconds | Accepts a non-negative safe integer. Invalid values throw AppError(unix-seconds-invalid, validation). |
systemClock | Wall time as whole Unix seconds through now(), and epoch milliseconds through nowMs(). |
FakeClock | A deterministic Clock with advance(seconds) and advanceMs(ms); fractional milliseconds remain visible to nowMs(). |
monotonicMs | A performance.now() value for durations and TTL budgets. Never persist or compare it with epoch time. |
Versioned domain events#
Register an event schema once with defineEvent, then create envelopes by type. Registration snapshots and freezes the definition. The same type and version cannot be registered twice; getEvent(type) returns the greatest registered version. envelope parses the payload, copies the subject array, stamps evt_<uuid>, and reads the supplied clock.
import { z } from "zod";
import {
AddressSchema,
FakeClock,
IdSchema,
UnixSecondsSchema,
assertPayloadTypes,
defineEvent,
envelope,
newCorrelationRef,
parseUnixSeconds,
} from "@tokenops/sdk/kernel";
const DistributionCreated = z.strictObject({
distributionRef: IdSchema("dst"),
tokenAddress: AddressSchema.meta({ kernelType: "publicAddress" }),
createdAt: UnixSecondsSchema,
});
assertPayloadTypes(DistributionCreated, ["publicAddress"]);
defineEvent({
type: "distribution.created",
version: 1,
data: DistributionCreated,
durable: true,
});
const clock = new FakeClock(parseUnixSeconds(1_800_000_000));
const event = envelope(
"distribution.created",
{
distributionRef: "dst_550e8400-e29b-41d4-a716-446655440000",
tokenAddress: "0x1111111111111111111111111111111111111111",
createdAt: clock.now(),
},
{
correlationRef: newCorrelationRef(),
organizationId: "org_550e8400-e29b-41d4-a716-446655440000",
actor: "per_550e8400-e29b-41d4-a716-446655440000",
subject: ["dst_550e8400-e29b-41d4-a716-446655440000"],
},
clock,
);An unknown type throws event-unknown; invalid data throws event-data-invalid; a duplicate version throws event-defined-twice. All three are non-retryable validation AppErrors.
Payload-type audit#
assertPayloadTypes walks a Zod 4 schema and rejects open or unclassified leaves. Kernel schemas mark ids and Unix seconds; enum, literal, boolean, null, and undefined are closed leaves. Pass application-defined metadata kinds only when your schema registry deliberately defines them, as the address transform does above.
One error shape across bundles#
AppError carries a stable code, category, retryability flag, and typed details. Use isAppError instead of instanceof: its symbol brand works when two bundles contain separate class copies.
import {
AppError,
httpStatus,
isAppError,
sanitize,
} from "@tokenops/sdk/kernel";
export function toResponse(error: unknown): Response {
const appError = isAppError(error)
? error
: new AppError("internal-error", "unavailable", true);
const { clean } = sanitize({
code: appError.code,
category: appError.category,
retryable: appError.retryable,
secret: "dropped",
});
return Response.json(
{ error: appError.code, details: appError.details, telemetry: clean },
{ status: httpStatus(appError.category) },
);
}| Category | HTTP |
|---|---|
| validation | 400 |
| unauthenticated | 401 |
| forbidden / step-up-required | 403 |
| not-found | 404 |
| conflict | 409 |
| refused-by-chain / refused-by-provider | 422 |
| rate-limited | 429 |
| unavailable | 503 |
Telemetry allowlist#
FIELD_CLASSES is a frozen name-to-class map. sanitize(record) preserves classified fields in input order and reports every dropped name. If any person_id field survives, it also removes all public_address fields, preventing a person id and on-chain address from being linked in one record. It classifies names only; the observability exporter separately keeps primitive values.
assertDimensionsthrowsdimension-not-allowedand lists refused names.ERROR_DETAIL_KEYSadds bounded error-only keys such aspath,expected, andlimit.- Adding a field name is a privacy decision; unknown names are dropped, never passed through optimistically.
Environment and correlation guards#
import { assertEnvironment } from "@tokenops/sdk/kernel";
const allowed = {
chainIds: [1, 11_155_111],
addresses: ["0x1111111111111111111111111111111111111111"],
relayerUrls: ["https://relayer.example.com"],
} as const;
assertEnvironment(allowed, {
chainIds: [11_155_111],
addresses: ["0x1111111111111111111111111111111111111111"],
relayerUrls: ["https://relayer.example.com"],
});assertEnvironment compares every used chain id, address, and relayer URL with the supplied lists. Addresses compare case-insensitively; relayer URLs compare exactly. The first miss throws environment-mismatch as a non-retryable unavailable error.
Correlation refs are 26-character lower-case base32 values. fromTraceparent derives one from a valid W3C version-00 header or creates a fresh ref; toTraceparent validates both the ref and a non-zero 16-hex-digit span id. shortRef is only the first eight characters for display, not a durable identifier.